From 5d4d5bee446510f242de6c7523b1648128895353 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:16:54 -0600 Subject: [PATCH 01/71] Reject reward path traversal names --- .../advancedcore/api/rewards/RewardRegistry.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java index fa360ea950..daf156c5af 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java @@ -1,5 +1,6 @@ package com.bencodez.advancedcore.api.rewards; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -93,6 +94,11 @@ public Reward getReward(String reward) { } } + if (!isSafeRewardFileName(reward)) { + plugin.getLogger().warning("Rejected unsafe reward file name: " + reward); + throw new IllegalArgumentException("Reward name must not contain path separators or be an absolute path"); + } + return new Reward(reward); } @@ -138,6 +144,10 @@ public static String normalizeDirectPath(String path) { return normalizeLookupName(path).replace('.', '_').toLowerCase(Locale.ROOT); } + static boolean isSafeRewardFileName(String reward) { + return reward != null && !reward.indexOf('\0') >= 0; + } + public boolean rewardExist(String reward) { reward = normalizeLookupName(reward); if (reward.isEmpty()) { From 1da8dd94036afea300e6afea6a1df57409126431 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:17:13 -0600 Subject: [PATCH 02/71] Fix reward file name validation --- .../bencodez/advancedcore/api/rewards/RewardRegistry.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java index daf156c5af..7d0ced6ece 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java @@ -145,7 +145,10 @@ public static String normalizeDirectPath(String path) { } static boolean isSafeRewardFileName(String reward) { - return reward != null && !reward.indexOf('\0') >= 0; + if (reward == null || reward.indexOf('\0') >= 0 || new File(reward).isAbsolute()) { + return false; + } + return reward.indexOf('/') < 0 && reward.indexOf('\\') < 0; } public boolean rewardExist(String reward) { From 8255a6d9c28caa84bbfab620abb93277cdcbd24d Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:17:21 -0600 Subject: [PATCH 03/71] Do not expose reward-file sub-sections as standalone rewards --- .../bencodez/advancedcore/api/rewards/SubRewardResolver.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubRewardResolver.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubRewardResolver.java index 65b2caa399..95bcff26fb 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubRewardResolver.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubRewardResolver.java @@ -25,11 +25,6 @@ public void checkSubRewards() { for (DirectlyDefinedReward direct : handler.getDirectlyDefinedRewards()) { checkSubRewards(direct, new HashSet<>()); } - - plugin.extraDebug("Checking reward file for sub rewards"); - for (Reward reward : handler.getRewards()) { - checkSubRewards(new RewardFileDefinedReward(reward), new HashSet<>()); - } } public void checkSubRewards(DefinedReward direct) { From 7e27e776e95548016ef852ebb761bab989209fd3 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:17:50 -0600 Subject: [PATCH 04/71] Require UUID match for permission name fallback --- .../java/com/bencodez/advancedcore/api/misc/PlayerManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/PlayerManager.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/PlayerManager.java index 3b75b01e3f..e822ea25ed 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/PlayerManager.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/PlayerManager.java @@ -211,7 +211,7 @@ public boolean hasServerPermission(UUID playerUUID, String playername, String pe return player.hasPermission(perm); } else { player = Bukkit.getPlayer(playername); - if (player != null) { + if (player != null && playerUUID.equals(player.getUniqueId())) { return player.hasPermission(perm); } } From a17d8fdaab9a88b45eef6934c465fca62b3f8cd2 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:17:59 -0600 Subject: [PATCH 05/71] Add reward path traversal regression tests --- .../rewards/RewardRegistrySecurityTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java new file mode 100644 index 0000000000..9762ed2527 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java @@ -0,0 +1,23 @@ +package com.bencodez.advancedcore.api.rewards; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class RewardRegistrySecurityTest { + + @Test + void rewardFileNameRejectsPathTraversal() { + assertFalse(RewardRegistry.isSafeRewardFileName("../../OtherPlugin/config")); + assertFalse(RewardRegistry.isSafeRewardFileName("..\\..\\OtherPlugin\\config")); + assertFalse(RewardRegistry.isSafeRewardFileName("folder/reward")); + } + + @Test + void rewardFileNameAllowsNormalRewardNames() { + assertTrue(RewardRegistry.isSafeRewardFileName("Daily")); + assertTrue(RewardRegistry.isSafeRewardFileName("Daily_Reward")); + assertTrue(RewardRegistry.isSafeRewardFileName("Daily.Reward-v2")); + } +} From 580e80660c3b2a8af97aca5c6fb6958181eb78ff Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:18:07 -0600 Subject: [PATCH 06/71] Add permission identity regression tests --- .../api/misc/PlayerManagerSecurityTest.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java new file mode 100644 index 0000000000..e6182d9bd0 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java @@ -0,0 +1,60 @@ +package com.bencodez.advancedcore.api.misc; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import com.bencodez.advancedcore.AdvancedCoreConfigOptions; +import com.bencodez.advancedcore.AdvancedCorePlugin; + +class PlayerManagerSecurityTest { + + @Test + void nameFallbackRequiresMatchingUuid() { + UUID requestedUuid = UUID.randomUUID(); + Player differentPlayer = mock(Player.class); + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); + + when(differentPlayer.getUniqueId()).thenReturn(UUID.randomUUID()); + when(differentPlayer.hasPermission("reward.permission")).thenReturn(true); + when(plugin.getOptions()).thenReturn(options); + + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic advancedCore = mockStatic(AdvancedCorePlugin.class)) { + bukkit.when(() -> Bukkit.getPlayer(requestedUuid)).thenReturn(null); + bukkit.when(() -> Bukkit.getPlayer("stored-name")).thenReturn(differentPlayer); + advancedCore.when(AdvancedCorePlugin::getInstance).thenReturn(plugin); + PlayerManager.getInstance().plugin = plugin; + + assertFalse(PlayerManager.getInstance().hasServerPermission(requestedUuid, "stored-name", + "reward.permission")); + } + } + + @Test + void nameFallbackAllowsMatchingUuid() { + UUID requestedUuid = UUID.randomUUID(); + Player requestedPlayer = mock(Player.class); + + when(requestedPlayer.getUniqueId()).thenReturn(requestedUuid); + when(requestedPlayer.hasPermission("reward.permission")).thenReturn(true); + + try (MockedStatic bukkit = mockStatic(Bukkit.class)) { + bukkit.when(() -> Bukkit.getPlayer(requestedUuid)).thenReturn(null); + bukkit.when(() -> Bukkit.getPlayer("stored-name")).thenReturn(requestedPlayer); + + assertTrue(PlayerManager.getInstance().hasServerPermission(requestedUuid, "stored-name", + "reward.permission")); + } + } +} From b326bdd987ed6221ae218ef90f8be5c48effc48d Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 19:48:41 -0600 Subject: [PATCH 07/71] Fix isolated static mock setup in permission security test --- .../advancedcore/api/misc/PlayerManagerSecurityTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java index e6182d9bd0..5692a21934 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java @@ -12,6 +12,7 @@ import org.bukkit.entity.Player; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; +import org.mockito.Mockito; import com.bencodez.advancedcore.AdvancedCoreConfigOptions; import com.bencodez.advancedcore.AdvancedCorePlugin; @@ -29,6 +30,9 @@ void nameFallbackRequiresMatchingUuid() { when(differentPlayer.hasPermission("reward.permission")).thenReturn(true); when(plugin.getOptions()).thenReturn(options); + // Some earlier tests initialize AdvancedCore's static singleton mock. Clear any + // inline registration before installing the isolated static mock used here. + Mockito.framework().clearInlineMocks(); try (MockedStatic bukkit = mockStatic(Bukkit.class); MockedStatic advancedCore = mockStatic(AdvancedCorePlugin.class)) { bukkit.when(() -> Bukkit.getPlayer(requestedUuid)).thenReturn(null); From b02d96b8183ee61abe8e7e93eb01f3de04342089 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 20:02:05 -0600 Subject: [PATCH 08/71] Keep file sub-rewards internal to dispatch --- .../api/rewards/SubRewardResolver.java | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubRewardResolver.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubRewardResolver.java index 95bcff26fb..994a7954b1 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubRewardResolver.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubRewardResolver.java @@ -2,17 +2,19 @@ import java.util.HashSet; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.rewards.injected.RewardInject; /** - * Discovers directly-defined sub rewards and prevents recursive discovery loops. + * Discovers sub rewards and prevents recursive discovery loops. */ public class SubRewardResolver { private final RewardHandler handler; private final AdvancedCorePlugin plugin; + private final CopyOnWriteArrayList fileBackedSubRewards = new CopyOnWriteArrayList<>(); public SubRewardResolver(RewardHandler handler, AdvancedCorePlugin plugin) { this.handler = handler; @@ -22,16 +24,32 @@ public SubRewardResolver(RewardHandler handler, AdvancedCorePlugin plugin) { public void checkSubRewards() { plugin.extraDebug("Checking directlydefined rewards for sub rewards"); handler.getRewardRegistry().resetSubDirectlyDefinedRewards(); + fileBackedSubRewards.clear(); for (DirectlyDefinedReward direct : handler.getDirectlyDefinedRewards()) { - checkSubRewards(direct, new HashSet<>()); + checkSubRewards(direct, new HashSet<>(), true); + } + + plugin.extraDebug("Checking reward files for internal sub rewards"); + for (Reward reward : handler.getRewards()) { + checkSubRewards(new RewardFileDefinedReward(reward), new HashSet<>(), false); } } public void checkSubRewards(DefinedReward direct) { - checkSubRewards(direct, new HashSet<>()); + checkSubRewards(direct, new HashSet<>(), !(direct instanceof RewardFileDefinedReward)); + } + + public SubDirectlyDefinedReward getFileBackedSubReward(String path) { + String normalized = RewardRegistry.normalizeDirectPath(path); + for (SubDirectlyDefinedReward sub : fileBackedSubRewards) { + if (RewardRegistry.normalizeDirectPath(sub.getFullPath()).equals(normalized)) { + return sub; + } + } + return null; } - private void checkSubRewards(DefinedReward direct, Set activePaths) { + private void checkSubRewards(DefinedReward direct, Set activePaths, boolean exposeStandalone) { String path = direct.getFullPath(); if (!activePaths.add(path)) { plugin.getLogger().warning("Detected recursive sub reward path, skipping: " + path); @@ -45,12 +63,22 @@ private void checkSubRewards(DefinedReward direct, Set activePaths) { plugin.getLogger().warning("Detected recursive sub reward path, skipping: " + subPath); continue; } - handler.addSubDirectlyDefined(sub); - checkSubRewards(sub, activePaths); + if (exposeStandalone) { + handler.addSubDirectlyDefined(sub); + } else { + addFileBackedSubReward(sub); + } + checkSubRewards(sub, activePaths, exposeStandalone); } } } finally { activePaths.remove(path); } } + + private void addFileBackedSubReward(SubDirectlyDefinedReward sub) { + if (getFileBackedSubReward(sub.getFullPath()) == null) { + fileBackedSubRewards.add(sub); + } + } } From 6c9ab5dcb4b8fc7104cd42e41c2c93a365c62cf5 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 20:02:23 -0600 Subject: [PATCH 09/71] Dispatch file sub-rewards without exposing lookup --- .../advancedcore/api/rewards/RewardExecutor.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java index 5f225d8aee..b48e7e38ea 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java @@ -141,8 +141,9 @@ private void giveSectionReward(AdvancedCoreUser user, ConfigurationSection data, String rewardName = context.buildRewardName(path); DirectlyDefinedReward direct = handler.getDirectlyDefined(path); SubDirectlyDefinedReward sub = handler.getSubDirectlyDefined(rewardName); + SubDirectlyDefinedReward fileSub = handler.getSubRewardResolver().getFileBackedSubReward(rewardName); - if (context.supportsDirectDispatch() && (direct != null || sub != null)) { + if (context.supportsDirectDispatch() && (direct != null || sub != null || fileSub != null)) { if (direct != null) { Reward reward = direct.getReward(); if (reward != null) { @@ -156,14 +157,14 @@ private void giveSectionReward(AdvancedCoreUser user, ConfigurationSection data, return; } - Reward reward = sub.getReward(); + SubDirectlyDefinedReward selectedSub = sub != null ? sub : fileSub; + Reward reward = selectedSub.getReward(); if (reward != null) { - plugin.debug("Giving subdirectlydefined reward " + rewardName + ", Options: " + options + " to " + plugin.debug("Giving sub reward " + rewardName + ", Options: " + options + " to " + user.getPlayerName() + "/" + user.getUUID()); giveReward(user, reward, options); } else { - plugin.debug("Failed to give subdirectlydefined reward " + path + ", Options: " + options - + ", Reward == null"); + plugin.debug("Failed to give sub reward " + path + ", Options: " + options + ", Reward == null"); } return; } From 9261525d87c1a4f93d686cba4e297d5c159dac63 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 20:02:44 -0600 Subject: [PATCH 10/71] Test internal file-backed sub-reward discovery --- .../tests/rewards/RewardServicesTest.java | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java index b3ad2b593c..d142491ded 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java @@ -1,6 +1,7 @@ package com.bencodez.advancedcore.tests.rewards; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -25,7 +26,9 @@ import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.rewards.DefinedReward; import com.bencodez.advancedcore.api.rewards.DirectlyDefinedReward; +import com.bencodez.advancedcore.api.rewards.Reward; import com.bencodez.advancedcore.api.rewards.RewardHandler; +import com.bencodez.advancedcore.api.rewards.RewardFileDefinedReward; import com.bencodez.advancedcore.api.rewards.SubDirectlyDefinedReward; import com.bencodez.advancedcore.api.rewards.injected.RewardInject; import com.bencodez.advancedcore.api.rewards.injectedrequirement.RequirementInject; @@ -118,8 +121,7 @@ public void subRewardResolverStopsRecursiveDiscovery() { when(loop.isDirectlyDefined()).thenReturn(true); RewardInject inject = mock(RewardInject.class); - when(inject.subRewards(any(DefinedReward.class))) - .thenReturn(new ArrayList<>(List.of(loop))); + when(inject.subRewards(any(DefinedReward.class))).thenReturn(new ArrayList<>(List.of(loop))); handler.getInjectedRewards().add(inject); handler.checkSubRewards(root); @@ -128,4 +130,29 @@ public void subRewardResolverStopsRecursiveDiscovery() { assertSame(loop, handler.getSubDirectlyDefinedRewards().get(0)); verify(logger).warning("Detected recursive sub reward path, skipping: Loop"); } + + @Test + public void fileBackedSubRewardsRemainInternalToSectionDispatch() { + Reward root = mock(Reward.class); + when(root.getName()).thenReturn("Daily"); + handler.getRewards().add(root); + + SubDirectlyDefinedReward sub = mock(SubDirectlyDefinedReward.class); + when(sub.getFullPath()).thenReturn("Daily.Rewards"); + + RewardInject inject = mock(RewardInject.class); + when(inject.subRewards(any(DefinedReward.class))).thenAnswer(invocation -> { + DefinedReward defined = invocation.getArgument(0); + if (defined instanceof RewardFileDefinedReward) { + return new ArrayList<>(List.of(sub)); + } + return new ArrayList<>(); + }); + handler.getInjectedRewards().add(inject); + + handler.checkSubRewards(); + + assertNull(handler.getSubDirectlyDefined("Daily_Rewards")); + assertSame(sub, handler.getSubRewardResolver().getFileBackedSubReward("Daily_Rewards")); + } } From 58d83859be12f0f71bdee38218a0c6ac75e48565 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 20:04:11 -0600 Subject: [PATCH 11/71] Keep internal sub-reward lookup null-safe --- .../com/bencodez/advancedcore/api/rewards/RewardExecutor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java index b48e7e38ea..69e1d073e2 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java @@ -141,7 +141,8 @@ private void giveSectionReward(AdvancedCoreUser user, ConfigurationSection data, String rewardName = context.buildRewardName(path); DirectlyDefinedReward direct = handler.getDirectlyDefined(path); SubDirectlyDefinedReward sub = handler.getSubDirectlyDefined(rewardName); - SubDirectlyDefinedReward fileSub = handler.getSubRewardResolver().getFileBackedSubReward(rewardName); + SubRewardResolver resolver = handler.getSubRewardResolver(); + SubDirectlyDefinedReward fileSub = resolver == null ? null : resolver.getFileBackedSubReward(rewardName); if (context.supportsDirectDispatch() && (direct != null || sub != null || fileSub != null)) { if (direct != null) { From 9742af825b543fa767e7407901ae40eea36cec22 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 20:04:23 -0600 Subject: [PATCH 12/71] Fix PlayerManager security test isolation --- .../api/misc/PlayerManagerSecurityTest.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java index 5692a21934..3cf1b41e94 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/misc/PlayerManagerSecurityTest.java @@ -10,15 +10,20 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; -import org.mockito.Mockito; import com.bencodez.advancedcore.AdvancedCoreConfigOptions; import com.bencodez.advancedcore.AdvancedCorePlugin; class PlayerManagerSecurityTest { + @AfterEach + void tearDown() { + AdvancedCorePlugin.setInstance(null); + } + @Test void nameFallbackRequiresMatchingUuid() { UUID requestedUuid = UUID.randomUUID(); @@ -29,16 +34,12 @@ void nameFallbackRequiresMatchingUuid() { when(differentPlayer.getUniqueId()).thenReturn(UUID.randomUUID()); when(differentPlayer.hasPermission("reward.permission")).thenReturn(true); when(plugin.getOptions()).thenReturn(options); + AdvancedCorePlugin.setInstance(plugin); + PlayerManager.getInstance().plugin = plugin; - // Some earlier tests initialize AdvancedCore's static singleton mock. Clear any - // inline registration before installing the isolated static mock used here. - Mockito.framework().clearInlineMocks(); - try (MockedStatic bukkit = mockStatic(Bukkit.class); - MockedStatic advancedCore = mockStatic(AdvancedCorePlugin.class)) { + try (MockedStatic bukkit = mockStatic(Bukkit.class)) { bukkit.when(() -> Bukkit.getPlayer(requestedUuid)).thenReturn(null); bukkit.when(() -> Bukkit.getPlayer("stored-name")).thenReturn(differentPlayer); - advancedCore.when(AdvancedCorePlugin::getInstance).thenReturn(plugin); - PlayerManager.getInstance().plugin = plugin; assertFalse(PlayerManager.getInstance().hasServerPermission(requestedUuid, "stored-name", "reward.permission")); From c8bf25b5b63dc5e9f7ccc124645a4486b574b481 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 20:15:20 -0600 Subject: [PATCH 13/71] Quarantine stale generated sub-reward files --- .../api/rewards/RewardLoader.java | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java index 14ac22011e..1cad9dd173 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java @@ -3,6 +3,8 @@ import java.io.File; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.exceptions.FileDirectoryException; @@ -16,6 +18,7 @@ public class RewardLoader { private final RewardHandler handler; private final AdvancedCorePlugin plugin; private final ArrayList rewardFolders = new ArrayList<>(); + private final Set suppressedDirectlyDefinedRewards = new HashSet<>(); public RewardLoader(RewardHandler handler, AdvancedCorePlugin plugin) { this.handler = handler; @@ -52,10 +55,13 @@ public void addRewardFolder(File file, boolean load, boolean create) { } public void checkDirectlyDefined() { + suppressedDirectlyDefinedRewards.clear(); for (Reward rewardFile : handler.getRewards()) { File folder = rewardFile.getConfig().getRewardFolder(); if (folder != null && folder.getName().equalsIgnoreCase("DirectlyDefined")) { - if (handler.hasDirectRewardHandle(rewardFile.getName())) { + if (handler.getSubRewardResolver().getFileBackedSubReward(rewardFile.getName()) != null) { + suppressStaleFileBackedSubReward(rewardFile); + } else if (handler.hasDirectRewardHandle(rewardFile.getName())) { rewardFile.getFile().delete(); } } @@ -63,6 +69,36 @@ public void checkDirectlyDefined() { loadRewards(); } + private void suppressStaleFileBackedSubReward(Reward rewardFile) { + String normalized = RewardRegistry.normalizeDirectPath(rewardFile.getName()); + suppressedDirectlyDefinedRewards.add(normalized); + + File staleFile = rewardFile.getFile(); + if (staleFile == null || !staleFile.exists()) { + return; + } + + File disabledFile = nextDisabledFile(staleFile); + if (staleFile.renameTo(disabledFile)) { + plugin.getLogger().warning("Disabled stale generated sub-reward file " + staleFile.getName() + + " because its section is now dispatched only through its parent reward. Preserved as " + + disabledFile.getName()); + } else { + plugin.getLogger().warning("Failed to quarantine stale generated sub-reward file " + staleFile.getName() + + "; it will remain suppressed from standalone reward lookup for this runtime"); + } + } + + static File nextDisabledFile(File staleFile) { + File parent = staleFile.getParentFile(); + File disabled = new File(parent, staleFile.getName() + ".disabled"); + int suffix = 1; + while (disabled.exists()) { + disabled = new File(parent, staleFile.getName() + ".disabled." + suffix++); + } + return disabled; + } + private void copyFile(String fileName) { File file = new File(plugin.getDataFolder(), "Rewards" + File.separator + fileName); if (!file.exists()) { @@ -160,6 +196,11 @@ public void loadRewards() { private void loadRewards(File file) { for (String reward : getRewardNames(file)) { if (!reward.equals("")) { + if (file.getName().equalsIgnoreCase("DirectlyDefined") + && suppressedDirectlyDefinedRewards.contains(RewardRegistry.normalizeDirectPath(reward))) { + plugin.getLogger().warning("Suppressing stale generated sub-reward file from standalone lookup: " + reward); + continue; + } if (!handler.rewardExist(reward)) { try { Reward reward1 = new Reward(file, reward); From 53bfa8d9b76bfed291f86e2cfcec90f368c3030a Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 20:15:37 -0600 Subject: [PATCH 14/71] Test stale sub-reward quarantine --- .../api/rewards/RewardLoaderSecurityTest.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java new file mode 100644 index 0000000000..dfec068c38 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -0,0 +1,78 @@ +package com.bencodez.advancedcore.api.rewards; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.bencodez.advancedcore.AdvancedCoreConfigOptions; +import com.bencodez.advancedcore.AdvancedCorePlugin; + +class RewardLoaderSecurityTest { + + @TempDir + File tempDir; + + @AfterEach + void tearDown() { + AdvancedCorePlugin.setInstance(null); + } + + @Test + void quarantinesPreviouslyMaterializedFileBackedSubReward() throws IOException { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); + RewardHandler handler = mock(RewardHandler.class); + RewardRegistry registry = mock(RewardRegistry.class); + SubRewardResolver resolver = mock(SubRewardResolver.class); + Reward reward = mock(Reward.class); + RewardFileData config = mock(RewardFileData.class); + Logger logger = mock(Logger.class); + + File rewardsFolder = new File(tempDir, "Rewards"); + File directlyDefined = new File(rewardsFolder, "DirectlyDefined"); + assertTrue(directlyDefined.mkdirs()); + File stale = new File(directlyDefined, "Daily_Rewards.yml"); + assertTrue(stale.createNewFile()); + + when(plugin.getDataFolder()).thenReturn(tempDir); + when(plugin.getOptions()).thenReturn(options); + when(plugin.getLogger()).thenReturn(logger); + when(options.isLoadDefaultRewards()).thenReturn(false); + when(handler.getRewardRegistry()).thenReturn(registry); + when(handler.getSubRewardResolver()).thenReturn(resolver); + when(handler.getRewards()).thenReturn(new ArrayList<>(java.util.List.of(reward))); + when(reward.getConfig()).thenReturn(config); + when(config.getRewardFolder()).thenReturn(directlyDefined); + when(reward.getName()).thenReturn("Daily_Rewards"); + when(reward.getFile()).thenReturn(stale); + when(resolver.getFileBackedSubReward("Daily_Rewards")).thenReturn(mock(SubDirectlyDefinedReward.class)); + + AdvancedCorePlugin.setInstance(plugin); + RewardLoader loader = new RewardLoader(handler, plugin); + loader.checkDirectlyDefined(); + + assertFalse(stale.exists()); + assertTrue(new File(directlyDefined, "Daily_Rewards.yml.disabled").exists()); + } + + @Test + void choosesNonConflictingDisabledBackupName() throws IOException { + File stale = new File(tempDir, "Daily_Rewards.yml"); + assertTrue(stale.createNewFile()); + assertTrue(new File(tempDir, "Daily_Rewards.yml.disabled").createNewFile()); + + File disabled = RewardLoader.nextDisabledFile(stale); + + assertTrue(disabled.getName().equals("Daily_Rewards.yml.disabled.1")); + } +} From 072892c203823d67e2f0b6f60299cc4ca4cb9ccb Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 20:32:10 -0600 Subject: [PATCH 15/71] Quarantine generated direct rewards during loading --- .../api/rewards/RewardLoader.java | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java index 1cad9dd173..f25ed30407 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java @@ -6,6 +6,8 @@ import java.util.HashSet; import java.util.Set; +import org.bukkit.configuration.file.YamlConfiguration; + import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.exceptions.FileDirectoryException; @@ -55,13 +57,10 @@ public void addRewardFolder(File file, boolean load, boolean create) { } public void checkDirectlyDefined() { - suppressedDirectlyDefinedRewards.clear(); for (Reward rewardFile : handler.getRewards()) { File folder = rewardFile.getConfig().getRewardFolder(); if (folder != null && folder.getName().equalsIgnoreCase("DirectlyDefined")) { - if (handler.getSubRewardResolver().getFileBackedSubReward(rewardFile.getName()) != null) { - suppressStaleFileBackedSubReward(rewardFile); - } else if (handler.hasDirectRewardHandle(rewardFile.getName())) { + if (handler.hasDirectRewardHandle(rewardFile.getName())) { rewardFile.getFile().delete(); } } @@ -69,23 +68,39 @@ public void checkDirectlyDefined() { loadRewards(); } - private void suppressStaleFileBackedSubReward(Reward rewardFile) { - String normalized = RewardRegistry.normalizeDirectPath(rewardFile.getName()); - suppressedDirectlyDefinedRewards.add(normalized); + private void quarantineGeneratedDirectlyDefinedFiles() { + suppressedDirectlyDefinedRewards.clear(); + for (File folder : rewardFolders) { + if (folder == null || !folder.getName().equalsIgnoreCase("DirectlyDefined")) { + continue; + } + for (String fileName : getRewardFiles(folder)) { + File staleFile = new File(folder, fileName); + YamlConfiguration data = YamlConfiguration.loadConfiguration(staleFile); + if (!data.getBoolean("DirectlyDefinedReward", false)) { + continue; + } + + String rewardName = fileName.substring(0, fileName.length() - ".yml".length()); + suppressStaleGeneratedReward(staleFile, rewardName); + } + } + } - File staleFile = rewardFile.getFile(); + private void suppressStaleGeneratedReward(File staleFile, String rewardName) { + suppressedDirectlyDefinedRewards.add(RewardRegistry.normalizeDirectPath(rewardName)); if (staleFile == null || !staleFile.exists()) { return; } File disabledFile = nextDisabledFile(staleFile); if (staleFile.renameTo(disabledFile)) { - plugin.getLogger().warning("Disabled stale generated sub-reward file " + staleFile.getName() - + " because its section is now dispatched only through its parent reward. Preserved as " + plugin.getLogger().warning("Disabled stale generated directly-defined reward file " + staleFile.getName() + + " because generated reward files are no longer exposed as standalone rewards. Preserved as " + disabledFile.getName()); } else { - plugin.getLogger().warning("Failed to quarantine stale generated sub-reward file " + staleFile.getName() - + "; it will remain suppressed from standalone reward lookup for this runtime"); + plugin.getLogger().warning("Failed to quarantine stale generated directly-defined reward file " + + staleFile.getName() + "; it will remain suppressed from standalone reward lookup for this runtime"); } } @@ -178,6 +193,7 @@ public ArrayList getRewardNames(File file) { } public void loadRewards() { + quarantineGeneratedDirectlyDefinedFiles(); handler.getRewardRegistry().resetRewards(); setupExample(); handler.addValidPath("DirectlyDefinedReward"); @@ -198,7 +214,8 @@ private void loadRewards(File file) { if (!reward.equals("")) { if (file.getName().equalsIgnoreCase("DirectlyDefined") && suppressedDirectlyDefinedRewards.contains(RewardRegistry.normalizeDirectPath(reward))) { - plugin.getLogger().warning("Suppressing stale generated sub-reward file from standalone lookup: " + reward); + plugin.getLogger().warning("Suppressing stale generated directly-defined reward from standalone lookup: " + + reward); continue; } if (!handler.rewardExist(reward)) { From e082e366d67de249641893b4a4be97d2c7800ca1 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 23 Aug 2026 20:32:26 -0600 Subject: [PATCH 16/71] Test stale generated reward quarantine during load --- .../api/rewards/RewardLoaderSecurityTest.java | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index dfec068c38..2d247bb1c5 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.logging.Logger; +import org.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -28,41 +29,70 @@ void tearDown() { } @Test - void quarantinesPreviouslyMaterializedFileBackedSubReward() throws IOException { + void loadRewardsQuarantinesOrphanedGeneratedDirectReward() throws IOException { AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); RewardHandler handler = mock(RewardHandler.class); RewardRegistry registry = mock(RewardRegistry.class); - SubRewardResolver resolver = mock(SubRewardResolver.class); - Reward reward = mock(Reward.class); - RewardFileData config = mock(RewardFileData.class); Logger logger = mock(Logger.class); File rewardsFolder = new File(tempDir, "Rewards"); File directlyDefined = new File(rewardsFolder, "DirectlyDefined"); assertTrue(directlyDefined.mkdirs()); File stale = new File(directlyDefined, "Daily_Rewards.yml"); - assertTrue(stale.createNewFile()); + YamlConfiguration staleData = new YamlConfiguration(); + staleData.set("DirectlyDefinedReward", true); + staleData.set("Messages", java.util.List.of("legacy generated reward")); + staleData.save(stale); when(plugin.getDataFolder()).thenReturn(tempDir); when(plugin.getOptions()).thenReturn(options); when(plugin.getLogger()).thenReturn(logger); when(options.isLoadDefaultRewards()).thenReturn(false); when(handler.getRewardRegistry()).thenReturn(registry); - when(handler.getSubRewardResolver()).thenReturn(resolver); - when(handler.getRewards()).thenReturn(new ArrayList<>(java.util.List.of(reward))); - when(reward.getConfig()).thenReturn(config); - when(config.getRewardFolder()).thenReturn(directlyDefined); - when(reward.getName()).thenReturn("Daily_Rewards"); - when(reward.getFile()).thenReturn(stale); - when(resolver.getFileBackedSubReward("Daily_Rewards")).thenReturn(mock(SubDirectlyDefinedReward.class)); + when(handler.getRewards()).thenReturn(new ArrayList<>()); AdvancedCorePlugin.setInstance(plugin); RewardLoader loader = new RewardLoader(handler, plugin); - loader.checkDirectlyDefined(); + loader.addRewardFolder(directlyDefined, false, false); + loader.loadRewards(); assertFalse(stale.exists()); assertTrue(new File(directlyDefined, "Daily_Rewards.yml.disabled").exists()); + assertTrue(handler.getRewards().isEmpty()); + } + + @Test + void loadRewardsDoesNotQuarantineNormalDirectFolderFileWithoutGeneratedMarker() throws IOException { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); + RewardHandler handler = mock(RewardHandler.class); + RewardRegistry registry = mock(RewardRegistry.class); + Logger logger = mock(Logger.class); + ArrayList rewards = new ArrayList<>(); + + File directlyDefined = new File(tempDir, "Rewards/DirectlyDefined"); + assertTrue(directlyDefined.mkdirs()); + File manual = new File(directlyDefined, "Manual.yml"); + YamlConfiguration manualData = new YamlConfiguration(); + manualData.set("Messages", java.util.List.of("manual reward")); + manualData.save(manual); + + when(plugin.getDataFolder()).thenReturn(tempDir); + when(plugin.getOptions()).thenReturn(options); + when(plugin.getLogger()).thenReturn(logger); + when(options.isLoadDefaultRewards()).thenReturn(false); + when(handler.getRewardRegistry()).thenReturn(registry); + when(handler.getRewards()).thenReturn(rewards); + when(handler.rewardExist("Manual")).thenReturn(false); + + AdvancedCorePlugin.setInstance(plugin); + RewardLoader loader = new RewardLoader(handler, plugin); + loader.addRewardFolder(directlyDefined, false, false); + loader.loadRewards(); + + assertTrue(manual.exists()); + assertFalse(new File(directlyDefined, "Manual.yml.disabled").exists()); } @Test From 398dff777d48991e8a7ac8cf2f97d0fbaf639421 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:51:11 -0600 Subject: [PATCH 17/71] Fix AdvancedRewards internal dispatch naming --- .../advancedcore/api/rewards/builtin/RewardAdvancedRewards.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardAdvancedRewards.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardAdvancedRewards.java index 56199e4232..f3701c64ff 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardAdvancedRewards.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardAdvancedRewards.java @@ -37,7 +37,7 @@ public String onRewardRequested(Reward reward, AdvancedCoreUser user, Configurat ArrayList rewards = ArrayUtils.convert(keys); for (String rewardName : rewards) { handler.giveReward(user, section, rewardName, new RewardOptions().setPlaceholders(placeholders) - .setPrefix(reward.getRewardName() + "_AdvancedRewards_" + rewardName)); + .setPrefix(reward.getRewardName() + "_AdvancedRewards")); } return null; } From 28a334d4c47cc112fa7e7dbc2800c4634ca9a991 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:51:48 -0600 Subject: [PATCH 18/71] Restrict queued generated rewards to referenced users --- .../api/rewards/QueuedGeneratedReward.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java new file mode 100644 index 0000000000..f0620c0902 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java @@ -0,0 +1,37 @@ +package com.bencodez.advancedcore.api.rewards; + +import java.io.File; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import com.bencodez.advancedcore.api.user.AdvancedCoreUser; + +/** + * A generated directly-defined reward that is still referenced by a persisted + * offline or timed queue. These rewards are intentionally restricted to the + * users whose persisted queue references the generated reward name. + */ +final class QueuedGeneratedReward extends Reward { + + private final Set allowedUserUuids; + + QueuedGeneratedReward(File folder, String reward, Set allowedUserUuids) { + super(folder, reward); + this.allowedUserUuids = Collections.unmodifiableSet(new HashSet<>(allowedUserUuids)); + } + + @Override + public void giveReward(AdvancedCoreUser user, RewardOptions rewardOptions) { + if (user == null || user.getUUID() == null || !allowedUserUuids.contains(user.getUUID())) { + plugin.getLogger().warning("Blocked generated queued reward " + getRewardName() + + " for a user without a matching persisted queue entry"); + return; + } + super.giveReward(user, rewardOptions); + } + + Set getAllowedUserUuids() { + return allowedUserUuids; + } +} From 0c0803076febed70b7960ee406e4ea87ca8bb964 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:52:14 -0600 Subject: [PATCH 19/71] Preserve queued generated rewards without public bypass --- .../api/rewards/RewardLoader.java | 70 ++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java index f25ed30407..ac894a04bd 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java @@ -3,13 +3,18 @@ import java.io.File; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.UUID; import org.bukkit.configuration.file.YamlConfiguration; import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.exceptions.FileDirectoryException; +import com.bencodez.advancedcore.api.user.AdvancedCoreUser; +import com.bencodez.advancedcore.api.user.UserManager; /** * Owns reward folder discovery and reward-file loading. @@ -21,6 +26,7 @@ public class RewardLoader { private final AdvancedCorePlugin plugin; private final ArrayList rewardFolders = new ArrayList<>(); private final Set suppressedDirectlyDefinedRewards = new HashSet<>(); + private final Map> queuedGeneratedRewardUsers = new HashMap<>(); public RewardLoader(RewardHandler handler, AdvancedCorePlugin plugin) { this.handler = handler; @@ -70,6 +76,9 @@ public void checkDirectlyDefined() { private void quarantineGeneratedDirectlyDefinedFiles() { suppressedDirectlyDefinedRewards.clear(); + queuedGeneratedRewardUsers.clear(); + collectQueuedGeneratedRewardUsers(); + for (File folder : rewardFolders) { if (folder == null || !folder.getName().equalsIgnoreCase("DirectlyDefined")) { continue; @@ -82,11 +91,53 @@ private void quarantineGeneratedDirectlyDefinedFiles() { } String rewardName = fileName.substring(0, fileName.length() - ".yml".length()); + String normalized = RewardRegistry.normalizeDirectPath(rewardName); + if (queuedGeneratedRewardUsers.containsKey(normalized)) { + plugin.extraDebug("Preserving generated reward file " + fileName + + " because it is still referenced by a persisted offline/timed reward queue"); + continue; + } + suppressStaleGeneratedReward(staleFile, rewardName); } } } + private void collectQueuedGeneratedRewardUsers() { + UserManager userManager = plugin.getUserManager(); + if (userManager == null) { + return; + } + + for (String uuidText : userManager.getAllUUIDs()) { + try { + UUID uuid = UUID.fromString(uuidText); + AdvancedCoreUser user = userManager.getUser(uuid); + if (user == null) { + continue; + } + for (String rewardEntry : user.getOfflineRewards()) { + addQueuedRewardReference(rewardEntry, uuidText); + } + for (String rewardEntry : user.getTimedRewards().keySet()) { + addQueuedRewardReference(rewardEntry, uuidText); + } + } catch (Exception e) { + plugin.debug("Failed to inspect queued generated rewards for user " + uuidText + ": " + e.getMessage()); + } + } + } + + private void addQueuedRewardReference(String rewardEntry, String uuid) { + if (rewardEntry == null || rewardEntry.isEmpty()) { + return; + } + String rewardName = rewardEntry.split("%placeholders%", 2)[0]; + rewardName = rewardName.split("%extime%", 2)[0]; + String normalized = RewardRegistry.normalizeDirectPath(rewardName); + queuedGeneratedRewardUsers.computeIfAbsent(normalized, ignored -> new HashSet<>()).add(uuid); + } + private void suppressStaleGeneratedReward(File staleFile, String rewardName) { suppressedDirectlyDefinedRewards.add(RewardRegistry.normalizeDirectPath(rewardName)); if (staleFile == null || !staleFile.exists()) { @@ -96,7 +147,7 @@ private void suppressStaleGeneratedReward(File staleFile, String rewardName) { File disabledFile = nextDisabledFile(staleFile); if (staleFile.renameTo(disabledFile)) { plugin.getLogger().warning("Disabled stale generated directly-defined reward file " + staleFile.getName() - + " because generated reward files are no longer exposed as standalone rewards. Preserved as " + + " because it is no longer referenced by a persisted queue and generated reward files are not standalone rewards. Preserved as " + disabledFile.getName()); } else { plugin.getLogger().warning("Failed to quarantine stale generated directly-defined reward file " @@ -160,6 +211,10 @@ public Reward getRewardDirectlyDefined(String reward) { File directFolder = new File(getDefaultFolder().getAbsolutePath() + File.separator + "DirectlyDefined"); directFolder.mkdirs(); + Set allowedUsers = queuedGeneratedRewardUsers.get(RewardRegistry.normalizeDirectPath(reward)); + if (allowedUsers != null) { + return new QueuedGeneratedReward(directFolder, reward, allowedUsers); + } return new Reward(directFolder, reward); } @@ -212,15 +267,24 @@ public void loadRewards() { private void loadRewards(File file) { for (String reward : getRewardNames(file)) { if (!reward.equals("")) { + String normalized = RewardRegistry.normalizeDirectPath(reward); if (file.getName().equalsIgnoreCase("DirectlyDefined") - && suppressedDirectlyDefinedRewards.contains(RewardRegistry.normalizeDirectPath(reward))) { + && suppressedDirectlyDefinedRewards.contains(normalized)) { plugin.getLogger().warning("Suppressing stale generated directly-defined reward from standalone lookup: " + reward); continue; } if (!handler.rewardExist(reward)) { try { - Reward reward1 = new Reward(file, reward); + Reward reward1; + Set allowedUsers = file.getName().equalsIgnoreCase("DirectlyDefined") + ? queuedGeneratedRewardUsers.get(normalized) + : null; + if (allowedUsers != null) { + reward1 = new QueuedGeneratedReward(file, reward, allowedUsers); + } else { + reward1 = new Reward(file, reward); + } reward1.validate(); if (!reward1.getConfig().isDirectlyDefinedReward() || file.getName().equalsIgnoreCase("DirectlyDefined")) { From e2ea1b644624cc05645d1ea02f0ae5d6915c8254 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:52:36 -0600 Subject: [PATCH 20/71] Test queued generated reward preservation --- .../api/rewards/RewardLoaderSecurityTest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index 2d247bb1c5..e775401688 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -1,5 +1,6 @@ package com.bencodez.advancedcore.api.rewards; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @@ -8,6 +9,9 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; import java.util.logging.Logger; import org.bukkit.configuration.file.YamlConfiguration; @@ -17,6 +21,8 @@ import com.bencodez.advancedcore.AdvancedCoreConfigOptions; import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.user.AdvancedCoreUser; +import com.bencodez.advancedcore.api.user.UserManager; class RewardLoaderSecurityTest { @@ -62,6 +68,53 @@ void loadRewardsQuarantinesOrphanedGeneratedDirectReward() throws IOException { assertTrue(handler.getRewards().isEmpty()); } + @Test + void loadRewardsPreservesQueuedGeneratedRewardAndRestrictsItToQueuedUser() throws IOException { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); + RewardHandler handler = mock(RewardHandler.class); + RewardRegistry registry = mock(RewardRegistry.class); + UserManager userManager = mock(UserManager.class); + AdvancedCoreUser queuedUser = mock(AdvancedCoreUser.class); + Logger logger = mock(Logger.class); + ArrayList rewards = new ArrayList<>(); + UUID queuedUuid = UUID.randomUUID(); + + File directlyDefined = new File(tempDir, "Rewards/DirectlyDefined"); + assertTrue(directlyDefined.mkdirs()); + File queued = new File(directlyDefined, "QueuedReward.yml"); + YamlConfiguration queuedData = new YamlConfiguration(); + queuedData.set("DirectlyDefinedReward", true); + queuedData.set("Messages", List.of("queued reward")); + queuedData.save(queued); + + when(plugin.getDataFolder()).thenReturn(tempDir); + when(plugin.getOptions()).thenReturn(options); + when(plugin.getLogger()).thenReturn(logger); + when(plugin.getUserManager()).thenReturn(userManager); + when(options.isLoadDefaultRewards()).thenReturn(false); + when(handler.getRewardRegistry()).thenReturn(registry); + when(handler.getRewards()).thenReturn(rewards); + when(handler.rewardExist("QueuedReward")).thenReturn(false); + when(userManager.getAllUUIDs()).thenReturn(new ArrayList<>(List.of(queuedUuid.toString()))); + when(userManager.getUser(queuedUuid)).thenReturn(queuedUser); + when(queuedUser.getOfflineRewards()) + .thenReturn(new ArrayList<>(List.of("QueuedReward%placeholders%player=Ben"))); + when(queuedUser.getTimedRewards()).thenReturn(new HashMap<>()); + + AdvancedCorePlugin.setInstance(plugin); + RewardLoader loader = new RewardLoader(handler, plugin); + loader.addRewardFolder(directlyDefined, false, false); + loader.loadRewards(); + + assertTrue(queued.exists()); + assertFalse(new File(directlyDefined, "QueuedReward.yml.disabled").exists()); + assertEquals(1, rewards.size()); + assertTrue(rewards.get(0) instanceof QueuedGeneratedReward); + assertEquals(java.util.Set.of(queuedUuid.toString()), + ((QueuedGeneratedReward) rewards.get(0)).getAllowedUserUuids()); + } + @Test void loadRewardsDoesNotQuarantineNormalDirectFolderFileWithoutGeneratedMarker() throws IOException { AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); From 5f87d56df1b80616d02ad71a8bb50d0096876839 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:52:55 -0600 Subject: [PATCH 21/71] Test AdvancedRewards dispatch naming --- .../AdvancedRewardsDispatchSecurityTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/AdvancedRewardsDispatchSecurityTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/AdvancedRewardsDispatchSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/AdvancedRewardsDispatchSecurityTest.java new file mode 100644 index 0000000000..2a669daad0 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/AdvancedRewardsDispatchSecurityTest.java @@ -0,0 +1,55 @@ +package com.bencodez.advancedcore.api.rewards; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import java.util.HashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Test; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedConstruction; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.item.ItemBuilder; +import com.bencodez.advancedcore.api.rewards.builtin.RewardAdvancedRewards; +import com.bencodez.advancedcore.api.rewards.injected.RewardInject; +import com.bencodez.advancedcore.api.rewards.injected.RewardInjectConfigurationSection; +import com.bencodez.advancedcore.api.user.AdvancedCoreUser; + +class AdvancedRewardsDispatchSecurityTest { + + @Test + void childDispatchPrefixDoesNotDuplicateChildName() { + RewardHandler handler = mock(RewardHandler.class); + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + AdvancedCoreUser user = mock(AdvancedCoreUser.class); + Reward reward = mock(Reward.class); + CopyOnWriteArrayList injects = new CopyOnWriteArrayList<>(); + when(handler.getInjectedRewards()).thenReturn(injects); + when(reward.getRewardName()).thenReturn("Daily"); + + try (MockedConstruction ignored = mockConstruction(ItemBuilder.class, + withSettings().defaultAnswer(Answers.RETURNS_SELF))) { + RewardAdvancedRewards.register(handler, plugin); + } + + YamlConfiguration data = new YamlConfiguration(); + ConfigurationSection section = data.createSection("AdvancedRewards"); + section.createSection("foo"); + + ((RewardInjectConfigurationSection) injects.get(0)).onRewardRequested(reward, user, section, new HashMap<>()); + + ArgumentCaptor options = ArgumentCaptor.forClass(RewardOptions.class); + verify(handler).giveReward(eq(user), eq(section), eq("foo"), options.capture()); + assertEquals("Daily_AdvancedRewards", options.getValue().getPrefix()); + } +} From 2a954a31b4ecb5b2937be39d9e9184ef84826b1a Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:54:44 -0600 Subject: [PATCH 22/71] Fix queued reward security test fixture --- .../api/rewards/RewardLoaderSecurityTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index e775401688..35fc51a458 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -12,6 +12,7 @@ import java.util.HashMap; import java.util.List; import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; import org.bukkit.configuration.file.YamlConfiguration; @@ -21,6 +22,8 @@ import com.bencodez.advancedcore.AdvancedCoreConfigOptions; import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.rewards.injected.RewardInject; +import com.bencodez.advancedcore.api.rewards.injectedrequirement.RequirementInject; import com.bencodez.advancedcore.api.user.AdvancedCoreUser; import com.bencodez.advancedcore.api.user.UserManager; @@ -92,9 +95,12 @@ void loadRewardsPreservesQueuedGeneratedRewardAndRestrictsItToQueuedUser() throw when(plugin.getOptions()).thenReturn(options); when(plugin.getLogger()).thenReturn(logger); when(plugin.getUserManager()).thenReturn(userManager); + when(plugin.getRewardHandler()).thenReturn(handler); when(options.isLoadDefaultRewards()).thenReturn(false); when(handler.getRewardRegistry()).thenReturn(registry); when(handler.getRewards()).thenReturn(rewards); + when(handler.getInjectedRequirements()).thenReturn(new CopyOnWriteArrayList()); + when(handler.getInjectedRewards()).thenReturn(new CopyOnWriteArrayList()); when(handler.rewardExist("QueuedReward")).thenReturn(false); when(userManager.getAllUUIDs()).thenReturn(new ArrayList<>(List.of(queuedUuid.toString()))); when(userManager.getUser(queuedUuid)).thenReturn(queuedUser); @@ -134,9 +140,12 @@ void loadRewardsDoesNotQuarantineNormalDirectFolderFileWithoutGeneratedMarker() when(plugin.getDataFolder()).thenReturn(tempDir); when(plugin.getOptions()).thenReturn(options); when(plugin.getLogger()).thenReturn(logger); + when(plugin.getRewardHandler()).thenReturn(handler); when(options.isLoadDefaultRewards()).thenReturn(false); when(handler.getRewardRegistry()).thenReturn(registry); when(handler.getRewards()).thenReturn(rewards); + when(handler.getInjectedRequirements()).thenReturn(new CopyOnWriteArrayList()); + when(handler.getInjectedRewards()).thenReturn(new CopyOnWriteArrayList()); when(handler.rewardExist("Manual")).thenReturn(false); AdvancedCorePlugin.setInstance(plugin); From cf64719cf687747de5926b12cd65fe134af09bf3 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:56:11 -0600 Subject: [PATCH 23/71] Avoid revalidating queued reward snapshots --- .../advancedcore/api/rewards/QueuedGeneratedReward.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java index f0620c0902..86b2f4c727 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java @@ -31,6 +31,14 @@ public void giveReward(AdvancedCoreUser user, RewardOptions rewardOptions) { super.giveReward(user, rewardOptions); } + @Override + public void validate() { + // This file is a persisted snapshot created from a reward that was already + // validated before it was queued. Revalidating requires the live injected + // registries and adds no security boundary; execution is instead restricted + // to UUIDs with a matching persisted queue reference. + } + Set getAllowedUserUuids() { return allowedUserUuids; } From fa1cf0f07b3bc0dc5436f7aa7c569ab70e8a9027 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 16:56:36 -0600 Subject: [PATCH 24/71] Remove plugin singleton dependency from loader tests --- .../api/rewards/RewardLoaderSecurityTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index 35fc51a458..bc3c662942 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -132,7 +132,7 @@ void loadRewardsDoesNotQuarantineNormalDirectFolderFileWithoutGeneratedMarker() File directlyDefined = new File(tempDir, "Rewards/DirectlyDefined"); assertTrue(directlyDefined.mkdirs()); - File manual = new File(directlyDefined, "Manual.yml"); + File manual = new File(directlyDefined, "ExampleBasic.yml"); YamlConfiguration manualData = new YamlConfiguration(); manualData.set("Messages", java.util.List.of("manual reward")); manualData.save(manual); @@ -140,13 +140,10 @@ void loadRewardsDoesNotQuarantineNormalDirectFolderFileWithoutGeneratedMarker() when(plugin.getDataFolder()).thenReturn(tempDir); when(plugin.getOptions()).thenReturn(options); when(plugin.getLogger()).thenReturn(logger); - when(plugin.getRewardHandler()).thenReturn(handler); when(options.isLoadDefaultRewards()).thenReturn(false); when(handler.getRewardRegistry()).thenReturn(registry); when(handler.getRewards()).thenReturn(rewards); - when(handler.getInjectedRequirements()).thenReturn(new CopyOnWriteArrayList()); - when(handler.getInjectedRewards()).thenReturn(new CopyOnWriteArrayList()); - when(handler.rewardExist("Manual")).thenReturn(false); + when(handler.rewardExist("ExampleBasic")).thenReturn(false); AdvancedCorePlugin.setInstance(plugin); RewardLoader loader = new RewardLoader(handler, plugin); @@ -154,7 +151,8 @@ void loadRewardsDoesNotQuarantineNormalDirectFolderFileWithoutGeneratedMarker() loader.loadRewards(); assertTrue(manual.exists()); - assertFalse(new File(directlyDefined, "Manual.yml.disabled").exists()); + assertFalse(new File(directlyDefined, "ExampleBasic.yml.disabled").exists()); + assertEquals(1, rewards.size()); } @Test From 1e7cb90c027e13a5c6354162e12a2dc4bdf6964e Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:23:54 -0600 Subject: [PATCH 25/71] Keep generated rewards queue-only without user scans --- .../api/rewards/RewardLoader.java | 143 +++++------------- 1 file changed, 42 insertions(+), 101 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java index ac894a04bd..20a782b8e2 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java @@ -3,18 +3,13 @@ import java.io.File; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Set; -import java.util.UUID; import org.bukkit.configuration.file.YamlConfiguration; import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.exceptions.FileDirectoryException; -import com.bencodez.advancedcore.api.user.AdvancedCoreUser; -import com.bencodez.advancedcore.api.user.UserManager; /** * Owns reward folder discovery and reward-file loading. @@ -26,7 +21,6 @@ public class RewardLoader { private final AdvancedCorePlugin plugin; private final ArrayList rewardFolders = new ArrayList<>(); private final Set suppressedDirectlyDefinedRewards = new HashSet<>(); - private final Map> queuedGeneratedRewardUsers = new HashMap<>(); public RewardLoader(RewardHandler handler, AdvancedCorePlugin plugin) { this.handler = handler; @@ -74,97 +68,29 @@ public void checkDirectlyDefined() { loadRewards(); } - private void quarantineGeneratedDirectlyDefinedFiles() { + /** + * Generated DirectlyDefinedReward files are persisted execution snapshots for + * delayed/offline queues. They must remain on disk, but they are never loaded + * into the public reward registry. Queue replay resolves them explicitly via + * getQueuedGeneratedReward(). + */ + private void suppressGeneratedDirectlyDefinedFiles() { suppressedDirectlyDefinedRewards.clear(); - queuedGeneratedRewardUsers.clear(); - collectQueuedGeneratedRewardUsers(); - for (File folder : rewardFolders) { if (folder == null || !folder.getName().equalsIgnoreCase("DirectlyDefined")) { continue; } for (String fileName : getRewardFiles(folder)) { - File staleFile = new File(folder, fileName); - YamlConfiguration data = YamlConfiguration.loadConfiguration(staleFile); - if (!data.getBoolean("DirectlyDefinedReward", false)) { - continue; - } - - String rewardName = fileName.substring(0, fileName.length() - ".yml".length()); - String normalized = RewardRegistry.normalizeDirectPath(rewardName); - if (queuedGeneratedRewardUsers.containsKey(normalized)) { - plugin.extraDebug("Preserving generated reward file " + fileName - + " because it is still referenced by a persisted offline/timed reward queue"); - continue; - } - - suppressStaleGeneratedReward(staleFile, rewardName); - } - } - } - - private void collectQueuedGeneratedRewardUsers() { - UserManager userManager = plugin.getUserManager(); - if (userManager == null) { - return; - } - - for (String uuidText : userManager.getAllUUIDs()) { - try { - UUID uuid = UUID.fromString(uuidText); - AdvancedCoreUser user = userManager.getUser(uuid); - if (user == null) { - continue; - } - for (String rewardEntry : user.getOfflineRewards()) { - addQueuedRewardReference(rewardEntry, uuidText); + File generatedFile = new File(folder, fileName); + YamlConfiguration data = YamlConfiguration.loadConfiguration(generatedFile); + if (data.getBoolean("DirectlyDefinedReward", false)) { + String rewardName = fileName.substring(0, fileName.length() - ".yml".length()); + suppressedDirectlyDefinedRewards.add(RewardRegistry.normalizeDirectPath(rewardName)); } - for (String rewardEntry : user.getTimedRewards().keySet()) { - addQueuedRewardReference(rewardEntry, uuidText); - } - } catch (Exception e) { - plugin.debug("Failed to inspect queued generated rewards for user " + uuidText + ": " + e.getMessage()); } } } - private void addQueuedRewardReference(String rewardEntry, String uuid) { - if (rewardEntry == null || rewardEntry.isEmpty()) { - return; - } - String rewardName = rewardEntry.split("%placeholders%", 2)[0]; - rewardName = rewardName.split("%extime%", 2)[0]; - String normalized = RewardRegistry.normalizeDirectPath(rewardName); - queuedGeneratedRewardUsers.computeIfAbsent(normalized, ignored -> new HashSet<>()).add(uuid); - } - - private void suppressStaleGeneratedReward(File staleFile, String rewardName) { - suppressedDirectlyDefinedRewards.add(RewardRegistry.normalizeDirectPath(rewardName)); - if (staleFile == null || !staleFile.exists()) { - return; - } - - File disabledFile = nextDisabledFile(staleFile); - if (staleFile.renameTo(disabledFile)) { - plugin.getLogger().warning("Disabled stale generated directly-defined reward file " + staleFile.getName() - + " because it is no longer referenced by a persisted queue and generated reward files are not standalone rewards. Preserved as " - + disabledFile.getName()); - } else { - plugin.getLogger().warning("Failed to quarantine stale generated directly-defined reward file " - + staleFile.getName() + "; it will remain suppressed from standalone reward lookup for this runtime"); - } - } - - static File nextDisabledFile(File staleFile) { - File parent = staleFile.getParentFile(); - File disabled = new File(parent, staleFile.getName() + ".disabled"); - int suffix = 1; - while (disabled.exists()) { - disabled = new File(parent, staleFile.getName() + ".disabled." + suffix++); - } - return disabled; - } - private void copyFile(String fileName) { File file = new File(plugin.getDataFolder(), "Rewards" + File.separator + fileName); if (!file.exists()) { @@ -211,13 +137,37 @@ public Reward getRewardDirectlyDefined(String reward) { File directFolder = new File(getDefaultFolder().getAbsolutePath() + File.separator + "DirectlyDefined"); directFolder.mkdirs(); - Set allowedUsers = queuedGeneratedRewardUsers.get(RewardRegistry.normalizeDirectPath(reward)); - if (allowedUsers != null) { - return new QueuedGeneratedReward(directFolder, reward, allowedUsers); + File existing = new File(directFolder, reward + ".yml"); + if (isGeneratedSnapshot(existing)) { + plugin.getLogger().warning("Blocked standalone lookup of generated queued reward " + reward); + return new QueuedGeneratedReward(directFolder, reward, Collections.emptySet()); } return new Reward(directFolder, reward); } + public Reward getQueuedGeneratedReward(String reward, String userUuid) { + if (reward == null || reward.isEmpty() || userUuid == null || userUuid.isEmpty()) { + return null; + } + String normalized = RewardRegistry.normalizeDirectPath(reward); + if (normalized.isEmpty() || !normalized.equals(reward.replace(" ", "_"))) { + return null; + } + File directFolder = new File(getDefaultFolder().getAbsolutePath() + File.separator + "DirectlyDefined"); + File file = new File(directFolder, reward + ".yml"); + if (!isGeneratedSnapshot(file)) { + return null; + } + return new QueuedGeneratedReward(directFolder, reward, Set.of(userUuid)); + } + + private boolean isGeneratedSnapshot(File file) { + if (file == null || !file.isFile()) { + return false; + } + return YamlConfiguration.loadConfiguration(file).getBoolean("DirectlyDefinedReward", false); + } + public ArrayList getRewardFiles(File folder) { ArrayList fileNames = new ArrayList<>(); if (folder != null && folder.exists()) { @@ -248,7 +198,7 @@ public ArrayList getRewardNames(File file) { } public void loadRewards() { - quarantineGeneratedDirectlyDefinedFiles(); + suppressGeneratedDirectlyDefinedFiles(); handler.getRewardRegistry().resetRewards(); setupExample(); handler.addValidPath("DirectlyDefinedReward"); @@ -270,21 +220,12 @@ private void loadRewards(File file) { String normalized = RewardRegistry.normalizeDirectPath(reward); if (file.getName().equalsIgnoreCase("DirectlyDefined") && suppressedDirectlyDefinedRewards.contains(normalized)) { - plugin.getLogger().warning("Suppressing stale generated directly-defined reward from standalone lookup: " - + reward); + plugin.extraDebug("Suppressing generated queued reward from standalone lookup: " + reward); continue; } if (!handler.rewardExist(reward)) { try { - Reward reward1; - Set allowedUsers = file.getName().equalsIgnoreCase("DirectlyDefined") - ? queuedGeneratedRewardUsers.get(normalized) - : null; - if (allowedUsers != null) { - reward1 = new QueuedGeneratedReward(file, reward, allowedUsers); - } else { - reward1 = new Reward(file, reward); - } + Reward reward1 = new Reward(file, reward); reward1.validate(); if (!reward1.getConfig().isDirectlyDefinedReward() || file.getName().equalsIgnoreCase("DirectlyDefined")) { From 1c7d9362030fab54e0e6e260ddf85088b0c00e10 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:24:20 -0600 Subject: [PATCH 26/71] Expose queue-only generated reward resolver --- .../com/bencodez/advancedcore/api/rewards/RewardHandler.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java index 137c224a26..fdd4934be8 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java @@ -143,6 +143,10 @@ public Reward getRewardDirectlyDefined(String reward) { return rewardLoader.getRewardDirectlyDefined(reward); } + public Reward getQueuedGeneratedReward(String reward, String userUuid) { + return rewardLoader.getQueuedGeneratedReward(reward, userUuid); + } + public ArrayList getRewardFiles(File folder) { return rewardLoader.getRewardFiles(folder); } From cf7d75b9dabcec38cbd43c6a1206a0f0b96ebc7c Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:25:17 -0600 Subject: [PATCH 27/71] Resolve generated snapshots only during queue replay --- .../api/rewards/RewardExecutor.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java index 69e1d073e2..55899dd926 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java @@ -122,7 +122,22 @@ public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewar MiscUtils.getInstance().executeConsoleCommands(user.getPlayerName(), reward, context.getPlaceholders()); return; } - giveReward(user, handler.getReward(reward), context.getOptions()); + + Reward resolved = handler.getReward(reward); + if (resolved == null && isPersistedQueueReplay(context.getOptions())) { + resolved = handler.getQueuedGeneratedReward(reward, user.getUUID()); + } + giveReward(user, resolved, context.getOptions()); + } + + private boolean isPersistedQueueReplay(RewardOptions options) { + if (options == null || options.isCheckTimed()) { + return false; + } + if (options.isOnlineSet() && !options.isOnline()) { + return true; + } + return options.getPlaceholders().containsKey("date"); } public void updateReward(Configuration data, String path, RewardOptions rewardOptions) { From ed271155fe7326daf71d9af8480ca343b02cf89c Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:25:37 -0600 Subject: [PATCH 28/71] Test queue-only generated reward snapshots --- .../api/rewards/RewardLoaderSecurityTest.java | 112 ++++++++---------- 1 file changed, 50 insertions(+), 62 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index bc3c662942..a18103a2b4 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -9,10 +9,8 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.UUID; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; import org.bukkit.configuration.file.YamlConfiguration; @@ -22,10 +20,6 @@ import com.bencodez.advancedcore.AdvancedCoreConfigOptions; import com.bencodez.advancedcore.AdvancedCorePlugin; -import com.bencodez.advancedcore.api.rewards.injected.RewardInject; -import com.bencodez.advancedcore.api.rewards.injectedrequirement.RequirementInject; -import com.bencodez.advancedcore.api.user.AdvancedCoreUser; -import com.bencodez.advancedcore.api.user.UserManager; class RewardLoaderSecurityTest { @@ -38,91 +32,97 @@ void tearDown() { } @Test - void loadRewardsQuarantinesOrphanedGeneratedDirectReward() throws IOException { + void loadRewardsSuppressesGeneratedSnapshotsWithoutDeletingThem() throws IOException { AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); RewardHandler handler = mock(RewardHandler.class); RewardRegistry registry = mock(RewardRegistry.class); Logger logger = mock(Logger.class); + ArrayList rewards = new ArrayList<>(); - File rewardsFolder = new File(tempDir, "Rewards"); - File directlyDefined = new File(rewardsFolder, "DirectlyDefined"); + File directlyDefined = new File(tempDir, "Rewards/DirectlyDefined"); assertTrue(directlyDefined.mkdirs()); - File stale = new File(directlyDefined, "Daily_Rewards.yml"); - YamlConfiguration staleData = new YamlConfiguration(); - staleData.set("DirectlyDefinedReward", true); - staleData.set("Messages", java.util.List.of("legacy generated reward")); - staleData.save(stale); + File generated = new File(directlyDefined, "Daily_Rewards.yml"); + YamlConfiguration data = new YamlConfiguration(); + data.set("DirectlyDefinedReward", true); + data.set("Messages", List.of("generated reward")); + data.save(generated); when(plugin.getDataFolder()).thenReturn(tempDir); when(plugin.getOptions()).thenReturn(options); when(plugin.getLogger()).thenReturn(logger); when(options.isLoadDefaultRewards()).thenReturn(false); when(handler.getRewardRegistry()).thenReturn(registry); - when(handler.getRewards()).thenReturn(new ArrayList<>()); + when(handler.getRewards()).thenReturn(rewards); AdvancedCorePlugin.setInstance(plugin); RewardLoader loader = new RewardLoader(handler, plugin); loader.addRewardFolder(directlyDefined, false, false); loader.loadRewards(); - assertFalse(stale.exists()); - assertTrue(new File(directlyDefined, "Daily_Rewards.yml.disabled").exists()); - assertTrue(handler.getRewards().isEmpty()); + assertTrue(generated.exists()); + assertFalse(new File(directlyDefined, "Daily_Rewards.yml.disabled").exists()); + assertTrue(rewards.isEmpty()); } @Test - void loadRewardsPreservesQueuedGeneratedRewardAndRestrictsItToQueuedUser() throws IOException { + void standaloneDirectLookupOfGeneratedSnapshotIsBlocked() throws IOException { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); + RewardHandler handler = mock(RewardHandler.class); + Logger logger = mock(Logger.class); + + File directlyDefined = new File(tempDir, "Rewards/DirectlyDefined"); + assertTrue(directlyDefined.mkdirs()); + YamlConfiguration data = new YamlConfiguration(); + data.set("DirectlyDefinedReward", true); + data.set("Messages", List.of("generated reward")); + data.save(new File(directlyDefined, "QueuedReward.yml")); + + when(plugin.getDataFolder()).thenReturn(tempDir); + when(plugin.getOptions()).thenReturn(options); + when(plugin.getLogger()).thenReturn(logger); + when(handler.getRewards()).thenReturn(new ArrayList<>()); + + AdvancedCorePlugin.setInstance(plugin); + RewardLoader loader = new RewardLoader(handler, plugin); + Reward reward = loader.getRewardDirectlyDefined("QueuedReward"); + + assertTrue(reward instanceof QueuedGeneratedReward); + assertTrue(((QueuedGeneratedReward) reward).getAllowedUserUuids().isEmpty()); + } + + @Test + void queueOnlyResolverBindsGeneratedSnapshotToRequestedUser() throws IOException { AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); RewardHandler handler = mock(RewardHandler.class); - RewardRegistry registry = mock(RewardRegistry.class); - UserManager userManager = mock(UserManager.class); - AdvancedCoreUser queuedUser = mock(AdvancedCoreUser.class); Logger logger = mock(Logger.class); - ArrayList rewards = new ArrayList<>(); UUID queuedUuid = UUID.randomUUID(); File directlyDefined = new File(tempDir, "Rewards/DirectlyDefined"); assertTrue(directlyDefined.mkdirs()); - File queued = new File(directlyDefined, "QueuedReward.yml"); - YamlConfiguration queuedData = new YamlConfiguration(); - queuedData.set("DirectlyDefinedReward", true); - queuedData.set("Messages", List.of("queued reward")); - queuedData.save(queued); + YamlConfiguration data = new YamlConfiguration(); + data.set("DirectlyDefinedReward", true); + data.set("Messages", List.of("queued reward")); + data.save(new File(directlyDefined, "QueuedReward.yml")); when(plugin.getDataFolder()).thenReturn(tempDir); when(plugin.getOptions()).thenReturn(options); when(plugin.getLogger()).thenReturn(logger); - when(plugin.getUserManager()).thenReturn(userManager); - when(plugin.getRewardHandler()).thenReturn(handler); - when(options.isLoadDefaultRewards()).thenReturn(false); - when(handler.getRewardRegistry()).thenReturn(registry); - when(handler.getRewards()).thenReturn(rewards); - when(handler.getInjectedRequirements()).thenReturn(new CopyOnWriteArrayList()); - when(handler.getInjectedRewards()).thenReturn(new CopyOnWriteArrayList()); - when(handler.rewardExist("QueuedReward")).thenReturn(false); - when(userManager.getAllUUIDs()).thenReturn(new ArrayList<>(List.of(queuedUuid.toString()))); - when(userManager.getUser(queuedUuid)).thenReturn(queuedUser); - when(queuedUser.getOfflineRewards()) - .thenReturn(new ArrayList<>(List.of("QueuedReward%placeholders%player=Ben"))); - when(queuedUser.getTimedRewards()).thenReturn(new HashMap<>()); + when(handler.getRewards()).thenReturn(new ArrayList<>()); AdvancedCorePlugin.setInstance(plugin); RewardLoader loader = new RewardLoader(handler, plugin); - loader.addRewardFolder(directlyDefined, false, false); - loader.loadRewards(); + Reward reward = loader.getQueuedGeneratedReward("QueuedReward", queuedUuid.toString()); - assertTrue(queued.exists()); - assertFalse(new File(directlyDefined, "QueuedReward.yml.disabled").exists()); - assertEquals(1, rewards.size()); - assertTrue(rewards.get(0) instanceof QueuedGeneratedReward); + assertTrue(reward instanceof QueuedGeneratedReward); assertEquals(java.util.Set.of(queuedUuid.toString()), - ((QueuedGeneratedReward) rewards.get(0)).getAllowedUserUuids()); + ((QueuedGeneratedReward) reward).getAllowedUserUuids()); } @Test - void loadRewardsDoesNotQuarantineNormalDirectFolderFileWithoutGeneratedMarker() throws IOException { + void loadRewardsDoesNotSuppressNormalDirectFolderFileWithoutGeneratedMarker() throws IOException { AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); RewardHandler handler = mock(RewardHandler.class); @@ -134,7 +134,7 @@ void loadRewardsDoesNotQuarantineNormalDirectFolderFileWithoutGeneratedMarker() assertTrue(directlyDefined.mkdirs()); File manual = new File(directlyDefined, "ExampleBasic.yml"); YamlConfiguration manualData = new YamlConfiguration(); - manualData.set("Messages", java.util.List.of("manual reward")); + manualData.set("Messages", List.of("manual reward")); manualData.save(manual); when(plugin.getDataFolder()).thenReturn(tempDir); @@ -151,18 +151,6 @@ void loadRewardsDoesNotQuarantineNormalDirectFolderFileWithoutGeneratedMarker() loader.loadRewards(); assertTrue(manual.exists()); - assertFalse(new File(directlyDefined, "ExampleBasic.yml.disabled").exists()); assertEquals(1, rewards.size()); } - - @Test - void choosesNonConflictingDisabledBackupName() throws IOException { - File stale = new File(tempDir, "Daily_Rewards.yml"); - assertTrue(stale.createNewFile()); - assertTrue(new File(tempDir, "Daily_Rewards.yml.disabled").createNewFile()); - - File disabled = RewardLoader.nextDisabledFile(stale); - - assertTrue(disabled.getName().equals("Daily_Rewards.yml.disabled.1")); - } } From e1943214dae5336379db8a845d3ce9d56a26cea7 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:25:57 -0600 Subject: [PATCH 29/71] Test queue-only generated reward dispatch --- .../QueuedGeneratedRewardDispatchTest.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java new file mode 100644 index 0000000000..54000a51ef --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java @@ -0,0 +1,82 @@ +package com.bencodez.advancedcore.tests.rewards; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.logging.Logger; + +import org.bukkit.Bukkit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.rewards.Reward; +import com.bencodez.advancedcore.api.rewards.RewardExecutor; +import com.bencodez.advancedcore.api.rewards.RewardHandler; +import com.bencodez.advancedcore.api.rewards.RewardOptions; +import com.bencodez.advancedcore.api.user.AdvancedCoreUser; + +class QueuedGeneratedRewardDispatchTest { + + private AdvancedCorePlugin plugin; + private RewardHandler handler; + private AdvancedCoreUser user; + private RewardExecutor executor; + + @BeforeEach + void setUp() { + plugin = mock(AdvancedCorePlugin.class); + handler = mock(RewardHandler.class); + user = mock(AdvancedCoreUser.class); + executor = new RewardExecutor(handler, plugin); + when(plugin.getLogger()).thenReturn(mock(Logger.class)); + when(user.getUUID()).thenReturn("uuid"); + when(user.getPlayerName()).thenReturn("Ben"); + when(user.isOnline()).thenReturn(true); + } + + @Test + void offlineQueueReplayCanResolveGeneratedSnapshot() { + Reward queued = mock(Reward.class); + RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false); + when(handler.getQueuedGeneratedReward("QueuedReward", "uuid")).thenReturn(queued); + + try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); + executor.giveReward(user, "QueuedReward", options); + } + + verify(handler).getQueuedGeneratedReward("QueuedReward", "uuid"); + verify(queued).giveReward(user, options); + } + + @Test + void timedQueueReplayCanResolveGeneratedSnapshot() { + Reward queued = mock(Reward.class); + RewardOptions options = new RewardOptions().setCheckTimed(false).addPlaceholder("date", "now"); + when(handler.getQueuedGeneratedReward("QueuedReward", "uuid")).thenReturn(queued); + + try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); + executor.giveReward(user, "QueuedReward", options); + } + + verify(handler).getQueuedGeneratedReward("QueuedReward", "uuid"); + verify(queued).giveReward(user, options); + } + + @Test + void normalStandaloneDispatchCannotResolveGeneratedSnapshot() { + RewardOptions options = new RewardOptions(); + + try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); + executor.giveReward(user, "QueuedReward", options); + } + + verify(handler, never()).getQueuedGeneratedReward("QueuedReward", "uuid"); + } +} From 0df49afa3707ba9446f9ae2c33a32fa2017e9ee4 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 17:29:04 -0600 Subject: [PATCH 30/71] Use safe lookup rules for queued reward names --- .../com/bencodez/advancedcore/api/rewards/RewardLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java index 20a782b8e2..c205e8081f 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java @@ -149,8 +149,8 @@ public Reward getQueuedGeneratedReward(String reward, String userUuid) { if (reward == null || reward.isEmpty() || userUuid == null || userUuid.isEmpty()) { return null; } - String normalized = RewardRegistry.normalizeDirectPath(reward); - if (normalized.isEmpty() || !normalized.equals(reward.replace(" ", "_"))) { + reward = RewardRegistry.normalizeLookupName(reward); + if (!RewardRegistry.isSafeRewardFileName(reward)) { return null; } File directFolder = new File(getDefaultFolder().getAbsolutePath() + File.separator + "DirectlyDefined"); From 6dfb241004d67babc12b9667b1016bad8e9aed07 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:11:53 -0600 Subject: [PATCH 31/71] Keep generated reward snapshots out of public registry --- .../bencodez/advancedcore/api/rewards/RewardRegistry.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java index 7d0ced6ece..01a6464b5f 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java @@ -173,6 +173,13 @@ public void resetSubDirectlyDefinedRewards() { } public void updateReward(Reward reward) { + if (reward != null && reward.getConfig().isDirectlyDefinedReward()) { + File folder = reward.getConfig().getRewardFolder(); + if (folder != null && folder.getName().equalsIgnoreCase("DirectlyDefined")) { + plugin.extraDebug("Keeping generated queued reward snapshot out of public registry: " + reward.getName()); + return; + } + } reward.validate(); for (int i = getRewards().size() - 1; i >= 0; i--) { if (getRewards().get(i).getFile().getPath().equals(reward.getFile().getPath())) { From 8d8c8dd652e0d62cf599d933b41d628777b381f2 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:12:16 -0600 Subject: [PATCH 32/71] Resolve queued snapshots before normal reward fallback --- .../bencodez/advancedcore/api/rewards/RewardExecutor.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java index 55899dd926..071d21123a 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java @@ -123,10 +123,13 @@ public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewar return; } - Reward resolved = handler.getReward(reward); - if (resolved == null && isPersistedQueueReplay(context.getOptions())) { + Reward resolved = null; + if (isPersistedQueueReplay(context.getOptions())) { resolved = handler.getQueuedGeneratedReward(reward, user.getUUID()); } + if (resolved == null) { + resolved = handler.getReward(reward); + } giveReward(user, resolved, context.getOptions()); } From c0523589f730bcf58f3171cd6e4d726be842dc24 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:12:39 -0600 Subject: [PATCH 33/71] Resolve timed queued snapshots through restricted loader --- .../api/rewards/RewardBuilder.java | 368 +++++++++--------- 1 file changed, 193 insertions(+), 175 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java index 8d9cc1787d..5a86627359 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java @@ -1,175 +1,193 @@ -package com.bencodez.advancedcore.api.rewards; - -import java.text.SimpleDateFormat; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; - -import org.bukkit.OfflinePlayer; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.entity.Player; - -import com.bencodez.advancedcore.AdvancedCorePlugin; -import com.bencodez.advancedcore.api.user.AdvancedCoreUser; - -public class RewardBuilder { - private ConfigurationSection data; - private String path; - private Reward reward; - private RewardOptions rewardOptions; - - public RewardBuilder(ConfigurationSection data, String path) { - this.data = data; - this.path = path; - this.rewardOptions = new RewardOptions(); - LocalDateTime ldt = LocalDateTime.now(); - Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); - this.rewardOptions.getPlaceholders().put("Date", - "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(date)); - } - - public RewardBuilder(ConfigurationSection data, String path, RewardOptions rewardOptions) { - this.data = data; - this.path = path; - this.rewardOptions = rewardOptions; - if (rewardOptions == null) { - this.rewardOptions = new RewardOptions(); - } - LocalDateTime ldt = LocalDateTime.now(); - Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); - this.rewardOptions.getPlaceholders().put("Date", - "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(date)); - } - - public RewardBuilder(Reward reward) { - this.reward = reward; - this.rewardOptions = new RewardOptions(); - LocalDateTime ldt = LocalDateTime.now(); - Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); - this.rewardOptions.getPlaceholders().put("Date", - "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(date)); - } - - public RewardBuilder(Reward reward, RewardOptions rewardOptions) { - this.reward = reward; - this.rewardOptions = rewardOptions; - if (rewardOptions == null) { - this.rewardOptions = new RewardOptions(); - } - LocalDateTime ldt = LocalDateTime.now(); - Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); - this.rewardOptions.getPlaceholders().put("Date", - "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(date)); - } - - public RewardBuilder disableDefaultWorlds() { - this.rewardOptions.disableDefaultWorlds(); - return this; - } - - public RewardOptions getRewardOptions() { - return rewardOptions; - } - - public RewardBuilder orginalTrigger(long trigger) { - this.rewardOptions.orginalTrigger(trigger); - return this; - } - - public void send(AdvancedCoreUser user) { - if (reward == null) { - if (data != null) { - user.getPlugin().getRewardHandler().giveReward(user, data, path, rewardOptions); - } - } else { - user.getPlugin().getRewardHandler().giveReward(user, reward, rewardOptions); - } - } - - public void send(AdvancedCoreUser... users) { - for (AdvancedCoreUser user : users) { - send(user); - } - } - - public void send(ArrayList users) { - for (AdvancedCoreUser user : users) { - send(user); - } - } - - public void send(OfflinePlayer p) { - send(AdvancedCorePlugin.getInstance().getUserManager().getUser(p)); - } - - public void send(Player p) { - send(AdvancedCorePlugin.getInstance().getUserManager().getUser(p)); - } - - public RewardBuilder setCheckTimed(boolean checkTimed) { - getRewardOptions().setCheckTimed(checkTimed); - return this; - } - - public RewardBuilder setGiveOffline(boolean giveOffline) { - getRewardOptions().setGiveOffline(giveOffline); - return this; - } - - public RewardBuilder setIgnoreChance(boolean ignoreChance) { - getRewardOptions().setIgnoreChance(ignoreChance); - return this; - } - - public RewardBuilder setIgnoreRequirements(boolean ignoreRequirements) { - getRewardOptions().setIgnoreRequirements(ignoreRequirements); - return this; - } - - public RewardBuilder setOnline(boolean online) { - getRewardOptions().setOnline(online); - return this; - } - - /** - * @param reward the reward to set - */ - public void setReward(Reward reward) { - this.reward = reward; - } - - public RewardBuilder setRewardOptions(RewardOptions rewardOptions) { - this.rewardOptions = rewardOptions; - return this; - } - - public RewardBuilder setServer(boolean b) { - if (b) { - getRewardOptions().setServer(b); - } - return this; - } - - public RewardBuilder withPlaceHolder(HashMap placeholders) { - this.rewardOptions.getPlaceholders().putAll(placeholders); - return this; - } - - public RewardBuilder withPlaceHolder(String toReplace, String replaceWith) { - this.rewardOptions.getPlaceholders().put(toReplace, replaceWith); - return this; - } - - public RewardBuilder withPrefix(String prefix) { - this.rewardOptions.setPrefix(prefix); - return this; - } - - public RewardBuilder withSuffix(String suffix) { - this.rewardOptions.setSuffix(suffix); - return this; - } - -} +package com.bencodez.advancedcore.api.rewards; + +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; + +import org.bukkit.OfflinePlayer; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.user.AdvancedCoreUser; + +public class RewardBuilder { + private ConfigurationSection data; + private String path; + private Reward reward; + private RewardOptions rewardOptions; + + public RewardBuilder(ConfigurationSection data, String path) { + this.data = data; + this.path = path; + this.rewardOptions = new RewardOptions(); + LocalDateTime ldt = LocalDateTime.now(); + Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + this.rewardOptions.getPlaceholders().put("Date", + "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(date)); + } + + public RewardBuilder(ConfigurationSection data, String path, RewardOptions rewardOptions) { + this.data = data; + this.path = path; + this.rewardOptions = rewardOptions; + if (rewardOptions == null) { + this.rewardOptions = new RewardOptions(); + } + LocalDateTime ldt = LocalDateTime.now(); + Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + this.rewardOptions.getPlaceholders().put("Date", + "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(date)); + } + + public RewardBuilder(Reward reward) { + this.reward = reward; + this.rewardOptions = new RewardOptions(); + LocalDateTime ldt = LocalDateTime.now(); + Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + this.rewardOptions.getPlaceholders().put("Date", + "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(date)); + } + + public RewardBuilder(Reward reward, RewardOptions rewardOptions) { + this.reward = reward; + this.rewardOptions = rewardOptions; + if (rewardOptions == null) { + this.rewardOptions = new RewardOptions(); + } + LocalDateTime ldt = LocalDateTime.now(); + Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + this.rewardOptions.getPlaceholders().put("Date", + "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(date)); + } + + public RewardBuilder disableDefaultWorlds() { + this.rewardOptions.disableDefaultWorlds(); + return this; + } + + public RewardOptions getRewardOptions() { + return rewardOptions; + } + + public RewardBuilder orginalTrigger(long trigger) { + this.rewardOptions.orginalTrigger(trigger); + return this; + } + + public void send(AdvancedCoreUser user) { + RewardHandler handler = user.getPlugin().getRewardHandler(); + if (reward == null) { + if (data != null) { + handler.giveReward(user, data, path, rewardOptions); + } + } else { + if (isPersistedQueueReplay()) { + Reward queued = handler.getQueuedGeneratedReward(reward.getRewardName(), user.getUUID()); + if (queued != null) { + handler.giveReward(user, queued, rewardOptions); + return; + } + } + handler.giveReward(user, reward, rewardOptions); + } + } + + private boolean isPersistedQueueReplay() { + if (rewardOptions == null || rewardOptions.isCheckTimed()) { + return false; + } + if (rewardOptions.isOnlineSet() && !rewardOptions.isOnline()) { + return true; + } + return rewardOptions.getPlaceholders().containsKey("date"); + } + + public void send(AdvancedCoreUser... users) { + for (AdvancedCoreUser user : users) { + send(user); + } + } + + public void send(ArrayList users) { + for (AdvancedCoreUser user : users) { + send(user); + } + } + + public void send(OfflinePlayer p) { + send(AdvancedCorePlugin.getInstance().getUserManager().getUser(p)); + } + + public void send(Player p) { + send(AdvancedCorePlugin.getInstance().getUserManager().getUser(p)); + } + + public RewardBuilder setCheckTimed(boolean checkTimed) { + getRewardOptions().setCheckTimed(checkTimed); + return this; + } + + public RewardBuilder setGiveOffline(boolean giveOffline) { + getRewardOptions().setGiveOffline(giveOffline); + return this; + } + + public RewardBuilder setIgnoreChance(boolean ignoreChance) { + getRewardOptions().setIgnoreChance(ignoreChance); + return this; + } + + public RewardBuilder setIgnoreRequirements(boolean ignoreRequirements) { + getRewardOptions().setIgnoreRequirements(ignoreRequirements); + return this; + } + + public RewardBuilder setOnline(boolean online) { + getRewardOptions().setOnline(online); + return this; + } + + /** + * @param reward the reward to set + */ + public void setReward(Reward reward) { + this.reward = reward; + } + + public RewardBuilder setRewardOptions(RewardOptions rewardOptions) { + this.rewardOptions = rewardOptions; + return this; + } + + public RewardBuilder setServer(boolean b) { + if (b) { + getRewardOptions().setServer(b); + } + return this; + } + + public RewardBuilder withPlaceHolder(HashMap placeholders) { + this.rewardOptions.getPlaceholders().putAll(placeholders); + return this; + } + + public RewardBuilder withPlaceHolder(String toReplace, String replaceWith) { + this.rewardOptions.getPlaceholders().put(toReplace, replaceWith); + return this; + } + + public RewardBuilder withPrefix(String prefix) { + this.rewardOptions.setPrefix(prefix); + return this; + } + + public RewardBuilder withSuffix(String suffix) { + this.rewardOptions.setSuffix(suffix); + return this; + } + +} From 2d72076df1a57183c329676cb05e1c4c7028cbf7 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:12:57 -0600 Subject: [PATCH 34/71] Test queued snapshot precedence for offline and timed replay --- .../QueuedGeneratedRewardDispatchTest.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java index 54000a51ef..3ec92a7bc9 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java @@ -14,6 +14,7 @@ import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.rewards.Reward; +import com.bencodez.advancedcore.api.rewards.RewardBuilder; import com.bencodez.advancedcore.api.rewards.RewardExecutor; import com.bencodez.advancedcore.api.rewards.RewardHandler; import com.bencodez.advancedcore.api.rewards.RewardOptions; @@ -33,16 +34,20 @@ void setUp() { user = mock(AdvancedCoreUser.class); executor = new RewardExecutor(handler, plugin); when(plugin.getLogger()).thenReturn(mock(Logger.class)); + when(plugin.getRewardHandler()).thenReturn(handler); + when(user.getPlugin()).thenReturn(plugin); when(user.getUUID()).thenReturn("uuid"); when(user.getPlayerName()).thenReturn("Ben"); when(user.isOnline()).thenReturn(true); } @Test - void offlineQueueReplayCanResolveGeneratedSnapshot() { + void offlineQueueReplayResolvesGeneratedSnapshotBeforeNormalFallback() { Reward queued = mock(Reward.class); + Reward fallback = mock(Reward.class); RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false); when(handler.getQueuedGeneratedReward("QueuedReward", "uuid")).thenReturn(queued); + when(handler.getReward("QueuedReward")).thenReturn(fallback); try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); @@ -50,7 +55,9 @@ void offlineQueueReplayCanResolveGeneratedSnapshot() { } verify(handler).getQueuedGeneratedReward("QueuedReward", "uuid"); + verify(handler, never()).getReward("QueuedReward"); verify(queued).giveReward(user, options); + verify(fallback, never()).giveReward(user, options); } @Test @@ -68,6 +75,21 @@ void timedQueueReplayCanResolveGeneratedSnapshot() { verify(queued).giveReward(user, options); } + @Test + void timedRewardBuilderReplacesNormalFallbackWithQueuedSnapshot() { + Reward fallback = mock(Reward.class); + Reward queued = mock(Reward.class); + when(fallback.getRewardName()).thenReturn("QueuedReward"); + when(handler.getQueuedGeneratedReward("QueuedReward", "uuid")).thenReturn(queued); + + RewardBuilder builder = new RewardBuilder(fallback).setCheckTimed(false).withPlaceHolder("date", "now"); + builder.send(user); + + verify(handler).getQueuedGeneratedReward("QueuedReward", "uuid"); + verify(handler).giveReward(user, queued, builder.getRewardOptions()); + verify(handler, never()).giveReward(user, fallback, builder.getRewardOptions()); + } + @Test void normalStandaloneDispatchCannotResolveGeneratedSnapshot() { RewardOptions options = new RewardOptions(); From b9bb9bdccf45541752bebbe17b4afc38a1723e6b Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:13:05 -0600 Subject: [PATCH 35/71] Test generated snapshots stay out of public registry --- .../rewards/RewardRegistrySecurityTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java index 9762ed2527..0b642c1bdf 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java @@ -2,9 +2,17 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; import org.junit.jupiter.api.Test; +import com.bencodez.advancedcore.AdvancedCorePlugin; + class RewardRegistrySecurityTest { @Test @@ -20,4 +28,22 @@ void rewardFileNameAllowsNormalRewardNames() { assertTrue(RewardRegistry.isSafeRewardFileName("Daily_Reward")); assertTrue(RewardRegistry.isSafeRewardFileName("Daily.Reward-v2")); } + + @Test + void generatedDirectlyDefinedSnapshotIsNeverPublishedToRegistry() { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + RewardRegistry registry = new RewardRegistry(plugin); + Reward reward = mock(Reward.class); + RewardFileData config = mock(RewardFileData.class); + + when(reward.getConfig()).thenReturn(config); + when(reward.getName()).thenReturn("PriorityChild"); + when(config.isDirectlyDefinedReward()).thenReturn(true); + when(config.getRewardFolder()).thenReturn(new File("Rewards", "DirectlyDefined")); + + registry.updateReward(reward); + + assertFalse(registry.getRewards().contains(reward)); + verify(reward, never()).validate(); + } } From bc128a1aa11908176b4c1623ebdd79504e04d411 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:14:05 -0600 Subject: [PATCH 36/71] Qualify AdvancedPriority generated reward names --- .../api/rewards/builtin/RewardAdvancedPriority.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardAdvancedPriority.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardAdvancedPriority.java index 92edd4b28c..fc1bb37eb7 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardAdvancedPriority.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardAdvancedPriority.java @@ -32,7 +32,9 @@ public static void register(RewardHandler handler, AdvancedCorePlugin plugin) { public String onRewardRequested(Reward sourceReward, AdvancedCoreUser user, ConfigurationSection section, HashMap placeholders) { for (String key : section.getKeys(false)) { - Reward reward = handler.getReward(section, key, new RewardOptions()); + RewardOptions namingOptions = new RewardOptions() + .setPrefix(sourceReward.getName() + "_AdvancedPriority"); + Reward reward = handler.getReward(section, key, namingOptions); if (reward != null && reward.canGiveReward(user, new RewardOptions().withPlaceHolder(placeholders))) { plugin.extraDebug("AdvancedPriority: Giving reward " + reward.getName()); reward.giveReward(user, new RewardOptions().setIgnoreChance(true).setIgnoreRequirements(true) From a37279eb0ee2c823846935ac668d1e39dd15ba19 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:14:27 -0600 Subject: [PATCH 37/71] Apply queued reward replay and registration fixes --- .../fix-queued-reward-resolution.yml | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/fix-queued-reward-resolution.yml diff --git a/.github/workflows/fix-queued-reward-resolution.yml b/.github/workflows/fix-queued-reward-resolution.yml new file mode 100644 index 0000000000..1b461f836a --- /dev/null +++ b/.github/workflows/fix-queued-reward-resolution.yml @@ -0,0 +1,60 @@ +name: Apply queued reward resolution fixes + +on: + push: + branches: + - security/reward-path-identity-hardening + paths: + - .github/workflows/fix-queued-reward-resolution.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/reward-path-identity-hardening + fetch-depth: 0 + - name: Patch timed replay and generated snapshot registration + run: | + python3 - <<'PY' + from pathlib import Path + + user_path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') + text = user_path.read_text() + old = '''\t\t\t\t\tnew RewardBuilder(plugin.getRewardHandler().getReward(rewardName)).setCheckTimed(false) + \t\t\t\t\t\t\t.withPlaceHolder(ArrayUtils.fromString(placeholders))''' + new = '''\t\t\t\t\tcom.bencodez.advancedcore.api.rewards.Reward queuedReward = plugin.getRewardHandler() + \t\t\t\t\t\t\t.getQueuedGeneratedReward(rewardName, getUUID()); + \t\t\t\t\tif (queuedReward == null) { + \t\t\t\t\t\tqueuedReward = plugin.getRewardHandler().getReward(rewardName); + \t\t\t\t\t} + \t\t\t\t\tnew RewardBuilder(queuedReward).setCheckTimed(false) + \t\t\t\t\t\t\t.withPlaceHolder(ArrayUtils.fromString(placeholders))''' + if old not in text: + raise SystemExit('Unable to locate timed reward replay lookup') + user_path.write_text(text.replace(old, new, 1)) + + reward_path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java') + text = reward_path.read_text() + old = '''\t\treward.getConfig().setDirectlyDefinedReward(true); + \t\treward.getConfig().save(reward.getConfig().getFileData()); + \t\tplugin.getRewardHandler().updateReward(reward);''' + new = '''\t\treward.getConfig().setDirectlyDefinedReward(true); + \t\treward.getConfig().save(reward.getConfig().getFileData());''' + if old not in text: + raise SystemExit('Unable to locate generated reward registration') + reward_path.write_text(text.replace(old, new, 1)) + PY + - name: Commit queued reward fixes + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java + git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java + git rm .github/workflows/fix-queued-reward-resolution.yml + git commit -m "Keep generated queue snapshots out of public lookup" + git push origin HEAD:security/reward-path-identity-hardening From 112ae9d8f42eeea27252523bbefaa555285bb8f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:14:39 +0000 Subject: [PATCH 38/71] Keep generated queue snapshots out of public lookup --- .../fix-queued-reward-resolution.yml | 60 - .../advancedcore/api/rewards/Reward.java | 1009 +++-- .../api/user/AdvancedCoreUser.java | 3325 +++++++++-------- 3 files changed, 2169 insertions(+), 2225 deletions(-) delete mode 100644 .github/workflows/fix-queued-reward-resolution.yml diff --git a/.github/workflows/fix-queued-reward-resolution.yml b/.github/workflows/fix-queued-reward-resolution.yml deleted file mode 100644 index 1b461f836a..0000000000 --- a/.github/workflows/fix-queued-reward-resolution.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Apply queued reward resolution fixes - -on: - push: - branches: - - security/reward-path-identity-hardening - paths: - - .github/workflows/fix-queued-reward-resolution.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/reward-path-identity-hardening - fetch-depth: 0 - - name: Patch timed replay and generated snapshot registration - run: | - python3 - <<'PY' - from pathlib import Path - - user_path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') - text = user_path.read_text() - old = '''\t\t\t\t\tnew RewardBuilder(plugin.getRewardHandler().getReward(rewardName)).setCheckTimed(false) - \t\t\t\t\t\t\t.withPlaceHolder(ArrayUtils.fromString(placeholders))''' - new = '''\t\t\t\t\tcom.bencodez.advancedcore.api.rewards.Reward queuedReward = plugin.getRewardHandler() - \t\t\t\t\t\t\t.getQueuedGeneratedReward(rewardName, getUUID()); - \t\t\t\t\tif (queuedReward == null) { - \t\t\t\t\t\tqueuedReward = plugin.getRewardHandler().getReward(rewardName); - \t\t\t\t\t} - \t\t\t\t\tnew RewardBuilder(queuedReward).setCheckTimed(false) - \t\t\t\t\t\t\t.withPlaceHolder(ArrayUtils.fromString(placeholders))''' - if old not in text: - raise SystemExit('Unable to locate timed reward replay lookup') - user_path.write_text(text.replace(old, new, 1)) - - reward_path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java') - text = reward_path.read_text() - old = '''\t\treward.getConfig().setDirectlyDefinedReward(true); - \t\treward.getConfig().save(reward.getConfig().getFileData()); - \t\tplugin.getRewardHandler().updateReward(reward);''' - new = '''\t\treward.getConfig().setDirectlyDefinedReward(true); - \t\treward.getConfig().save(reward.getConfig().getFileData());''' - if old not in text: - raise SystemExit('Unable to locate generated reward registration') - reward_path.write_text(text.replace(old, new, 1)) - PY - - name: Commit queued reward fixes - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java - git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java - git rm .github/workflows/fix-queued-reward-resolution.yml - git commit -m "Keep generated queue snapshots out of public lookup" - git push origin HEAD:security/reward-path-identity-hardening diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java index 6f7857eadd..848a2817c1 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java @@ -1,505 +1,504 @@ -package com.bencodez.advancedcore.api.rewards; - -import java.io.File; -import java.text.SimpleDateFormat; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; - -import com.bencodez.advancedcore.AdvancedCorePlugin; -import com.bencodez.advancedcore.api.item.ItemBuilder; -import com.bencodez.advancedcore.api.rewards.injected.RewardInject; -import com.bencodez.advancedcore.api.rewards.injectedrequirement.RequirementInject; -import com.bencodez.advancedcore.api.user.AdvancedCoreUser; -import com.bencodez.advancedcore.listeners.PlayerRewardEvent; -import com.bencodez.simpleapi.file.annotation.AnnotationHandler; - -import lombok.Getter; -import lombok.Setter; - -/** - * The Class Reward. - */ -public class Reward { - - @Getter - @Setter - private RewardFileData config; - - @Getter - @Setter - private boolean delayEnabled; - - @Getter - @Setter - private int delayHours; - - @Getter - @Setter - private int delayMinutes; - - @Getter - @Setter - private int delaySeconds; - - @Getter - @Setter - private int delayMilliSeconds; - - @Getter - @Setter - private File file; - - @Getter - @Setter - private boolean forceOffline; - - @Getter - @Setter - private String name; - - @Getter - private boolean needsRewardFile = true; - - /** The plugin. */ - AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); - - @Getter - @Setter - private boolean timedEnabled; - - @Getter - @Setter - private int timedHour; - - @Getter - @Setter - private int timedMinute; - - /** - * Instantiates a new reward. - * - * @param file the file - * @param reward the reward - */ - public Reward(File file, String reward) { - load(file, reward); - } - - /** - * Instantiates a new reward. - * - * @param reward the reward - */ - public Reward(String reward) { - load(plugin.getRewardHandler().getDefaultFolder(), reward); - } - - public Reward(String name, ConfigurationSection section) { - load(name, section); - } - - public boolean canGiveReward(AdvancedCoreUser user, RewardOptions options) { - for (RequirementInject inject : plugin.getRewardHandler().getInjectedRequirements()) { - try { - plugin.extraDebug(getRewardName() + ": Checking " + inject.getPath() + ":" + inject.getPriority()); - if (!inject.onRequirementRequest(this, user, getConfig().getConfigData(), options)) { - return false; - } - } catch (Exception e) { - plugin.debug("Failed to check requirement"); - e.printStackTrace(); - return false; - } - } - return true; - } - - public boolean checkDelayed(AdvancedCoreUser user, HashMap placeholders) { - if (!isDelayEnabled()) { - return false; - } - - LocalDateTime time = LocalDateTime.now(); - time = time.plus(getDelayHours(), ChronoUnit.HOURS); - time = time.plus(getDelayMinutes(), ChronoUnit.MINUTES); - time = time.plus(getDelaySeconds(), ChronoUnit.SECONDS); - time = time.plus(getDelayMilliSeconds(), ChronoUnit.MILLIS); - checkRewardFile(); - user.addTimedReward(this, placeholders, time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); - - plugin.debug("Giving reward " + name + " in " + getDelayHours() + " hours, " + getDelayMinutes() + " minutes, " - + getDelaySeconds() + " seconds (" + time.toString() + ")"); - return true; - } - - public void checkRewardFile() { - if (!getConfig().hasRewardFile() && needsRewardFile) { - setRewardFile(); - } - } - - public boolean checkTimed(AdvancedCoreUser user, HashMap placeholders) { - if (!isTimedEnabled()) { - return false; - } - - LocalDateTime time = LocalDateTime.now(); - time = time.withHour(getTimedHour()); - time = time.withMinute(getTimedMinute()); - - if (LocalDateTime.now().isAfter(time)) { - time = time.plusDays(1); - } - checkRewardFile(); - user.addTimedReward(this, placeholders, time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); - - plugin.debug("Giving reward " + name + " at " + time.toString()); - return true; - } - - public ItemStack getItem() { - return new ItemStack(Material.STONE); - } - - public ItemStack getItemStack(AdvancedCoreUser user, String item) { - return new ItemBuilder(getConfig().getItemSection(item)).setSkullOwner(user.getOfflinePlayer()) - .toItemStack(user.getPlayer()); - } - - /** - * Gets the reward name. - * - * @return the reward name - */ - public String getRewardName() { - return name; - } - - public void giveInjectedRewards(AdvancedCoreUser user, HashMap placeholders) { - - ArrayList postReward = new ArrayList<>(); - - for (final RewardInject inject : plugin.getRewardHandler().getInjectedRewards()) { - boolean Addplaceholder = inject.isAddAsPlaceholder(); - try { - Object obj = null; - plugin.extraDebug( - getRewardName() + ": Attempting to give " + inject.getPath() + ":" + inject.getPriority()); - if (!inject.isPostReward()) { - if (inject.isSynchronize()) { - synchronized (inject.getObject()) { - obj = inject.onRewardRequest(this, user, getConfig().getConfigData(), placeholders); - } - } else { - obj = inject.onRewardRequest(this, user, getConfig().getConfigData(), placeholders); - } - if (Addplaceholder && obj != null) { - String placeholderName = inject.getPlaceholderName(); - String value = ""; - if (obj instanceof Boolean) { - Boolean b = (Boolean) obj; - value = b.toString(); - } else if (obj instanceof String) { - String b = (String) obj; - value = b; - } else if (obj instanceof Double) { - Double b = (Double) obj; - value = b.toString(); - } else if (obj instanceof Integer) { - Integer b = (Integer) obj; - value = b.toString(); - } - plugin.extraDebug("Adding placeholder " + placeholderName + ":" + value); - placeholders.put(placeholderName, value); - } - } else { - postReward.add(inject); - - } - - } catch (Exception e) { - e.printStackTrace(); - } - - } - - for (RewardInject inject : postReward) { - try { - inject.onRewardRequest(this, user, getConfig().getConfigData(), placeholders); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - public void giveReward(AdvancedCoreUser user, RewardOptions rewardOptions) { - if (!AdvancedCorePlugin.getInstance().getOptions().isProcessRewards()) { - AdvancedCorePlugin.getInstance().debug("Processing rewards is disabled"); - return; - } - - if (rewardOptions == null) { - rewardOptions = new RewardOptions(); - } - - if (!rewardOptions.getPlaceholders().containsKey("ExecDate")) { - rewardOptions.addPlaceholder("ExecDate", "" + System.currentTimeMillis()); - } - - if (!rewardOptions.getPlaceholders().containsKey("date")) { - try { - LocalDateTime ldt = LocalDateTime.now(); - Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); - rewardOptions.addPlaceholder("Date", - "" + new SimpleDateFormat(plugin.getOptions().getFormatRewardTimeFormat()).format(date)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - PlayerRewardEvent event = new PlayerRewardEvent(this, user, rewardOptions); - Bukkit.getPluginManager().callEvent(event); - - if (event.isCancelled()) { - plugin.debug("Reward " + name + " was cancelled for " + user.getPlayerName()); - return; - } - - if (rewardOptions.isCheckTimed()) { - if (checkDelayed(user, rewardOptions.getPlaceholders()) - || checkTimed(user, rewardOptions.getPlaceholders())) { - return; - } - } - - if (!rewardOptions.isOnlineSet()) { - rewardOptions.setOnline(user.isOnline()); - } - - for (RewardPlaceholderHandle handle : plugin.getRewardHandler().getPlaceholders()) { - if (handle.isPreProcess()) { - rewardOptions.addPlaceholder(handle.getKey(), handle.getValue(this, user)); - } - } - - // Check requirements - boolean allowOffline = false; - boolean canGive = true; - if (!rewardOptions.isIgnoreRequirements()) { - for (RequirementInject inject : plugin.getRewardHandler().getInjectedRequirements()) { - try { - plugin.extraDebug(getRewardName() + ": Checking requirement " + inject.getPath() + ":" - + inject.getPriority()); - if (!inject.onRequirementRequest(this, user, getConfig().getConfigData(), rewardOptions)) { - plugin.debug(getRewardName() + ": Requirement failed " + inject.getPath() + ":" - + inject.isAllowReattempt()); - canGive = false; - if (!inject.isAllowReattempt()) { - return; - } - allowOffline = true; - } - } catch (Exception e) { - plugin.debug("Failed to check requirement " + inject.getPath()); - e.printStackTrace(); - canGive = false; - } - } - } - - if (plugin.getOptions().isPauseRewards()) { - checkRewardFile(); - user.addOfflineRewards(this, rewardOptions.getPlaceholders()); - plugin.getLogger() - .info("Rewards are paused, saving offline reward " + getRewardName() + ": " + user.getPlayerName()); - return; - } - - if ((plugin.getOptions().isTreatVanishAsOffline() && user.isVanished())) { - checkRewardFile(); - user.addOfflineRewards(this, rewardOptions.getPlaceholders()); - plugin.getLogger() - .info(getRewardName() + ": " + user.getPlayerName() + " is vanished, saving reward offline"); - return; - } - - // save reward for offline - if (((((!rewardOptions.isOnline() || rewardOptions.getServer() != null) && !user.isOnline()) || allowOffline) - && (!isForceOffline() && !rewardOptions.isForceOffline()))) { - if (rewardOptions.isGiveOffline()) { - checkRewardFile(); - user.addOfflineRewards(this, rewardOptions.getPlaceholders()); - plugin.debug("Saving offline reward " + getRewardName() + " for " + user.getPlayerName()); - } - return; - } - - // give reward - if (canGive || isForceOffline() || rewardOptions.isForceOffline()) { - plugin.debug(name + ": Passed requirements, attempting to give to " + user.getPlayerName() + "/" - + user.getUUID()); - giveRewardUser(user, rewardOptions.getPlaceholders(), rewardOptions); - } - } - - /** - * Give reward user. - * - * @param user the user - * @param phs placeholders - * @param rewardOptions rewardOptions - */ - public void giveRewardUser(AdvancedCoreUser user, HashMap phs, RewardOptions rewardOptions) { - - Player player = user.getPlayer(); - if (player == null) { - player = Bukkit.getPlayer(user.getPlayerName()); - } - if (player != null || isForceOffline()) { - - // placeholders - if (phs == null) { - phs = new HashMap<>(); - } - final String playerName = user.getPlayerName(); - phs.put("player", playerName); - if (player != null) { - phs.put("displayname", player.getDisplayName()); - } - phs.put("@p", playerName); - LocalDateTime ldt = LocalDateTime.now(); - Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); - phs.put("CurrentDate", "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(date)); - phs.put("uuid", user.getUUID()); - - for (RewardPlaceholderHandle handle : plugin.getRewardHandler().getPlaceholders()) { - if (!handle.isPreProcess()) { - phs.put(handle.getKey(), handle.getValue(this, user)); - } - } - - final HashMap placeholders = new HashMap<>(phs); - - giveInjectedRewards(user, placeholders); - - plugin.debug("Gave " + user.getPlayerName() + " reward " + name); - } else { - plugin.debug(getRewardName() + ": Player == null & forceoffline false, player: " + user.getPlayerName() - + "/" + user.getUUID()); - } - } - - /** - * Load. - * - * @param folder the folder - * @param reward the reward - */ - public void load(File folder, String reward) { - name = reward; - if (folder.isDirectory()) { - file = new File(folder, reward + ".yml"); - } else { - file = folder; - } - config = new RewardFileData(this, folder); - loadValues(); - } - - public void load(String name, ConfigurationSection section) { - config = new RewardFileData(this, section); - this.name = name; - loadValues(); - } - - public void loadValues() { - forceOffline = getConfig().getForceOffline(); - - setDelayEnabled(getConfig().getDelayedEnabled()); - if (delayEnabled) { - setDelayHours(getConfig().getDelayedHours()); - setDelayMinutes(getConfig().getDelayedMinutes()); - setDelaySeconds(getConfig().getDelayedSeconds()); - setDelayMilliSeconds(getConfig().getDelayedMilliSeconds()); - } - - setTimedEnabled(getConfig().getTimedEnabled()); - if (timedEnabled) { - setTimedHour(getConfig().getTimedHour()); - setTimedMinute(getConfig().getTimedMinute()); - } - - new AnnotationHandler().load(getConfig().getConfigData(), this); - } - - public Reward needsRewardFile(boolean value) { - needsRewardFile = value; - return this; - } - - @SuppressWarnings("deprecation") - private void setRewardFile() { - Reward reward = plugin.getRewardHandler().getRewardDirectlyDefined(name); - ConfigurationSection section = getConfig().getConfigData(); - reward.getConfig().setData(section); - reward.getConfig().getFileData().options() - .header("Directly defined reward file. WRONG PLACE TO EDIT THIS! DO NOT EDIT"); - reward.getConfig().setDirectlyDefinedReward(true); - reward.getConfig().save(reward.getConfig().getFileData()); - plugin.getRewardHandler().updateReward(reward); - } - - public void validate() { - if (getName().equalsIgnoreCase("examplebasic") || getName().equalsIgnoreCase("exampleadvanced")) { - return; - } - for (RequirementInject inject : plugin.getRewardHandler().getInjectedRequirements()) { - inject.validate(this, getConfig().getConfigData()); - } - for (RewardInject inject : plugin.getRewardHandler().getInjectedRewards()) { - inject.validate(this, getConfig().getConfigData()); - } - for (String str : getConfig().getConfigData().getKeys(false)) { - boolean valid = false; - for (RequirementInject inject : plugin.getRewardHandler().getInjectedRequirements()) { - if (inject.hasValidator()) { - if (inject.getValidate().isValid(inject, str)) { - valid = true; - } - } else if (inject.getPath().startsWith(str)) { - valid = true; - } - } - for (RewardInject inject : plugin.getRewardHandler().getInjectedRewards()) { - if (inject.isAlwaysValid()) { - valid = true; - } else { - if (inject.hasValidator()) { - if (inject.getValidate().isValid(inject, str)) { - valid = true; - } - } else if (inject.getPath().startsWith(str)) { - valid = true; - } - } - } - if (plugin.getRewardHandler().getValidPaths().contains(str)) { - valid = true; - } - if (!valid) { - plugin.getLogger().warning(str + " possibly not valid in reward " + getRewardName()); - } - } - } - -} +package com.bencodez.advancedcore.api.rewards; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.item.ItemBuilder; +import com.bencodez.advancedcore.api.rewards.injected.RewardInject; +import com.bencodez.advancedcore.api.rewards.injectedrequirement.RequirementInject; +import com.bencodez.advancedcore.api.user.AdvancedCoreUser; +import com.bencodez.advancedcore.listeners.PlayerRewardEvent; +import com.bencodez.simpleapi.file.annotation.AnnotationHandler; + +import lombok.Getter; +import lombok.Setter; + +/** + * The Class Reward. + */ +public class Reward { + + @Getter + @Setter + private RewardFileData config; + + @Getter + @Setter + private boolean delayEnabled; + + @Getter + @Setter + private int delayHours; + + @Getter + @Setter + private int delayMinutes; + + @Getter + @Setter + private int delaySeconds; + + @Getter + @Setter + private int delayMilliSeconds; + + @Getter + @Setter + private File file; + + @Getter + @Setter + private boolean forceOffline; + + @Getter + @Setter + private String name; + + @Getter + private boolean needsRewardFile = true; + + /** The plugin. */ + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + + @Getter + @Setter + private boolean timedEnabled; + + @Getter + @Setter + private int timedHour; + + @Getter + @Setter + private int timedMinute; + + /** + * Instantiates a new reward. + * + * @param file the file + * @param reward the reward + */ + public Reward(File file, String reward) { + load(file, reward); + } + + /** + * Instantiates a new reward. + * + * @param reward the reward + */ + public Reward(String reward) { + load(plugin.getRewardHandler().getDefaultFolder(), reward); + } + + public Reward(String name, ConfigurationSection section) { + load(name, section); + } + + public boolean canGiveReward(AdvancedCoreUser user, RewardOptions options) { + for (RequirementInject inject : plugin.getRewardHandler().getInjectedRequirements()) { + try { + plugin.extraDebug(getRewardName() + ": Checking " + inject.getPath() + ":" + inject.getPriority()); + if (!inject.onRequirementRequest(this, user, getConfig().getConfigData(), options)) { + return false; + } + } catch (Exception e) { + plugin.debug("Failed to check requirement"); + e.printStackTrace(); + return false; + } + } + return true; + } + + public boolean checkDelayed(AdvancedCoreUser user, HashMap placeholders) { + if (!isDelayEnabled()) { + return false; + } + + LocalDateTime time = LocalDateTime.now(); + time = time.plus(getDelayHours(), ChronoUnit.HOURS); + time = time.plus(getDelayMinutes(), ChronoUnit.MINUTES); + time = time.plus(getDelaySeconds(), ChronoUnit.SECONDS); + time = time.plus(getDelayMilliSeconds(), ChronoUnit.MILLIS); + checkRewardFile(); + user.addTimedReward(this, placeholders, time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + + plugin.debug("Giving reward " + name + " in " + getDelayHours() + " hours, " + getDelayMinutes() + " minutes, " + + getDelaySeconds() + " seconds (" + time.toString() + ")"); + return true; + } + + public void checkRewardFile() { + if (!getConfig().hasRewardFile() && needsRewardFile) { + setRewardFile(); + } + } + + public boolean checkTimed(AdvancedCoreUser user, HashMap placeholders) { + if (!isTimedEnabled()) { + return false; + } + + LocalDateTime time = LocalDateTime.now(); + time = time.withHour(getTimedHour()); + time = time.withMinute(getTimedMinute()); + + if (LocalDateTime.now().isAfter(time)) { + time = time.plusDays(1); + } + checkRewardFile(); + user.addTimedReward(this, placeholders, time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + + plugin.debug("Giving reward " + name + " at " + time.toString()); + return true; + } + + public ItemStack getItem() { + return new ItemStack(Material.STONE); + } + + public ItemStack getItemStack(AdvancedCoreUser user, String item) { + return new ItemBuilder(getConfig().getItemSection(item)).setSkullOwner(user.getOfflinePlayer()) + .toItemStack(user.getPlayer()); + } + + /** + * Gets the reward name. + * + * @return the reward name + */ + public String getRewardName() { + return name; + } + + public void giveInjectedRewards(AdvancedCoreUser user, HashMap placeholders) { + + ArrayList postReward = new ArrayList<>(); + + for (final RewardInject inject : plugin.getRewardHandler().getInjectedRewards()) { + boolean Addplaceholder = inject.isAddAsPlaceholder(); + try { + Object obj = null; + plugin.extraDebug( + getRewardName() + ": Attempting to give " + inject.getPath() + ":" + inject.getPriority()); + if (!inject.isPostReward()) { + if (inject.isSynchronize()) { + synchronized (inject.getObject()) { + obj = inject.onRewardRequest(this, user, getConfig().getConfigData(), placeholders); + } + } else { + obj = inject.onRewardRequest(this, user, getConfig().getConfigData(), placeholders); + } + if (Addplaceholder && obj != null) { + String placeholderName = inject.getPlaceholderName(); + String value = ""; + if (obj instanceof Boolean) { + Boolean b = (Boolean) obj; + value = b.toString(); + } else if (obj instanceof String) { + String b = (String) obj; + value = b; + } else if (obj instanceof Double) { + Double b = (Double) obj; + value = b.toString(); + } else if (obj instanceof Integer) { + Integer b = (Integer) obj; + value = b.toString(); + } + plugin.extraDebug("Adding placeholder " + placeholderName + ":" + value); + placeholders.put(placeholderName, value); + } + } else { + postReward.add(inject); + + } + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + for (RewardInject inject : postReward) { + try { + inject.onRewardRequest(this, user, getConfig().getConfigData(), placeholders); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public void giveReward(AdvancedCoreUser user, RewardOptions rewardOptions) { + if (!AdvancedCorePlugin.getInstance().getOptions().isProcessRewards()) { + AdvancedCorePlugin.getInstance().debug("Processing rewards is disabled"); + return; + } + + if (rewardOptions == null) { + rewardOptions = new RewardOptions(); + } + + if (!rewardOptions.getPlaceholders().containsKey("ExecDate")) { + rewardOptions.addPlaceholder("ExecDate", "" + System.currentTimeMillis()); + } + + if (!rewardOptions.getPlaceholders().containsKey("date")) { + try { + LocalDateTime ldt = LocalDateTime.now(); + Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + rewardOptions.addPlaceholder("Date", + "" + new SimpleDateFormat(plugin.getOptions().getFormatRewardTimeFormat()).format(date)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + PlayerRewardEvent event = new PlayerRewardEvent(this, user, rewardOptions); + Bukkit.getPluginManager().callEvent(event); + + if (event.isCancelled()) { + plugin.debug("Reward " + name + " was cancelled for " + user.getPlayerName()); + return; + } + + if (rewardOptions.isCheckTimed()) { + if (checkDelayed(user, rewardOptions.getPlaceholders()) + || checkTimed(user, rewardOptions.getPlaceholders())) { + return; + } + } + + if (!rewardOptions.isOnlineSet()) { + rewardOptions.setOnline(user.isOnline()); + } + + for (RewardPlaceholderHandle handle : plugin.getRewardHandler().getPlaceholders()) { + if (handle.isPreProcess()) { + rewardOptions.addPlaceholder(handle.getKey(), handle.getValue(this, user)); + } + } + + // Check requirements + boolean allowOffline = false; + boolean canGive = true; + if (!rewardOptions.isIgnoreRequirements()) { + for (RequirementInject inject : plugin.getRewardHandler().getInjectedRequirements()) { + try { + plugin.extraDebug(getRewardName() + ": Checking requirement " + inject.getPath() + ":" + + inject.getPriority()); + if (!inject.onRequirementRequest(this, user, getConfig().getConfigData(), rewardOptions)) { + plugin.debug(getRewardName() + ": Requirement failed " + inject.getPath() + ":" + + inject.isAllowReattempt()); + canGive = false; + if (!inject.isAllowReattempt()) { + return; + } + allowOffline = true; + } + } catch (Exception e) { + plugin.debug("Failed to check requirement " + inject.getPath()); + e.printStackTrace(); + canGive = false; + } + } + } + + if (plugin.getOptions().isPauseRewards()) { + checkRewardFile(); + user.addOfflineRewards(this, rewardOptions.getPlaceholders()); + plugin.getLogger() + .info("Rewards are paused, saving offline reward " + getRewardName() + ": " + user.getPlayerName()); + return; + } + + if ((plugin.getOptions().isTreatVanishAsOffline() && user.isVanished())) { + checkRewardFile(); + user.addOfflineRewards(this, rewardOptions.getPlaceholders()); + plugin.getLogger() + .info(getRewardName() + ": " + user.getPlayerName() + " is vanished, saving reward offline"); + return; + } + + // save reward for offline + if (((((!rewardOptions.isOnline() || rewardOptions.getServer() != null) && !user.isOnline()) || allowOffline) + && (!isForceOffline() && !rewardOptions.isForceOffline()))) { + if (rewardOptions.isGiveOffline()) { + checkRewardFile(); + user.addOfflineRewards(this, rewardOptions.getPlaceholders()); + plugin.debug("Saving offline reward " + getRewardName() + " for " + user.getPlayerName()); + } + return; + } + + // give reward + if (canGive || isForceOffline() || rewardOptions.isForceOffline()) { + plugin.debug(name + ": Passed requirements, attempting to give to " + user.getPlayerName() + "/" + + user.getUUID()); + giveRewardUser(user, rewardOptions.getPlaceholders(), rewardOptions); + } + } + + /** + * Give reward user. + * + * @param user the user + * @param phs placeholders + * @param rewardOptions rewardOptions + */ + public void giveRewardUser(AdvancedCoreUser user, HashMap phs, RewardOptions rewardOptions) { + + Player player = user.getPlayer(); + if (player == null) { + player = Bukkit.getPlayer(user.getPlayerName()); + } + if (player != null || isForceOffline()) { + + // placeholders + if (phs == null) { + phs = new HashMap<>(); + } + final String playerName = user.getPlayerName(); + phs.put("player", playerName); + if (player != null) { + phs.put("displayname", player.getDisplayName()); + } + phs.put("@p", playerName); + LocalDateTime ldt = LocalDateTime.now(); + Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + phs.put("CurrentDate", "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(date)); + phs.put("uuid", user.getUUID()); + + for (RewardPlaceholderHandle handle : plugin.getRewardHandler().getPlaceholders()) { + if (!handle.isPreProcess()) { + phs.put(handle.getKey(), handle.getValue(this, user)); + } + } + + final HashMap placeholders = new HashMap<>(phs); + + giveInjectedRewards(user, placeholders); + + plugin.debug("Gave " + user.getPlayerName() + " reward " + name); + } else { + plugin.debug(getRewardName() + ": Player == null & forceoffline false, player: " + user.getPlayerName() + + "/" + user.getUUID()); + } + } + + /** + * Load. + * + * @param folder the folder + * @param reward the reward + */ + public void load(File folder, String reward) { + name = reward; + if (folder.isDirectory()) { + file = new File(folder, reward + ".yml"); + } else { + file = folder; + } + config = new RewardFileData(this, folder); + loadValues(); + } + + public void load(String name, ConfigurationSection section) { + config = new RewardFileData(this, section); + this.name = name; + loadValues(); + } + + public void loadValues() { + forceOffline = getConfig().getForceOffline(); + + setDelayEnabled(getConfig().getDelayedEnabled()); + if (delayEnabled) { + setDelayHours(getConfig().getDelayedHours()); + setDelayMinutes(getConfig().getDelayedMinutes()); + setDelaySeconds(getConfig().getDelayedSeconds()); + setDelayMilliSeconds(getConfig().getDelayedMilliSeconds()); + } + + setTimedEnabled(getConfig().getTimedEnabled()); + if (timedEnabled) { + setTimedHour(getConfig().getTimedHour()); + setTimedMinute(getConfig().getTimedMinute()); + } + + new AnnotationHandler().load(getConfig().getConfigData(), this); + } + + public Reward needsRewardFile(boolean value) { + needsRewardFile = value; + return this; + } + + @SuppressWarnings("deprecation") + private void setRewardFile() { + Reward reward = plugin.getRewardHandler().getRewardDirectlyDefined(name); + ConfigurationSection section = getConfig().getConfigData(); + reward.getConfig().setData(section); + reward.getConfig().getFileData().options() + .header("Directly defined reward file. WRONG PLACE TO EDIT THIS! DO NOT EDIT"); + reward.getConfig().setDirectlyDefinedReward(true); + reward.getConfig().save(reward.getConfig().getFileData()); + } + + public void validate() { + if (getName().equalsIgnoreCase("examplebasic") || getName().equalsIgnoreCase("exampleadvanced")) { + return; + } + for (RequirementInject inject : plugin.getRewardHandler().getInjectedRequirements()) { + inject.validate(this, getConfig().getConfigData()); + } + for (RewardInject inject : plugin.getRewardHandler().getInjectedRewards()) { + inject.validate(this, getConfig().getConfigData()); + } + for (String str : getConfig().getConfigData().getKeys(false)) { + boolean valid = false; + for (RequirementInject inject : plugin.getRewardHandler().getInjectedRequirements()) { + if (inject.hasValidator()) { + if (inject.getValidate().isValid(inject, str)) { + valid = true; + } + } else if (inject.getPath().startsWith(str)) { + valid = true; + } + } + for (RewardInject inject : plugin.getRewardHandler().getInjectedRewards()) { + if (inject.isAlwaysValid()) { + valid = true; + } else { + if (inject.hasValidator()) { + if (inject.getValidate().isValid(inject, str)) { + valid = true; + } + } else if (inject.getPath().startsWith(str)) { + valid = true; + } + } + } + if (plugin.getRewardHandler().getValidPaths().contains(str)) { + valid = true; + } + if (!valid) { + plugin.getLogger().warning(str + " possibly not valid in reward " + getRewardName()); + } + } + } + +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java index 4cb9445372..e41e916ba4 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java @@ -1,1661 +1,1666 @@ -package com.bencodez.advancedcore.api.user; - -import java.text.SimpleDateFormat; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Map.Entry; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -import org.bukkit.Bukkit; -import org.bukkit.Effect; -import org.bukkit.NamespacedKey; -import org.bukkit.OfflinePlayer; -import org.bukkit.Particle; -import org.bukkit.Registry; -import org.bukkit.Sound; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.metadata.MetadataValue; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; - -import com.bencodez.advancedcore.AdvancedCorePlugin; -import com.bencodez.advancedcore.api.item.ItemBuilder; -import com.bencodez.advancedcore.api.messages.PlaceholderUtils; -import com.bencodez.advancedcore.api.misc.PlayerManager; -import com.bencodez.advancedcore.api.misc.effects.BossBar; -import com.bencodez.advancedcore.api.misc.effects.Title; -import com.bencodez.advancedcore.api.rewards.Reward; -import com.bencodez.advancedcore.api.rewards.RewardBuilder; -import com.bencodez.advancedcore.api.rewards.RewardHandler; -import com.bencodez.advancedcore.api.rewards.RewardOptions; -import com.bencodez.advancedcore.api.user.usercache.UserDataCache; -import com.bencodez.simpleapi.array.ArrayUtils; -import com.bencodez.simpleapi.messages.actionbar.ActionBar; -import com.bencodez.simpleapi.player.PlayerUtils; -import com.bencodez.simpleapi.sql.Column; - -import lombok.Getter; -import lombok.Setter; -import net.md_5.bungee.api.chat.BaseComponent; -import net.md_5.bungee.api.chat.TextComponent; -import net.md_5.bungee.chat.ComponentSerializer; - -/** - * The Class User. - */ -public class AdvancedCoreUser { - - /** - * User data fetch mode for this user. - * - * @return the user data fetch mode - */ - @Getter - private UserDataFetchMode userDataFetchMode = UserDataFetchMode.DEFAULT; - - /** - * User data for this user. - * - * @param data the user data to set - */ - @Setter - private UserData data; - - private boolean loadName = true; - - /** The player name. */ - private String playerName; - - /** - * Plugin instance. - * - * @return the plugin instance - */ - @Getter - private AdvancedCorePlugin plugin = null; - - /** The uuid. */ - private String uuid; - - /** - * Instantiates a new user from an existing user. - * - * @param plugin the plugin - * @param user the user to copy from - */ - public AdvancedCoreUser(AdvancedCorePlugin plugin, AdvancedCoreUser user) { - this.userDataFetchMode = user.userDataFetchMode; - this.data = user.getUserData(); - this.uuid = user.getUUID(); - this.playerName = user.getPlayerName(); - this.loadName = user.loadName; - this.plugin = plugin; - loadData(); - getUserData().setTempCache(user.getUserData().getTempCache()); - } - - /** - * Instantiates a new user. - * - * @param plugin the plugin - * @param player the player - */ - @Deprecated - public AdvancedCoreUser(AdvancedCorePlugin plugin, Player player) { - this.plugin = plugin; - loadData(); - uuid = player.getUniqueId().toString(); - setPlayerName(player.getName()); - } - - /** - * Instantiates a new user. - * - * @param plugin the plugin - * @param playerName the player name - */ - @Deprecated - public AdvancedCoreUser(AdvancedCorePlugin plugin, String playerName) { - this.plugin = plugin; - loadData(); - uuid = PlayerManager.getInstance().getUUID(playerName); - setPlayerName(playerName); - } - - /** - * Instantiates a new user. - * - * @param plugin the plugin - * @param uuid the uuid - */ - @Deprecated - public AdvancedCoreUser(AdvancedCorePlugin plugin, UUID uuid) { - this.plugin = plugin; - this.uuid = uuid.toString(); - loadData(); - setPlayerName(PlayerManager.getInstance().getPlayerName(this, this.uuid, false)); - } - - /** - * Instantiates a new user. - * - * @param plugin the plugin - * @param uuid the uuid - * @param loadName the load name - */ - @Deprecated - public AdvancedCoreUser(AdvancedCorePlugin plugin, UUID uuid, boolean loadName) { - this.plugin = plugin; - this.uuid = uuid.toString(); - this.loadName = loadName; - loadData(); - if (this.loadName) { - setPlayerName(PlayerManager.getInstance().getPlayerName(this, this.uuid)); - } - - } - - /** - * Instantiates a new user. - * - * @param plugin the plugin - * @param uuid the uuid - * @param loadName the load name - * @param loadData the load data - */ - @Deprecated - public AdvancedCoreUser(AdvancedCorePlugin plugin, UUID uuid, boolean loadName, boolean loadData) { - this.plugin = plugin; - this.uuid = uuid.toString(); - this.loadName = loadName; - if (loadData) { - loadData(); - } - if (this.loadName) { - setPlayerName(PlayerManager.getInstance().getPlayerName(this, this.uuid)); - } - - } - - /** - * Instantiates a new user. - * - * @param plugin the plugin - * @param uuid the uuid - * @param playerName the player name - */ - @Deprecated - public AdvancedCoreUser(AdvancedCorePlugin plugin, UUID uuid, String playerName) { - this.plugin = plugin; - this.uuid = uuid.toString(); - if (!plugin.getOptions().isOnlineMode()) { - this.uuid = PlayerManager.getInstance().getUUID(playerName); - } - loadData(); - setPlayerName(playerName); - } - - /** - * Adds offline rewards to be given when the player next logs in. - * - * @param reward the reward - * @param placeholders the placeholders - */ - public void addOfflineRewards(Reward reward, HashMap placeholders) { - synchronized (plugin) { - ArrayList offlineRewards = getOfflineRewards(); - offlineRewards.add(reward.getRewardName() + "%placeholders%" + ArrayUtils.makeString(placeholders)); - setOfflineRewards(offlineRewards); - } - } - - /** - * Adds a permission to the player. - * - * @param permission the permission - */ - public void addPermission(String permission) { - plugin.getPermissionHandler().addPermission(getPlayer(), permission); - } - - /** - * Adds a permission to the player with a delay. - * - * @param permission the permission - * @param delay the delay in milliseconds - */ - public void addPermission(String permission, long delay) { - plugin.getPermissionHandler().addPermission(getPlayer(), permission, delay); - } - - /** - * Adds a timed reward to be given at a specific time. - * - * @param reward the reward - * @param placeholders the placeholders - * @param epochMilli the epoch time in milliseconds when the reward should be - * given - */ - public synchronized void addTimedReward(Reward reward, HashMap placeholders, long epochMilli) { - HashMap timed = getTimedRewards(); - String rewardName = reward.getRewardName(); - rewardName += "%extime%" + System.currentTimeMillis(); - - timed.put(rewardName + "%placeholders%" + ArrayUtils.makeString(placeholders), epochMilli); - setTimedRewards(timed); - loadTimedDelayedTimer(epochMilli); - } - - /** - * Adds an unclaimed choice reward. - * - * @param name the reward name - */ - public void addUnClaimedChoiceReward(String name) { - ArrayList choices = getUnClaimedChoices(); - choices.add(name); - setUnClaimedChoice(choices); - } - - /** - * Caches the user data. - */ - public void cache() { - plugin.getUserManager().getDataManager().cacheUser(UUID.fromString(uuid), getPlayerName()); - } - - /** - * Caches the user data asynchronously. - */ - public void cacheAsync() { - getPlugin().getBukkitScheduler().runTaskAsynchronously(plugin, new Runnable() { - - @Override - public void run() { - cache(); - } - }); - } - - /** - * Caches the user data if needed. - */ - public void cacheIfNeeded() { - plugin.getUserManager().getDataManager().cacheUserIfNeeded(UUID.fromString(uuid)); - } - - /** - * Checks and processes delayed/timed rewards. - */ - public void checkDelayedTimedRewards() { - plugin.debug("Checking timed/delayed for " + getPlayerName()); - HashMap timed = getTimedRewards(); - HashMap newTimed = new HashMap<>(); - for (Entry entry : timed.entrySet()) { - long time = entry.getValue(); - - if (time != 0) { - Date timeDate = new Date(time); - if (new Date().after(timeDate)) { - String[] data = entry.getKey().split("%placeholders%"); - String rewardName = data[0]; - rewardName = rewardName.split("%extime%")[0]; - String placeholders = ""; - if (data.length > 1) { - placeholders = data[1]; - } - new RewardBuilder(plugin.getRewardHandler().getReward(rewardName)).setCheckTimed(false) - .withPlaceHolder(ArrayUtils.fromString(placeholders)) - .withPlaceHolder("date", - "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(new Date(time))) - .send(this); - plugin.debug("Giving timed/delayed reward " + rewardName + " for " + getPlayerName() - + " with placeholders " + ArrayUtils.fromString(placeholders)); - } else { - newTimed.put(entry.getKey(), time); - } - } - - } - setTimedRewards(newTimed); - } - - /** - * Check offline rewards. - */ - public void checkOfflineRewards() { - if (!plugin.getOptions().isProcessRewards()) { - plugin.debug("Processing rewards is disabled"); - return; - } - if (isCheckWorld()) { - setCheckWorld(false); - } - ArrayList rewards = getOfflineRewards(); - if (rewards.isEmpty()) { - return; - } - - setOfflineRewards(new ArrayList<>()); - RewardHandler rewardHandler = plugin.getRewardHandler(); - AdvancedCoreUser user = this; - - for (String rewardEntry : rewards) { - if (rewardEntry == null || rewardEntry.equals("null")) { - continue; - } - - String[] parts = rewardEntry.split("%placeholders%", 2); - String rewardName = parts[0]; - String placeholderStr = parts.length > 1 ? parts[1] : ""; - - RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false) - .withPlaceHolder(ArrayUtils.fromString(placeholderStr)); - - rewardHandler.giveReward(user, rewardName, options); - } - - } - - /** - * Clears the user cache. - */ - public void clearCache() { - if (isCached()) { - getCache().clearCache(); - } - } - - /** - * Clears the temporary cache. - */ - public void clearTempCache() { - getUserData().clearTempCache(); - } - - /** - * Closes the player's inventory. - */ - public void closeInv() { - if (plugin.isEnabled()) { - getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { - - @Override - public void run() { - Player player = getPlayer(); - if (player != null) { - player.closeInventory(); - } - } - }, getPlayer()); - } - } - - /** - * Sets the user data fetch mode. - * - * @param mode the fetch mode - * @return this user instance - */ - public AdvancedCoreUser userDataFetechMode(UserDataFetchMode mode) { - this.userDataFetchMode = mode; - return this; - } - - /** - * Forces running of offline rewards without processing checks. - */ - public void forceRunOfflineRewards() { - if (!plugin.getOptions().isProcessRewards()) { - plugin.debug("Processing rewards is disabled"); - return; - } - - setCheckWorld(false); - ArrayList rewards = getOfflineRewards(); - if (rewards.isEmpty()) { - return; - } - - setOfflineRewards(new ArrayList<>()); - RewardHandler rewardHandler = plugin.getRewardHandler(); - AdvancedCoreUser user = this; - - for (String rewardEntry : rewards) { - if (rewardEntry == null || rewardEntry.equals("null")) { - continue; - } - - String[] parts = rewardEntry.split("%placeholders%", 2); - String rewardName = parts[0]; - String placeholderStr = parts.length > 1 ? parts[1] : ""; - - RewardOptions options = new RewardOptions().setOnline(false).setGiveOffline(false).forceOffline() - .setCheckTimed(false).withPlaceHolder(ArrayUtils.fromString(placeholderStr)); - - rewardHandler.giveReward(user, rewardName, options); - } - } - - /** - * Gets the user data cache. - * - * @return the cache - */ - public UserDataCache getCache() { - return plugin.getUserManager().getDataManager().getCache(java.util.UUID.fromString(getUUID())); - } - - /** - * Gets the user's choice preference for a reward. - * - * @param rewardName the reward name - * @return the choice preference - */ - public String getChoicePreference(String rewardName) { - ArrayList data = getChoicePreferenceData(); - - for (String str : data) { - String[] data1 = str.split(":"); - if (data1.length > 1) { - if (data1[0].equals(rewardName)) { - return data1[1]; - } - } - } - return ""; - } - - /** - * Gets the choice preference data. - * - * @return the choice preference data list - */ - public ArrayList getChoicePreferenceData() { - return getData().getStringList("ChoicePreference", userDataFetchMode); - } - - /** - * Gets the user data. - * - * @return the user data - */ - public UserData getData() { - if (data == null) { - loadData(); - } - return data; - } - - /** - * Gets the input method. - * - * @return the input method string - */ - public String getInputMethod() { - return getUserData().getString("InputMethod", userDataFetchMode); - } - - /** - * Gets the Java UUID object. - * - * @return the Java UUID - */ - public UUID getJavaUUID() { - return UUID.fromString(uuid); - } - - /** - * Gets the last online time in milliseconds. - * - * @return the last online time - */ - public long getLastOnline() { - String d = getData().getString("LastOnline", userDataFetchMode); - long time = 0; - if (d != null && !d.equals("") && !d.equals("null")) { - time = Long.valueOf(d); - } - if (time == 0 && getPlugin().getOptions().isOnlineMode()) { - OfflinePlayer player = getOfflinePlayer(); - if (player != null) { - time = player.getLastPlayed(); - if (time > 0) { - setLastOnline(time); - } - } - - } - return time; - } - - /** - * Gets the number of days since the player's last login. - * - * @return the number of days since login - */ - public int getNumberOfDaysSinceLogin() { - long time = getLastOnline(); - if (time > 0) { - LocalDateTime online = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()); - LocalDateTime now = LocalDateTime.now(); - Duration dur = Duration.between(online, now); - return (int) dur.toDays(); - } - - return -1; - } - - /** - * Gets the offline player. - * - * @return the offline player - */ - @SuppressWarnings("deprecation") - public OfflinePlayer getOfflinePlayer() { - if (getPlayerName().isBlank()) { - return null; - } - if (!plugin.getOptions().isOnlineMode()) { - return Bukkit.getOfflinePlayer(getPlayerName()); - } - if (uuid != null && !uuid.equals("")) { - return Bukkit.getOfflinePlayer(java.util.UUID.fromString(uuid)); - } - return null; - } - - /** - * Gets the offline rewards list. - * - * @return the offline rewards - */ - public ArrayList getOfflineRewards() { - return getUserData().getStringList(plugin.getUserManager().getOfflineRewardsPath(), userDataFetchMode); - } - - /** - * Gets the player. - * - * @return the player - */ - public Player getPlayer() { - if (!plugin.getOptions().isOnlineMode()) { - return Bukkit.getPlayer(getPlayerName()); - } - if (uuid != null && !uuid.isEmpty()) { - return Bukkit.getPlayer(java.util.UUID.fromString(uuid)); - } - return null; - } - - /** - * Gets the player's head as an ItemStack. - * - * @return the player head - */ - public ItemStack getPlayerHead() { - return PlayerManager.getInstance().getPlayerSkull(getJavaUUID(), getPlayerName(), false); - } - - /** - * Gets the player name. - * - * @return the player name - */ - public String getPlayerName() { - if (playerName != null) { - return playerName; - } - if (userDataFetchMode.allowTempCache()) { - return getUserData().getString("PlayerName", UserDataFetchMode.TEMP_ONLY); - } - return ""; - } - - /** - * Gets the repeat amount for a reward. - * - * @param reward the reward - * @return the repeat amount - */ - public int getRepeatAmount(Reward reward) { - return getData().getInt("Repeat" + reward.getName(), userDataFetchMode); - } - - /** - * Gets the timed rewards map. - * - * @return the timed rewards - */ - public HashMap getTimedRewards() { - ArrayList timedReward = getUserData().getStringList("TimedRewards", userDataFetchMode); - HashMap timedRewards = new HashMap<>(); - for (String str : timedReward) { - if (str != null && !str.equals("null")) { - String[] data = str.split("%ExecutionTime/%"); - plugin.extraDebug("TimedReward: " + str); - if (data.length > 1) { - String name = data[0]; - - String timeStr = data[1]; - timedRewards.put(name, Long.valueOf(timeStr)); - } - } - } - return timedRewards; - } - - /** - * Gets the unclaimed choices list. - * - * @return the unclaimed choices - */ - public ArrayList getUnClaimedChoices() { - return getData().getStringList("UnClaimedChoices", userDataFetchMode); - } - - /** - * Gets the user data instance. - * - * @return the user data - */ - public UserData getUserData() { - if (data == null) { - loadData(); - } - return data; - } - - /** - * Gets the uuid. - * - * @return the uuid - */ - public String getUUID() { - return uuid; - } - - /** - * Give exp. - * - * @param exp the exp - */ - public void giveExp(int exp) { - Player player = getPlayer(); - if (player != null) { - player.giveExp(exp); - } - } - - /** - * Gives experience levels to the player. - * - * @param num the number of levels to give - */ - public void giveExpLevels(int num) { - Player p = getPlayer(); - if (p != null) { - p.setLevel(p.getLevel() + num); - } - } - - /** - * Gives an item to the player from an ItemBuilder. - * - * @param builder the item builder - */ - public void giveItem(ItemBuilder builder) { - giveItem(builder.toItemStack(getPlayer())); - } - - /** - * Give item. - * - * @param item the item - */ - public void giveItem(ItemStack item) { - if ((item == null) || (item.getAmount() == 0)) { - return; - } - - final Player player = getPlayer(); - - if (plugin.isEnabled()) { - getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { - - @Override - public void run() { - if (player != null) { - plugin.getFullInventoryHandler().giveItem(player, item); - } - } - }, player); - } - - } - - /** - * Gives an item to the player with placeholders. - * - * @param itemStack the item stack - * @param placeholders the placeholders - */ - public void giveItem(ItemStack itemStack, HashMap placeholders) { - giveItem(new ItemBuilder(itemStack).setPlaceholders(placeholders).toItemStack(getPlayer())); - } - - /** - * Gives multiple items to the player. - * - * @param item the items to give - */ - public void giveItems(ItemStack... item) { - if (item == null) { - return; - } - - final Player player = getPlayer(); - - if (plugin.isEnabled()) { - getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { - - @Override - public void run() { - if (player != null) { - plugin.getFullInventoryHandler().giveItem(player, item); - } - } - }, player); - } - - } - - /** - * Give user money, needs vault installed - * - * @param m Amount of money to give - */ - public void giveMoney(double m) { - if (!plugin.isEnabled()) { - return; - } - if (plugin.getVaultHandler() != null && plugin.getVaultHandler().getEcon() != null) { - try { - if (m > 0) { - final double money = m; - getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { - - @Override - public void run() { - plugin.getVaultHandler().getEcon().depositPlayer(getOfflinePlayer(), money); - } - }); - - } else if (m < 0) { - m = m * -1; - final double money = m; - getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { - - @Override - public void run() { - plugin.getVaultHandler().getEcon().withdrawPlayer(getOfflinePlayer(), money); - } - }); - - } - } catch ( - - IllegalStateException e) { - e.printStackTrace(); - } - } - } - - /** - * Give money. - * - * @param money the money - */ - public void giveMoney(int money) { - giveMoney((double) money); - } - - /** - * Give potion effect. - * - * @param potionName the potion name - * @param duration the duration - * @param amplifier the amplifier - */ - public void givePotionEffect(String potionName, int duration, int amplifier) { - Player player = getPlayer(); - if (player != null && plugin.isEnabled()) { - getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { - - @SuppressWarnings("deprecation") - @Override - public void run() { - player.addPotionEffect( - new PotionEffect(PotionEffectType.getByName(potionName), 20 * duration, amplifier)); - } - }, player); - - } - } - - /** - * Gives a reward from a configuration file. - * - * @param data the configuration data - * @param path the path to the reward - * @param rewardOptions the reward options - */ - public void giveReward(FileConfiguration data, String path, RewardOptions rewardOptions) { - plugin.getRewardHandler().giveReward(this, data, path, rewardOptions); - } - - /** - * Gives a reward to the user. - * - * @param reward the reward - * @param rewardOptions the reward options - */ - public void giveReward(Reward reward, RewardOptions rewardOptions) { - reward.giveReward(this, rewardOptions); - } - - /** - * Checks if the user has unclaimed choices. - * - * @return true if the user has choices - */ - public boolean hasChoices() { - return getUnClaimedChoices().size() > 0; - } - - /** - * Check if player joined before - * - * @return true, if successful - */ - public boolean hasLoggedOnBefore() { - OfflinePlayer player = Bukkit.getOfflinePlayer(java.util.UUID.fromString(uuid)); - if (player != null) { - if (player.hasPlayedBefore() || player.isOnline()) { - return true; - } - - } - ArrayList uuids = plugin.getUserManager().getAllUUIDs(); - if (uuids.contains(getUUID())) { - return true; - } - return false; - } - - /** - * Checks if the user has a permission. - * - * @param perm the permission - * @return true if the user has the permission - */ - public boolean hasPermission(String perm) { - return hasPermission(perm, true); - } - - /** - * Checks if the user has a permission. - * - * @param perm the permission - * @param offlineCheck whether to check offline permissions - * @return true if the user has the permission - */ - public boolean hasPermission(String perm, boolean offlineCheck) { - boolean negate = perm != null && perm.startsWith("!"); - if (negate) { - perm = perm.substring(1); - } - - Player player = getPlayer(); - - // Online fast path - if (player != null) { - boolean has = player.hasPermission(perm); - return negate ? !has : has; - } - - if (!offlineCheck) { - plugin.debug("Unable to get player for permission check for " + getPlayerName() + "/" + getUUID() - + " (offlineCheck is false)"); - return false; - } - // Offline path: LuckPerms (if available) - if (plugin.getLuckPermsHandle() != null && plugin.getLuckPermsHandle().luckpermsApiLoaded()) { - boolean has = plugin.getLuckPermsHandle().hasPermission(getJavaUUID(), perm); - return negate ? !has : has; - } - - plugin.debug("Unable to get player for permission check for " + getPlayerName() + "/" + getUUID() - + " (offline and no LuckPerms hook)"); - return false; - } - - /** - * Checks if the player is banned. - * - * @return true if the player is banned - */ - public boolean isBanned() { - if (plugin.getBannedPlayers().contains(getUUID())) { - return true; - } - return false; - } - - /** - * Checks if the player is a Bedrock user. - * - * @return true if the player is a Bedrock user - */ - public boolean isBedrockUser() { - return getData().getBoolean("isBedrock", userDataFetchMode); - } - - /** - * Sets whether the player is a Bedrock user. - * - * @param isBedrock true if the player is a Bedrock user - */ - public void setBedrockUser(boolean isBedrock) { - getData().setBoolean("isBedrock", isBedrock); - } - - /** - * Checks if the user data is cached. - * - * @return true if cached - */ - public boolean isCached() { - return plugin.getUserManager().getDataManager().isCached(UUID.fromString(uuid)); - } - - /** - * Checks if world checking is enabled for the user. - * - * @return true if world checking is enabled - */ - public boolean isCheckWorld() { - if (!plugin.isLoadUserData()) { - return false; - } - return Boolean.valueOf(getData().getString("CheckWorld", userDataFetchMode)); - } - - /** - * Checks if the player is in any of the specified worlds. - * - * @param worlds the list of world names - * @return true if the player is in one of the worlds - */ - public boolean isInWorld(ArrayList worlds) { - Player p = getPlayer(); - if (p != null) { - for (String world : worlds) { - if (p.getWorld().getName().equalsIgnoreCase(world)) { - return true; - } - } - } - - return false; - } - - /** - * Checks if the player is in a specific world. - * - * @param world the world name - * @return true if the player is in the world - */ - public boolean isInWorld(String world) { - Player p = getPlayer(); - if (p != null) { - return p.getWorld().getName().equalsIgnoreCase(world); - } - - return false; - } - - /** - * Checks if is online. - * - * @return true, if is online - */ - public boolean isOnline() { - boolean online = PlayerUtils.isPlayerOnline(getPlayerName()); - if (!online) { - return false; - } - if (plugin.getOptions().isTreatVanishAsOffline()) { - if (isVanished()) { - return false; - } - } - return true; - } - - /** - * Checks if the player is vanished. - * - * @return true if the player is vanished - */ - public boolean isVanished() { - Player player = getPlayer(); - if (player != null) { - for (MetadataValue meta : player.getMetadata("vanished")) { - if (meta.asBoolean()) { - return true; - } - } - - try { - try { - if (plugin.getCmiHandle() != null) { - return plugin.getCmiHandle().isVanished(player); - } - } catch (Exception e) { - } - } catch (Exception e) { - plugin.debug(e); - } - } - return false; - } - - /** - * Loads the user cache. - */ - public void loadCache() { - plugin.getUserManager().getDataManager().cacheUser(UUID.fromString(uuid), getPlayerName()); - } - - /** - * Loads the user data. - */ - public void loadData() { - data = new UserData(this); - } - - /** - * Loads a timer for delayed/timed rewards. - * - * @param time the time in milliseconds - */ - public void loadTimedDelayedTimer(long time) { - long delay = time - System.currentTimeMillis(); - if (delay < 0) { - delay = 0; - } - delay += 500; - plugin.getRewardHandler().getDelayedTimer().schedule(new Runnable() { - - @Override - public void run() { - checkDelayedTimedRewards(); - } - }, delay, TimeUnit.MILLISECONDS); - } - - /** - * Play particle effect. - * - * @param effectName the effect name - * @param data the data - * @param particles the particles - * @param radius the radius - */ - public void playEffect(String effectName, int data, int particles, int radius) { - Player player = getPlayer(); - if ((player != null) && (effectName != null)) { - try { - Effect effect = Effect.valueOf(effectName); - for (int i = 0; i < particles; i++) { - player.getWorld().playEffect(player.getLocation(), effect, data, radius); - } - } catch (Exception e) { - e.printStackTrace(); - } - - } - } - - /** - * Plays a particle effect for the player. - * - * @param effectName the particle effect name - * @param data the data value - * @param particles the number of particles - * @param radius the radius - */ - public void playParticle(String effectName, int data, int particles, int radius) { - Player player = getPlayer(); - if ((player != null) && (effectName != null)) { - try { - Particle effect = Particle.valueOf(effectName); - for (int i = 0; i < particles; i++) { - player.getWorld().spawnParticle(effect, player.getLocation(), particles, radius, radius, radius, - data); - } - - } catch (Exception e) { - plugin.getLogger().warning( - "Failed to create particle: " + effectName + ", " + data + ", " + particles + ", " + radius); - e.printStackTrace(); - } - } - } - - /** - * Plays a particle effect for the player. - * - * @param effectName the particle effect name - * @param data the data value - * @param particles the number of particles - * @param radius the radius - */ - @Deprecated - public void playParticleEffect(String effectName, int data, int particles, int radius) { - playParticle(effectName, data, particles, radius); - } - - /** - * Play sound. - * - * @param soundName the sound name - * @param volume the volume - * @param pitch the pitch - */ - public void playSound(String soundName, float volume, float pitch) { - Player player = getPlayer(); - if (player != null) { - Sound sound = null; - try { - sound = Registry.SOUNDS.get(NamespacedKey.minecraft(soundName)); - } catch (Exception e) { - plugin.debug(e); - } - if (sound != null) { - player.playSound(player.getLocation(), sound, volume, pitch); - } else { - plugin.debug("Invalid sound: " + soundName); - } - } - } - - /** - * Performs commands as the player with placeholders. - * - * @param commands the list of commands - * @param placeholders the placeholders - */ - public void preformCommand(ArrayList commands, HashMap placeholders) { - if (commands != null && !commands.isEmpty()) { - final ArrayList cmds = PlaceholderUtils.replaceJavascript(getPlayer(), - PlaceholderUtils.replacePlaceHolder(commands, placeholders)); - - final Player player = getPlayer(); - if (player != null && plugin.isEnabled()) { - for (final String cmd : cmds) { - plugin.debug("Executing player command for " + getPlayerName() + ": " + cmd); - getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { - - @Override - public void run() { - player.chat("/" + cmd); - } - }); - } - } - } - } - - /** - * Performs a command as the player with placeholders. - * - * @param command the command - * @param placeholders the placeholders - */ - public void preformCommand(String command, HashMap placeholders) { - if (command != null && !command.isEmpty()) { - final String cmd = PlaceholderUtils.replaceJavascript(getPlayer(), - PlaceholderUtils.replacePlaceHolder(command, placeholders)); - plugin.debug("Executing player command for " + getPlayerName() + ": " + command); - if (plugin.isEnabled()) { - getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { - - @Override - public void run() { - Player player = getPlayer(); - if (player != null) { - player.chat("/" + cmd); - } - } - }); - } - } - } - - /** - * Removes the user from storage. - */ - public void remove() { - plugin.debug("Removing " + getUUID() + " (" + getPlayerName() + ") from storage..."); - getData().remove(); - } - - /** - * Removes a permission from the player. - * - * @param permission the permission - */ - public void removePermission(String permission) { - plugin.getPermissionHandler().removePermission(UUID.fromString(getUUID()), getPlayerName(), permission); - } - - /** - * Removes an unclaimed choice reward. - * - * @param name the reward name - */ - public void removeUnClaimedChoiceReward(String name) { - ArrayList choices = getUnClaimedChoices(); - choices.remove(name); - setUnClaimedChoice(choices); - } - - /** - * Send action bar. - * - * @param msg the msg - * @param delay the delay - */ - public void sendActionBar(String msg, int delay) { - // plugin.debug("attempting to send action bar"); - if (msg != null && msg != "") { - Player player = getPlayer(); - if (player != null) { - - try { - ActionBar actionBar = new ActionBar(PlaceholderUtils.replaceJavascript(getPlayer(), msg), delay); - actionBar.send(player); - } catch (Exception ex) { - plugin.debug("Failed to send ActionBar, turn debug on to see stack trace"); - plugin.debug(ex); - } - } - } - } - - /** - * Send boss bar. - * - * @param msg the msg - * @param color the color - * @param style the style - * @param progress the progress - * @param delay the delay - */ - public void sendBossBar(String msg, String color, String style, double progress, int delay) { - if (msg != null && msg != "") { - Player player = getPlayer(); - if (player != null) { - try { - BossBar bossBar = new BossBar(PlaceholderUtils.replaceJavascript(getPlayer(), msg), color, style, - progress); - bossBar.send(player, delay); - } catch (Exception ex) { - plugin.debug("Failed to send BossBar"); - plugin.debug(ex); - } - } - } - } - - /** - * Send json. - * - * @param messages the messages - */ - public void sendJson(ArrayList messages) { - sendJson(messages, true); - } - - /** - * Sends JSON messages to the player. - * - * @param messages the text component messages - * @param javascript whether to process javascript placeholders - */ - public void sendJson(ArrayList messages, boolean javascript) { - Player player = getPlayer(); - if ((player != null) && (messages != null)) { - ArrayList texts = new ArrayList<>(); - TextComponent newLine = new TextComponent(ComponentSerializer.parse("{text: \"\n\"}")); - for (int i = 0; i < messages.size(); i++) { - TextComponent txt = messages.get(i); - if (javascript) { - txt.setText(PlaceholderUtils.replaceJavascript(getPlayer(), txt.getText())); - } - texts.add(txt); - if (i + 1 < messages.size()) { - texts.add(newLine); - } - - } - - PlayerUtils.getServerHandle().sendMessage(player, ArrayUtils.convertBaseComponent(texts)); - } - - } - - /** - * Send json. - * - * @param message the message - */ - public void sendJson(TextComponent message) { - Player player = getPlayer(); - if ((player != null) && (message != null)) { - message.setText(PlaceholderUtils.replaceJavascript(getPlayer(), message.getText())); - PlayerUtils.getServerHandle().sendMessage(player, message); - } - } - - /** - * Send message. - * - * @param msg the msg - */ - public void sendMessage(ArrayList msg) { - sendMessage(ArrayUtils.convert(msg)); - } - - /** - * Sends a message with placeholders to the player. - * - * @param msg the message list - * @param placeholders the placeholders - */ - public void sendMessage(ArrayList msg, HashMap placeholders) { - sendMessage(ArrayUtils.convert(PlaceholderUtils.replacePlaceHolder(msg, placeholders))); - } - - /** - * Send message. - * - * @param msg the msg - */ - public void sendMessage(String msg) { - Player player = getPlayer(); - if ((player != null) && (msg != null)) { - if (!msg.equals("")) { - for (String str : msg.split("%NewLine%")) { - PlayerUtils.getServerHandle().sendMessage(player, - PlaceholderUtils.parseJson(PlaceholderUtils.parseText(player, str))); - } - } - } - } - - /** - * Sends a message with placeholders to the player. - * - * @param msg the message - * @param placeholders the placeholders - */ - public void sendMessage(String msg, HashMap placeholders) { - sendMessage(PlaceholderUtils.replacePlaceHolder(msg, placeholders)); - } - - /** - * Sends a message with a single placeholder replacement to the player. - * - * @param msg the message - * @param toReplace the placeholder to replace - * @param replace the replacement value - */ - public void sendMessage(String msg, String toReplace, String replace) { - sendMessage(PlaceholderUtils.replacePlaceHolder(msg, toReplace, replace)); - } - - /** - * Send message. - * - * @param msg the msg - */ - public void sendMessage(String[] msg) { - Player player = getPlayer(); - if ((player != null) && (msg != null)) { - - ArrayList texts = new ArrayList<>(); - for (String str : msg) { - if ((player != null) && (msg != null)) { - if (!str.equals("")) { - for (String str1 : str.split("%NewLine%")) { - TextComponent text = PlaceholderUtils.parseJson(PlaceholderUtils.parseText(player, str1)); - text.setText(PlaceholderUtils.replaceJavascript(getPlayer(), text.getText())); - texts.add(text); - } - } - - } - } - if (texts.size() > 0) { - sendJson(texts, false); - } - } - - } - - /** - * Send title. - * - * @param title the title - * @param subTitle the sub title - * @param fadeIn the fade in - * @param showTime the show time - * @param fadeOut the fade out - */ - public void sendTitle(String title, String subTitle, int fadeIn, int showTime, int fadeOut) { - Player player = getPlayer(); - if (player != null) { - try { - Title titleObject = new Title(PlaceholderUtils.replaceJavascript(getPlayer(), title), - PlaceholderUtils.replaceJavascript(getPlayer(), subTitle), fadeIn, showTime, fadeOut); - titleObject.send(player); - } catch (Exception ex) { - plugin.getLogger().info("Failed to send Title, turn debug on to see stack trace"); - plugin.debug(ex); - } - } - } - - /** - * Sets whether to check the world for the user. - * - * @param b true to enable world checking - */ - public void setCheckWorld(boolean b) { - getData().setString("CheckWorld", "" + b); - } - - /** - * Sets the user's choice preference for a reward. - * - * @param reward the reward name - * @param preference the preference - */ - public void setChoicePreference(String reward, String preference) { - ArrayList data = getChoicePreferenceData(); - ArrayList choices = new ArrayList<>(); - - boolean added = false; - for (String str : data) { - String[] data1 = str.split(":"); - if (data1.length > 1) { - if (data1[0].equals(reward)) { - choices.add(reward + ":" + preference); - added = true; - } else { - choices.add(str); - } - } - } - if (!added) { - choices.add(reward + ":" + preference); - } - getData().setStringList("ChoicePreference", choices); - } - - /** - * Sets the input method for the user. - * - * @param inputMethod the input method - */ - public void setInputMethod(String inputMethod) { - data.setString("InputMethod", inputMethod); - } - - /** - * Sets the last online time for the user. - * - * @param online the last online time in milliseconds - */ - public void setLastOnline(long online) { - getData().setString("LastOnline", "" + online); - } - - /** - * Sets the offline rewards list. - * - * @param offlineRewards the offline rewards - */ - public void setOfflineRewards(ArrayList offlineRewards) { - // MySQL TEXT max length is 65535 bytes - int maxLength = 65535; - String str = String.join("%line%", offlineRewards); - - // Remove oldest rewards until within limit - while (str.getBytes().length > maxLength && !offlineRewards.isEmpty()) { - offlineRewards.remove(0); - str = String.join("%line%", offlineRewards); - } - - data.setStringList(plugin.getUserManager().getOfflineRewardsPath(), offlineRewards); - } - - /** - * Sets the player name. - * - * @param playerName the player name - */ - public void setPlayerName(String playerName) { - this.playerName = playerName; - } - - /** - * Sets the repeat amount for a reward. - * - * @param reward the reward - * @param amount the repeat amount - */ - public void setRepeatAmount(Reward reward, int amount) { - getData().setInt("Repeat" + reward.getName(), amount); - } - - /** - * Sets the timed rewards map. - * - * @param timed the timed rewards - */ - public void setTimedRewards(HashMap timed) { - ArrayList timedRewards = new ArrayList<>(); - for (Entry entry : timed.entrySet()) { - - String str = ""; - str += entry.getKey() + "%ExecutionTime/%"; - str += entry.getValue(); - timedRewards.add(str); - - } - data.setStringList("TimedRewards", timedRewards); - } - - /** - * Sets the unclaimed choice rewards list. - * - * @param rewards the unclaimed rewards - */ - public void setUnClaimedChoice(ArrayList rewards) { - getData().setStringList("UnClaimedChoices", rewards); - } - - /** - * Sets the user to not cache data. - */ - @Deprecated - public void dontCache() { - userDataFetchMode = UserDataFetchMode.NO_CACHE; - } - - /** - * Sets the uuid. - * - * @param uuid the new uuid - */ - public void setUUID(String uuid) { - this.uuid = uuid; - } - - /** - * Enables temporary caching for this user. - * - * @return this user instance - */ - public AdvancedCoreUser tempCache() { - getUserData().tempCache(); - return this; - } - - /** - * Updates the player name in storage. - * - * @param force whether to force the update - */ - public void updateName(boolean force) { - if (getData().hasData() || force) { - String playerName = getData().getString("PlayerName", userDataFetchMode); - if (playerName == null || !playerName.equals(getPlayerName())) { - getData().setString("PlayerName", getPlayerName(), true); - } - } - } - - /** - * Updates the temporary cache with specific columns. - * - * @param cols the columns to update - */ - public void updateTempCacheWithColumns(ArrayList cols) { - getUserData().updateTempCacheWithColumns(cols); - } - +package com.bencodez.advancedcore.api.user; + +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.bukkit.Bukkit; +import org.bukkit.Effect; +import org.bukkit.NamespacedKey; +import org.bukkit.OfflinePlayer; +import org.bukkit.Particle; +import org.bukkit.Registry; +import org.bukkit.Sound; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.metadata.MetadataValue; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.item.ItemBuilder; +import com.bencodez.advancedcore.api.messages.PlaceholderUtils; +import com.bencodez.advancedcore.api.misc.PlayerManager; +import com.bencodez.advancedcore.api.misc.effects.BossBar; +import com.bencodez.advancedcore.api.misc.effects.Title; +import com.bencodez.advancedcore.api.rewards.Reward; +import com.bencodez.advancedcore.api.rewards.RewardBuilder; +import com.bencodez.advancedcore.api.rewards.RewardHandler; +import com.bencodez.advancedcore.api.rewards.RewardOptions; +import com.bencodez.advancedcore.api.user.usercache.UserDataCache; +import com.bencodez.simpleapi.array.ArrayUtils; +import com.bencodez.simpleapi.messages.actionbar.ActionBar; +import com.bencodez.simpleapi.player.PlayerUtils; +import com.bencodez.simpleapi.sql.Column; + +import lombok.Getter; +import lombok.Setter; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.chat.ComponentSerializer; + +/** + * The Class User. + */ +public class AdvancedCoreUser { + + /** + * User data fetch mode for this user. + * + * @return the user data fetch mode + */ + @Getter + private UserDataFetchMode userDataFetchMode = UserDataFetchMode.DEFAULT; + + /** + * User data for this user. + * + * @param data the user data to set + */ + @Setter + private UserData data; + + private boolean loadName = true; + + /** The player name. */ + private String playerName; + + /** + * Plugin instance. + * + * @return the plugin instance + */ + @Getter + private AdvancedCorePlugin plugin = null; + + /** The uuid. */ + private String uuid; + + /** + * Instantiates a new user from an existing user. + * + * @param plugin the plugin + * @param user the user to copy from + */ + public AdvancedCoreUser(AdvancedCorePlugin plugin, AdvancedCoreUser user) { + this.userDataFetchMode = user.userDataFetchMode; + this.data = user.getUserData(); + this.uuid = user.getUUID(); + this.playerName = user.getPlayerName(); + this.loadName = user.loadName; + this.plugin = plugin; + loadData(); + getUserData().setTempCache(user.getUserData().getTempCache()); + } + + /** + * Instantiates a new user. + * + * @param plugin the plugin + * @param player the player + */ + @Deprecated + public AdvancedCoreUser(AdvancedCorePlugin plugin, Player player) { + this.plugin = plugin; + loadData(); + uuid = player.getUniqueId().toString(); + setPlayerName(player.getName()); + } + + /** + * Instantiates a new user. + * + * @param plugin the plugin + * @param playerName the player name + */ + @Deprecated + public AdvancedCoreUser(AdvancedCorePlugin plugin, String playerName) { + this.plugin = plugin; + loadData(); + uuid = PlayerManager.getInstance().getUUID(playerName); + setPlayerName(playerName); + } + + /** + * Instantiates a new user. + * + * @param plugin the plugin + * @param uuid the uuid + */ + @Deprecated + public AdvancedCoreUser(AdvancedCorePlugin plugin, UUID uuid) { + this.plugin = plugin; + this.uuid = uuid.toString(); + loadData(); + setPlayerName(PlayerManager.getInstance().getPlayerName(this, this.uuid, false)); + } + + /** + * Instantiates a new user. + * + * @param plugin the plugin + * @param uuid the uuid + * @param loadName the load name + */ + @Deprecated + public AdvancedCoreUser(AdvancedCorePlugin plugin, UUID uuid, boolean loadName) { + this.plugin = plugin; + this.uuid = uuid.toString(); + this.loadName = loadName; + loadData(); + if (this.loadName) { + setPlayerName(PlayerManager.getInstance().getPlayerName(this, this.uuid)); + } + + } + + /** + * Instantiates a new user. + * + * @param plugin the plugin + * @param uuid the uuid + * @param loadName the load name + * @param loadData the load data + */ + @Deprecated + public AdvancedCoreUser(AdvancedCorePlugin plugin, UUID uuid, boolean loadName, boolean loadData) { + this.plugin = plugin; + this.uuid = uuid.toString(); + this.loadName = loadName; + if (loadData) { + loadData(); + } + if (this.loadName) { + setPlayerName(PlayerManager.getInstance().getPlayerName(this, this.uuid)); + } + + } + + /** + * Instantiates a new user. + * + * @param plugin the plugin + * @param uuid the uuid + * @param playerName the player name + */ + @Deprecated + public AdvancedCoreUser(AdvancedCorePlugin plugin, UUID uuid, String playerName) { + this.plugin = plugin; + this.uuid = uuid.toString(); + if (!plugin.getOptions().isOnlineMode()) { + this.uuid = PlayerManager.getInstance().getUUID(playerName); + } + loadData(); + setPlayerName(playerName); + } + + /** + * Adds offline rewards to be given when the player next logs in. + * + * @param reward the reward + * @param placeholders the placeholders + */ + public void addOfflineRewards(Reward reward, HashMap placeholders) { + synchronized (plugin) { + ArrayList offlineRewards = getOfflineRewards(); + offlineRewards.add(reward.getRewardName() + "%placeholders%" + ArrayUtils.makeString(placeholders)); + setOfflineRewards(offlineRewards); + } + } + + /** + * Adds a permission to the player. + * + * @param permission the permission + */ + public void addPermission(String permission) { + plugin.getPermissionHandler().addPermission(getPlayer(), permission); + } + + /** + * Adds a permission to the player with a delay. + * + * @param permission the permission + * @param delay the delay in milliseconds + */ + public void addPermission(String permission, long delay) { + plugin.getPermissionHandler().addPermission(getPlayer(), permission, delay); + } + + /** + * Adds a timed reward to be given at a specific time. + * + * @param reward the reward + * @param placeholders the placeholders + * @param epochMilli the epoch time in milliseconds when the reward should be + * given + */ + public synchronized void addTimedReward(Reward reward, HashMap placeholders, long epochMilli) { + HashMap timed = getTimedRewards(); + String rewardName = reward.getRewardName(); + rewardName += "%extime%" + System.currentTimeMillis(); + + timed.put(rewardName + "%placeholders%" + ArrayUtils.makeString(placeholders), epochMilli); + setTimedRewards(timed); + loadTimedDelayedTimer(epochMilli); + } + + /** + * Adds an unclaimed choice reward. + * + * @param name the reward name + */ + public void addUnClaimedChoiceReward(String name) { + ArrayList choices = getUnClaimedChoices(); + choices.add(name); + setUnClaimedChoice(choices); + } + + /** + * Caches the user data. + */ + public void cache() { + plugin.getUserManager().getDataManager().cacheUser(UUID.fromString(uuid), getPlayerName()); + } + + /** + * Caches the user data asynchronously. + */ + public void cacheAsync() { + getPlugin().getBukkitScheduler().runTaskAsynchronously(plugin, new Runnable() { + + @Override + public void run() { + cache(); + } + }); + } + + /** + * Caches the user data if needed. + */ + public void cacheIfNeeded() { + plugin.getUserManager().getDataManager().cacheUserIfNeeded(UUID.fromString(uuid)); + } + + /** + * Checks and processes delayed/timed rewards. + */ + public void checkDelayedTimedRewards() { + plugin.debug("Checking timed/delayed for " + getPlayerName()); + HashMap timed = getTimedRewards(); + HashMap newTimed = new HashMap<>(); + for (Entry entry : timed.entrySet()) { + long time = entry.getValue(); + + if (time != 0) { + Date timeDate = new Date(time); + if (new Date().after(timeDate)) { + String[] data = entry.getKey().split("%placeholders%"); + String rewardName = data[0]; + rewardName = rewardName.split("%extime%")[0]; + String placeholders = ""; + if (data.length > 1) { + placeholders = data[1]; + } + com.bencodez.advancedcore.api.rewards.Reward queuedReward = plugin.getRewardHandler() + .getQueuedGeneratedReward(rewardName, getUUID()); + if (queuedReward == null) { + queuedReward = plugin.getRewardHandler().getReward(rewardName); + } + new RewardBuilder(queuedReward).setCheckTimed(false) + .withPlaceHolder(ArrayUtils.fromString(placeholders)) + .withPlaceHolder("date", + "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(new Date(time))) + .send(this); + plugin.debug("Giving timed/delayed reward " + rewardName + " for " + getPlayerName() + + " with placeholders " + ArrayUtils.fromString(placeholders)); + } else { + newTimed.put(entry.getKey(), time); + } + } + + } + setTimedRewards(newTimed); + } + + /** + * Check offline rewards. + */ + public void checkOfflineRewards() { + if (!plugin.getOptions().isProcessRewards()) { + plugin.debug("Processing rewards is disabled"); + return; + } + if (isCheckWorld()) { + setCheckWorld(false); + } + ArrayList rewards = getOfflineRewards(); + if (rewards.isEmpty()) { + return; + } + + setOfflineRewards(new ArrayList<>()); + RewardHandler rewardHandler = plugin.getRewardHandler(); + AdvancedCoreUser user = this; + + for (String rewardEntry : rewards) { + if (rewardEntry == null || rewardEntry.equals("null")) { + continue; + } + + String[] parts = rewardEntry.split("%placeholders%", 2); + String rewardName = parts[0]; + String placeholderStr = parts.length > 1 ? parts[1] : ""; + + RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false) + .withPlaceHolder(ArrayUtils.fromString(placeholderStr)); + + rewardHandler.giveReward(user, rewardName, options); + } + + } + + /** + * Clears the user cache. + */ + public void clearCache() { + if (isCached()) { + getCache().clearCache(); + } + } + + /** + * Clears the temporary cache. + */ + public void clearTempCache() { + getUserData().clearTempCache(); + } + + /** + * Closes the player's inventory. + */ + public void closeInv() { + if (plugin.isEnabled()) { + getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { + + @Override + public void run() { + Player player = getPlayer(); + if (player != null) { + player.closeInventory(); + } + } + }, getPlayer()); + } + } + + /** + * Sets the user data fetch mode. + * + * @param mode the fetch mode + * @return this user instance + */ + public AdvancedCoreUser userDataFetechMode(UserDataFetchMode mode) { + this.userDataFetchMode = mode; + return this; + } + + /** + * Forces running of offline rewards without processing checks. + */ + public void forceRunOfflineRewards() { + if (!plugin.getOptions().isProcessRewards()) { + plugin.debug("Processing rewards is disabled"); + return; + } + + setCheckWorld(false); + ArrayList rewards = getOfflineRewards(); + if (rewards.isEmpty()) { + return; + } + + setOfflineRewards(new ArrayList<>()); + RewardHandler rewardHandler = plugin.getRewardHandler(); + AdvancedCoreUser user = this; + + for (String rewardEntry : rewards) { + if (rewardEntry == null || rewardEntry.equals("null")) { + continue; + } + + String[] parts = rewardEntry.split("%placeholders%", 2); + String rewardName = parts[0]; + String placeholderStr = parts.length > 1 ? parts[1] : ""; + + RewardOptions options = new RewardOptions().setOnline(false).setGiveOffline(false).forceOffline() + .setCheckTimed(false).withPlaceHolder(ArrayUtils.fromString(placeholderStr)); + + rewardHandler.giveReward(user, rewardName, options); + } + } + + /** + * Gets the user data cache. + * + * @return the cache + */ + public UserDataCache getCache() { + return plugin.getUserManager().getDataManager().getCache(java.util.UUID.fromString(getUUID())); + } + + /** + * Gets the user's choice preference for a reward. + * + * @param rewardName the reward name + * @return the choice preference + */ + public String getChoicePreference(String rewardName) { + ArrayList data = getChoicePreferenceData(); + + for (String str : data) { + String[] data1 = str.split(":"); + if (data1.length > 1) { + if (data1[0].equals(rewardName)) { + return data1[1]; + } + } + } + return ""; + } + + /** + * Gets the choice preference data. + * + * @return the choice preference data list + */ + public ArrayList getChoicePreferenceData() { + return getData().getStringList("ChoicePreference", userDataFetchMode); + } + + /** + * Gets the user data. + * + * @return the user data + */ + public UserData getData() { + if (data == null) { + loadData(); + } + return data; + } + + /** + * Gets the input method. + * + * @return the input method string + */ + public String getInputMethod() { + return getUserData().getString("InputMethod", userDataFetchMode); + } + + /** + * Gets the Java UUID object. + * + * @return the Java UUID + */ + public UUID getJavaUUID() { + return UUID.fromString(uuid); + } + + /** + * Gets the last online time in milliseconds. + * + * @return the last online time + */ + public long getLastOnline() { + String d = getData().getString("LastOnline", userDataFetchMode); + long time = 0; + if (d != null && !d.equals("") && !d.equals("null")) { + time = Long.valueOf(d); + } + if (time == 0 && getPlugin().getOptions().isOnlineMode()) { + OfflinePlayer player = getOfflinePlayer(); + if (player != null) { + time = player.getLastPlayed(); + if (time > 0) { + setLastOnline(time); + } + } + + } + return time; + } + + /** + * Gets the number of days since the player's last login. + * + * @return the number of days since login + */ + public int getNumberOfDaysSinceLogin() { + long time = getLastOnline(); + if (time > 0) { + LocalDateTime online = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()); + LocalDateTime now = LocalDateTime.now(); + Duration dur = Duration.between(online, now); + return (int) dur.toDays(); + } + + return -1; + } + + /** + * Gets the offline player. + * + * @return the offline player + */ + @SuppressWarnings("deprecation") + public OfflinePlayer getOfflinePlayer() { + if (getPlayerName().isBlank()) { + return null; + } + if (!plugin.getOptions().isOnlineMode()) { + return Bukkit.getOfflinePlayer(getPlayerName()); + } + if (uuid != null && !uuid.equals("")) { + return Bukkit.getOfflinePlayer(java.util.UUID.fromString(uuid)); + } + return null; + } + + /** + * Gets the offline rewards list. + * + * @return the offline rewards + */ + public ArrayList getOfflineRewards() { + return getUserData().getStringList(plugin.getUserManager().getOfflineRewardsPath(), userDataFetchMode); + } + + /** + * Gets the player. + * + * @return the player + */ + public Player getPlayer() { + if (!plugin.getOptions().isOnlineMode()) { + return Bukkit.getPlayer(getPlayerName()); + } + if (uuid != null && !uuid.isEmpty()) { + return Bukkit.getPlayer(java.util.UUID.fromString(uuid)); + } + return null; + } + + /** + * Gets the player's head as an ItemStack. + * + * @return the player head + */ + public ItemStack getPlayerHead() { + return PlayerManager.getInstance().getPlayerSkull(getJavaUUID(), getPlayerName(), false); + } + + /** + * Gets the player name. + * + * @return the player name + */ + public String getPlayerName() { + if (playerName != null) { + return playerName; + } + if (userDataFetchMode.allowTempCache()) { + return getUserData().getString("PlayerName", UserDataFetchMode.TEMP_ONLY); + } + return ""; + } + + /** + * Gets the repeat amount for a reward. + * + * @param reward the reward + * @return the repeat amount + */ + public int getRepeatAmount(Reward reward) { + return getData().getInt("Repeat" + reward.getName(), userDataFetchMode); + } + + /** + * Gets the timed rewards map. + * + * @return the timed rewards + */ + public HashMap getTimedRewards() { + ArrayList timedReward = getUserData().getStringList("TimedRewards", userDataFetchMode); + HashMap timedRewards = new HashMap<>(); + for (String str : timedReward) { + if (str != null && !str.equals("null")) { + String[] data = str.split("%ExecutionTime/%"); + plugin.extraDebug("TimedReward: " + str); + if (data.length > 1) { + String name = data[0]; + + String timeStr = data[1]; + timedRewards.put(name, Long.valueOf(timeStr)); + } + } + } + return timedRewards; + } + + /** + * Gets the unclaimed choices list. + * + * @return the unclaimed choices + */ + public ArrayList getUnClaimedChoices() { + return getData().getStringList("UnClaimedChoices", userDataFetchMode); + } + + /** + * Gets the user data instance. + * + * @return the user data + */ + public UserData getUserData() { + if (data == null) { + loadData(); + } + return data; + } + + /** + * Gets the uuid. + * + * @return the uuid + */ + public String getUUID() { + return uuid; + } + + /** + * Give exp. + * + * @param exp the exp + */ + public void giveExp(int exp) { + Player player = getPlayer(); + if (player != null) { + player.giveExp(exp); + } + } + + /** + * Gives experience levels to the player. + * + * @param num the number of levels to give + */ + public void giveExpLevels(int num) { + Player p = getPlayer(); + if (p != null) { + p.setLevel(p.getLevel() + num); + } + } + + /** + * Gives an item to the player from an ItemBuilder. + * + * @param builder the item builder + */ + public void giveItem(ItemBuilder builder) { + giveItem(builder.toItemStack(getPlayer())); + } + + /** + * Give item. + * + * @param item the item + */ + public void giveItem(ItemStack item) { + if ((item == null) || (item.getAmount() == 0)) { + return; + } + + final Player player = getPlayer(); + + if (plugin.isEnabled()) { + getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { + + @Override + public void run() { + if (player != null) { + plugin.getFullInventoryHandler().giveItem(player, item); + } + } + }, player); + } + + } + + /** + * Gives an item to the player with placeholders. + * + * @param itemStack the item stack + * @param placeholders the placeholders + */ + public void giveItem(ItemStack itemStack, HashMap placeholders) { + giveItem(new ItemBuilder(itemStack).setPlaceholders(placeholders).toItemStack(getPlayer())); + } + + /** + * Gives multiple items to the player. + * + * @param item the items to give + */ + public void giveItems(ItemStack... item) { + if (item == null) { + return; + } + + final Player player = getPlayer(); + + if (plugin.isEnabled()) { + getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { + + @Override + public void run() { + if (player != null) { + plugin.getFullInventoryHandler().giveItem(player, item); + } + } + }, player); + } + + } + + /** + * Give user money, needs vault installed + * + * @param m Amount of money to give + */ + public void giveMoney(double m) { + if (!plugin.isEnabled()) { + return; + } + if (plugin.getVaultHandler() != null && plugin.getVaultHandler().getEcon() != null) { + try { + if (m > 0) { + final double money = m; + getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { + + @Override + public void run() { + plugin.getVaultHandler().getEcon().depositPlayer(getOfflinePlayer(), money); + } + }); + + } else if (m < 0) { + m = m * -1; + final double money = m; + getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { + + @Override + public void run() { + plugin.getVaultHandler().getEcon().withdrawPlayer(getOfflinePlayer(), money); + } + }); + + } + } catch ( + + IllegalStateException e) { + e.printStackTrace(); + } + } + } + + /** + * Give money. + * + * @param money the money + */ + public void giveMoney(int money) { + giveMoney((double) money); + } + + /** + * Give potion effect. + * + * @param potionName the potion name + * @param duration the duration + * @param amplifier the amplifier + */ + public void givePotionEffect(String potionName, int duration, int amplifier) { + Player player = getPlayer(); + if (player != null && plugin.isEnabled()) { + getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { + + @SuppressWarnings("deprecation") + @Override + public void run() { + player.addPotionEffect( + new PotionEffect(PotionEffectType.getByName(potionName), 20 * duration, amplifier)); + } + }, player); + + } + } + + /** + * Gives a reward from a configuration file. + * + * @param data the configuration data + * @param path the path to the reward + * @param rewardOptions the reward options + */ + public void giveReward(FileConfiguration data, String path, RewardOptions rewardOptions) { + plugin.getRewardHandler().giveReward(this, data, path, rewardOptions); + } + + /** + * Gives a reward to the user. + * + * @param reward the reward + * @param rewardOptions the reward options + */ + public void giveReward(Reward reward, RewardOptions rewardOptions) { + reward.giveReward(this, rewardOptions); + } + + /** + * Checks if the user has unclaimed choices. + * + * @return true if the user has choices + */ + public boolean hasChoices() { + return getUnClaimedChoices().size() > 0; + } + + /** + * Check if player joined before + * + * @return true, if successful + */ + public boolean hasLoggedOnBefore() { + OfflinePlayer player = Bukkit.getOfflinePlayer(java.util.UUID.fromString(uuid)); + if (player != null) { + if (player.hasPlayedBefore() || player.isOnline()) { + return true; + } + + } + ArrayList uuids = plugin.getUserManager().getAllUUIDs(); + if (uuids.contains(getUUID())) { + return true; + } + return false; + } + + /** + * Checks if the user has a permission. + * + * @param perm the permission + * @return true if the user has the permission + */ + public boolean hasPermission(String perm) { + return hasPermission(perm, true); + } + + /** + * Checks if the user has a permission. + * + * @param perm the permission + * @param offlineCheck whether to check offline permissions + * @return true if the user has the permission + */ + public boolean hasPermission(String perm, boolean offlineCheck) { + boolean negate = perm != null && perm.startsWith("!"); + if (negate) { + perm = perm.substring(1); + } + + Player player = getPlayer(); + + // Online fast path + if (player != null) { + boolean has = player.hasPermission(perm); + return negate ? !has : has; + } + + if (!offlineCheck) { + plugin.debug("Unable to get player for permission check for " + getPlayerName() + "/" + getUUID() + + " (offlineCheck is false)"); + return false; + } + // Offline path: LuckPerms (if available) + if (plugin.getLuckPermsHandle() != null && plugin.getLuckPermsHandle().luckpermsApiLoaded()) { + boolean has = plugin.getLuckPermsHandle().hasPermission(getJavaUUID(), perm); + return negate ? !has : has; + } + + plugin.debug("Unable to get player for permission check for " + getPlayerName() + "/" + getUUID() + + " (offline and no LuckPerms hook)"); + return false; + } + + /** + * Checks if the player is banned. + * + * @return true if the player is banned + */ + public boolean isBanned() { + if (plugin.getBannedPlayers().contains(getUUID())) { + return true; + } + return false; + } + + /** + * Checks if the player is a Bedrock user. + * + * @return true if the player is a Bedrock user + */ + public boolean isBedrockUser() { + return getData().getBoolean("isBedrock", userDataFetchMode); + } + + /** + * Sets whether the player is a Bedrock user. + * + * @param isBedrock true if the player is a Bedrock user + */ + public void setBedrockUser(boolean isBedrock) { + getData().setBoolean("isBedrock", isBedrock); + } + + /** + * Checks if the user data is cached. + * + * @return true if cached + */ + public boolean isCached() { + return plugin.getUserManager().getDataManager().isCached(UUID.fromString(uuid)); + } + + /** + * Checks if world checking is enabled for the user. + * + * @return true if world checking is enabled + */ + public boolean isCheckWorld() { + if (!plugin.isLoadUserData()) { + return false; + } + return Boolean.valueOf(getData().getString("CheckWorld", userDataFetchMode)); + } + + /** + * Checks if the player is in any of the specified worlds. + * + * @param worlds the list of world names + * @return true if the player is in one of the worlds + */ + public boolean isInWorld(ArrayList worlds) { + Player p = getPlayer(); + if (p != null) { + for (String world : worlds) { + if (p.getWorld().getName().equalsIgnoreCase(world)) { + return true; + } + } + } + + return false; + } + + /** + * Checks if the player is in a specific world. + * + * @param world the world name + * @return true if the player is in the world + */ + public boolean isInWorld(String world) { + Player p = getPlayer(); + if (p != null) { + return p.getWorld().getName().equalsIgnoreCase(world); + } + + return false; + } + + /** + * Checks if is online. + * + * @return true, if is online + */ + public boolean isOnline() { + boolean online = PlayerUtils.isPlayerOnline(getPlayerName()); + if (!online) { + return false; + } + if (plugin.getOptions().isTreatVanishAsOffline()) { + if (isVanished()) { + return false; + } + } + return true; + } + + /** + * Checks if the player is vanished. + * + * @return true if the player is vanished + */ + public boolean isVanished() { + Player player = getPlayer(); + if (player != null) { + for (MetadataValue meta : player.getMetadata("vanished")) { + if (meta.asBoolean()) { + return true; + } + } + + try { + try { + if (plugin.getCmiHandle() != null) { + return plugin.getCmiHandle().isVanished(player); + } + } catch (Exception e) { + } + } catch (Exception e) { + plugin.debug(e); + } + } + return false; + } + + /** + * Loads the user cache. + */ + public void loadCache() { + plugin.getUserManager().getDataManager().cacheUser(UUID.fromString(uuid), getPlayerName()); + } + + /** + * Loads the user data. + */ + public void loadData() { + data = new UserData(this); + } + + /** + * Loads a timer for delayed/timed rewards. + * + * @param time the time in milliseconds + */ + public void loadTimedDelayedTimer(long time) { + long delay = time - System.currentTimeMillis(); + if (delay < 0) { + delay = 0; + } + delay += 500; + plugin.getRewardHandler().getDelayedTimer().schedule(new Runnable() { + + @Override + public void run() { + checkDelayedTimedRewards(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + /** + * Play particle effect. + * + * @param effectName the effect name + * @param data the data + * @param particles the particles + * @param radius the radius + */ + public void playEffect(String effectName, int data, int particles, int radius) { + Player player = getPlayer(); + if ((player != null) && (effectName != null)) { + try { + Effect effect = Effect.valueOf(effectName); + for (int i = 0; i < particles; i++) { + player.getWorld().playEffect(player.getLocation(), effect, data, radius); + } + } catch (Exception e) { + e.printStackTrace(); + } + + } + } + + /** + * Plays a particle effect for the player. + * + * @param effectName the particle effect name + * @param data the data value + * @param particles the number of particles + * @param radius the radius + */ + public void playParticle(String effectName, int data, int particles, int radius) { + Player player = getPlayer(); + if ((player != null) && (effectName != null)) { + try { + Particle effect = Particle.valueOf(effectName); + for (int i = 0; i < particles; i++) { + player.getWorld().spawnParticle(effect, player.getLocation(), particles, radius, radius, radius, + data); + } + + } catch (Exception e) { + plugin.getLogger().warning( + "Failed to create particle: " + effectName + ", " + data + ", " + particles + ", " + radius); + e.printStackTrace(); + } + } + } + + /** + * Plays a particle effect for the player. + * + * @param effectName the particle effect name + * @param data the data value + * @param particles the number of particles + * @param radius the radius + */ + @Deprecated + public void playParticleEffect(String effectName, int data, int particles, int radius) { + playParticle(effectName, data, particles, radius); + } + + /** + * Play sound. + * + * @param soundName the sound name + * @param volume the volume + * @param pitch the pitch + */ + public void playSound(String soundName, float volume, float pitch) { + Player player = getPlayer(); + if (player != null) { + Sound sound = null; + try { + sound = Registry.SOUNDS.get(NamespacedKey.minecraft(soundName)); + } catch (Exception e) { + plugin.debug(e); + } + if (sound != null) { + player.playSound(player.getLocation(), sound, volume, pitch); + } else { + plugin.debug("Invalid sound: " + soundName); + } + } + } + + /** + * Performs commands as the player with placeholders. + * + * @param commands the list of commands + * @param placeholders the placeholders + */ + public void preformCommand(ArrayList commands, HashMap placeholders) { + if (commands != null && !commands.isEmpty()) { + final ArrayList cmds = PlaceholderUtils.replaceJavascript(getPlayer(), + PlaceholderUtils.replacePlaceHolder(commands, placeholders)); + + final Player player = getPlayer(); + if (player != null && plugin.isEnabled()) { + for (final String cmd : cmds) { + plugin.debug("Executing player command for " + getPlayerName() + ": " + cmd); + getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { + + @Override + public void run() { + player.chat("/" + cmd); + } + }); + } + } + } + } + + /** + * Performs a command as the player with placeholders. + * + * @param command the command + * @param placeholders the placeholders + */ + public void preformCommand(String command, HashMap placeholders) { + if (command != null && !command.isEmpty()) { + final String cmd = PlaceholderUtils.replaceJavascript(getPlayer(), + PlaceholderUtils.replacePlaceHolder(command, placeholders)); + plugin.debug("Executing player command for " + getPlayerName() + ": " + command); + if (plugin.isEnabled()) { + getPlugin().getBukkitScheduler().runTask(plugin, new Runnable() { + + @Override + public void run() { + Player player = getPlayer(); + if (player != null) { + player.chat("/" + cmd); + } + } + }); + } + } + } + + /** + * Removes the user from storage. + */ + public void remove() { + plugin.debug("Removing " + getUUID() + " (" + getPlayerName() + ") from storage..."); + getData().remove(); + } + + /** + * Removes a permission from the player. + * + * @param permission the permission + */ + public void removePermission(String permission) { + plugin.getPermissionHandler().removePermission(UUID.fromString(getUUID()), getPlayerName(), permission); + } + + /** + * Removes an unclaimed choice reward. + * + * @param name the reward name + */ + public void removeUnClaimedChoiceReward(String name) { + ArrayList choices = getUnClaimedChoices(); + choices.remove(name); + setUnClaimedChoice(choices); + } + + /** + * Send action bar. + * + * @param msg the msg + * @param delay the delay + */ + public void sendActionBar(String msg, int delay) { + // plugin.debug("attempting to send action bar"); + if (msg != null && msg != "") { + Player player = getPlayer(); + if (player != null) { + + try { + ActionBar actionBar = new ActionBar(PlaceholderUtils.replaceJavascript(getPlayer(), msg), delay); + actionBar.send(player); + } catch (Exception ex) { + plugin.debug("Failed to send ActionBar, turn debug on to see stack trace"); + plugin.debug(ex); + } + } + } + } + + /** + * Send boss bar. + * + * @param msg the msg + * @param color the color + * @param style the style + * @param progress the progress + * @param delay the delay + */ + public void sendBossBar(String msg, String color, String style, double progress, int delay) { + if (msg != null && msg != "") { + Player player = getPlayer(); + if (player != null) { + try { + BossBar bossBar = new BossBar(PlaceholderUtils.replaceJavascript(getPlayer(), msg), color, style, + progress); + bossBar.send(player, delay); + } catch (Exception ex) { + plugin.debug("Failed to send BossBar"); + plugin.debug(ex); + } + } + } + } + + /** + * Send json. + * + * @param messages the messages + */ + public void sendJson(ArrayList messages) { + sendJson(messages, true); + } + + /** + * Sends JSON messages to the player. + * + * @param messages the text component messages + * @param javascript whether to process javascript placeholders + */ + public void sendJson(ArrayList messages, boolean javascript) { + Player player = getPlayer(); + if ((player != null) && (messages != null)) { + ArrayList texts = new ArrayList<>(); + TextComponent newLine = new TextComponent(ComponentSerializer.parse("{text: \"\n\"}")); + for (int i = 0; i < messages.size(); i++) { + TextComponent txt = messages.get(i); + if (javascript) { + txt.setText(PlaceholderUtils.replaceJavascript(getPlayer(), txt.getText())); + } + texts.add(txt); + if (i + 1 < messages.size()) { + texts.add(newLine); + } + + } + + PlayerUtils.getServerHandle().sendMessage(player, ArrayUtils.convertBaseComponent(texts)); + } + + } + + /** + * Send json. + * + * @param message the message + */ + public void sendJson(TextComponent message) { + Player player = getPlayer(); + if ((player != null) && (message != null)) { + message.setText(PlaceholderUtils.replaceJavascript(getPlayer(), message.getText())); + PlayerUtils.getServerHandle().sendMessage(player, message); + } + } + + /** + * Send message. + * + * @param msg the msg + */ + public void sendMessage(ArrayList msg) { + sendMessage(ArrayUtils.convert(msg)); + } + + /** + * Sends a message with placeholders to the player. + * + * @param msg the message list + * @param placeholders the placeholders + */ + public void sendMessage(ArrayList msg, HashMap placeholders) { + sendMessage(ArrayUtils.convert(PlaceholderUtils.replacePlaceHolder(msg, placeholders))); + } + + /** + * Send message. + * + * @param msg the msg + */ + public void sendMessage(String msg) { + Player player = getPlayer(); + if ((player != null) && (msg != null)) { + if (!msg.equals("")) { + for (String str : msg.split("%NewLine%")) { + PlayerUtils.getServerHandle().sendMessage(player, + PlaceholderUtils.parseJson(PlaceholderUtils.parseText(player, str))); + } + } + } + } + + /** + * Sends a message with placeholders to the player. + * + * @param msg the message + * @param placeholders the placeholders + */ + public void sendMessage(String msg, HashMap placeholders) { + sendMessage(PlaceholderUtils.replacePlaceHolder(msg, placeholders)); + } + + /** + * Sends a message with a single placeholder replacement to the player. + * + * @param msg the message + * @param toReplace the placeholder to replace + * @param replace the replacement value + */ + public void sendMessage(String msg, String toReplace, String replace) { + sendMessage(PlaceholderUtils.replacePlaceHolder(msg, toReplace, replace)); + } + + /** + * Send message. + * + * @param msg the msg + */ + public void sendMessage(String[] msg) { + Player player = getPlayer(); + if ((player != null) && (msg != null)) { + + ArrayList texts = new ArrayList<>(); + for (String str : msg) { + if ((player != null) && (msg != null)) { + if (!str.equals("")) { + for (String str1 : str.split("%NewLine%")) { + TextComponent text = PlaceholderUtils.parseJson(PlaceholderUtils.parseText(player, str1)); + text.setText(PlaceholderUtils.replaceJavascript(getPlayer(), text.getText())); + texts.add(text); + } + } + + } + } + if (texts.size() > 0) { + sendJson(texts, false); + } + } + + } + + /** + * Send title. + * + * @param title the title + * @param subTitle the sub title + * @param fadeIn the fade in + * @param showTime the show time + * @param fadeOut the fade out + */ + public void sendTitle(String title, String subTitle, int fadeIn, int showTime, int fadeOut) { + Player player = getPlayer(); + if (player != null) { + try { + Title titleObject = new Title(PlaceholderUtils.replaceJavascript(getPlayer(), title), + PlaceholderUtils.replaceJavascript(getPlayer(), subTitle), fadeIn, showTime, fadeOut); + titleObject.send(player); + } catch (Exception ex) { + plugin.getLogger().info("Failed to send Title, turn debug on to see stack trace"); + plugin.debug(ex); + } + } + } + + /** + * Sets whether to check the world for the user. + * + * @param b true to enable world checking + */ + public void setCheckWorld(boolean b) { + getData().setString("CheckWorld", "" + b); + } + + /** + * Sets the user's choice preference for a reward. + * + * @param reward the reward name + * @param preference the preference + */ + public void setChoicePreference(String reward, String preference) { + ArrayList data = getChoicePreferenceData(); + ArrayList choices = new ArrayList<>(); + + boolean added = false; + for (String str : data) { + String[] data1 = str.split(":"); + if (data1.length > 1) { + if (data1[0].equals(reward)) { + choices.add(reward + ":" + preference); + added = true; + } else { + choices.add(str); + } + } + } + if (!added) { + choices.add(reward + ":" + preference); + } + getData().setStringList("ChoicePreference", choices); + } + + /** + * Sets the input method for the user. + * + * @param inputMethod the input method + */ + public void setInputMethod(String inputMethod) { + data.setString("InputMethod", inputMethod); + } + + /** + * Sets the last online time for the user. + * + * @param online the last online time in milliseconds + */ + public void setLastOnline(long online) { + getData().setString("LastOnline", "" + online); + } + + /** + * Sets the offline rewards list. + * + * @param offlineRewards the offline rewards + */ + public void setOfflineRewards(ArrayList offlineRewards) { + // MySQL TEXT max length is 65535 bytes + int maxLength = 65535; + String str = String.join("%line%", offlineRewards); + + // Remove oldest rewards until within limit + while (str.getBytes().length > maxLength && !offlineRewards.isEmpty()) { + offlineRewards.remove(0); + str = String.join("%line%", offlineRewards); + } + + data.setStringList(plugin.getUserManager().getOfflineRewardsPath(), offlineRewards); + } + + /** + * Sets the player name. + * + * @param playerName the player name + */ + public void setPlayerName(String playerName) { + this.playerName = playerName; + } + + /** + * Sets the repeat amount for a reward. + * + * @param reward the reward + * @param amount the repeat amount + */ + public void setRepeatAmount(Reward reward, int amount) { + getData().setInt("Repeat" + reward.getName(), amount); + } + + /** + * Sets the timed rewards map. + * + * @param timed the timed rewards + */ + public void setTimedRewards(HashMap timed) { + ArrayList timedRewards = new ArrayList<>(); + for (Entry entry : timed.entrySet()) { + + String str = ""; + str += entry.getKey() + "%ExecutionTime/%"; + str += entry.getValue(); + timedRewards.add(str); + + } + data.setStringList("TimedRewards", timedRewards); + } + + /** + * Sets the unclaimed choice rewards list. + * + * @param rewards the unclaimed rewards + */ + public void setUnClaimedChoice(ArrayList rewards) { + getData().setStringList("UnClaimedChoices", rewards); + } + + /** + * Sets the user to not cache data. + */ + @Deprecated + public void dontCache() { + userDataFetchMode = UserDataFetchMode.NO_CACHE; + } + + /** + * Sets the uuid. + * + * @param uuid the new uuid + */ + public void setUUID(String uuid) { + this.uuid = uuid; + } + + /** + * Enables temporary caching for this user. + * + * @return this user instance + */ + public AdvancedCoreUser tempCache() { + getUserData().tempCache(); + return this; + } + + /** + * Updates the player name in storage. + * + * @param force whether to force the update + */ + public void updateName(boolean force) { + if (getData().hasData() || force) { + String playerName = getData().getString("PlayerName", userDataFetchMode); + if (playerName == null || !playerName.equals(getPlayerName())) { + getData().setString("PlayerName", getPlayerName(), true); + } + } + } + + /** + * Updates the temporary cache with specific columns. + * + * @param cols the columns to update + */ + public void updateTempCacheWithColumns(ArrayList cols) { + getUserData().updateTempCacheWithColumns(cols); + } + } \ No newline at end of file From db1673135f7de00ebe6f4e2e4d324219ca465818 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:15:18 -0600 Subject: [PATCH 39/71] Keep generated snapshot registry guard null-safe --- .../bencodez/advancedcore/api/rewards/RewardRegistry.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java index 01a6464b5f..242c6d4e6f 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardRegistry.java @@ -173,8 +173,9 @@ public void resetSubDirectlyDefinedRewards() { } public void updateReward(Reward reward) { - if (reward != null && reward.getConfig().isDirectlyDefinedReward()) { - File folder = reward.getConfig().getRewardFolder(); + RewardFileData config = reward == null ? null : reward.getConfig(); + if (config != null && config.isDirectlyDefinedReward()) { + File folder = config.getRewardFolder(); if (folder != null && folder.getName().equalsIgnoreCase("DirectlyDefined")) { plugin.extraDebug("Keeping generated queued reward snapshot out of public registry: " + reward.getName()); return; From 2373d9ac888c3944608edbec45cc5f7415463a18 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:31:55 -0600 Subject: [PATCH 40/71] Apply Codex queue provenance fixes for PR 295 --- .../fix-codex-295-queue-provenance.yml | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 .github/workflows/fix-codex-295-queue-provenance.yml diff --git a/.github/workflows/fix-codex-295-queue-provenance.yml b/.github/workflows/fix-codex-295-queue-provenance.yml new file mode 100644 index 0000000000..c4550b516a --- /dev/null +++ b/.github/workflows/fix-codex-295-queue-provenance.yml @@ -0,0 +1,136 @@ +name: Apply Codex 295 queue provenance fixes + +on: + push: + branches: + - security/reward-path-identity-hardening + paths: + - .github/workflows/fix-codex-295-queue-provenance.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/reward-path-identity-hardening + fetch-depth: 0 + - name: Patch queue provenance and exact suppression + run: | + python3 - <<'PY' + from pathlib import Path + import re + + # Reward: remember whether this in-memory reward actually created a snapshot. + path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java') + text = path.read_text() + text = text.replace( + '\t@Getter\n\tprivate boolean needsRewardFile = true;\n', + '\t@Getter\n\tprivate boolean needsRewardFile = true;\n\n' + '\t@Getter\n\tprivate boolean generatedSnapshotCreated = false;\n') + text = text.replace( + '\t\treward.getConfig().save(reward.getConfig().getFileData());\n\t}\n\n\tpublic void validate()', + '\t\treward.getConfig().save(reward.getConfig().getFileData());\n' + '\t\tgeneratedSnapshotCreated = true;\n\t}\n\n\tpublic void validate()') + path.write_text(text) + + # AdvancedCoreUser: persist explicit snapshot provenance in offline/timed queue references. + path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') + text = path.read_text() + text = text.replace( + 'public class AdvancedCoreUser {\n', + 'public class AdvancedCoreUser {\n\n\tprivate static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";\n', 1) + text = text.replace( + 'offlineRewards.add(reward.getRewardName() + "%placeholders%" + ArrayUtils.makeString(placeholders));', + 'offlineRewards.add(queuedRewardReference(reward) + "%placeholders%" + ArrayUtils.makeString(placeholders));') + text = text.replace( + '\t\tString rewardName = reward.getRewardName();\n\t\trewardName += "%extime%" + System.currentTimeMillis();', + '\t\tString rewardName = queuedRewardReference(reward);\n\t\trewardName += "%extime%" + System.currentTimeMillis();') + + insert_after = '''\tpublic synchronized void addTimedReward(Reward reward, HashMap placeholders, long epochMilli) {\n\t\tHashMap timed = getTimedRewards();\n\t\tString rewardName = queuedRewardReference(reward);\n\t\trewardName += "%extime%" + System.currentTimeMillis();\n\n\t\ttimed.put(rewardName + "%placeholders%" + ArrayUtils.makeString(placeholders), epochMilli);\n\t\tsetTimedRewards(timed);\n\t\tloadTimedDelayedTimer(epochMilli);\n\t}\n''' + helper = insert_after + '''\n\tprivate String queuedRewardReference(Reward reward) {\n\t\treturn reward.getRewardName() + QUEUED_SNAPSHOT_MARKER + reward.isGeneratedSnapshotCreated();\n\t}\n''' + if insert_after not in text: + raise SystemExit('Unable to locate addTimedReward') + text = text.replace(insert_after, helper, 1) + + old_timed = '''\t\t\t\t\tString[] data = entry.getKey().split("%placeholders%");\n\t\t\t\t\tString rewardName = data[0];\n\t\t\t\t\trewardName = rewardName.split("%extime%")[0];\n\t\t\t\t\tString placeholders = "";\n\t\t\t\t\tif (data.length > 1) {\n\t\t\t\t\t\tplaceholders = data[1];\n\t\t\t\t\t}\n\t\t\t\t\tcom.bencodez.advancedcore.api.rewards.Reward queuedReward = plugin.getRewardHandler()\n\t\t\t\t\t\t\t.getQueuedGeneratedReward(rewardName, getUUID());\n\t\t\t\t\tif (queuedReward == null) {\n\t\t\t\t\t\tqueuedReward = plugin.getRewardHandler().getReward(rewardName);\n\t\t\t\t\t}\n\t\t\t\t\tnew RewardBuilder(queuedReward).setCheckTimed(false)\n\t\t\t\t\t\t\t.withPlaceHolder(ArrayUtils.fromString(placeholders))\n\t\t\t\t\t\t\t.withPlaceHolder("date",\n\t\t\t\t\t\t\t\t\t"" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(new Date(time)))\n\t\t\t\t\t\t\t.send(this);\n\t\t\t\t\tplugin.debug("Giving timed/delayed reward " + rewardName + " for " + getPlayerName()\n''' + new_timed = '''\t\t\t\t\tString[] data = entry.getKey().split("%placeholders%", 2);\n\t\t\t\t\tString rewardReference = data[0].split("%extime%", 2)[0];\n\t\t\t\t\tString placeholders = "";\n\t\t\t\t\tif (data.length > 1) {\n\t\t\t\t\t\tplaceholders = data[1];\n\t\t\t\t\t}\n\t\t\t\t\tRewardOptions replayOptions = new RewardOptions().setCheckTimed(false)\n\t\t\t\t\t\t\t.withPlaceHolder(ArrayUtils.fromString(placeholders));\n\t\t\t\t\treplayOptions.addPlaceholder("date",\n\t\t\t\t\t\t\t"" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(new Date(time)));\n\t\t\t\t\tplugin.getRewardHandler().giveReward(this, rewardReference, replayOptions);\n\t\t\t\t\tString rewardName = rewardReference.split(QUEUED_SNAPSHOT_MARKER, 2)[0];\n\t\t\t\t\tplugin.debug("Giving timed/delayed reward " + rewardName + " for " + getPlayerName()\n''' + if old_timed not in text: + raise SystemExit('Unable to locate timed replay block') + text = text.replace(old_timed, new_timed, 1) + + # Offline and forced-offline replay retain the raw reference so RewardExecutor can honor provenance. + text = text.replace( + '\t\t\tString rewardName = parts[0];\n\t\t\tString placeholderStr = parts.length > 1 ? parts[1] : "";\n', + '\t\t\tString rewardReference = parts[0];\n\t\t\tString placeholderStr = parts.length > 1 ? parts[1] : "";\n', 2) + text = text.replace('rewardHandler.giveReward(user, rewardName, options);', + 'rewardHandler.giveReward(user, rewardReference, options);', 2) + path.write_text(text) + + # RewardExecutor: honor explicit provenance; legacy entries prefer an existing registered reward. + path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java') + text = path.read_text() + text = text.replace( + 'public class RewardExecutor {\n\n private final RewardHandler handler;', + 'public class RewardExecutor {\n\n private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";\n\n private final RewardHandler handler;') + old = ''' Reward resolved = null;\n if (isPersistedQueueReplay(context.getOptions())) {\n resolved = handler.getQueuedGeneratedReward(reward, user.getUUID());\n }\n if (resolved == null) {\n resolved = handler.getReward(reward);\n }\n giveReward(user, resolved, context.getOptions());\n''' + new = ''' String rewardName = reward;\n Boolean generatedSnapshot = null;\n int snapshotMarker = reward.indexOf(QUEUED_SNAPSHOT_MARKER);\n if (snapshotMarker >= 0) {\n rewardName = reward.substring(0, snapshotMarker);\n generatedSnapshot = Boolean.valueOf(reward.substring(snapshotMarker + QUEUED_SNAPSHOT_MARKER.length()));\n }\n\n Reward resolved = null;\n if (isPersistedQueueReplay(context.getOptions())) {\n if (generatedSnapshot != null) {\n if (generatedSnapshot.booleanValue()) {\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n } else {\n resolved = handler.getReward(rewardName);\n }\n } else if (handler.rewardExist(rewardName) || handler.hasDirectRewardHandle(rewardName)) {\n // Legacy queue entry: if a normal registered reward exists, it wins over any\n // stale generated file with the same name.\n resolved = handler.getReward(rewardName);\n } else {\n // Legacy generated entries did not persist provenance. Preserve them only when\n // there is no registered reward they could shadow.\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n if (resolved == null) {\n resolved = handler.getReward(rewardName);\n }\n }\n } else {\n resolved = handler.getReward(rewardName);\n }\n giveReward(user, resolved, context.getOptions());\n''' + if old not in text: + raise SystemExit('Unable to locate RewardExecutor queue block') + text = text.replace(old, new, 1) + path.write_text(text) + + # RewardBuilder: do not independently re-resolve by name; AdvancedCoreUser now carries provenance to RewardExecutor. + path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java') + text = path.read_text() + old = '''\t\t} else {\n\t\t\tif (isPersistedQueueReplay()) {\n\t\t\t\tReward queued = handler.getQueuedGeneratedReward(reward.getRewardName(), user.getUUID());\n\t\t\t\tif (queued != null) {\n\t\t\t\t\thandler.giveReward(user, queued, rewardOptions);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\thandler.giveReward(user, reward, rewardOptions);\n\t\t}\n\t}\n\n\tprivate boolean isPersistedQueueReplay() {\n\t\tif (rewardOptions == null || rewardOptions.isCheckTimed()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (rewardOptions.isOnlineSet() && !rewardOptions.isOnline()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn rewardOptions.getPlaceholders().containsKey("date");\n\t}\n''' + new = '''\t\t} else {\n\t\t\thandler.giveReward(user, reward, rewardOptions);\n\t\t}\n\t}\n''' + if old not in text: + raise SystemExit('Unable to locate RewardBuilder queue block') + text = text.replace(old, new, 1) + path.write_text(text) + + # RewardLoader: suppress the exact marked filename, not a dot/underscore-normalized alias. + path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java') + text = path.read_text() + if 'import java.util.Locale;' not in text: + text = text.replace('import java.util.HashSet;\n', 'import java.util.HashSet;\nimport java.util.Locale;\n') + text = text.replace( + 'String rewardName = fileName.substring(0, fileName.length() - ".yml".length());\n\t\t\t\t\tsuppressedDirectlyDefinedRewards.add(RewardRegistry.normalizeDirectPath(rewardName));', + 'suppressedDirectlyDefinedRewards.add(fileName.toLowerCase(Locale.ROOT));') + text = text.replace( + '\t\t\t\tString normalized = RewardRegistry.normalizeDirectPath(reward);\n\t\t\t\tif (file.getName().equalsIgnoreCase("DirectlyDefined")\n\t\t\t\t\t\t&& suppressedDirectlyDefinedRewards.contains(normalized)) {', + '\t\t\t\tString exactFileName = (reward + ".yml").toLowerCase(Locale.ROOT);\n\t\t\t\tif (file.getName().equalsIgnoreCase("DirectlyDefined")\n\t\t\t\t\t\t&& suppressedDirectlyDefinedRewards.contains(exactFileName)) {') + path.write_text(text) + + # Queue dispatch regression coverage. + path = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java') + text = path.read_text() + marker = ''' @Test\n void normalStandaloneDispatchCannotResolveGeneratedSnapshot() {\n''' + additions = ''' @Test\n void explicitNormalQueueReferenceDoesNotUseStaleSnapshot() {\n Reward normal = mock(Reward.class);\n Reward stale = mock(Reward.class);\n RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false);\n when(handler.getReward("Daily")).thenReturn(normal);\n when(handler.getQueuedGeneratedReward("Daily", "uuid")).thenReturn(stale);\n\n try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) {\n bukkit.when(Bukkit::isPrimaryThread).thenReturn(false);\n executor.giveReward(user, "Daily%generatedsnapshot%false", options);\n }\n\n verify(normal).giveReward(user, options);\n verify(handler, never()).getQueuedGeneratedReward("Daily", "uuid");\n }\n\n @Test\n void explicitSnapshotQueueReferenceUsesGeneratedSnapshot() {\n Reward queued = mock(Reward.class);\n Reward normal = mock(Reward.class);\n RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false);\n when(handler.getQueuedGeneratedReward("Daily", "uuid")).thenReturn(queued);\n when(handler.getReward("Daily")).thenReturn(normal);\n\n try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) {\n bukkit.when(Bukkit::isPrimaryThread).thenReturn(false);\n executor.giveReward(user, "Daily%generatedsnapshot%true", options);\n }\n\n verify(queued).giveReward(user, options);\n verify(handler, never()).getReward("Daily");\n }\n\n @Test\n void legacyQueueReferencePrefersRegisteredRewardOverStaleSnapshot() {\n Reward normal = mock(Reward.class);\n Reward stale = mock(Reward.class);\n RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false);\n when(handler.rewardExist("Daily")).thenReturn(true);\n when(handler.getReward("Daily")).thenReturn(normal);\n when(handler.getQueuedGeneratedReward("Daily", "uuid")).thenReturn(stale);\n\n try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) {\n bukkit.when(Bukkit::isPrimaryThread).thenReturn(false);\n executor.giveReward(user, "Daily", options);\n }\n\n verify(normal).giveReward(user, options);\n verify(handler, never()).getQueuedGeneratedReward("Daily", "uuid");\n }\n\n''' + if marker not in text: + raise SystemExit('Unable to locate queue test insertion point') + text = text.replace(marker, additions + marker, 1) + path.write_text(text) + + # Exact filename suppression regression. + path = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java') + text = path.read_text() + marker = '''\t@Test\n\tvoid loadRewardsDoesNotSuppressNormalDirectFolderFileWithoutGeneratedMarker() throws IOException {\n''' + additions = '''\t@Test\n\tvoid generatedDotNameDoesNotSuppressManualUnderscoreName() throws IOException {\n\t\tAdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class);\n\t\tAdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class);\n\t\tRewardHandler handler = mock(RewardHandler.class);\n\t\tRewardRegistry registry = mock(RewardRegistry.class);\n\t\tLogger logger = mock(Logger.class);\n\t\tArrayList rewards = new ArrayList<>();\n\n\t\tFile directlyDefined = new File(tempDir, "Rewards/DirectlyDefined");\n\t\tassertTrue(directlyDefined.mkdirs());\n\t\tYamlConfiguration generatedData = new YamlConfiguration();\n\t\tgeneratedData.set("DirectlyDefinedReward", true);\n\t\tgeneratedData.set("Messages", List.of("generated"));\n\t\tgeneratedData.save(new File(directlyDefined, "Foo.Bar.yml"));\n\t\tYamlConfiguration manualData = new YamlConfiguration();\n\t\tmanualData.set("Messages", List.of("manual"));\n\t\tmanualData.save(new File(directlyDefined, "Foo_Bar.yml"));\n\n\t\twhen(plugin.getDataFolder()).thenReturn(tempDir);\n\t\twhen(plugin.getOptions()).thenReturn(options);\n\t\twhen(plugin.getLogger()).thenReturn(logger);\n\t\twhen(options.isLoadDefaultRewards()).thenReturn(false);\n\t\twhen(handler.getRewardRegistry()).thenReturn(registry);\n\t\twhen(handler.getRewards()).thenReturn(rewards);\n\t\twhen(handler.rewardExist("Foo_Bar")).thenReturn(false);\n\n\t\tAdvancedCorePlugin.setInstance(plugin);\n\t\tRewardLoader loader = new RewardLoader(handler, plugin);\n\t\tloader.addRewardFolder(directlyDefined, false, false);\n\t\tloader.loadRewards();\n\n\t\tassertEquals(1, rewards.size());\n\t\tassertEquals("Foo_Bar", rewards.get(0).getName());\n\t}\n\n''' + if marker not in text: + raise SystemExit('Unable to locate loader test insertion point') + text = text.replace(marker, additions + marker, 1) + path.write_text(text) + PY + - name: Commit fixes + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java + git rm .github/workflows/fix-codex-295-queue-provenance.yml + git commit -m "Bind queued snapshots to persisted provenance" + git push origin HEAD:security/reward-path-identity-hardening From 0258b21185091f96a9155da01bc98a7e0037e7ce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:32:06 +0000 Subject: [PATCH 41/71] Bind queued snapshots to persisted provenance --- .../fix-codex-295-queue-provenance.yml | 136 ------------------ .../advancedcore/api/rewards/Reward.java | 4 + .../api/rewards/RewardBuilder.java | 17 --- .../api/rewards/RewardExecutor.java | 34 ++++- .../api/rewards/RewardLoader.java | 8 +- .../api/user/AdvancedCoreUser.java | 39 ++--- .../api/rewards/RewardLoaderSecurityTest.java | 36 +++++ .../QueuedGeneratedRewardDispatchTest.java | 52 +++++++ 8 files changed, 146 insertions(+), 180 deletions(-) delete mode 100644 .github/workflows/fix-codex-295-queue-provenance.yml diff --git a/.github/workflows/fix-codex-295-queue-provenance.yml b/.github/workflows/fix-codex-295-queue-provenance.yml deleted file mode 100644 index c4550b516a..0000000000 --- a/.github/workflows/fix-codex-295-queue-provenance.yml +++ /dev/null @@ -1,136 +0,0 @@ -name: Apply Codex 295 queue provenance fixes - -on: - push: - branches: - - security/reward-path-identity-hardening - paths: - - .github/workflows/fix-codex-295-queue-provenance.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/reward-path-identity-hardening - fetch-depth: 0 - - name: Patch queue provenance and exact suppression - run: | - python3 - <<'PY' - from pathlib import Path - import re - - # Reward: remember whether this in-memory reward actually created a snapshot. - path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java') - text = path.read_text() - text = text.replace( - '\t@Getter\n\tprivate boolean needsRewardFile = true;\n', - '\t@Getter\n\tprivate boolean needsRewardFile = true;\n\n' - '\t@Getter\n\tprivate boolean generatedSnapshotCreated = false;\n') - text = text.replace( - '\t\treward.getConfig().save(reward.getConfig().getFileData());\n\t}\n\n\tpublic void validate()', - '\t\treward.getConfig().save(reward.getConfig().getFileData());\n' - '\t\tgeneratedSnapshotCreated = true;\n\t}\n\n\tpublic void validate()') - path.write_text(text) - - # AdvancedCoreUser: persist explicit snapshot provenance in offline/timed queue references. - path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') - text = path.read_text() - text = text.replace( - 'public class AdvancedCoreUser {\n', - 'public class AdvancedCoreUser {\n\n\tprivate static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";\n', 1) - text = text.replace( - 'offlineRewards.add(reward.getRewardName() + "%placeholders%" + ArrayUtils.makeString(placeholders));', - 'offlineRewards.add(queuedRewardReference(reward) + "%placeholders%" + ArrayUtils.makeString(placeholders));') - text = text.replace( - '\t\tString rewardName = reward.getRewardName();\n\t\trewardName += "%extime%" + System.currentTimeMillis();', - '\t\tString rewardName = queuedRewardReference(reward);\n\t\trewardName += "%extime%" + System.currentTimeMillis();') - - insert_after = '''\tpublic synchronized void addTimedReward(Reward reward, HashMap placeholders, long epochMilli) {\n\t\tHashMap timed = getTimedRewards();\n\t\tString rewardName = queuedRewardReference(reward);\n\t\trewardName += "%extime%" + System.currentTimeMillis();\n\n\t\ttimed.put(rewardName + "%placeholders%" + ArrayUtils.makeString(placeholders), epochMilli);\n\t\tsetTimedRewards(timed);\n\t\tloadTimedDelayedTimer(epochMilli);\n\t}\n''' - helper = insert_after + '''\n\tprivate String queuedRewardReference(Reward reward) {\n\t\treturn reward.getRewardName() + QUEUED_SNAPSHOT_MARKER + reward.isGeneratedSnapshotCreated();\n\t}\n''' - if insert_after not in text: - raise SystemExit('Unable to locate addTimedReward') - text = text.replace(insert_after, helper, 1) - - old_timed = '''\t\t\t\t\tString[] data = entry.getKey().split("%placeholders%");\n\t\t\t\t\tString rewardName = data[0];\n\t\t\t\t\trewardName = rewardName.split("%extime%")[0];\n\t\t\t\t\tString placeholders = "";\n\t\t\t\t\tif (data.length > 1) {\n\t\t\t\t\t\tplaceholders = data[1];\n\t\t\t\t\t}\n\t\t\t\t\tcom.bencodez.advancedcore.api.rewards.Reward queuedReward = plugin.getRewardHandler()\n\t\t\t\t\t\t\t.getQueuedGeneratedReward(rewardName, getUUID());\n\t\t\t\t\tif (queuedReward == null) {\n\t\t\t\t\t\tqueuedReward = plugin.getRewardHandler().getReward(rewardName);\n\t\t\t\t\t}\n\t\t\t\t\tnew RewardBuilder(queuedReward).setCheckTimed(false)\n\t\t\t\t\t\t\t.withPlaceHolder(ArrayUtils.fromString(placeholders))\n\t\t\t\t\t\t\t.withPlaceHolder("date",\n\t\t\t\t\t\t\t\t\t"" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(new Date(time)))\n\t\t\t\t\t\t\t.send(this);\n\t\t\t\t\tplugin.debug("Giving timed/delayed reward " + rewardName + " for " + getPlayerName()\n''' - new_timed = '''\t\t\t\t\tString[] data = entry.getKey().split("%placeholders%", 2);\n\t\t\t\t\tString rewardReference = data[0].split("%extime%", 2)[0];\n\t\t\t\t\tString placeholders = "";\n\t\t\t\t\tif (data.length > 1) {\n\t\t\t\t\t\tplaceholders = data[1];\n\t\t\t\t\t}\n\t\t\t\t\tRewardOptions replayOptions = new RewardOptions().setCheckTimed(false)\n\t\t\t\t\t\t\t.withPlaceHolder(ArrayUtils.fromString(placeholders));\n\t\t\t\t\treplayOptions.addPlaceholder("date",\n\t\t\t\t\t\t\t"" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(new Date(time)));\n\t\t\t\t\tplugin.getRewardHandler().giveReward(this, rewardReference, replayOptions);\n\t\t\t\t\tString rewardName = rewardReference.split(QUEUED_SNAPSHOT_MARKER, 2)[0];\n\t\t\t\t\tplugin.debug("Giving timed/delayed reward " + rewardName + " for " + getPlayerName()\n''' - if old_timed not in text: - raise SystemExit('Unable to locate timed replay block') - text = text.replace(old_timed, new_timed, 1) - - # Offline and forced-offline replay retain the raw reference so RewardExecutor can honor provenance. - text = text.replace( - '\t\t\tString rewardName = parts[0];\n\t\t\tString placeholderStr = parts.length > 1 ? parts[1] : "";\n', - '\t\t\tString rewardReference = parts[0];\n\t\t\tString placeholderStr = parts.length > 1 ? parts[1] : "";\n', 2) - text = text.replace('rewardHandler.giveReward(user, rewardName, options);', - 'rewardHandler.giveReward(user, rewardReference, options);', 2) - path.write_text(text) - - # RewardExecutor: honor explicit provenance; legacy entries prefer an existing registered reward. - path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java') - text = path.read_text() - text = text.replace( - 'public class RewardExecutor {\n\n private final RewardHandler handler;', - 'public class RewardExecutor {\n\n private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";\n\n private final RewardHandler handler;') - old = ''' Reward resolved = null;\n if (isPersistedQueueReplay(context.getOptions())) {\n resolved = handler.getQueuedGeneratedReward(reward, user.getUUID());\n }\n if (resolved == null) {\n resolved = handler.getReward(reward);\n }\n giveReward(user, resolved, context.getOptions());\n''' - new = ''' String rewardName = reward;\n Boolean generatedSnapshot = null;\n int snapshotMarker = reward.indexOf(QUEUED_SNAPSHOT_MARKER);\n if (snapshotMarker >= 0) {\n rewardName = reward.substring(0, snapshotMarker);\n generatedSnapshot = Boolean.valueOf(reward.substring(snapshotMarker + QUEUED_SNAPSHOT_MARKER.length()));\n }\n\n Reward resolved = null;\n if (isPersistedQueueReplay(context.getOptions())) {\n if (generatedSnapshot != null) {\n if (generatedSnapshot.booleanValue()) {\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n } else {\n resolved = handler.getReward(rewardName);\n }\n } else if (handler.rewardExist(rewardName) || handler.hasDirectRewardHandle(rewardName)) {\n // Legacy queue entry: if a normal registered reward exists, it wins over any\n // stale generated file with the same name.\n resolved = handler.getReward(rewardName);\n } else {\n // Legacy generated entries did not persist provenance. Preserve them only when\n // there is no registered reward they could shadow.\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n if (resolved == null) {\n resolved = handler.getReward(rewardName);\n }\n }\n } else {\n resolved = handler.getReward(rewardName);\n }\n giveReward(user, resolved, context.getOptions());\n''' - if old not in text: - raise SystemExit('Unable to locate RewardExecutor queue block') - text = text.replace(old, new, 1) - path.write_text(text) - - # RewardBuilder: do not independently re-resolve by name; AdvancedCoreUser now carries provenance to RewardExecutor. - path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java') - text = path.read_text() - old = '''\t\t} else {\n\t\t\tif (isPersistedQueueReplay()) {\n\t\t\t\tReward queued = handler.getQueuedGeneratedReward(reward.getRewardName(), user.getUUID());\n\t\t\t\tif (queued != null) {\n\t\t\t\t\thandler.giveReward(user, queued, rewardOptions);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\thandler.giveReward(user, reward, rewardOptions);\n\t\t}\n\t}\n\n\tprivate boolean isPersistedQueueReplay() {\n\t\tif (rewardOptions == null || rewardOptions.isCheckTimed()) {\n\t\t\treturn false;\n\t\t}\n\t\tif (rewardOptions.isOnlineSet() && !rewardOptions.isOnline()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn rewardOptions.getPlaceholders().containsKey("date");\n\t}\n''' - new = '''\t\t} else {\n\t\t\thandler.giveReward(user, reward, rewardOptions);\n\t\t}\n\t}\n''' - if old not in text: - raise SystemExit('Unable to locate RewardBuilder queue block') - text = text.replace(old, new, 1) - path.write_text(text) - - # RewardLoader: suppress the exact marked filename, not a dot/underscore-normalized alias. - path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java') - text = path.read_text() - if 'import java.util.Locale;' not in text: - text = text.replace('import java.util.HashSet;\n', 'import java.util.HashSet;\nimport java.util.Locale;\n') - text = text.replace( - 'String rewardName = fileName.substring(0, fileName.length() - ".yml".length());\n\t\t\t\t\tsuppressedDirectlyDefinedRewards.add(RewardRegistry.normalizeDirectPath(rewardName));', - 'suppressedDirectlyDefinedRewards.add(fileName.toLowerCase(Locale.ROOT));') - text = text.replace( - '\t\t\t\tString normalized = RewardRegistry.normalizeDirectPath(reward);\n\t\t\t\tif (file.getName().equalsIgnoreCase("DirectlyDefined")\n\t\t\t\t\t\t&& suppressedDirectlyDefinedRewards.contains(normalized)) {', - '\t\t\t\tString exactFileName = (reward + ".yml").toLowerCase(Locale.ROOT);\n\t\t\t\tif (file.getName().equalsIgnoreCase("DirectlyDefined")\n\t\t\t\t\t\t&& suppressedDirectlyDefinedRewards.contains(exactFileName)) {') - path.write_text(text) - - # Queue dispatch regression coverage. - path = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java') - text = path.read_text() - marker = ''' @Test\n void normalStandaloneDispatchCannotResolveGeneratedSnapshot() {\n''' - additions = ''' @Test\n void explicitNormalQueueReferenceDoesNotUseStaleSnapshot() {\n Reward normal = mock(Reward.class);\n Reward stale = mock(Reward.class);\n RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false);\n when(handler.getReward("Daily")).thenReturn(normal);\n when(handler.getQueuedGeneratedReward("Daily", "uuid")).thenReturn(stale);\n\n try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) {\n bukkit.when(Bukkit::isPrimaryThread).thenReturn(false);\n executor.giveReward(user, "Daily%generatedsnapshot%false", options);\n }\n\n verify(normal).giveReward(user, options);\n verify(handler, never()).getQueuedGeneratedReward("Daily", "uuid");\n }\n\n @Test\n void explicitSnapshotQueueReferenceUsesGeneratedSnapshot() {\n Reward queued = mock(Reward.class);\n Reward normal = mock(Reward.class);\n RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false);\n when(handler.getQueuedGeneratedReward("Daily", "uuid")).thenReturn(queued);\n when(handler.getReward("Daily")).thenReturn(normal);\n\n try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) {\n bukkit.when(Bukkit::isPrimaryThread).thenReturn(false);\n executor.giveReward(user, "Daily%generatedsnapshot%true", options);\n }\n\n verify(queued).giveReward(user, options);\n verify(handler, never()).getReward("Daily");\n }\n\n @Test\n void legacyQueueReferencePrefersRegisteredRewardOverStaleSnapshot() {\n Reward normal = mock(Reward.class);\n Reward stale = mock(Reward.class);\n RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false);\n when(handler.rewardExist("Daily")).thenReturn(true);\n when(handler.getReward("Daily")).thenReturn(normal);\n when(handler.getQueuedGeneratedReward("Daily", "uuid")).thenReturn(stale);\n\n try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) {\n bukkit.when(Bukkit::isPrimaryThread).thenReturn(false);\n executor.giveReward(user, "Daily", options);\n }\n\n verify(normal).giveReward(user, options);\n verify(handler, never()).getQueuedGeneratedReward("Daily", "uuid");\n }\n\n''' - if marker not in text: - raise SystemExit('Unable to locate queue test insertion point') - text = text.replace(marker, additions + marker, 1) - path.write_text(text) - - # Exact filename suppression regression. - path = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java') - text = path.read_text() - marker = '''\t@Test\n\tvoid loadRewardsDoesNotSuppressNormalDirectFolderFileWithoutGeneratedMarker() throws IOException {\n''' - additions = '''\t@Test\n\tvoid generatedDotNameDoesNotSuppressManualUnderscoreName() throws IOException {\n\t\tAdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class);\n\t\tAdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class);\n\t\tRewardHandler handler = mock(RewardHandler.class);\n\t\tRewardRegistry registry = mock(RewardRegistry.class);\n\t\tLogger logger = mock(Logger.class);\n\t\tArrayList rewards = new ArrayList<>();\n\n\t\tFile directlyDefined = new File(tempDir, "Rewards/DirectlyDefined");\n\t\tassertTrue(directlyDefined.mkdirs());\n\t\tYamlConfiguration generatedData = new YamlConfiguration();\n\t\tgeneratedData.set("DirectlyDefinedReward", true);\n\t\tgeneratedData.set("Messages", List.of("generated"));\n\t\tgeneratedData.save(new File(directlyDefined, "Foo.Bar.yml"));\n\t\tYamlConfiguration manualData = new YamlConfiguration();\n\t\tmanualData.set("Messages", List.of("manual"));\n\t\tmanualData.save(new File(directlyDefined, "Foo_Bar.yml"));\n\n\t\twhen(plugin.getDataFolder()).thenReturn(tempDir);\n\t\twhen(plugin.getOptions()).thenReturn(options);\n\t\twhen(plugin.getLogger()).thenReturn(logger);\n\t\twhen(options.isLoadDefaultRewards()).thenReturn(false);\n\t\twhen(handler.getRewardRegistry()).thenReturn(registry);\n\t\twhen(handler.getRewards()).thenReturn(rewards);\n\t\twhen(handler.rewardExist("Foo_Bar")).thenReturn(false);\n\n\t\tAdvancedCorePlugin.setInstance(plugin);\n\t\tRewardLoader loader = new RewardLoader(handler, plugin);\n\t\tloader.addRewardFolder(directlyDefined, false, false);\n\t\tloader.loadRewards();\n\n\t\tassertEquals(1, rewards.size());\n\t\tassertEquals("Foo_Bar", rewards.get(0).getName());\n\t}\n\n''' - if marker not in text: - raise SystemExit('Unable to locate loader test insertion point') - text = text.replace(marker, additions + marker, 1) - path.write_text(text) - PY - - name: Commit fixes - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java - git rm .github/workflows/fix-codex-295-queue-provenance.yml - git commit -m "Bind queued snapshots to persisted provenance" - git push origin HEAD:security/reward-path-identity-hardening diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java index 848a2817c1..d86d8d2110 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/Reward.java @@ -70,6 +70,9 @@ public class Reward { @Getter private boolean needsRewardFile = true; + @Getter + private boolean generatedSnapshotCreated = false; + /** The plugin. */ AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); @@ -456,6 +459,7 @@ private void setRewardFile() { .header("Directly defined reward file. WRONG PLACE TO EDIT THIS! DO NOT EDIT"); reward.getConfig().setDirectlyDefinedReward(true); reward.getConfig().save(reward.getConfig().getFileData()); + generatedSnapshotCreated = true; } public void validate() { diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java index 5a86627359..0c16f10e0a 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardBuilder.java @@ -85,27 +85,10 @@ public void send(AdvancedCoreUser user) { handler.giveReward(user, data, path, rewardOptions); } } else { - if (isPersistedQueueReplay()) { - Reward queued = handler.getQueuedGeneratedReward(reward.getRewardName(), user.getUUID()); - if (queued != null) { - handler.giveReward(user, queued, rewardOptions); - return; - } - } handler.giveReward(user, reward, rewardOptions); } } - private boolean isPersistedQueueReplay() { - if (rewardOptions == null || rewardOptions.isCheckTimed()) { - return false; - } - if (rewardOptions.isOnlineSet() && !rewardOptions.isOnline()) { - return true; - } - return rewardOptions.getPlaceholders().containsKey("date"); - } - public void send(AdvancedCoreUser... users) { for (AdvancedCoreUser user : users) { send(user); diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java index 071d21123a..151299f99e 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java @@ -17,6 +17,8 @@ */ public class RewardExecutor { + private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%"; + private final RewardHandler handler; private final AdvancedCorePlugin plugin; @@ -123,12 +125,36 @@ public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewar return; } + String rewardName = reward; + Boolean generatedSnapshot = null; + int snapshotMarker = reward.indexOf(QUEUED_SNAPSHOT_MARKER); + if (snapshotMarker >= 0) { + rewardName = reward.substring(0, snapshotMarker); + generatedSnapshot = Boolean.valueOf(reward.substring(snapshotMarker + QUEUED_SNAPSHOT_MARKER.length())); + } + Reward resolved = null; if (isPersistedQueueReplay(context.getOptions())) { - resolved = handler.getQueuedGeneratedReward(reward, user.getUUID()); - } - if (resolved == null) { - resolved = handler.getReward(reward); + if (generatedSnapshot != null) { + if (generatedSnapshot.booleanValue()) { + resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID()); + } else { + resolved = handler.getReward(rewardName); + } + } else if (handler.rewardExist(rewardName) || handler.hasDirectRewardHandle(rewardName)) { + // Legacy queue entry: if a normal registered reward exists, it wins over any + // stale generated file with the same name. + resolved = handler.getReward(rewardName); + } else { + // Legacy generated entries did not persist provenance. Preserve them only when + // there is no registered reward they could shadow. + resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID()); + if (resolved == null) { + resolved = handler.getReward(rewardName); + } + } + } else { + resolved = handler.getReward(rewardName); } giveReward(user, resolved, context.getOptions()); } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java index c205e8081f..0912da1b3c 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.Locale; import java.util.Set; import org.bukkit.configuration.file.YamlConfiguration; @@ -84,8 +85,7 @@ private void suppressGeneratedDirectlyDefinedFiles() { File generatedFile = new File(folder, fileName); YamlConfiguration data = YamlConfiguration.loadConfiguration(generatedFile); if (data.getBoolean("DirectlyDefinedReward", false)) { - String rewardName = fileName.substring(0, fileName.length() - ".yml".length()); - suppressedDirectlyDefinedRewards.add(RewardRegistry.normalizeDirectPath(rewardName)); + suppressedDirectlyDefinedRewards.add(fileName.toLowerCase(Locale.ROOT)); } } } @@ -217,9 +217,9 @@ public void loadRewards() { private void loadRewards(File file) { for (String reward : getRewardNames(file)) { if (!reward.equals("")) { - String normalized = RewardRegistry.normalizeDirectPath(reward); + String exactFileName = (reward + ".yml").toLowerCase(Locale.ROOT); if (file.getName().equalsIgnoreCase("DirectlyDefined") - && suppressedDirectlyDefinedRewards.contains(normalized)) { + && suppressedDirectlyDefinedRewards.contains(exactFileName)) { plugin.extraDebug("Suppressing generated queued reward from standalone lookup: " + reward); continue; } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java index e41e916ba4..fd7d125039 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java @@ -53,6 +53,8 @@ */ public class AdvancedCoreUser { + private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%"; + /** * User data fetch mode for this user. * @@ -212,7 +214,7 @@ public AdvancedCoreUser(AdvancedCorePlugin plugin, UUID uuid, String playerName) public void addOfflineRewards(Reward reward, HashMap placeholders) { synchronized (plugin) { ArrayList offlineRewards = getOfflineRewards(); - offlineRewards.add(reward.getRewardName() + "%placeholders%" + ArrayUtils.makeString(placeholders)); + offlineRewards.add(queuedRewardReference(reward) + "%placeholders%" + ArrayUtils.makeString(placeholders)); setOfflineRewards(offlineRewards); } } @@ -246,7 +248,7 @@ public void addPermission(String permission, long delay) { */ public synchronized void addTimedReward(Reward reward, HashMap placeholders, long epochMilli) { HashMap timed = getTimedRewards(); - String rewardName = reward.getRewardName(); + String rewardName = queuedRewardReference(reward); rewardName += "%extime%" + System.currentTimeMillis(); timed.put(rewardName + "%placeholders%" + ArrayUtils.makeString(placeholders), epochMilli); @@ -254,6 +256,10 @@ public synchronized void addTimedReward(Reward reward, HashMap p loadTimedDelayedTimer(epochMilli); } + private String queuedRewardReference(Reward reward) { + return reward.getRewardName() + QUEUED_SNAPSHOT_MARKER + reward.isGeneratedSnapshotCreated(); + } + /** * Adds an unclaimed choice reward. * @@ -305,23 +311,18 @@ public void checkDelayedTimedRewards() { if (time != 0) { Date timeDate = new Date(time); if (new Date().after(timeDate)) { - String[] data = entry.getKey().split("%placeholders%"); - String rewardName = data[0]; - rewardName = rewardName.split("%extime%")[0]; + String[] data = entry.getKey().split("%placeholders%", 2); + String rewardReference = data[0].split("%extime%", 2)[0]; String placeholders = ""; if (data.length > 1) { placeholders = data[1]; } - com.bencodez.advancedcore.api.rewards.Reward queuedReward = plugin.getRewardHandler() - .getQueuedGeneratedReward(rewardName, getUUID()); - if (queuedReward == null) { - queuedReward = plugin.getRewardHandler().getReward(rewardName); - } - new RewardBuilder(queuedReward).setCheckTimed(false) - .withPlaceHolder(ArrayUtils.fromString(placeholders)) - .withPlaceHolder("date", - "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(new Date(time))) - .send(this); + RewardOptions replayOptions = new RewardOptions().setCheckTimed(false) + .withPlaceHolder(ArrayUtils.fromString(placeholders)); + replayOptions.addPlaceholder("date", + "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(new Date(time))); + plugin.getRewardHandler().giveReward(this, rewardReference, replayOptions); + String rewardName = rewardReference.split(QUEUED_SNAPSHOT_MARKER, 2)[0]; plugin.debug("Giving timed/delayed reward " + rewardName + " for " + getPlayerName() + " with placeholders " + ArrayUtils.fromString(placeholders)); } else { @@ -359,13 +360,13 @@ public void checkOfflineRewards() { } String[] parts = rewardEntry.split("%placeholders%", 2); - String rewardName = parts[0]; + String rewardReference = parts[0]; String placeholderStr = parts.length > 1 ? parts[1] : ""; RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false) .withPlaceHolder(ArrayUtils.fromString(placeholderStr)); - rewardHandler.giveReward(user, rewardName, options); + rewardHandler.giveReward(user, rewardReference, options); } } @@ -440,13 +441,13 @@ public void forceRunOfflineRewards() { } String[] parts = rewardEntry.split("%placeholders%", 2); - String rewardName = parts[0]; + String rewardReference = parts[0]; String placeholderStr = parts.length > 1 ? parts[1] : ""; RewardOptions options = new RewardOptions().setOnline(false).setGiveOffline(false).forceOffline() .setCheckTimed(false).withPlaceHolder(ArrayUtils.fromString(placeholderStr)); - rewardHandler.giveReward(user, rewardName, options); + rewardHandler.giveReward(user, rewardReference, options); } } diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index a18103a2b4..18f149a512 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -121,6 +121,42 @@ void queueOnlyResolverBindsGeneratedSnapshotToRequestedUser() throws IOException ((QueuedGeneratedReward) reward).getAllowedUserUuids()); } + @Test + void generatedDotNameDoesNotSuppressManualUnderscoreName() throws IOException { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); + RewardHandler handler = mock(RewardHandler.class); + RewardRegistry registry = mock(RewardRegistry.class); + Logger logger = mock(Logger.class); + ArrayList rewards = new ArrayList<>(); + + File directlyDefined = new File(tempDir, "Rewards/DirectlyDefined"); + assertTrue(directlyDefined.mkdirs()); + YamlConfiguration generatedData = new YamlConfiguration(); + generatedData.set("DirectlyDefinedReward", true); + generatedData.set("Messages", List.of("generated")); + generatedData.save(new File(directlyDefined, "Foo.Bar.yml")); + YamlConfiguration manualData = new YamlConfiguration(); + manualData.set("Messages", List.of("manual")); + manualData.save(new File(directlyDefined, "Foo_Bar.yml")); + + when(plugin.getDataFolder()).thenReturn(tempDir); + when(plugin.getOptions()).thenReturn(options); + when(plugin.getLogger()).thenReturn(logger); + when(options.isLoadDefaultRewards()).thenReturn(false); + when(handler.getRewardRegistry()).thenReturn(registry); + when(handler.getRewards()).thenReturn(rewards); + when(handler.rewardExist("Foo_Bar")).thenReturn(false); + + AdvancedCorePlugin.setInstance(plugin); + RewardLoader loader = new RewardLoader(handler, plugin); + loader.addRewardFolder(directlyDefined, false, false); + loader.loadRewards(); + + assertEquals(1, rewards.size()); + assertEquals("Foo_Bar", rewards.get(0).getName()); + } + @Test void loadRewardsDoesNotSuppressNormalDirectFolderFileWithoutGeneratedMarker() throws IOException { AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java index 3ec92a7bc9..1bc1461bdc 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java @@ -90,6 +90,58 @@ void timedRewardBuilderReplacesNormalFallbackWithQueuedSnapshot() { verify(handler, never()).giveReward(user, fallback, builder.getRewardOptions()); } + @Test + void explicitNormalQueueReferenceDoesNotUseStaleSnapshot() { + Reward normal = mock(Reward.class); + Reward stale = mock(Reward.class); + RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false); + when(handler.getReward("Daily")).thenReturn(normal); + when(handler.getQueuedGeneratedReward("Daily", "uuid")).thenReturn(stale); + + try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); + executor.giveReward(user, "Daily%generatedsnapshot%false", options); + } + + verify(normal).giveReward(user, options); + verify(handler, never()).getQueuedGeneratedReward("Daily", "uuid"); + } + + @Test + void explicitSnapshotQueueReferenceUsesGeneratedSnapshot() { + Reward queued = mock(Reward.class); + Reward normal = mock(Reward.class); + RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false); + when(handler.getQueuedGeneratedReward("Daily", "uuid")).thenReturn(queued); + when(handler.getReward("Daily")).thenReturn(normal); + + try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); + executor.giveReward(user, "Daily%generatedsnapshot%true", options); + } + + verify(queued).giveReward(user, options); + verify(handler, never()).getReward("Daily"); + } + + @Test + void legacyQueueReferencePrefersRegisteredRewardOverStaleSnapshot() { + Reward normal = mock(Reward.class); + Reward stale = mock(Reward.class); + RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false); + when(handler.rewardExist("Daily")).thenReturn(true); + when(handler.getReward("Daily")).thenReturn(normal); + when(handler.getQueuedGeneratedReward("Daily", "uuid")).thenReturn(stale); + + try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); + executor.giveReward(user, "Daily", options); + } + + verify(normal).giveReward(user, options); + verify(handler, never()).getQueuedGeneratedReward("Daily", "uuid"); + } + @Test void normalStandaloneDispatchCannotResolveGeneratedSnapshot() { RewardOptions options = new RewardOptions(); From be39e33995a4344602ae71f7f9ed9168e036f8a0 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:33:46 -0600 Subject: [PATCH 42/71] Extend reward filename security regression coverage --- .../advancedcore/api/rewards/RewardRegistrySecurityTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java index 0b642c1bdf..0590c6f7dc 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardRegistrySecurityTest.java @@ -20,6 +20,7 @@ void rewardFileNameRejectsPathTraversal() { assertFalse(RewardRegistry.isSafeRewardFileName("../../OtherPlugin/config")); assertFalse(RewardRegistry.isSafeRewardFileName("..\\..\\OtherPlugin\\config")); assertFalse(RewardRegistry.isSafeRewardFileName("folder/reward")); + assertFalse(RewardRegistry.isSafeRewardFileName("reward\0name")); } @Test From 9186bfa33a2c7020042f0196e0b677bc09d56aa0 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:36:02 -0600 Subject: [PATCH 43/71] Align queued reward tests with persisted provenance --- .../QueuedGeneratedRewardDispatchTest.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java index 1bc1461bdc..4326aff2e5 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java @@ -76,18 +76,15 @@ void timedQueueReplayCanResolveGeneratedSnapshot() { } @Test - void timedRewardBuilderReplacesNormalFallbackWithQueuedSnapshot() { - Reward fallback = mock(Reward.class); - Reward queued = mock(Reward.class); - when(fallback.getRewardName()).thenReturn("QueuedReward"); - when(handler.getQueuedGeneratedReward("QueuedReward", "uuid")).thenReturn(queued); + void rewardBuilderUsesAlreadyResolvedQueueRewardWithoutReresolving() { + Reward resolved = mock(Reward.class); + when(resolved.getRewardName()).thenReturn("QueuedReward"); - RewardBuilder builder = new RewardBuilder(fallback).setCheckTimed(false).withPlaceHolder("date", "now"); + RewardBuilder builder = new RewardBuilder(resolved).setCheckTimed(false).withPlaceHolder("date", "now"); builder.send(user); - verify(handler).getQueuedGeneratedReward("QueuedReward", "uuid"); - verify(handler).giveReward(user, queued, builder.getRewardOptions()); - verify(handler, never()).giveReward(user, fallback, builder.getRewardOptions()); + verify(handler, never()).getQueuedGeneratedReward("QueuedReward", "uuid"); + verify(handler).giveReward(user, resolved, builder.getRewardOptions()); } @Test From d2f28b92d4be2df0f29e937c85c927442ff86253 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:36:24 -0600 Subject: [PATCH 44/71] Fix queue provenance security test setup --- .../api/rewards/RewardLoaderSecurityTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index 18f149a512..13cdc28b4a 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -10,7 +10,9 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; import org.bukkit.configuration.file.YamlConfiguration; @@ -20,6 +22,8 @@ import com.bencodez.advancedcore.AdvancedCoreConfigOptions; import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.rewards.injected.RewardInject; +import com.bencodez.advancedcore.api.rewards.injectedrequirement.RequirementInject; class RewardLoaderSecurityTest { @@ -143,9 +147,13 @@ void generatedDotNameDoesNotSuppressManualUnderscoreName() throws IOException { when(plugin.getDataFolder()).thenReturn(tempDir); when(plugin.getOptions()).thenReturn(options); when(plugin.getLogger()).thenReturn(logger); + when(plugin.getRewardHandler()).thenReturn(handler); when(options.isLoadDefaultRewards()).thenReturn(false); when(handler.getRewardRegistry()).thenReturn(registry); when(handler.getRewards()).thenReturn(rewards); + when(handler.getInjectedRequirements()).thenReturn(new CopyOnWriteArrayList()); + when(handler.getInjectedRewards()).thenReturn(new CopyOnWriteArrayList()); + when(handler.getValidPaths()).thenReturn(Set.of("Messages")); when(handler.rewardExist("Foo_Bar")).thenReturn(false); AdvancedCorePlugin.setInstance(plugin); From 056c06e0be9b41351a048fb05ea5e5d152530fca Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:38:35 -0600 Subject: [PATCH 45/71] Test exact generated filename suppression directly --- .../api/rewards/RewardLoaderSecurityTest.java | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index 13cdc28b4a..d659b5aac9 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -12,7 +12,6 @@ import java.util.List; import java.util.Set; import java.util.UUID; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; import org.bukkit.configuration.file.YamlConfiguration; @@ -22,8 +21,6 @@ import com.bencodez.advancedcore.AdvancedCoreConfigOptions; import com.bencodez.advancedcore.AdvancedCorePlugin; -import com.bencodez.advancedcore.api.rewards.injected.RewardInject; -import com.bencodez.advancedcore.api.rewards.injectedrequirement.RequirementInject; class RewardLoaderSecurityTest { @@ -126,13 +123,10 @@ void queueOnlyResolverBindsGeneratedSnapshotToRequestedUser() throws IOException } @Test - void generatedDotNameDoesNotSuppressManualUnderscoreName() throws IOException { + @SuppressWarnings("unchecked") + void generatedDotNameDoesNotSuppressManualUnderscoreName() throws Exception { AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); - AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); RewardHandler handler = mock(RewardHandler.class); - RewardRegistry registry = mock(RewardRegistry.class); - Logger logger = mock(Logger.class); - ArrayList rewards = new ArrayList<>(); File directlyDefined = new File(tempDir, "Rewards/DirectlyDefined"); assertTrue(directlyDefined.mkdirs()); @@ -145,24 +139,22 @@ void generatedDotNameDoesNotSuppressManualUnderscoreName() throws IOException { manualData.save(new File(directlyDefined, "Foo_Bar.yml")); when(plugin.getDataFolder()).thenReturn(tempDir); - when(plugin.getOptions()).thenReturn(options); - when(plugin.getLogger()).thenReturn(logger); - when(plugin.getRewardHandler()).thenReturn(handler); - when(options.isLoadDefaultRewards()).thenReturn(false); - when(handler.getRewardRegistry()).thenReturn(registry); - when(handler.getRewards()).thenReturn(rewards); - when(handler.getInjectedRequirements()).thenReturn(new CopyOnWriteArrayList()); - when(handler.getInjectedRewards()).thenReturn(new CopyOnWriteArrayList()); - when(handler.getValidPaths()).thenReturn(Set.of("Messages")); - when(handler.rewardExist("Foo_Bar")).thenReturn(false); - AdvancedCorePlugin.setInstance(plugin); RewardLoader loader = new RewardLoader(handler, plugin); loader.addRewardFolder(directlyDefined, false, false); - loader.loadRewards(); - assertEquals(1, rewards.size()); - assertEquals("Foo_Bar", rewards.get(0).getName()); + java.lang.reflect.Method suppress = RewardLoader.class + .getDeclaredMethod("suppressGeneratedDirectlyDefinedFiles"); + suppress.setAccessible(true); + suppress.invoke(loader); + + java.lang.reflect.Field suppressedField = RewardLoader.class + .getDeclaredField("suppressedDirectlyDefinedRewards"); + suppressedField.setAccessible(true); + Set suppressed = (Set) suppressedField.get(loader); + + assertTrue(suppressed.contains("foo.bar.yml")); + assertFalse(suppressed.contains("foo_bar.yml")); } @Test From 87491f77dc97a1d646acc237097d4a4e65c147f8 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:41:29 -0600 Subject: [PATCH 46/71] Parse snapshot provenance only for queue replay --- .../api/rewards/RewardExecutor.java | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java index 151299f99e..dff36c6d12 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java @@ -125,36 +125,41 @@ public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewar return; } + boolean queueReplay = isPersistedQueueReplay(context.getOptions()); + if (!queueReplay) { + giveReward(user, handler.getReward(reward), context.getOptions()); + return; + } + String rewardName = reward; Boolean generatedSnapshot = null; - int snapshotMarker = reward.indexOf(QUEUED_SNAPSHOT_MARKER); + int snapshotMarker = reward.lastIndexOf(QUEUED_SNAPSHOT_MARKER); if (snapshotMarker >= 0) { - rewardName = reward.substring(0, snapshotMarker); - generatedSnapshot = Boolean.valueOf(reward.substring(snapshotMarker + QUEUED_SNAPSHOT_MARKER.length())); + String markerValue = reward.substring(snapshotMarker + QUEUED_SNAPSHOT_MARKER.length()); + if (markerValue.equalsIgnoreCase("true") || markerValue.equalsIgnoreCase("false")) { + rewardName = reward.substring(0, snapshotMarker); + generatedSnapshot = Boolean.valueOf(markerValue); + } } - Reward resolved = null; - if (isPersistedQueueReplay(context.getOptions())) { - if (generatedSnapshot != null) { - if (generatedSnapshot.booleanValue()) { - resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID()); - } else { - resolved = handler.getReward(rewardName); - } - } else if (handler.rewardExist(rewardName) || handler.hasDirectRewardHandle(rewardName)) { - // Legacy queue entry: if a normal registered reward exists, it wins over any - // stale generated file with the same name. - resolved = handler.getReward(rewardName); - } else { - // Legacy generated entries did not persist provenance. Preserve them only when - // there is no registered reward they could shadow. + Reward resolved; + if (generatedSnapshot != null) { + if (generatedSnapshot.booleanValue()) { resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID()); - if (resolved == null) { - resolved = handler.getReward(rewardName); - } + } else { + resolved = handler.getReward(rewardName); } - } else { + } else if (handler.rewardExist(rewardName) || handler.hasDirectRewardHandle(rewardName)) { + // Legacy queue entry: if a normal registered reward exists, it wins over any + // stale generated file with the same name. resolved = handler.getReward(rewardName); + } else { + // Legacy generated entries did not persist provenance. Preserve them only when + // there is no registered reward they could shadow. + resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID()); + if (resolved == null) { + resolved = handler.getReward(rewardName); + } } giveReward(user, resolved, context.getOptions()); } From 0ad6989e473ab21d525bb204dd67cbded4bb1bfc Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:41:51 -0600 Subject: [PATCH 47/71] Test normal reward names containing snapshot marker --- .../QueuedGeneratedRewardDispatchTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java index 4326aff2e5..527264afa8 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java @@ -139,6 +139,24 @@ void legacyQueueReferencePrefersRegisteredRewardOverStaleSnapshot() { verify(handler, never()).getQueuedGeneratedReward("Daily", "uuid"); } + @Test + void normalDispatchPreservesRewardNameContainingSnapshotMarkerText() { + String rewardName = "Promo%generatedsnapshot%Bonus"; + Reward normal = mock(Reward.class); + RewardOptions options = new RewardOptions(); + when(handler.getReward(rewardName)).thenReturn(normal); + + try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); + executor.giveReward(user, rewardName, options); + } + + verify(handler).getReward(rewardName); + verify(normal).giveReward(user, options); + verify(handler, never()).getReward("Promo"); + verify(handler, never()).getQueuedGeneratedReward("Promo", "uuid"); + } + @Test void normalStandaloneDispatchCannotResolveGeneratedSnapshot() { RewardOptions options = new RewardOptions(); From f51a06c44145ae3afdb9ac359073e24b60894051 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:43:13 -0600 Subject: [PATCH 48/71] Add versioned queued reward reference envelope --- .../api/rewards/QueuedRewardReference.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedRewardReference.java diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedRewardReference.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedRewardReference.java new file mode 100644 index 0000000000..d1a426cba8 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedRewardReference.java @@ -0,0 +1,62 @@ +package com.bencodez.advancedcore.api.rewards; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Versioned envelope used only for persisted offline/timed reward references. + * The prefix contains a backslash, which is not a valid reward file name, so a + * normal reward name cannot be mistaken for queue metadata. + */ +public final class QueuedRewardReference { + + private static final String PREFIX = "\\AdvancedCoreQueueV1\\"; + + private QueuedRewardReference() { + } + + public static String encode(String rewardName, boolean generatedSnapshot) { + String encodedName = Base64.getUrlEncoder().withoutPadding() + .encodeToString(rewardName.getBytes(StandardCharsets.UTF_8)); + return PREFIX + (generatedSnapshot ? "1:" : "0:") + encodedName; + } + + public static Parsed parse(String reference) { + if (reference == null || !reference.startsWith(PREFIX)) { + return null; + } + int flagIndex = PREFIX.length(); + if (reference.length() <= flagIndex + 2 || reference.charAt(flagIndex + 1) != ':') { + return null; + } + char flag = reference.charAt(flagIndex); + if (flag != '0' && flag != '1') { + return null; + } + try { + String rewardName = new String(Base64.getUrlDecoder().decode(reference.substring(flagIndex + 2)), + StandardCharsets.UTF_8); + return new Parsed(rewardName, flag == '1'); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + public static final class Parsed { + private final boolean generatedSnapshot; + private final String rewardName; + + private Parsed(String rewardName, boolean generatedSnapshot) { + this.rewardName = rewardName; + this.generatedSnapshot = generatedSnapshot; + } + + public boolean isGeneratedSnapshot() { + return generatedSnapshot; + } + + public String getRewardName() { + return rewardName; + } + } +} From e36bd7b4605a30f28675c8517882173e1cbb6f2c Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:43:36 -0600 Subject: [PATCH 49/71] Apply versioned queue-envelope fix for PR 295 --- .../fix-codex-295-queue-envelope.yml | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/fix-codex-295-queue-envelope.yml diff --git a/.github/workflows/fix-codex-295-queue-envelope.yml b/.github/workflows/fix-codex-295-queue-envelope.yml new file mode 100644 index 0000000000..e462ecc7ae --- /dev/null +++ b/.github/workflows/fix-codex-295-queue-envelope.yml @@ -0,0 +1,77 @@ +name: Apply Codex 295 queue-envelope fix + +on: + push: + branches: + - security/reward-path-identity-hardening + paths: + - .github/workflows/fix-codex-295-queue-envelope.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/reward-path-identity-hardening + fetch-depth: 0 + - name: Patch queue reference handling and tests + run: | + python3 - <<'PY' + from pathlib import Path + + # AdvancedCoreUser: persist and decode the versioned queue-only envelope. + path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') + text = path.read_text() + text = text.replace('import com.bencodez.advancedcore.api.rewards.RewardOptions;\n', + 'import com.bencodez.advancedcore.api.rewards.RewardOptions;\nimport com.bencodez.advancedcore.api.rewards.QueuedRewardReference;\n') + text = text.replace('\n\tprivate static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";\n', '\n') + text = text.replace( + '\tprivate String queuedRewardReference(Reward reward) {\n\t\treturn reward.getRewardName() + QUEUED_SNAPSHOT_MARKER + reward.isGeneratedSnapshotCreated();\n\t}\n', + '\tprivate String queuedRewardReference(Reward reward) {\n' + '\t\treturn QueuedRewardReference.encode(reward.getRewardName(), reward.isGeneratedSnapshotCreated());\n' + '\t}\n') + text = text.replace( + '\t\t\t\t\tString rewardName = rewardReference.split(QUEUED_SNAPSHOT_MARKER, 2)[0];\n', + '\t\t\t\t\tQueuedRewardReference.Parsed parsedReference = QueuedRewardReference.parse(rewardReference);\n' + '\t\t\t\t\tString rewardName = parsedReference == null ? rewardReference : parsedReference.getRewardName();\n') + path.write_text(text) + + # RewardExecutor: decode metadata only during persisted replay; normal dispatch keeps names byte-for-byte. + path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java') + text = path.read_text() + text = text.replace(' private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";\n\n', '') + old = ''' String rewardName = reward;\n Boolean generatedSnapshot = null;\n int snapshotMarker = reward.indexOf(QUEUED_SNAPSHOT_MARKER);\n if (snapshotMarker >= 0) {\n rewardName = reward.substring(0, snapshotMarker);\n generatedSnapshot = Boolean.valueOf(reward.substring(snapshotMarker + QUEUED_SNAPSHOT_MARKER.length()));\n }\n\n Reward resolved = null;\n if (isPersistedQueueReplay(context.getOptions())) {\n if (generatedSnapshot != null) {\n if (generatedSnapshot.booleanValue()) {\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n } else {\n resolved = handler.getReward(rewardName);\n }\n } else if (handler.rewardExist(rewardName) || handler.hasDirectRewardHandle(rewardName)) {\n // Legacy queue entry: if a normal registered reward exists, it wins over any\n // stale generated file with the same name.\n resolved = handler.getReward(rewardName);\n } else {\n // Legacy generated entries did not persist provenance. Preserve them only when\n // there is no registered reward they could shadow.\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n if (resolved == null) {\n resolved = handler.getReward(rewardName);\n }\n }\n } else {\n resolved = handler.getReward(rewardName);\n }\n giveReward(user, resolved, context.getOptions());\n''' + new = ''' Reward resolved = null;\n if (isPersistedQueueReplay(context.getOptions())) {\n QueuedRewardReference.Parsed queuedReference = QueuedRewardReference.parse(reward);\n if (queuedReference != null) {\n String rewardName = queuedReference.getRewardName();\n if (queuedReference.isGeneratedSnapshot()) {\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n } else {\n resolved = handler.getReward(rewardName);\n }\n } else if (handler.rewardExist(reward) || handler.hasDirectRewardHandle(reward)) {\n // Legacy queue entry: if a normal registered reward exists, it wins over any\n // stale generated file with the same name.\n resolved = handler.getReward(reward);\n } else {\n // Legacy generated entries did not persist provenance. Preserve them only when\n // there is no registered reward they could shadow.\n resolved = handler.getQueuedGeneratedReward(reward, user.getUUID());\n if (resolved == null) {\n resolved = handler.getReward(reward);\n }\n }\n } else {\n // Queue metadata is never decoded on ordinary dispatch, so every valid reward\n // name remains unchanged.\n resolved = handler.getReward(reward);\n }\n giveReward(user, resolved, context.getOptions());\n''' + if old not in text: + raise SystemExit('Unable to locate old queue marker handling') + text = text.replace(old, new, 1) + path.write_text(text) + + # Update queue dispatch tests to use the versioned envelope and cover a marker-like normal name. + path = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java') + text = path.read_text() + text = text.replace('import com.bencodez.advancedcore.api.rewards.RewardOptions;\n', + 'import com.bencodez.advancedcore.api.rewards.RewardOptions;\nimport com.bencodez.advancedcore.api.rewards.QueuedRewardReference;\n') + text = text.replace('executor.giveReward(user, "Daily%generatedsnapshot%false", options);', + 'executor.giveReward(user, QueuedRewardReference.encode("Daily", false), options);') + text = text.replace('executor.giveReward(user, "Daily%generatedsnapshot%true", options);', + 'executor.giveReward(user, QueuedRewardReference.encode("Daily", true), options);') + marker = ''' @Test\n void normalStandaloneDispatchCannotResolveGeneratedSnapshot() {\n''' + additions = ''' @Test\n void normalRewardNameContainingOldMarkerTextIsNotDecoded() {\n String rewardName = "Promo%generatedsnapshot%Bonus";\n Reward normal = mock(Reward.class);\n RewardOptions options = new RewardOptions();\n when(handler.getReward(rewardName)).thenReturn(normal);\n\n try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) {\n bukkit.when(Bukkit::isPrimaryThread).thenReturn(false);\n executor.giveReward(user, rewardName, options);\n }\n\n verify(handler).getReward(rewardName);\n verify(normal).giveReward(user, options);\n }\n\n''' + if marker not in text: + raise SystemExit('Unable to locate test insertion point') + text = text.replace(marker, additions + marker, 1) + path.write_text(text) + PY + - name: Commit fixes + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java + git rm .github/workflows/fix-codex-295-queue-envelope.yml + git commit -m "Version persisted queued reward references" + git push origin HEAD:security/reward-path-identity-hardening From cb18df5cd8525b92dd5dcdcbf866bc24130fa180 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:48:34 -0600 Subject: [PATCH 50/71] Remove temporary queue-envelope patch workflow --- .../fix-codex-295-queue-envelope.yml | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 .github/workflows/fix-codex-295-queue-envelope.yml diff --git a/.github/workflows/fix-codex-295-queue-envelope.yml b/.github/workflows/fix-codex-295-queue-envelope.yml deleted file mode 100644 index e462ecc7ae..0000000000 --- a/.github/workflows/fix-codex-295-queue-envelope.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Apply Codex 295 queue-envelope fix - -on: - push: - branches: - - security/reward-path-identity-hardening - paths: - - .github/workflows/fix-codex-295-queue-envelope.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/reward-path-identity-hardening - fetch-depth: 0 - - name: Patch queue reference handling and tests - run: | - python3 - <<'PY' - from pathlib import Path - - # AdvancedCoreUser: persist and decode the versioned queue-only envelope. - path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') - text = path.read_text() - text = text.replace('import com.bencodez.advancedcore.api.rewards.RewardOptions;\n', - 'import com.bencodez.advancedcore.api.rewards.RewardOptions;\nimport com.bencodez.advancedcore.api.rewards.QueuedRewardReference;\n') - text = text.replace('\n\tprivate static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";\n', '\n') - text = text.replace( - '\tprivate String queuedRewardReference(Reward reward) {\n\t\treturn reward.getRewardName() + QUEUED_SNAPSHOT_MARKER + reward.isGeneratedSnapshotCreated();\n\t}\n', - '\tprivate String queuedRewardReference(Reward reward) {\n' - '\t\treturn QueuedRewardReference.encode(reward.getRewardName(), reward.isGeneratedSnapshotCreated());\n' - '\t}\n') - text = text.replace( - '\t\t\t\t\tString rewardName = rewardReference.split(QUEUED_SNAPSHOT_MARKER, 2)[0];\n', - '\t\t\t\t\tQueuedRewardReference.Parsed parsedReference = QueuedRewardReference.parse(rewardReference);\n' - '\t\t\t\t\tString rewardName = parsedReference == null ? rewardReference : parsedReference.getRewardName();\n') - path.write_text(text) - - # RewardExecutor: decode metadata only during persisted replay; normal dispatch keeps names byte-for-byte. - path = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java') - text = path.read_text() - text = text.replace(' private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";\n\n', '') - old = ''' String rewardName = reward;\n Boolean generatedSnapshot = null;\n int snapshotMarker = reward.indexOf(QUEUED_SNAPSHOT_MARKER);\n if (snapshotMarker >= 0) {\n rewardName = reward.substring(0, snapshotMarker);\n generatedSnapshot = Boolean.valueOf(reward.substring(snapshotMarker + QUEUED_SNAPSHOT_MARKER.length()));\n }\n\n Reward resolved = null;\n if (isPersistedQueueReplay(context.getOptions())) {\n if (generatedSnapshot != null) {\n if (generatedSnapshot.booleanValue()) {\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n } else {\n resolved = handler.getReward(rewardName);\n }\n } else if (handler.rewardExist(rewardName) || handler.hasDirectRewardHandle(rewardName)) {\n // Legacy queue entry: if a normal registered reward exists, it wins over any\n // stale generated file with the same name.\n resolved = handler.getReward(rewardName);\n } else {\n // Legacy generated entries did not persist provenance. Preserve them only when\n // there is no registered reward they could shadow.\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n if (resolved == null) {\n resolved = handler.getReward(rewardName);\n }\n }\n } else {\n resolved = handler.getReward(rewardName);\n }\n giveReward(user, resolved, context.getOptions());\n''' - new = ''' Reward resolved = null;\n if (isPersistedQueueReplay(context.getOptions())) {\n QueuedRewardReference.Parsed queuedReference = QueuedRewardReference.parse(reward);\n if (queuedReference != null) {\n String rewardName = queuedReference.getRewardName();\n if (queuedReference.isGeneratedSnapshot()) {\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n } else {\n resolved = handler.getReward(rewardName);\n }\n } else if (handler.rewardExist(reward) || handler.hasDirectRewardHandle(reward)) {\n // Legacy queue entry: if a normal registered reward exists, it wins over any\n // stale generated file with the same name.\n resolved = handler.getReward(reward);\n } else {\n // Legacy generated entries did not persist provenance. Preserve them only when\n // there is no registered reward they could shadow.\n resolved = handler.getQueuedGeneratedReward(reward, user.getUUID());\n if (resolved == null) {\n resolved = handler.getReward(reward);\n }\n }\n } else {\n // Queue metadata is never decoded on ordinary dispatch, so every valid reward\n // name remains unchanged.\n resolved = handler.getReward(reward);\n }\n giveReward(user, resolved, context.getOptions());\n''' - if old not in text: - raise SystemExit('Unable to locate old queue marker handling') - text = text.replace(old, new, 1) - path.write_text(text) - - # Update queue dispatch tests to use the versioned envelope and cover a marker-like normal name. - path = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java') - text = path.read_text() - text = text.replace('import com.bencodez.advancedcore.api.rewards.RewardOptions;\n', - 'import com.bencodez.advancedcore.api.rewards.RewardOptions;\nimport com.bencodez.advancedcore.api.rewards.QueuedRewardReference;\n') - text = text.replace('executor.giveReward(user, "Daily%generatedsnapshot%false", options);', - 'executor.giveReward(user, QueuedRewardReference.encode("Daily", false), options);') - text = text.replace('executor.giveReward(user, "Daily%generatedsnapshot%true", options);', - 'executor.giveReward(user, QueuedRewardReference.encode("Daily", true), options);') - marker = ''' @Test\n void normalStandaloneDispatchCannotResolveGeneratedSnapshot() {\n''' - additions = ''' @Test\n void normalRewardNameContainingOldMarkerTextIsNotDecoded() {\n String rewardName = "Promo%generatedsnapshot%Bonus";\n Reward normal = mock(Reward.class);\n RewardOptions options = new RewardOptions();\n when(handler.getReward(rewardName)).thenReturn(normal);\n\n try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) {\n bukkit.when(Bukkit::isPrimaryThread).thenReturn(false);\n executor.giveReward(user, rewardName, options);\n }\n\n verify(handler).getReward(rewardName);\n verify(normal).giveReward(user, options);\n }\n\n''' - if marker not in text: - raise SystemExit('Unable to locate test insertion point') - text = text.replace(marker, additions + marker, 1) - path.write_text(text) - PY - - name: Commit fixes - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java - git rm .github/workflows/fix-codex-295-queue-envelope.yml - git commit -m "Version persisted queued reward references" - git push origin HEAD:security/reward-path-identity-hardening From a973f815772a66014ccfc19d993eabb44c8bc4d0 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 18:48:46 -0600 Subject: [PATCH 51/71] Remove unused queue envelope experiment --- .../api/rewards/QueuedRewardReference.java | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedRewardReference.java diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedRewardReference.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedRewardReference.java deleted file mode 100644 index d1a426cba8..0000000000 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedRewardReference.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.bencodez.advancedcore.api.rewards; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -/** - * Versioned envelope used only for persisted offline/timed reward references. - * The prefix contains a backslash, which is not a valid reward file name, so a - * normal reward name cannot be mistaken for queue metadata. - */ -public final class QueuedRewardReference { - - private static final String PREFIX = "\\AdvancedCoreQueueV1\\"; - - private QueuedRewardReference() { - } - - public static String encode(String rewardName, boolean generatedSnapshot) { - String encodedName = Base64.getUrlEncoder().withoutPadding() - .encodeToString(rewardName.getBytes(StandardCharsets.UTF_8)); - return PREFIX + (generatedSnapshot ? "1:" : "0:") + encodedName; - } - - public static Parsed parse(String reference) { - if (reference == null || !reference.startsWith(PREFIX)) { - return null; - } - int flagIndex = PREFIX.length(); - if (reference.length() <= flagIndex + 2 || reference.charAt(flagIndex + 1) != ':') { - return null; - } - char flag = reference.charAt(flagIndex); - if (flag != '0' && flag != '1') { - return null; - } - try { - String rewardName = new String(Base64.getUrlDecoder().decode(reference.substring(flagIndex + 2)), - StandardCharsets.UTF_8); - return new Parsed(rewardName, flag == '1'); - } catch (IllegalArgumentException ignored) { - return null; - } - } - - public static final class Parsed { - private final boolean generatedSnapshot; - private final String rewardName; - - private Parsed(String rewardName, boolean generatedSnapshot) { - this.rewardName = rewardName; - this.generatedSnapshot = generatedSnapshot; - } - - public boolean isGeneratedSnapshot() { - return generatedSnapshot; - } - - public String getRewardName() { - return rewardName; - } - } -} From 74d0509d2611f49212ac7b64a7de31c8ea2cff81 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:20:55 -0600 Subject: [PATCH 52/71] Add focused Codex 295 queue envelope patch --- .../fix-codex-295-queue-envelope.yml | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/fix-codex-295-queue-envelope.yml diff --git a/.github/workflows/fix-codex-295-queue-envelope.yml b/.github/workflows/fix-codex-295-queue-envelope.yml new file mode 100644 index 0000000000..9f660bd3ef --- /dev/null +++ b/.github/workflows/fix-codex-295-queue-envelope.yml @@ -0,0 +1,57 @@ +name: Apply Codex 295 queue envelope fix + +on: + push: + branches: + - security/reward-path-identity-hardening + paths: + - .github/workflows/fix-codex-295-queue-envelope.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/reward-path-identity-hardening + fetch-depth: 0 + - name: Replace ambiguous queue suffix with versioned envelope + run: | + python3 - <<'PY' + from pathlib import Path + + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') + text = p.read_text() + if 'import java.nio.charset.StandardCharsets;' not in text: + text = text.replace('import java.text.SimpleDateFormat;\n', 'import java.nio.charset.StandardCharsets;\nimport java.text.SimpleDateFormat;\n', 1) + if 'import java.util.Base64;' not in text: + text = text.replace('import java.util.ArrayList;\n', 'import java.util.ArrayList;\nimport java.util.Base64;\n', 1) + text = text.replace('private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";', + 'private static final String QUEUED_REFERENCE_PREFIX = "\\\\AdvancedCoreQueue/1/";', 1) + old = '''\tprivate String queuedRewardReference(Reward reward) {\n\t\treturn reward.getRewardName() + QUEUED_SNAPSHOT_MARKER + reward.isGeneratedSnapshotCreated();\n\t}\n''' + new = '''\tprivate String queuedRewardReference(Reward reward) {\n\t\tString encodedName = Base64.getUrlEncoder().withoutPadding()\n\t\t\t\t.encodeToString(reward.getRewardName().getBytes(StandardCharsets.UTF_8));\n\t\treturn QUEUED_REFERENCE_PREFIX + (reward.isGeneratedSnapshotCreated() ? "snapshot/" : "normal/")\n\t\t\t\t+ encodedName;\n\t}\n''' + if old not in text: + raise SystemExit('queuedRewardReference method not found') + p.write_text(text.replace(old, new, 1)) + + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java') + text = p.read_text() + if 'import java.nio.charset.StandardCharsets;' not in text: + text = text.replace('import java.util.ArrayList;\n', 'import java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.Base64;\n', 1) + text = text.replace('private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";', + 'private static final String QUEUED_REFERENCE_PREFIX = "\\\\AdvancedCoreQueue/1/";', 1) + old = ''' String rewardName = reward;\n Boolean generatedSnapshot = null;\n int snapshotMarker = reward.lastIndexOf(QUEUED_SNAPSHOT_MARKER);\n if (snapshotMarker >= 0) {\n String markerValue = reward.substring(snapshotMarker + QUEUED_SNAPSHOT_MARKER.length());\n if (markerValue.equalsIgnoreCase("true") || markerValue.equalsIgnoreCase("false")) {\n rewardName = reward.substring(0, snapshotMarker);\n generatedSnapshot = Boolean.valueOf(markerValue);\n }\n }\n\n Reward resolved;\n''' + new = ''' String rewardName = reward;\n Boolean generatedSnapshot = null;\n if (reward.startsWith(QUEUED_REFERENCE_PREFIX)) {\n String encoded = reward.substring(QUEUED_REFERENCE_PREFIX.length());\n int modeEnd = encoded.indexOf('/');\n if (modeEnd > 0) {\n String mode = encoded.substring(0, modeEnd);\n String encodedName = encoded.substring(modeEnd + 1);\n if ((mode.equals("snapshot") || mode.equals("normal")) && !encodedName.isEmpty()) {\n try {\n rewardName = new String(Base64.getUrlDecoder().decode(encodedName), StandardCharsets.UTF_8);\n generatedSnapshot = Boolean.valueOf(mode.equals("snapshot"));\n } catch (IllegalArgumentException ignored) {\n rewardName = reward;\n }\n }\n }\n }\n\n Reward resolved;\n''' + if old not in text: + raise SystemExit('RewardExecutor queue suffix block not found') + p.write_text(text.replace(old, new, 1)) + PY + rm .github/workflows/fix-codex-295-queue-envelope.yml + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git add -A + git commit -m "Use unambiguous persisted reward queue references" + git push origin HEAD:security/reward-path-identity-hardening From b17f0a114adc0dc43f6ab1c0aee38126d6ac6b19 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:21:05 +0000 Subject: [PATCH 53/71] Use unambiguous persisted reward queue references --- .../fix-codex-295-queue-envelope.yml | 57 ------------------- .../api/rewards/RewardExecutor.java | 24 +++++--- .../api/user/AdvancedCoreUser.java | 9 ++- 3 files changed, 24 insertions(+), 66 deletions(-) delete mode 100644 .github/workflows/fix-codex-295-queue-envelope.yml diff --git a/.github/workflows/fix-codex-295-queue-envelope.yml b/.github/workflows/fix-codex-295-queue-envelope.yml deleted file mode 100644 index 9f660bd3ef..0000000000 --- a/.github/workflows/fix-codex-295-queue-envelope.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Apply Codex 295 queue envelope fix - -on: - push: - branches: - - security/reward-path-identity-hardening - paths: - - .github/workflows/fix-codex-295-queue-envelope.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/reward-path-identity-hardening - fetch-depth: 0 - - name: Replace ambiguous queue suffix with versioned envelope - run: | - python3 - <<'PY' - from pathlib import Path - - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') - text = p.read_text() - if 'import java.nio.charset.StandardCharsets;' not in text: - text = text.replace('import java.text.SimpleDateFormat;\n', 'import java.nio.charset.StandardCharsets;\nimport java.text.SimpleDateFormat;\n', 1) - if 'import java.util.Base64;' not in text: - text = text.replace('import java.util.ArrayList;\n', 'import java.util.ArrayList;\nimport java.util.Base64;\n', 1) - text = text.replace('private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";', - 'private static final String QUEUED_REFERENCE_PREFIX = "\\\\AdvancedCoreQueue/1/";', 1) - old = '''\tprivate String queuedRewardReference(Reward reward) {\n\t\treturn reward.getRewardName() + QUEUED_SNAPSHOT_MARKER + reward.isGeneratedSnapshotCreated();\n\t}\n''' - new = '''\tprivate String queuedRewardReference(Reward reward) {\n\t\tString encodedName = Base64.getUrlEncoder().withoutPadding()\n\t\t\t\t.encodeToString(reward.getRewardName().getBytes(StandardCharsets.UTF_8));\n\t\treturn QUEUED_REFERENCE_PREFIX + (reward.isGeneratedSnapshotCreated() ? "snapshot/" : "normal/")\n\t\t\t\t+ encodedName;\n\t}\n''' - if old not in text: - raise SystemExit('queuedRewardReference method not found') - p.write_text(text.replace(old, new, 1)) - - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java') - text = p.read_text() - if 'import java.nio.charset.StandardCharsets;' not in text: - text = text.replace('import java.util.ArrayList;\n', 'import java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.Base64;\n', 1) - text = text.replace('private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%";', - 'private static final String QUEUED_REFERENCE_PREFIX = "\\\\AdvancedCoreQueue/1/";', 1) - old = ''' String rewardName = reward;\n Boolean generatedSnapshot = null;\n int snapshotMarker = reward.lastIndexOf(QUEUED_SNAPSHOT_MARKER);\n if (snapshotMarker >= 0) {\n String markerValue = reward.substring(snapshotMarker + QUEUED_SNAPSHOT_MARKER.length());\n if (markerValue.equalsIgnoreCase("true") || markerValue.equalsIgnoreCase("false")) {\n rewardName = reward.substring(0, snapshotMarker);\n generatedSnapshot = Boolean.valueOf(markerValue);\n }\n }\n\n Reward resolved;\n''' - new = ''' String rewardName = reward;\n Boolean generatedSnapshot = null;\n if (reward.startsWith(QUEUED_REFERENCE_PREFIX)) {\n String encoded = reward.substring(QUEUED_REFERENCE_PREFIX.length());\n int modeEnd = encoded.indexOf('/');\n if (modeEnd > 0) {\n String mode = encoded.substring(0, modeEnd);\n String encodedName = encoded.substring(modeEnd + 1);\n if ((mode.equals("snapshot") || mode.equals("normal")) && !encodedName.isEmpty()) {\n try {\n rewardName = new String(Base64.getUrlDecoder().decode(encodedName), StandardCharsets.UTF_8);\n generatedSnapshot = Boolean.valueOf(mode.equals("snapshot"));\n } catch (IllegalArgumentException ignored) {\n rewardName = reward;\n }\n }\n }\n }\n\n Reward resolved;\n''' - if old not in text: - raise SystemExit('RewardExecutor queue suffix block not found') - p.write_text(text.replace(old, new, 1)) - PY - rm .github/workflows/fix-codex-295-queue-envelope.yml - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - git add -A - git commit -m "Use unambiguous persisted reward queue references" - git push origin HEAD:security/reward-path-identity-hardening diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java index dff36c6d12..3c6fc83d12 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java @@ -1,6 +1,8 @@ package com.bencodez.advancedcore.api.rewards; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; import org.bukkit.Bukkit; import org.bukkit.configuration.Configuration; @@ -17,7 +19,7 @@ */ public class RewardExecutor { - private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%"; + private static final String QUEUED_REFERENCE_PREFIX = "\\AdvancedCoreQueue/1/"; private final RewardHandler handler; private final AdvancedCorePlugin plugin; @@ -133,12 +135,20 @@ public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewar String rewardName = reward; Boolean generatedSnapshot = null; - int snapshotMarker = reward.lastIndexOf(QUEUED_SNAPSHOT_MARKER); - if (snapshotMarker >= 0) { - String markerValue = reward.substring(snapshotMarker + QUEUED_SNAPSHOT_MARKER.length()); - if (markerValue.equalsIgnoreCase("true") || markerValue.equalsIgnoreCase("false")) { - rewardName = reward.substring(0, snapshotMarker); - generatedSnapshot = Boolean.valueOf(markerValue); + if (reward.startsWith(QUEUED_REFERENCE_PREFIX)) { + String encoded = reward.substring(QUEUED_REFERENCE_PREFIX.length()); + int modeEnd = encoded.indexOf('/'); + if (modeEnd > 0) { + String mode = encoded.substring(0, modeEnd); + String encodedName = encoded.substring(modeEnd + 1); + if ((mode.equals("snapshot") || mode.equals("normal")) && !encodedName.isEmpty()) { + try { + rewardName = new String(Base64.getUrlDecoder().decode(encodedName), StandardCharsets.UTF_8); + generatedSnapshot = Boolean.valueOf(mode.equals("snapshot")); + } catch (IllegalArgumentException ignored) { + rewardName = reward; + } + } } } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java index fd7d125039..6a35dd0eae 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java @@ -1,11 +1,13 @@ package com.bencodez.advancedcore.api.user; +import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; +import java.util.Base64; import java.util.Date; import java.util.HashMap; import java.util.Map.Entry; @@ -53,7 +55,7 @@ */ public class AdvancedCoreUser { - private static final String QUEUED_SNAPSHOT_MARKER = "%generatedsnapshot%"; + private static final String QUEUED_REFERENCE_PREFIX = "\\AdvancedCoreQueue/1/"; /** * User data fetch mode for this user. @@ -257,7 +259,10 @@ public synchronized void addTimedReward(Reward reward, HashMap p } private String queuedRewardReference(Reward reward) { - return reward.getRewardName() + QUEUED_SNAPSHOT_MARKER + reward.isGeneratedSnapshotCreated(); + String encodedName = Base64.getUrlEncoder().withoutPadding() + .encodeToString(reward.getRewardName().getBytes(StandardCharsets.UTF_8)); + return QUEUED_REFERENCE_PREFIX + (reward.isGeneratedSnapshotCreated() ? "snapshot/" : "normal/") + + encodedName; } /** From b21362672e716f8c60ce4ae9ed65cfdcf1028794 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:23:40 -0600 Subject: [PATCH 54/71] Apply queued reward envelope cleanup --- .github/workflows/fix-codex-295-queue-log.yml | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/fix-codex-295-queue-log.yml diff --git a/.github/workflows/fix-codex-295-queue-log.yml b/.github/workflows/fix-codex-295-queue-log.yml new file mode 100644 index 0000000000..95cd497a5d --- /dev/null +++ b/.github/workflows/fix-codex-295-queue-log.yml @@ -0,0 +1,38 @@ +name: Fix Codex 295 queue logging + +on: + push: + branches: + - security/reward-path-identity-hardening + paths: + - .github/workflows/fix-codex-295-queue-log.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/reward-path-identity-hardening + fetch-depth: 0 + - name: Remove stale queue marker reference + run: | + python3 - <<'PY' + from pathlib import Path + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') + text = p.read_text() + old = 'String rewardName = rewardReference.split(QUEUED_SNAPSHOT_MARKER, 2)[0];' + new = 'String rewardName = rewardReference;' + if old not in text: + raise SystemExit('stale queue marker log reference not found') + p.write_text(text.replace(old, new, 1)) + PY + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java + git rm .github/workflows/fix-codex-295-queue-log.yml + git commit -m "Remove stale queued reward marker reference" + git push origin HEAD:security/reward-path-identity-hardening From df4ea8808edf8e730a79ad97aaf6f392fab2d5d5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:23:50 +0000 Subject: [PATCH 55/71] Remove stale queued reward marker reference --- .github/workflows/fix-codex-295-queue-log.yml | 38 ------------------- .../api/user/AdvancedCoreUser.java | 2 +- 2 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 .github/workflows/fix-codex-295-queue-log.yml diff --git a/.github/workflows/fix-codex-295-queue-log.yml b/.github/workflows/fix-codex-295-queue-log.yml deleted file mode 100644 index 95cd497a5d..0000000000 --- a/.github/workflows/fix-codex-295-queue-log.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Fix Codex 295 queue logging - -on: - push: - branches: - - security/reward-path-identity-hardening - paths: - - .github/workflows/fix-codex-295-queue-log.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/reward-path-identity-hardening - fetch-depth: 0 - - name: Remove stale queue marker reference - run: | - python3 - <<'PY' - from pathlib import Path - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') - text = p.read_text() - old = 'String rewardName = rewardReference.split(QUEUED_SNAPSHOT_MARKER, 2)[0];' - new = 'String rewardName = rewardReference;' - if old not in text: - raise SystemExit('stale queue marker log reference not found') - p.write_text(text.replace(old, new, 1)) - PY - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java - git rm .github/workflows/fix-codex-295-queue-log.yml - git commit -m "Remove stale queued reward marker reference" - git push origin HEAD:security/reward-path-identity-hardening diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java index 6a35dd0eae..15fd31001e 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java @@ -327,7 +327,7 @@ public void checkDelayedTimedRewards() { replayOptions.addPlaceholder("date", "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(new Date(time))); plugin.getRewardHandler().giveReward(this, rewardReference, replayOptions); - String rewardName = rewardReference.split(QUEUED_SNAPSHOT_MARKER, 2)[0]; + String rewardName = rewardReference; plugin.debug("Giving timed/delayed reward " + rewardName + " for " + getPlayerName() + " with placeholders " + ArrayUtils.fromString(placeholders)); } else { From 61af682a33a44b043a0c46bbaa33bcfc50e7bcf0 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:24:34 -0600 Subject: [PATCH 56/71] Test versioned queued reward references --- .../QueuedGeneratedRewardDispatchTest.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java index 527264afa8..b62c42920e 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java @@ -5,6 +5,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.logging.Logger; import org.bukkit.Bukkit; @@ -22,6 +24,8 @@ class QueuedGeneratedRewardDispatchTest { + private static final String QUEUE_PREFIX = "\\AdvancedCoreQueue/1/"; + private AdvancedCorePlugin plugin; private RewardHandler handler; private AdvancedCoreUser user; @@ -41,6 +45,12 @@ void setUp() { when(user.isOnline()).thenReturn(true); } + private String queuedReference(String rewardName, boolean snapshot) { + String encodedName = Base64.getUrlEncoder().withoutPadding() + .encodeToString(rewardName.getBytes(StandardCharsets.UTF_8)); + return QUEUE_PREFIX + (snapshot ? "snapshot/" : "normal/") + encodedName; + } + @Test void offlineQueueReplayResolvesGeneratedSnapshotBeforeNormalFallback() { Reward queued = mock(Reward.class); @@ -97,7 +107,7 @@ void explicitNormalQueueReferenceDoesNotUseStaleSnapshot() { try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); - executor.giveReward(user, "Daily%generatedsnapshot%false", options); + executor.giveReward(user, queuedReference("Daily", false), options); } verify(normal).giveReward(user, options); @@ -114,7 +124,7 @@ void explicitSnapshotQueueReferenceUsesGeneratedSnapshot() { try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); - executor.giveReward(user, "Daily%generatedsnapshot%true", options); + executor.giveReward(user, queuedReference("Daily", true), options); } verify(queued).giveReward(user, options); @@ -139,6 +149,25 @@ void legacyQueueReferencePrefersRegisteredRewardOverStaleSnapshot() { verify(handler, never()).getQueuedGeneratedReward("Daily", "uuid"); } + @Test + void legacyQueueNameEndingInOldMarkerIsPreservedExactly() { + String rewardName = "Promo%generatedsnapshot%true"; + Reward normal = mock(Reward.class); + RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false); + when(handler.rewardExist(rewardName)).thenReturn(true); + when(handler.getReward(rewardName)).thenReturn(normal); + + try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); + executor.giveReward(user, rewardName, options); + } + + verify(handler).getReward(rewardName); + verify(normal).giveReward(user, options); + verify(handler, never()).getQueuedGeneratedReward("Promo", "uuid"); + verify(handler, never()).getReward("Promo"); + } + @Test void normalDispatchPreservesRewardNameContainingSnapshotMarkerText() { String rewardName = "Promo%generatedsnapshot%Bonus"; From cf3f2b229759caf1c4bf493da1fe447cfd32ffa4 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:50:37 -0600 Subject: [PATCH 57/71] Preserve queued file-backed sub rewards --- .../api/rewards/SubDirectlyDefinedReward.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubDirectlyDefinedReward.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubDirectlyDefinedReward.java index 2b5641cb36..b7d7c88f98 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubDirectlyDefinedReward.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/SubDirectlyDefinedReward.java @@ -37,12 +37,25 @@ public String getFullPath() { public Reward getReward() { if (isDirectlyDefined()) { - return new Reward((master.getFullPath() + master.needsDot() + getPath()).replace(".", "_"), - getFileData().getConfigurationSection(getPath())).needsRewardFile(false); + Reward reward = new Reward((master.getFullPath() + master.needsDot() + getPath()).replace(".", "_"), + getFileData().getConfigurationSection(getPath())); + // File-backed sub rewards are not exposed through standalone lookup, but they + // still need a generated queue snapshot if delayed/timed/offline execution + // defers them. Directly-defined sub rewards already have a stable internal + // definition and do not need snapshot materialization. + return isFileBacked() ? reward : reward.needsRewardFile(false); } return null; } + private boolean isFileBacked() { + DefinedReward current = master; + while (current instanceof SubDirectlyDefinedReward) { + current = ((SubDirectlyDefinedReward) current).getMaster(); + } + return current instanceof RewardFileDefinedReward; + } + public Object getValue(String path) { return getFileData().get(getPath() + "." + path); } From 76748fdc98da092e95558b873f86d37d3d93941a Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:50:49 -0600 Subject: [PATCH 58/71] Preserve generated snapshot provenance on requeue --- .../advancedcore/api/rewards/QueuedGeneratedReward.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java index 86b2f4c727..580a8a344e 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java @@ -31,6 +31,13 @@ public void giveReward(AdvancedCoreUser user, RewardOptions rewardOptions) { super.giveReward(user, rewardOptions); } + @Override + public boolean isGeneratedSnapshotCreated() { + // A loaded generated snapshot must retain snapshot provenance if execution is + // deferred again (paused rewards, vanish-as-offline, or another offline retry). + return true; + } + @Override public void validate() { // This file is a persisted snapshot created from a reward that was already From bac6a720665176092f3a9813e84a2d867d524167 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:51:13 -0600 Subject: [PATCH 59/71] Test queued snapshot requeue provenance --- .../advancedcore/api/rewards/RewardLoaderSecurityTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index d659b5aac9..af28c08644 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -118,6 +118,7 @@ void queueOnlyResolverBindsGeneratedSnapshotToRequestedUser() throws IOException Reward reward = loader.getQueuedGeneratedReward("QueuedReward", queuedUuid.toString()); assertTrue(reward instanceof QueuedGeneratedReward); + assertTrue(reward.isGeneratedSnapshotCreated()); assertEquals(java.util.Set.of(queuedUuid.toString()), ((QueuedGeneratedReward) reward).getAllowedUserUuids()); } From 684451d2f0677ad3bbcccd6cd08bb6c665f08ce9 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:51:41 -0600 Subject: [PATCH 60/71] Track internal file subreward queue provenance --- .../api/rewards/InternalFileQueuedReward.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/InternalFileQueuedReward.java diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/InternalFileQueuedReward.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/InternalFileQueuedReward.java new file mode 100644 index 0000000000..32a64216de --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/InternalFileQueuedReward.java @@ -0,0 +1,15 @@ +package com.bencodez.advancedcore.api.rewards; + +import org.bukkit.configuration.ConfigurationSection; + +/** + * Marker reward used when a file-backed nested reward must be persisted for + * offline or timed replay. These rewards are resolved only through the internal + * file-backed sub-reward registry and are never treated as standalone rewards. + */ +public final class InternalFileQueuedReward extends Reward { + + InternalFileQueuedReward(String name, ConfigurationSection section) { + super(name, section); + } +} From 61d5494814c979425d96dae443e7929d34b54b08 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:51:44 -0600 Subject: [PATCH 61/71] Test deferred file-backed sub reward snapshots --- .../tests/rewards/RewardServicesTest.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java index d142491ded..a1755a4520 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java @@ -1,6 +1,7 @@ package com.bencodez.advancedcore.tests.rewards; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -27,8 +28,8 @@ import com.bencodez.advancedcore.api.rewards.DefinedReward; import com.bencodez.advancedcore.api.rewards.DirectlyDefinedReward; import com.bencodez.advancedcore.api.rewards.Reward; -import com.bencodez.advancedcore.api.rewards.RewardHandler; import com.bencodez.advancedcore.api.rewards.RewardFileDefinedReward; +import com.bencodez.advancedcore.api.rewards.RewardHandler; import com.bencodez.advancedcore.api.rewards.SubDirectlyDefinedReward; import com.bencodez.advancedcore.api.rewards.injected.RewardInject; import com.bencodez.advancedcore.api.rewards.injectedrequirement.RequirementInject; @@ -155,4 +156,27 @@ public void fileBackedSubRewardsRemainInternalToSectionDispatch() { assertNull(handler.getSubDirectlyDefined("Daily_Rewards")); assertSame(sub, handler.getSubRewardResolver().getFileBackedSubReward("Daily_Rewards")); } + + @Test + public void fileBackedSubRewardsRemainSnapshotCapableForDeferredExecution() { + YamlConfiguration config = new YamlConfiguration(); + config.createSection("Parent.Child").set("Messages", List.of("queued")); + + RewardFileDefinedReward fileMaster = mock(RewardFileDefinedReward.class); + when(fileMaster.getFullPath()).thenReturn("Daily"); + when(fileMaster.needsDot()).thenReturn("."); + when(fileMaster.getPath()).thenReturn("Parent"); + when(fileMaster.getFileData()).thenReturn(config); + Reward fileBacked = new SubDirectlyDefinedReward(fileMaster, "Child").getReward(); + + DirectlyDefinedReward directMaster = mock(DirectlyDefinedReward.class); + when(directMaster.getFullPath()).thenReturn("Direct"); + when(directMaster.needsDot()).thenReturn("."); + when(directMaster.getPath()).thenReturn("Parent"); + when(directMaster.getFileData()).thenReturn(config); + Reward directlyDefined = new SubDirectlyDefinedReward(directMaster, "Child").getReward(); + + assertTrue(fileBacked.isNeedsRewardFile()); + assertFalse(directlyDefined.isNeedsRewardFile()); + } } From 668fa6f0f4af9ea753d44062e6f9b55ea3b178ae Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 19:52:43 -0600 Subject: [PATCH 62/71] Remove unused queue marker class --- .../api/rewards/InternalFileQueuedReward.java | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/InternalFileQueuedReward.java diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/InternalFileQueuedReward.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/InternalFileQueuedReward.java deleted file mode 100644 index 32a64216de..0000000000 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/InternalFileQueuedReward.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.bencodez.advancedcore.api.rewards; - -import org.bukkit.configuration.ConfigurationSection; - -/** - * Marker reward used when a file-backed nested reward must be persisted for - * offline or timed replay. These rewards are resolved only through the internal - * file-backed sub-reward registry and are never treated as standalone rewards. - */ -public final class InternalFileQueuedReward extends Reward { - - InternalFileQueuedReward(String name, ConfigurationSection section) { - super(name, section); - } -} From f7250118365ff74ffc1d3398119f0d540b93dbea Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 20:03:35 -0600 Subject: [PATCH 63/71] Add one-shot queue provenance fix --- .../fix-codex-295-queue-provenance.yml | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .github/workflows/fix-codex-295-queue-provenance.yml diff --git a/.github/workflows/fix-codex-295-queue-provenance.yml b/.github/workflows/fix-codex-295-queue-provenance.yml new file mode 100644 index 0000000000..f6a022ae40 --- /dev/null +++ b/.github/workflows/fix-codex-295-queue-provenance.yml @@ -0,0 +1,107 @@ +name: Apply Codex 295 queue provenance fix + +on: + push: + branches: + - security/reward-path-identity-hardening + paths: + - .github/workflows/fix-codex-295-queue-provenance.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/reward-path-identity-hardening + fetch-depth: 0 + - name: Require real persisted queue provenance + run: | + python3 - <<'PY' + from pathlib import Path + + # Capability object: only code in api.user can construct one normally. + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/PersistedQueueReference.java') + p.write_text('''package com.bencodez.advancedcore.api.user;\n\n/**\n * Capability wrapper for a reward reference read from this user's persisted\n * offline/timed queue. The constructor is package-private so ordinary reward\n * callers cannot manufacture queue provenance through RewardOptions.\n */\npublic final class PersistedQueueReference {\n\n\tprivate final String reference;\n\n\tPersistedQueueReference(String reference) {\n\t\tthis.reference = reference;\n\t}\n\n\tpublic String getReference() {\n\t\treturn reference;\n\t}\n}\n''') + + # Public reward facade gets a queue-specific capability entry point. + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java') + text = p.read_text() + imp = 'import com.bencodez.advancedcore.api.user.AdvancedCoreUser;\n' + if 'import com.bencodez.advancedcore.api.user.PersistedQueueReference;' not in text: + text = text.replace(imp, imp + 'import com.bencodez.advancedcore.api.user.PersistedQueueReference;\n', 1) + old = ''' public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewardOptions) {\n rewardExecutor.giveReward(user, reward, rewardOptions);\n }\n''' + new = old + '''\n public void givePersistedQueueReward(AdvancedCoreUser user, PersistedQueueReference rewardReference,\n RewardOptions rewardOptions) {\n if (rewardReference == null) {\n return;\n }\n rewardExecutor.givePersistedQueueReward(user, rewardReference.getReference(), rewardOptions);\n }\n''' + if old not in text: + raise SystemExit('RewardHandler string giveReward block not found') + p.write_text(text.replace(old, new, 1)) + + # Ordinary String dispatch no longer infers queue provenance from user-controlled options. + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java') + text = p.read_text() + start = text.index(' public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewardOptions) {') + end = text.index(' public void updateReward(Configuration data, String path, RewardOptions rewardOptions) {', start) + replacement = ''' public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewardOptions) {\n RewardExecutionContext context = new RewardExecutionContext(rewardOptions).initializeOnlineState(user);\n if (reward == null || reward.isEmpty()) {\n return;\n }\n\n if (reward.startsWith("/")) {\n MiscUtils.getInstance().executeConsoleCommands(user.getPlayerName(), reward, context.getPlaceholders());\n return;\n }\n\n giveReward(user, handler.getReward(reward), context.getOptions());\n }\n\n void givePersistedQueueReward(AdvancedCoreUser user, String reward, RewardOptions rewardOptions) {\n RewardExecutionContext context = new RewardExecutionContext(rewardOptions).initializeOnlineState(user);\n if (reward == null || reward.isEmpty()) {\n return;\n }\n\n String rewardName = reward;\n Boolean generatedSnapshot = null;\n if (reward.startsWith(QUEUED_REFERENCE_PREFIX)) {\n String encoded = reward.substring(QUEUED_REFERENCE_PREFIX.length());\n int modeEnd = encoded.indexOf('/');\n if (modeEnd > 0) {\n String mode = encoded.substring(0, modeEnd);\n String encodedName = encoded.substring(modeEnd + 1);\n if ((mode.equals("snapshot") || mode.equals("normal")) && !encodedName.isEmpty()) {\n try {\n rewardName = new String(Base64.getUrlDecoder().decode(encodedName), StandardCharsets.UTF_8);\n generatedSnapshot = Boolean.valueOf(mode.equals("snapshot"));\n } catch (IllegalArgumentException ignored) {\n rewardName = reward;\n }\n }\n }\n }\n\n Reward resolved;\n if (generatedSnapshot != null) {\n if (generatedSnapshot.booleanValue()) {\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n } else {\n resolved = handler.getReward(rewardName);\n }\n } else if (handler.rewardExist(rewardName) || handler.hasDirectRewardHandle(rewardName)) {\n // Legacy persisted queue entry: a real registered reward wins over any\n // stale generated file with the same name.\n resolved = handler.getReward(rewardName);\n } else {\n // Legacy generated queue entries predate explicit provenance. This branch is\n // reachable only through the PersistedQueueReference capability path.\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n if (resolved == null) {\n resolved = handler.getReward(rewardName);\n }\n }\n giveReward(user, resolved, context.getOptions());\n }\n\n''' + p.write_text(text[:start] + replacement + text[end:]) + + # Only actual queue-draining code can construct PersistedQueueReference. + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') + text = p.read_text() + old = 'plugin.getRewardHandler().giveReward(this, rewardReference, replayOptions);' + new = 'plugin.getRewardHandler().givePersistedQueueReward(this, new PersistedQueueReference(rewardReference), replayOptions);' + if old not in text: + raise SystemExit('timed replay call not found') + text = text.replace(old, new, 1) + old = 'rewardHandler.giveReward(user, rewardReference, options);' + count = text.count(old) + if count != 2: + raise SystemExit(f'expected 2 offline replay calls, found {count}') + text = text.replace(old, 'rewardHandler.givePersistedQueueReward(user, new PersistedQueueReference(rewardReference), options);') + p.write_text(text) + + # Keep queue-resolver tests on the internal executor path and add the exact forged-options regression. + p = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java') + text = p.read_text() + text = text.replace('package com.bencodez.advancedcore.tests.rewards;', 'package com.bencodez.advancedcore.api.rewards;', 1) + for imp in [ + 'import com.bencodez.advancedcore.api.rewards.Reward;\n', + 'import com.bencodez.advancedcore.api.rewards.RewardBuilder;\n', + 'import com.bencodez.advancedcore.api.rewards.RewardExecutor;\n', + 'import com.bencodez.advancedcore.api.rewards.RewardHandler;\n', + 'import com.bencodez.advancedcore.api.rewards.RewardOptions;\n', + ]: + text = text.replace(imp, '') + queue_calls = [ + 'executor.giveReward(user, "QueuedReward", options);', + 'executor.giveReward(user, queuedReference("Daily", false), options);', + 'executor.giveReward(user, queuedReference("Daily", true), options);', + 'executor.giveReward(user, "Daily", options);', + 'executor.giveReward(user, rewardName, options);', + ] + # First two QueuedReward calls are persisted replay tests; change both occurrences. + text = text.replace('executor.giveReward(user, "QueuedReward", options);', 'executor.givePersistedQueueReward(user, "QueuedReward", options);', 2) + text = text.replace('executor.giveReward(user, queuedReference("Daily", false), options);', 'executor.givePersistedQueueReward(user, queuedReference("Daily", false), options);', 1) + text = text.replace('executor.giveReward(user, queuedReference("Daily", true), options);', 'executor.givePersistedQueueReward(user, queuedReference("Daily", true), options);', 1) + text = text.replace('executor.giveReward(user, "Daily", options);', 'executor.givePersistedQueueReward(user, "Daily", options);', 1) + # Only the legacy marker-ending queue test should use persisted path; normal marker text test remains public. + marker_test = ''' executor.giveReward(user, rewardName, options);\n }\n\n verify(handler).getReward(rewardName);\n verify(normal).giveReward(user, options);\n verify(handler, never()).getQueuedGeneratedReward("Promo", "uuid");\n verify(handler, never()).getReward("Promo");\n }\n\n @Test\n void normalDispatchPreservesRewardNameContainingSnapshotMarkerText()''' + marker_repl = ''' executor.givePersistedQueueReward(user, rewardName, options);\n }\n\n verify(handler).getReward(rewardName);\n verify(normal).giveReward(user, options);\n verify(handler, never()).getQueuedGeneratedReward("Promo", "uuid");\n verify(handler, never()).getReward("Promo");\n }\n\n @Test\n void normalDispatchPreservesRewardNameContainingSnapshotMarkerText()''' + if marker_test not in text: + raise SystemExit('legacy marker-ending test block not found') + text = text.replace(marker_test, marker_repl, 1) + insert = '''\n @Test\n void forgedOfflineOptionsCannotGrantQueueSnapshotAccess() {\n Reward normal = mock(Reward.class);\n Reward queued = mock(Reward.class);\n RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false);\n when(handler.getReward("QueuedReward")).thenReturn(normal);\n when(handler.getQueuedGeneratedReward("QueuedReward", "uuid")).thenReturn(queued);\n\n try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) {\n bukkit.when(Bukkit::isPrimaryThread).thenReturn(false);\n executor.giveReward(user, "QueuedReward", options);\n }\n\n verify(handler).getReward("QueuedReward");\n verify(handler, never()).getQueuedGeneratedReward("QueuedReward", "uuid");\n verify(normal).giveReward(user, options);\n verify(queued, never()).giveReward(user, options);\n }\n''' + anchor = '\n @Test\n void normalStandaloneDispatchCannotResolveGeneratedSnapshot() {' + if anchor not in text: + raise SystemExit('test insertion anchor not found') + text = text.replace(anchor, insert + anchor, 1) + p.write_text(text) + PY + rm .github/workflows/fix-codex-295-queue-provenance.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "Require persisted queue provenance for snapshot replay" + git push origin HEAD:security/reward-path-identity-hardening \ No newline at end of file From 0b12e2be4968f7e801a3620941bb0e93337e0ee6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:03:47 +0000 Subject: [PATCH 64/71] Require persisted queue provenance for snapshot replay --- .../fix-codex-295-queue-provenance.yml | 107 ------------------ .../api/rewards/RewardExecutor.java | 25 ++-- .../api/rewards/RewardHandler.java | 9 ++ .../api/user/AdvancedCoreUser.java | 6 +- .../api/user/PersistedQueueReference.java | 19 ++++ .../QueuedGeneratedRewardDispatchTest.java | 38 +++++-- 6 files changed, 66 insertions(+), 138 deletions(-) delete mode 100644 .github/workflows/fix-codex-295-queue-provenance.yml create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/PersistedQueueReference.java diff --git a/.github/workflows/fix-codex-295-queue-provenance.yml b/.github/workflows/fix-codex-295-queue-provenance.yml deleted file mode 100644 index f6a022ae40..0000000000 --- a/.github/workflows/fix-codex-295-queue-provenance.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: Apply Codex 295 queue provenance fix - -on: - push: - branches: - - security/reward-path-identity-hardening - paths: - - .github/workflows/fix-codex-295-queue-provenance.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/reward-path-identity-hardening - fetch-depth: 0 - - name: Require real persisted queue provenance - run: | - python3 - <<'PY' - from pathlib import Path - - # Capability object: only code in api.user can construct one normally. - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/PersistedQueueReference.java') - p.write_text('''package com.bencodez.advancedcore.api.user;\n\n/**\n * Capability wrapper for a reward reference read from this user's persisted\n * offline/timed queue. The constructor is package-private so ordinary reward\n * callers cannot manufacture queue provenance through RewardOptions.\n */\npublic final class PersistedQueueReference {\n\n\tprivate final String reference;\n\n\tPersistedQueueReference(String reference) {\n\t\tthis.reference = reference;\n\t}\n\n\tpublic String getReference() {\n\t\treturn reference;\n\t}\n}\n''') - - # Public reward facade gets a queue-specific capability entry point. - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java') - text = p.read_text() - imp = 'import com.bencodez.advancedcore.api.user.AdvancedCoreUser;\n' - if 'import com.bencodez.advancedcore.api.user.PersistedQueueReference;' not in text: - text = text.replace(imp, imp + 'import com.bencodez.advancedcore.api.user.PersistedQueueReference;\n', 1) - old = ''' public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewardOptions) {\n rewardExecutor.giveReward(user, reward, rewardOptions);\n }\n''' - new = old + '''\n public void givePersistedQueueReward(AdvancedCoreUser user, PersistedQueueReference rewardReference,\n RewardOptions rewardOptions) {\n if (rewardReference == null) {\n return;\n }\n rewardExecutor.givePersistedQueueReward(user, rewardReference.getReference(), rewardOptions);\n }\n''' - if old not in text: - raise SystemExit('RewardHandler string giveReward block not found') - p.write_text(text.replace(old, new, 1)) - - # Ordinary String dispatch no longer infers queue provenance from user-controlled options. - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java') - text = p.read_text() - start = text.index(' public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewardOptions) {') - end = text.index(' public void updateReward(Configuration data, String path, RewardOptions rewardOptions) {', start) - replacement = ''' public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewardOptions) {\n RewardExecutionContext context = new RewardExecutionContext(rewardOptions).initializeOnlineState(user);\n if (reward == null || reward.isEmpty()) {\n return;\n }\n\n if (reward.startsWith("/")) {\n MiscUtils.getInstance().executeConsoleCommands(user.getPlayerName(), reward, context.getPlaceholders());\n return;\n }\n\n giveReward(user, handler.getReward(reward), context.getOptions());\n }\n\n void givePersistedQueueReward(AdvancedCoreUser user, String reward, RewardOptions rewardOptions) {\n RewardExecutionContext context = new RewardExecutionContext(rewardOptions).initializeOnlineState(user);\n if (reward == null || reward.isEmpty()) {\n return;\n }\n\n String rewardName = reward;\n Boolean generatedSnapshot = null;\n if (reward.startsWith(QUEUED_REFERENCE_PREFIX)) {\n String encoded = reward.substring(QUEUED_REFERENCE_PREFIX.length());\n int modeEnd = encoded.indexOf('/');\n if (modeEnd > 0) {\n String mode = encoded.substring(0, modeEnd);\n String encodedName = encoded.substring(modeEnd + 1);\n if ((mode.equals("snapshot") || mode.equals("normal")) && !encodedName.isEmpty()) {\n try {\n rewardName = new String(Base64.getUrlDecoder().decode(encodedName), StandardCharsets.UTF_8);\n generatedSnapshot = Boolean.valueOf(mode.equals("snapshot"));\n } catch (IllegalArgumentException ignored) {\n rewardName = reward;\n }\n }\n }\n }\n\n Reward resolved;\n if (generatedSnapshot != null) {\n if (generatedSnapshot.booleanValue()) {\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n } else {\n resolved = handler.getReward(rewardName);\n }\n } else if (handler.rewardExist(rewardName) || handler.hasDirectRewardHandle(rewardName)) {\n // Legacy persisted queue entry: a real registered reward wins over any\n // stale generated file with the same name.\n resolved = handler.getReward(rewardName);\n } else {\n // Legacy generated queue entries predate explicit provenance. This branch is\n // reachable only through the PersistedQueueReference capability path.\n resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID());\n if (resolved == null) {\n resolved = handler.getReward(rewardName);\n }\n }\n giveReward(user, resolved, context.getOptions());\n }\n\n''' - p.write_text(text[:start] + replacement + text[end:]) - - # Only actual queue-draining code can construct PersistedQueueReference. - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java') - text = p.read_text() - old = 'plugin.getRewardHandler().giveReward(this, rewardReference, replayOptions);' - new = 'plugin.getRewardHandler().givePersistedQueueReward(this, new PersistedQueueReference(rewardReference), replayOptions);' - if old not in text: - raise SystemExit('timed replay call not found') - text = text.replace(old, new, 1) - old = 'rewardHandler.giveReward(user, rewardReference, options);' - count = text.count(old) - if count != 2: - raise SystemExit(f'expected 2 offline replay calls, found {count}') - text = text.replace(old, 'rewardHandler.givePersistedQueueReward(user, new PersistedQueueReference(rewardReference), options);') - p.write_text(text) - - # Keep queue-resolver tests on the internal executor path and add the exact forged-options regression. - p = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java') - text = p.read_text() - text = text.replace('package com.bencodez.advancedcore.tests.rewards;', 'package com.bencodez.advancedcore.api.rewards;', 1) - for imp in [ - 'import com.bencodez.advancedcore.api.rewards.Reward;\n', - 'import com.bencodez.advancedcore.api.rewards.RewardBuilder;\n', - 'import com.bencodez.advancedcore.api.rewards.RewardExecutor;\n', - 'import com.bencodez.advancedcore.api.rewards.RewardHandler;\n', - 'import com.bencodez.advancedcore.api.rewards.RewardOptions;\n', - ]: - text = text.replace(imp, '') - queue_calls = [ - 'executor.giveReward(user, "QueuedReward", options);', - 'executor.giveReward(user, queuedReference("Daily", false), options);', - 'executor.giveReward(user, queuedReference("Daily", true), options);', - 'executor.giveReward(user, "Daily", options);', - 'executor.giveReward(user, rewardName, options);', - ] - # First two QueuedReward calls are persisted replay tests; change both occurrences. - text = text.replace('executor.giveReward(user, "QueuedReward", options);', 'executor.givePersistedQueueReward(user, "QueuedReward", options);', 2) - text = text.replace('executor.giveReward(user, queuedReference("Daily", false), options);', 'executor.givePersistedQueueReward(user, queuedReference("Daily", false), options);', 1) - text = text.replace('executor.giveReward(user, queuedReference("Daily", true), options);', 'executor.givePersistedQueueReward(user, queuedReference("Daily", true), options);', 1) - text = text.replace('executor.giveReward(user, "Daily", options);', 'executor.givePersistedQueueReward(user, "Daily", options);', 1) - # Only the legacy marker-ending queue test should use persisted path; normal marker text test remains public. - marker_test = ''' executor.giveReward(user, rewardName, options);\n }\n\n verify(handler).getReward(rewardName);\n verify(normal).giveReward(user, options);\n verify(handler, never()).getQueuedGeneratedReward("Promo", "uuid");\n verify(handler, never()).getReward("Promo");\n }\n\n @Test\n void normalDispatchPreservesRewardNameContainingSnapshotMarkerText()''' - marker_repl = ''' executor.givePersistedQueueReward(user, rewardName, options);\n }\n\n verify(handler).getReward(rewardName);\n verify(normal).giveReward(user, options);\n verify(handler, never()).getQueuedGeneratedReward("Promo", "uuid");\n verify(handler, never()).getReward("Promo");\n }\n\n @Test\n void normalDispatchPreservesRewardNameContainingSnapshotMarkerText()''' - if marker_test not in text: - raise SystemExit('legacy marker-ending test block not found') - text = text.replace(marker_test, marker_repl, 1) - insert = '''\n @Test\n void forgedOfflineOptionsCannotGrantQueueSnapshotAccess() {\n Reward normal = mock(Reward.class);\n Reward queued = mock(Reward.class);\n RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false);\n when(handler.getReward("QueuedReward")).thenReturn(normal);\n when(handler.getQueuedGeneratedReward("QueuedReward", "uuid")).thenReturn(queued);\n\n try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) {\n bukkit.when(Bukkit::isPrimaryThread).thenReturn(false);\n executor.giveReward(user, "QueuedReward", options);\n }\n\n verify(handler).getReward("QueuedReward");\n verify(handler, never()).getQueuedGeneratedReward("QueuedReward", "uuid");\n verify(normal).giveReward(user, options);\n verify(queued, never()).giveReward(user, options);\n }\n''' - anchor = '\n @Test\n void normalStandaloneDispatchCannotResolveGeneratedSnapshot() {' - if anchor not in text: - raise SystemExit('test insertion anchor not found') - text = text.replace(anchor, insert + anchor, 1) - p.write_text(text) - PY - rm .github/workflows/fix-codex-295-queue-provenance.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "Require persisted queue provenance for snapshot replay" - git push origin HEAD:security/reward-path-identity-hardening \ No newline at end of file diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java index 3c6fc83d12..dc85523835 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardExecutor.java @@ -127,9 +127,12 @@ public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewar return; } - boolean queueReplay = isPersistedQueueReplay(context.getOptions()); - if (!queueReplay) { - giveReward(user, handler.getReward(reward), context.getOptions()); + giveReward(user, handler.getReward(reward), context.getOptions()); + } + + void givePersistedQueueReward(AdvancedCoreUser user, String reward, RewardOptions rewardOptions) { + RewardExecutionContext context = new RewardExecutionContext(rewardOptions).initializeOnlineState(user); + if (reward == null || reward.isEmpty()) { return; } @@ -160,12 +163,12 @@ public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewar resolved = handler.getReward(rewardName); } } else if (handler.rewardExist(rewardName) || handler.hasDirectRewardHandle(rewardName)) { - // Legacy queue entry: if a normal registered reward exists, it wins over any + // Legacy persisted queue entry: a real registered reward wins over any // stale generated file with the same name. resolved = handler.getReward(rewardName); } else { - // Legacy generated entries did not persist provenance. Preserve them only when - // there is no registered reward they could shadow. + // Legacy generated queue entries predate explicit provenance. This branch is + // reachable only through the PersistedQueueReference capability path. resolved = handler.getQueuedGeneratedReward(rewardName, user.getUUID()); if (resolved == null) { resolved = handler.getReward(rewardName); @@ -174,16 +177,6 @@ public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewar giveReward(user, resolved, context.getOptions()); } - private boolean isPersistedQueueReplay(RewardOptions options) { - if (options == null || options.isCheckTimed()) { - return false; - } - if (options.isOnlineSet() && !options.isOnline()) { - return true; - } - return options.getPlaceholders().containsKey("date"); - } - public void updateReward(Configuration data, String path, RewardOptions rewardOptions) { if (data == null || path == null || !data.isConfigurationSection(path)) { return; diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java index fdd4934be8..885e062615 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java @@ -20,6 +20,7 @@ import com.bencodez.advancedcore.api.rewards.injected.RewardInject; import com.bencodez.advancedcore.api.rewards.injectedrequirement.RequirementInject; import com.bencodez.advancedcore.api.user.AdvancedCoreUser; +import com.bencodez.advancedcore.api.user.PersistedQueueReference; import com.bencodez.advancedcore.api.user.UserStartup; import com.bencodez.advancedcore.command.gui.RewardEditGUI; @@ -191,6 +192,14 @@ public void giveReward(AdvancedCoreUser user, String reward, RewardOptions rewar rewardExecutor.giveReward(user, reward, rewardOptions); } + public void givePersistedQueueReward(AdvancedCoreUser user, PersistedQueueReference rewardReference, + RewardOptions rewardOptions) { + if (rewardReference == null) { + return; + } + rewardExecutor.givePersistedQueueReward(user, rewardReference.getReference(), rewardOptions); + } + public boolean hasDirectRewardHandle(String reward) { return rewardRegistry.hasDirectRewardHandle(reward); } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java index 15fd31001e..666e041500 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java @@ -326,7 +326,7 @@ public void checkDelayedTimedRewards() { .withPlaceHolder(ArrayUtils.fromString(placeholders)); replayOptions.addPlaceholder("date", "" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm").format(new Date(time))); - plugin.getRewardHandler().giveReward(this, rewardReference, replayOptions); + plugin.getRewardHandler().givePersistedQueueReward(this, new PersistedQueueReference(rewardReference), replayOptions); String rewardName = rewardReference; plugin.debug("Giving timed/delayed reward " + rewardName + " for " + getPlayerName() + " with placeholders " + ArrayUtils.fromString(placeholders)); @@ -371,7 +371,7 @@ public void checkOfflineRewards() { RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false) .withPlaceHolder(ArrayUtils.fromString(placeholderStr)); - rewardHandler.giveReward(user, rewardReference, options); + rewardHandler.givePersistedQueueReward(user, new PersistedQueueReference(rewardReference), options); } } @@ -452,7 +452,7 @@ public void forceRunOfflineRewards() { RewardOptions options = new RewardOptions().setOnline(false).setGiveOffline(false).forceOffline() .setCheckTimed(false).withPlaceHolder(ArrayUtils.fromString(placeholderStr)); - rewardHandler.giveReward(user, rewardReference, options); + rewardHandler.givePersistedQueueReward(user, new PersistedQueueReference(rewardReference), options); } } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/PersistedQueueReference.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/PersistedQueueReference.java new file mode 100644 index 0000000000..c6f002b5bd --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/PersistedQueueReference.java @@ -0,0 +1,19 @@ +package com.bencodez.advancedcore.api.user; + +/** + * Capability wrapper for a reward reference read from this user's persisted + * offline/timed queue. The constructor is package-private so ordinary reward + * callers cannot manufacture queue provenance through RewardOptions. + */ +public final class PersistedQueueReference { + + private final String reference; + + PersistedQueueReference(String reference) { + this.reference = reference; + } + + public String getReference() { + return reference; + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java index b62c42920e..6177a5eae3 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java @@ -1,4 +1,4 @@ -package com.bencodez.advancedcore.tests.rewards; +package com.bencodez.advancedcore.api.rewards; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -15,11 +15,6 @@ import org.mockito.MockedStatic; import com.bencodez.advancedcore.AdvancedCorePlugin; -import com.bencodez.advancedcore.api.rewards.Reward; -import com.bencodez.advancedcore.api.rewards.RewardBuilder; -import com.bencodez.advancedcore.api.rewards.RewardExecutor; -import com.bencodez.advancedcore.api.rewards.RewardHandler; -import com.bencodez.advancedcore.api.rewards.RewardOptions; import com.bencodez.advancedcore.api.user.AdvancedCoreUser; class QueuedGeneratedRewardDispatchTest { @@ -61,7 +56,7 @@ void offlineQueueReplayResolvesGeneratedSnapshotBeforeNormalFallback() { try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); - executor.giveReward(user, "QueuedReward", options); + executor.givePersistedQueueReward(user, "QueuedReward", options); } verify(handler).getQueuedGeneratedReward("QueuedReward", "uuid"); @@ -78,7 +73,7 @@ void timedQueueReplayCanResolveGeneratedSnapshot() { try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); - executor.giveReward(user, "QueuedReward", options); + executor.givePersistedQueueReward(user, "QueuedReward", options); } verify(handler).getQueuedGeneratedReward("QueuedReward", "uuid"); @@ -107,7 +102,7 @@ void explicitNormalQueueReferenceDoesNotUseStaleSnapshot() { try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); - executor.giveReward(user, queuedReference("Daily", false), options); + executor.givePersistedQueueReward(user, queuedReference("Daily", false), options); } verify(normal).giveReward(user, options); @@ -124,7 +119,7 @@ void explicitSnapshotQueueReferenceUsesGeneratedSnapshot() { try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); - executor.giveReward(user, queuedReference("Daily", true), options); + executor.givePersistedQueueReward(user, queuedReference("Daily", true), options); } verify(queued).giveReward(user, options); @@ -142,7 +137,7 @@ void legacyQueueReferencePrefersRegisteredRewardOverStaleSnapshot() { try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); - executor.giveReward(user, "Daily", options); + executor.givePersistedQueueReward(user, "Daily", options); } verify(normal).giveReward(user, options); @@ -159,7 +154,7 @@ void legacyQueueNameEndingInOldMarkerIsPreservedExactly() { try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); - executor.giveReward(user, rewardName, options); + executor.givePersistedQueueReward(user, rewardName, options); } verify(handler).getReward(rewardName); @@ -186,6 +181,25 @@ void normalDispatchPreservesRewardNameContainingSnapshotMarkerText() { verify(handler, never()).getQueuedGeneratedReward("Promo", "uuid"); } + @Test + void forgedOfflineOptionsCannotGrantQueueSnapshotAccess() { + Reward normal = mock(Reward.class); + Reward queued = mock(Reward.class); + RewardOptions options = new RewardOptions().setOnline(false).setCheckTimed(false); + when(handler.getReward("QueuedReward")).thenReturn(normal); + when(handler.getQueuedGeneratedReward("QueuedReward", "uuid")).thenReturn(queued); + + try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); + executor.giveReward(user, "QueuedReward", options); + } + + verify(handler).getReward("QueuedReward"); + verify(handler, never()).getQueuedGeneratedReward("QueuedReward", "uuid"); + verify(normal).giveReward(user, options); + verify(queued, never()).giveReward(user, options); + } + @Test void normalStandaloneDispatchCannotResolveGeneratedSnapshot() { RewardOptions options = new RewardOptions(); From f364a0788940519f7912f20a7bf3891f0dc57c58 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 20:04:47 -0600 Subject: [PATCH 65/71] Test forged timed queue options stay public --- .../QueuedGeneratedRewardDispatchTest.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java index 6177a5eae3..ec2ee9fadc 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/QueuedGeneratedRewardDispatchTest.java @@ -200,6 +200,25 @@ void forgedOfflineOptionsCannotGrantQueueSnapshotAccess() { verify(queued, never()).giveReward(user, options); } + @Test + void forgedTimedPlaceholderCannotGrantQueueSnapshotAccess() { + Reward normal = mock(Reward.class); + Reward queued = mock(Reward.class); + RewardOptions options = new RewardOptions().setCheckTimed(false).addPlaceholder("date", "now"); + when(handler.getReward("QueuedReward")).thenReturn(normal); + when(handler.getQueuedGeneratedReward("QueuedReward", "uuid")).thenReturn(queued); + + try (MockedStatic bukkit = org.mockito.Mockito.mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::isPrimaryThread).thenReturn(false); + executor.giveReward(user, "QueuedReward", options); + } + + verify(handler).getReward("QueuedReward"); + verify(handler, never()).getQueuedGeneratedReward("QueuedReward", "uuid"); + verify(normal).giveReward(user, options); + verify(queued, never()).giveReward(user, options); + } + @Test void normalStandaloneDispatchCannotResolveGeneratedSnapshot() { RewardOptions options = new RewardOptions(); From a7d534c1e55e2b58e8552f0bddc3fb0d186645e9 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 20:06:07 -0600 Subject: [PATCH 66/71] Update queue replay user tests --- .../advancedcore/tests/user/AdvancedCoreUserTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/AdvancedCoreUserTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/AdvancedCoreUserTest.java index 692030ae56..780107790d 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/AdvancedCoreUserTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/AdvancedCoreUserTest.java @@ -20,6 +20,7 @@ import com.bencodez.advancedcore.api.rewards.RewardHandler; import com.bencodez.advancedcore.api.rewards.RewardOptions; import com.bencodez.advancedcore.api.user.AdvancedCoreUser; +import com.bencodez.advancedcore.api.user.PersistedQueueReference; import com.bencodez.advancedcore.api.user.UserData; import com.bencodez.advancedcore.api.user.UserDataFetchMode; import com.bencodez.advancedcore.api.user.UserManager; @@ -67,8 +68,10 @@ void checkOfflineRewards_preservesServerRequirementForNormalReplay() { user.checkOfflineRewards(); + ArgumentCaptor referenceCaptor = ArgumentCaptor.forClass(PersistedQueueReference.class); ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(RewardOptions.class); - verify(rewardHandler).giveReward(eq(user), eq("VoteReward"), optionsCaptor.capture()); + verify(rewardHandler).givePersistedQueueReward(eq(user), referenceCaptor.capture(), optionsCaptor.capture()); + assertEquals("VoteReward", referenceCaptor.getValue().getReference()); RewardOptions options = optionsCaptor.getValue(); assertFalse(options.isForceOffline()); assertTrue(options.isGiveOffline()); @@ -84,8 +87,10 @@ void forceRunOfflineRewards_stillBypassesNormalReplayChecks() { user.forceRunOfflineRewards(); + ArgumentCaptor referenceCaptor = ArgumentCaptor.forClass(PersistedQueueReference.class); ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(RewardOptions.class); - verify(rewardHandler).giveReward(eq(user), eq("VoteReward"), optionsCaptor.capture()); + verify(rewardHandler).givePersistedQueueReward(eq(user), referenceCaptor.capture(), optionsCaptor.capture()); + assertEquals("VoteReward", referenceCaptor.getValue().getReference()); RewardOptions options = optionsCaptor.getValue(); assertTrue(options.isForceOffline()); assertFalse(options.isGiveOffline()); From ab75b25284e296e983badd37404cc04d2b99ccdf Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 20:35:25 -0600 Subject: [PATCH 67/71] Apply snapshot replacement fix --- .../fix-codex-295-snapshot-replace.yml | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/fix-codex-295-snapshot-replace.yml diff --git a/.github/workflows/fix-codex-295-snapshot-replace.yml b/.github/workflows/fix-codex-295-snapshot-replace.yml new file mode 100644 index 0000000000..98dbb2aff7 --- /dev/null +++ b/.github/workflows/fix-codex-295-snapshot-replace.yml @@ -0,0 +1,48 @@ +name: Apply Codex 295 snapshot replacement fix + +on: + push: + branches: + - security/reward-path-identity-hardening + paths: + - .github/workflows/fix-codex-295-snapshot-replace.yml + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/reward-path-identity-hardening + fetch-depth: 0 + - name: Replace generated snapshot data instead of merging stale keys + run: | + python3 - <<'PY' + from pathlib import Path + + p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardFileData.java') + text = p.read_text() + old = '''\tpublic void setData(ConfigurationSection value) {\n\t\t// getFileData().set("", value);\n\t\tMap map = value.getConfigurationSection("").getValues(true);\n\t\tfor (Entry entry : map.entrySet()) {\n\t\t\tset(entry.getKey(), entry.getValue());\n\t\t}\n\t\treward.loadValues();\n\t}\n''' + new = '''\tpublic void setData(ConfigurationSection value) {\n\t\tif (fileData == null || value == null) {\n\t\t\tplugin.debug("Editing invalid reward: " + reward.getName());\n\t\t\treturn;\n\t\t}\n\n\t\t// Snapshot writes replace the previous contents completely. Reusing a generated\n\t\t// queue file must not retain reward actions that were removed from the source\n\t\t// section after an earlier snapshot was created.\n\t\tfor (String key : new HashSet<>(fileData.getKeys(false))) {\n\t\t\tfileData.set(key, null);\n\t\t}\n\t\tMap map = value.getConfigurationSection("").getValues(true);\n\t\tfor (Entry entry : map.entrySet()) {\n\t\t\tfileData.set(entry.getKey(), entry.getValue());\n\t\t}\n\t\tsave(fileData);\n\t\treload();\n\t\treward.loadValues();\n\t}\n''' + if old not in text: + raise SystemExit('RewardFileData.setData block not found') + p.write_text(text.replace(old, new, 1)) + + p = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java') + text = p.read_text() + insert = '''\n\t@Test\n\tvoid reusedGeneratedSnapshotDropsRemovedRewardKeys() throws IOException {\n\t\tAdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class);\n\t\tAdvancedCorePlugin.setInstance(plugin);\n\n\t\tFile directlyDefined = new File(tempDir, "Rewards/DirectlyDefined");\n\t\tassertTrue(directlyDefined.mkdirs());\n\t\tFile snapshot = new File(directlyDefined, "Daily_AdvancedRewards_foo.yml");\n\t\tYamlConfiguration oldData = new YamlConfiguration();\n\t\toldData.set("Commands", List.of("old command"));\n\t\toldData.set("Messages", List.of("old message"));\n\t\toldData.save(snapshot);\n\n\t\tReward reward = new Reward(directlyDefined, "Daily_AdvancedRewards_foo");\n\t\tYamlConfiguration replacement = new YamlConfiguration();\n\t\treplacement.set("Messages", List.of("new message"));\n\t\treward.getConfig().setData(replacement);\n\n\t\tYamlConfiguration saved = YamlConfiguration.loadConfiguration(snapshot);\n\t\tassertFalse(saved.contains("Commands"));\n\t\tassertEquals(List.of("new message"), saved.getStringList("Messages"));\n\t}\n''' + anchor = '\n\t@Test\n\tvoid loadRewardsDoesNotSuppressNormalDirectFolderFileWithoutGeneratedMarker() throws IOException {' + if anchor not in text: + raise SystemExit('RewardLoaderSecurityTest insertion anchor not found') + text = text.replace(anchor, insert + anchor, 1) + p.write_text(text) + PY + rm .github/workflows/fix-codex-295-snapshot-replace.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "Replace reused generated reward snapshots" + git push origin HEAD:security/reward-path-identity-hardening From 44ce9758a5c31a491ff59498a91ccfee3eae8b1b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:35:36 +0000 Subject: [PATCH 68/71] Replace reused generated reward snapshots --- .../fix-codex-295-snapshot-replace.yml | 48 - .../api/rewards/RewardFileData.java | 1378 +++++++++-------- .../api/rewards/RewardLoaderSecurityTest.java | 23 + 3 files changed, 718 insertions(+), 731 deletions(-) delete mode 100644 .github/workflows/fix-codex-295-snapshot-replace.yml diff --git a/.github/workflows/fix-codex-295-snapshot-replace.yml b/.github/workflows/fix-codex-295-snapshot-replace.yml deleted file mode 100644 index 98dbb2aff7..0000000000 --- a/.github/workflows/fix-codex-295-snapshot-replace.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Apply Codex 295 snapshot replacement fix - -on: - push: - branches: - - security/reward-path-identity-hardening - paths: - - .github/workflows/fix-codex-295-snapshot-replace.yml - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/reward-path-identity-hardening - fetch-depth: 0 - - name: Replace generated snapshot data instead of merging stale keys - run: | - python3 - <<'PY' - from pathlib import Path - - p = Path('AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardFileData.java') - text = p.read_text() - old = '''\tpublic void setData(ConfigurationSection value) {\n\t\t// getFileData().set("", value);\n\t\tMap map = value.getConfigurationSection("").getValues(true);\n\t\tfor (Entry entry : map.entrySet()) {\n\t\t\tset(entry.getKey(), entry.getValue());\n\t\t}\n\t\treward.loadValues();\n\t}\n''' - new = '''\tpublic void setData(ConfigurationSection value) {\n\t\tif (fileData == null || value == null) {\n\t\t\tplugin.debug("Editing invalid reward: " + reward.getName());\n\t\t\treturn;\n\t\t}\n\n\t\t// Snapshot writes replace the previous contents completely. Reusing a generated\n\t\t// queue file must not retain reward actions that were removed from the source\n\t\t// section after an earlier snapshot was created.\n\t\tfor (String key : new HashSet<>(fileData.getKeys(false))) {\n\t\t\tfileData.set(key, null);\n\t\t}\n\t\tMap map = value.getConfigurationSection("").getValues(true);\n\t\tfor (Entry entry : map.entrySet()) {\n\t\t\tfileData.set(entry.getKey(), entry.getValue());\n\t\t}\n\t\tsave(fileData);\n\t\treload();\n\t\treward.loadValues();\n\t}\n''' - if old not in text: - raise SystemExit('RewardFileData.setData block not found') - p.write_text(text.replace(old, new, 1)) - - p = Path('AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java') - text = p.read_text() - insert = '''\n\t@Test\n\tvoid reusedGeneratedSnapshotDropsRemovedRewardKeys() throws IOException {\n\t\tAdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class);\n\t\tAdvancedCorePlugin.setInstance(plugin);\n\n\t\tFile directlyDefined = new File(tempDir, "Rewards/DirectlyDefined");\n\t\tassertTrue(directlyDefined.mkdirs());\n\t\tFile snapshot = new File(directlyDefined, "Daily_AdvancedRewards_foo.yml");\n\t\tYamlConfiguration oldData = new YamlConfiguration();\n\t\toldData.set("Commands", List.of("old command"));\n\t\toldData.set("Messages", List.of("old message"));\n\t\toldData.save(snapshot);\n\n\t\tReward reward = new Reward(directlyDefined, "Daily_AdvancedRewards_foo");\n\t\tYamlConfiguration replacement = new YamlConfiguration();\n\t\treplacement.set("Messages", List.of("new message"));\n\t\treward.getConfig().setData(replacement);\n\n\t\tYamlConfiguration saved = YamlConfiguration.loadConfiguration(snapshot);\n\t\tassertFalse(saved.contains("Commands"));\n\t\tassertEquals(List.of("new message"), saved.getStringList("Messages"));\n\t}\n''' - anchor = '\n\t@Test\n\tvoid loadRewardsDoesNotSuppressNormalDirectFolderFileWithoutGeneratedMarker() throws IOException {' - if anchor not in text: - raise SystemExit('RewardLoaderSecurityTest insertion anchor not found') - text = text.replace(anchor, insert + anchor, 1) - p.write_text(text) - PY - rm .github/workflows/fix-codex-295-snapshot-replace.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "Replace reused generated reward snapshots" - git push origin HEAD:security/reward-path-identity-hardening diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardFileData.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardFileData.java index 0a9703f2cc..6844b1ea2d 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardFileData.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardFileData.java @@ -1,683 +1,695 @@ -package com.bencodez.advancedcore.api.rewards; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; - -import com.bencodez.advancedcore.AdvancedCorePlugin; -import com.bencodez.advancedcore.api.misc.files.FilesManager; -import com.bencodez.simpleapi.file.CaseInsensitiveSection; - -import lombok.Getter; -import net.md_5.bungee.api.ChatColor; - -/** - * The Class RewardFileData. - */ -public class RewardFileData { - - private ConfigurationSection configData; - - /** The data file. */ - private File dataFile; - - /** The data. */ - private FileConfiguration fileData; - - /** The plugin. */ - AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); - - /** The reward. */ - private Reward reward; - - @Getter - private File rewardFolder; - - public RewardFileData(Reward reward, ConfigurationSection section) { - this.reward = reward; - configData = new CaseInsensitiveSection(section); - } - - /** - * Instantiates a new reward file data. - * - * @param reward the reward - * @param rewardFolder the reward folder - */ - public RewardFileData(Reward reward, File rewardFolder) { - this.reward = reward; - dataFile = reward.getFile(); - - if (!rewardFolder.isDirectory()) { - rewardFolder = rewardFolder.getParentFile(); - } - - this.rewardFolder = rewardFolder; - - setup(); - } - - public void createSection(String path) { - if (fileData != null) { - fileData.createSection(path); - save(fileData); - reload(); - } - } - - /** - * Gets the chance. - * - * @return the chance - */ - public double getChance() { - return getConfigData().getDouble("Chance"); - } - - public Set getChoices() { - if (getConfigData().isConfigurationSection("Choices")) { - return getConfigData().getConfigurationSection("Choices").getKeys(false); - } - return new HashSet<>(); - } - - public ConfigurationSection getChoicesItem(String choice) { - return getConfigData().getConfigurationSection("Choices." + choice + ".DisplayItem"); - } - - public String getChoicesRewardsPath(String choice) { - return "Choices." + choice + ".Rewards"; - } - - /** - * Gets the commands console. - * - * @return the commands console - */ - @SuppressWarnings("unchecked") - public ArrayList getCommandsConsole() { - if (getConfigData().isList("Commands")) { - return (ArrayList) getConfigData().getList("Commands", new ArrayList<>()); - } - return (ArrayList) getConfigData().getList("Commands.Console", new ArrayList<>()); - - } - - /** - * Gets the commands player. - * - * @return the commands player - */ - @SuppressWarnings("unchecked") - public ArrayList getCommandsPlayer() { - return (ArrayList) getConfigData().getList("Commands.Player", new ArrayList<>()); - } - - public ConfigurationSection getConfigData() { - return configData; - } - - /** - * @return the dFile - */ - public File getDataFile() { - return dataFile; - } - - /** - * Gets the delayed enabled. - * - * @return the delayed enabled - */ - public boolean getDelayedEnabled() { - return getConfigData().getBoolean("Delayed.Enabled"); - } - - /** - * Gets the delayed hours. - * - * @return the delayed hours - */ - public int getDelayedHours() { - return getConfigData().getInt("Delayed.Hours"); - } - - public int getDelayedMilliSeconds() { - return getConfigData().getInt("Delayed.MilliSeconds"); - } - - /** - * Gets the delayed minutes. - * - * @return the delayed minutes - */ - public int getDelayedMinutes() { - return getConfigData().getInt("Delayed.Minutes"); - } - - public int getDelayedSeconds() { - return getConfigData().getInt("Delayed.Seconds"); - } - - /** - * Gets the data. - * - * @return the data - */ - /* - * private FileConfiguration getFileData() { return fileData; } - */ - - public ConfigurationSection getDisplayItem() { - return getConfigData().getConfigurationSection("DisplayItem"); - } - - /** - * Gets the choice rewards enabled. - * - * @return the choice rewards enabled - */ - public boolean getEnableChoices() { - return getConfigData().getBoolean("EnableChoices"); - } - - public FileConfiguration getFileData() { - return fileData; - } - - public boolean getForceOffline() { - return getConfigData().getBoolean("ForceOffline"); - } - - /** - * Gets the item amount. - * - * @param item the item - * @return the item amount - */ - @Deprecated - public int getItemAmount(String item) { - return getConfigData().getInt("Items." + item + ".Amount"); - } - - /** - * Gets the item data. - * - * @param item the item - * @return the item data - */ - @Deprecated - public int getItemData(String item) { - return getConfigData().getInt("Items." + item + ".Data"); - } - - /** - * Gets the item durability. - * - * @param item the item - * @return the item durability - */ - @Deprecated - public int getItemDurability(String item) { - return getConfigData().getInt("Items." + item + ".Durability"); - } - - /** - * Gets the item enchants. - * - * @param item the item - * @return the item enchants - */ - @Deprecated - public Set getItemEnchants(String item) { - try { - return getConfigData().getConfigurationSection("Items." + item + ".Enchants").getKeys(false); - } catch (Exception ex) { - return new HashSet<>(); - } - } - - /** - * Gets the item enchants level. - * - * @param item the item - * @param enchant the enchant - * @return the item enchants level - */ - @Deprecated - public int getItemEnchantsLevel(String item, String enchant) { - return getConfigData().getInt("Items." + item + ".Enchants." + enchant); - } - - /** - * Gets the item lore. - * - * @param item the item - * @return the item lore - */ - @SuppressWarnings("unchecked") - @Deprecated - public ArrayList getItemLore(String item) { - return (ArrayList) getConfigData().getList("Items." + item + ".Lore"); - } - - /** - * Gets the item material. - * - * @param item the item - * @return the item material - */ - @Deprecated - public String getItemMaterial(String item) { - return getConfigData().getString("Items." + item + ".Material"); - } - - /** - * Gets the item max amount. - * - * @param item the item - * @return the item max amount - */ - @Deprecated - public int getItemMaxAmount(String item) { - return getConfigData().getInt("Items." + item + ".MaxAmount"); - } - - /** - * Gets the item min amount. - * - * @param item the item - * @return the item min amount - */ - @Deprecated - public int getItemMinAmount(String item) { - return getConfigData().getInt("Items." + item + ".MinAmount"); - } - - /** - * Gets the item name. - * - * @param item the item - * @return the item name - */ - @Deprecated - public String getItemName(String item) { - return getConfigData().getString("Items." + item + ".Name"); - } - - /** - * Gets the items. - * - * @return the items - */ - public Set getItems() { - try { - return getConfigData().getConfigurationSection("Items").getKeys(false); - } catch (Exception ex) { - return new HashSet<>(); - } - } - - public ConfigurationSection getItemSection(String item) { - return getConfigData().getConfigurationSection("Items." + item); - } - - /** - * Gets the item skull. - * - * @param item the item - * @return the item skull - */ - @Deprecated - public String getItemSkull(String item) { - return getConfigData().getString("Items." + item + ".Skull"); - } - - /** - * Gets the permission. - * - * @return the permission - */ - public String getPermission() { - return getConfigData().getString("Permission", "AdvancedCore.Reward." + reward.getRewardName()); - } - - @SuppressWarnings("unchecked") - public ArrayList getPriority() { - return (ArrayList) getConfigData().getList("Priority", new ArrayList<>()); - } - - /** - * Gets the require permission. - * - * @return the require permission - */ - public boolean getRequirePermission() { - return getConfigData().getBoolean("RequirePermission"); - } - - /** - * Gets the reward type. - * - * @return the reward type - */ - public String getRewardType() { - String str = getConfigData().getString("RewardType", "BOTH"); - if (str != null) { - if (str.equalsIgnoreCase("online")) { - return "ONLINE"; - } - if (str.equalsIgnoreCase("offline")) { - return "OFFLINE"; - } else { - return "BOTH"; - } - } - return "BOTH"; - } - - public String getServer() { - return getConfigData().getString("Server", ""); - } - - /** - * Gets the timed enabled. - * - * @return the timed enabled - */ - public boolean getTimedEnabled() { - return getConfigData().getBoolean("Timed.Enabled"); - } - - /** - * Gets the timed hour. - * - * @return the timed hour - */ - public int getTimedHour() { - return getConfigData().getInt("Timed.Hour"); - } - - /** - * Gets the timed minute. - * - * @return the timed minute - */ - public int getTimedMinute() { - return getConfigData().getInt("Timed.Minute"); - } - - /** - * Gets the worlds. - * - * @return the worlds - */ - @SuppressWarnings("unchecked") - public ArrayList getWorlds() { - - return (ArrayList) getConfigData().getList("Worlds", new ArrayList<>()); - - } - - public boolean hasRewardFile() { - return dataFile != null; - } - - public boolean isDirectlyDefinedReward() { - return getConfigData().getBoolean("DirectlyDefinedReward"); - } - - public boolean isRewardFile() { - return dataFile != null && !isDirectlyDefinedReward(); - } - - /** - * Reload. - */ - public void reload() { - fileData = YamlConfiguration.loadConfiguration(dataFile); - configData = fileData.getConfigurationSection(""); - } - - public void save() { - FilesManager.getInstance().editFile(dataFile, fileData); - } - - public void save(FileConfiguration fileData) { - FilesManager.getInstance().editFile(dataFile, fileData); - } - - /** - * Sets the. - * - * @param path the path - * @param value the value - */ - public void set(String path, Object value) { - if (fileData != null) { - fileData.set(path, value); - save(fileData); - reload(); - } else { - plugin.debug("Editing invalid reward: " + reward.getName()); - } - } - - /** - * Sets the chance. - * - * @param d the new chance - */ - public void setChance(double d) { - set("Chance", d); - } - - /** - * Sets the commands console. - * - * @param value the new commands console - */ - public void setCommandsConsole(ArrayList value) { - set("Commands.Console", value); - } - - /** - * Sets the commands player. - * - * @param value the new commands player - */ - public void setCommandsPlayer(ArrayList value) { - set("Commands.Player", value); - } - - public void setConfigData(ConfigurationSection configData) { - this.configData = configData; - } - - /* - * public void setData(ConfigurationSection value) { getFileData().set("", - * value); reward.loadValues(); } - */ - - public void setData(ConfigurationSection value) { - // getFileData().set("", value); - Map map = value.getConfigurationSection("").getValues(true); - for (Entry entry : map.entrySet()) { - set(entry.getKey(), entry.getValue()); - } - reward.loadValues(); - } - - /** - * @param dFile the dFile to set - */ - public void setDataFile(File dFile) { - this.dataFile = dFile; - } - - public void setDirectlyDefinedReward(boolean b) { - set("DirectlyDefinedReward", b); - } - - /** - * Sets the give in each world. - * - * @param value the new give in each world - */ - public void setGiveInEachWorld(boolean value) { - set("GiveInEachWorld", value); - } - - /** - * Sets the item amount. - * - * @param item the item - * @param value the value - */ - public void setItemAmount(String item, int value) { - set("Items." + item + ".Amount", value); - } - - /** - * Sets the item data. - * - * @param item the item - * @param value the value - */ - public void setItemData(String item, int value) { - set("Items." + item + ".Data", value); - } - - /** - * Sets the item durability. - * - * @param item the item - * @param value the value - */ - public void setItemDurability(String item, int value) { - set("Items." + item + ".Durability", value); - } - - /** - * Sets the item enchant. - * - * @param item the item - * @param enchant the enchant - * @param value the value - */ - public void setItemEnchant(String item, String enchant, int value) { - set("Items." + item + ".Enchants." + enchant, value); - } - - /** - * Sets the item lore. - * - * @param item the item - * @param value the value - */ - public void setItemLore(String item, ArrayList value) { - set("Items." + item + ".Lore", value); - } - - /** - * Sets the item material. - * - * @param item the item - * @param value the value - */ - public void setItemMaterial(String item, String value) { - set("Items." + item + ".Material", value); - } - - /** - * Sets the item max amount. - * - * @param item the item - * @param value the value - */ - public void setItemMaxAmount(String item, int value) { - set("Items." + item + ".MaxAmount", value); - } - - /** - * Sets the item min amount. - * - * @param item the item - * @param value the value - */ - public void setItemMinAmount(String item, int value) { - set("Items." + item + ".MinAmount", value); - } - - /** - * Sets the item name. - * - * @param item the item - * @param value the value - */ - public void setItemName(String item, String value) { - set("Items." + item + ".Name", value); - } - - /** - * Sets the permission. - * - * @param perm the new permission - */ - public void setPermission(String perm) { - set("Permission", perm); - } - - /** - * Sets the require permission. - * - * @param value the new require permission - */ - public void setRequirePermission(boolean value) { - set("RequirePermission", value); - } - - /** - * Sets the reward type. - * - * @param value the new reward type - */ - public void setRewardType(String value) { - set("RewardType", value); - } - - /** - * Setup. - */ - public void setup() { - fileData = YamlConfiguration.loadConfiguration(dataFile); - - if (!dataFile.exists()) { - try { - fileData.save(dataFile); - } catch (IOException e) { - plugin.getLogger().severe(ChatColor.RED + "Could not create " + dataFile.getAbsolutePath()); - - } - } - configData = new CaseInsensitiveSection(fileData.getConfigurationSection("")); - } - - /** - * Sets the worlds. - * - * @param value the new worlds - */ - public void setWorlds(ArrayList value) { - set("Worlds", value); - } - -} +package com.bencodez.advancedcore.api.rewards; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.misc.files.FilesManager; +import com.bencodez.simpleapi.file.CaseInsensitiveSection; + +import lombok.Getter; +import net.md_5.bungee.api.ChatColor; + +/** + * The Class RewardFileData. + */ +public class RewardFileData { + + private ConfigurationSection configData; + + /** The data file. */ + private File dataFile; + + /** The data. */ + private FileConfiguration fileData; + + /** The plugin. */ + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + + /** The reward. */ + private Reward reward; + + @Getter + private File rewardFolder; + + public RewardFileData(Reward reward, ConfigurationSection section) { + this.reward = reward; + configData = new CaseInsensitiveSection(section); + } + + /** + * Instantiates a new reward file data. + * + * @param reward the reward + * @param rewardFolder the reward folder + */ + public RewardFileData(Reward reward, File rewardFolder) { + this.reward = reward; + dataFile = reward.getFile(); + + if (!rewardFolder.isDirectory()) { + rewardFolder = rewardFolder.getParentFile(); + } + + this.rewardFolder = rewardFolder; + + setup(); + } + + public void createSection(String path) { + if (fileData != null) { + fileData.createSection(path); + save(fileData); + reload(); + } + } + + /** + * Gets the chance. + * + * @return the chance + */ + public double getChance() { + return getConfigData().getDouble("Chance"); + } + + public Set getChoices() { + if (getConfigData().isConfigurationSection("Choices")) { + return getConfigData().getConfigurationSection("Choices").getKeys(false); + } + return new HashSet<>(); + } + + public ConfigurationSection getChoicesItem(String choice) { + return getConfigData().getConfigurationSection("Choices." + choice + ".DisplayItem"); + } + + public String getChoicesRewardsPath(String choice) { + return "Choices." + choice + ".Rewards"; + } + + /** + * Gets the commands console. + * + * @return the commands console + */ + @SuppressWarnings("unchecked") + public ArrayList getCommandsConsole() { + if (getConfigData().isList("Commands")) { + return (ArrayList) getConfigData().getList("Commands", new ArrayList<>()); + } + return (ArrayList) getConfigData().getList("Commands.Console", new ArrayList<>()); + + } + + /** + * Gets the commands player. + * + * @return the commands player + */ + @SuppressWarnings("unchecked") + public ArrayList getCommandsPlayer() { + return (ArrayList) getConfigData().getList("Commands.Player", new ArrayList<>()); + } + + public ConfigurationSection getConfigData() { + return configData; + } + + /** + * @return the dFile + */ + public File getDataFile() { + return dataFile; + } + + /** + * Gets the delayed enabled. + * + * @return the delayed enabled + */ + public boolean getDelayedEnabled() { + return getConfigData().getBoolean("Delayed.Enabled"); + } + + /** + * Gets the delayed hours. + * + * @return the delayed hours + */ + public int getDelayedHours() { + return getConfigData().getInt("Delayed.Hours"); + } + + public int getDelayedMilliSeconds() { + return getConfigData().getInt("Delayed.MilliSeconds"); + } + + /** + * Gets the delayed minutes. + * + * @return the delayed minutes + */ + public int getDelayedMinutes() { + return getConfigData().getInt("Delayed.Minutes"); + } + + public int getDelayedSeconds() { + return getConfigData().getInt("Delayed.Seconds"); + } + + /** + * Gets the data. + * + * @return the data + */ + /* + * private FileConfiguration getFileData() { return fileData; } + */ + + public ConfigurationSection getDisplayItem() { + return getConfigData().getConfigurationSection("DisplayItem"); + } + + /** + * Gets the choice rewards enabled. + * + * @return the choice rewards enabled + */ + public boolean getEnableChoices() { + return getConfigData().getBoolean("EnableChoices"); + } + + public FileConfiguration getFileData() { + return fileData; + } + + public boolean getForceOffline() { + return getConfigData().getBoolean("ForceOffline"); + } + + /** + * Gets the item amount. + * + * @param item the item + * @return the item amount + */ + @Deprecated + public int getItemAmount(String item) { + return getConfigData().getInt("Items." + item + ".Amount"); + } + + /** + * Gets the item data. + * + * @param item the item + * @return the item data + */ + @Deprecated + public int getItemData(String item) { + return getConfigData().getInt("Items." + item + ".Data"); + } + + /** + * Gets the item durability. + * + * @param item the item + * @return the item durability + */ + @Deprecated + public int getItemDurability(String item) { + return getConfigData().getInt("Items." + item + ".Durability"); + } + + /** + * Gets the item enchants. + * + * @param item the item + * @return the item enchants + */ + @Deprecated + public Set getItemEnchants(String item) { + try { + return getConfigData().getConfigurationSection("Items." + item + ".Enchants").getKeys(false); + } catch (Exception ex) { + return new HashSet<>(); + } + } + + /** + * Gets the item enchants level. + * + * @param item the item + * @param enchant the enchant + * @return the item enchants level + */ + @Deprecated + public int getItemEnchantsLevel(String item, String enchant) { + return getConfigData().getInt("Items." + item + ".Enchants." + enchant); + } + + /** + * Gets the item lore. + * + * @param item the item + * @return the item lore + */ + @SuppressWarnings("unchecked") + @Deprecated + public ArrayList getItemLore(String item) { + return (ArrayList) getConfigData().getList("Items." + item + ".Lore"); + } + + /** + * Gets the item material. + * + * @param item the item + * @return the item material + */ + @Deprecated + public String getItemMaterial(String item) { + return getConfigData().getString("Items." + item + ".Material"); + } + + /** + * Gets the item max amount. + * + * @param item the item + * @return the item max amount + */ + @Deprecated + public int getItemMaxAmount(String item) { + return getConfigData().getInt("Items." + item + ".MaxAmount"); + } + + /** + * Gets the item min amount. + * + * @param item the item + * @return the item min amount + */ + @Deprecated + public int getItemMinAmount(String item) { + return getConfigData().getInt("Items." + item + ".MinAmount"); + } + + /** + * Gets the item name. + * + * @param item the item + * @return the item name + */ + @Deprecated + public String getItemName(String item) { + return getConfigData().getString("Items." + item + ".Name"); + } + + /** + * Gets the items. + * + * @return the items + */ + public Set getItems() { + try { + return getConfigData().getConfigurationSection("Items").getKeys(false); + } catch (Exception ex) { + return new HashSet<>(); + } + } + + public ConfigurationSection getItemSection(String item) { + return getConfigData().getConfigurationSection("Items." + item); + } + + /** + * Gets the item skull. + * + * @param item the item + * @return the item skull + */ + @Deprecated + public String getItemSkull(String item) { + return getConfigData().getString("Items." + item + ".Skull"); + } + + /** + * Gets the permission. + * + * @return the permission + */ + public String getPermission() { + return getConfigData().getString("Permission", "AdvancedCore.Reward." + reward.getRewardName()); + } + + @SuppressWarnings("unchecked") + public ArrayList getPriority() { + return (ArrayList) getConfigData().getList("Priority", new ArrayList<>()); + } + + /** + * Gets the require permission. + * + * @return the require permission + */ + public boolean getRequirePermission() { + return getConfigData().getBoolean("RequirePermission"); + } + + /** + * Gets the reward type. + * + * @return the reward type + */ + public String getRewardType() { + String str = getConfigData().getString("RewardType", "BOTH"); + if (str != null) { + if (str.equalsIgnoreCase("online")) { + return "ONLINE"; + } + if (str.equalsIgnoreCase("offline")) { + return "OFFLINE"; + } else { + return "BOTH"; + } + } + return "BOTH"; + } + + public String getServer() { + return getConfigData().getString("Server", ""); + } + + /** + * Gets the timed enabled. + * + * @return the timed enabled + */ + public boolean getTimedEnabled() { + return getConfigData().getBoolean("Timed.Enabled"); + } + + /** + * Gets the timed hour. + * + * @return the timed hour + */ + public int getTimedHour() { + return getConfigData().getInt("Timed.Hour"); + } + + /** + * Gets the timed minute. + * + * @return the timed minute + */ + public int getTimedMinute() { + return getConfigData().getInt("Timed.Minute"); + } + + /** + * Gets the worlds. + * + * @return the worlds + */ + @SuppressWarnings("unchecked") + public ArrayList getWorlds() { + + return (ArrayList) getConfigData().getList("Worlds", new ArrayList<>()); + + } + + public boolean hasRewardFile() { + return dataFile != null; + } + + public boolean isDirectlyDefinedReward() { + return getConfigData().getBoolean("DirectlyDefinedReward"); + } + + public boolean isRewardFile() { + return dataFile != null && !isDirectlyDefinedReward(); + } + + /** + * Reload. + */ + public void reload() { + fileData = YamlConfiguration.loadConfiguration(dataFile); + configData = fileData.getConfigurationSection(""); + } + + public void save() { + FilesManager.getInstance().editFile(dataFile, fileData); + } + + public void save(FileConfiguration fileData) { + FilesManager.getInstance().editFile(dataFile, fileData); + } + + /** + * Sets the. + * + * @param path the path + * @param value the value + */ + public void set(String path, Object value) { + if (fileData != null) { + fileData.set(path, value); + save(fileData); + reload(); + } else { + plugin.debug("Editing invalid reward: " + reward.getName()); + } + } + + /** + * Sets the chance. + * + * @param d the new chance + */ + public void setChance(double d) { + set("Chance", d); + } + + /** + * Sets the commands console. + * + * @param value the new commands console + */ + public void setCommandsConsole(ArrayList value) { + set("Commands.Console", value); + } + + /** + * Sets the commands player. + * + * @param value the new commands player + */ + public void setCommandsPlayer(ArrayList value) { + set("Commands.Player", value); + } + + public void setConfigData(ConfigurationSection configData) { + this.configData = configData; + } + + /* + * public void setData(ConfigurationSection value) { getFileData().set("", + * value); reward.loadValues(); } + */ + + public void setData(ConfigurationSection value) { + if (fileData == null || value == null) { + plugin.debug("Editing invalid reward: " + reward.getName()); + return; + } + + // Snapshot writes replace the previous contents completely. Reusing a generated + // queue file must not retain reward actions that were removed from the source + // section after an earlier snapshot was created. + for (String key : new HashSet<>(fileData.getKeys(false))) { + fileData.set(key, null); + } + Map map = value.getConfigurationSection("").getValues(true); + for (Entry entry : map.entrySet()) { + fileData.set(entry.getKey(), entry.getValue()); + } + save(fileData); + reload(); + reward.loadValues(); + } + + /** + * @param dFile the dFile to set + */ + public void setDataFile(File dFile) { + this.dataFile = dFile; + } + + public void setDirectlyDefinedReward(boolean b) { + set("DirectlyDefinedReward", b); + } + + /** + * Sets the give in each world. + * + * @param value the new give in each world + */ + public void setGiveInEachWorld(boolean value) { + set("GiveInEachWorld", value); + } + + /** + * Sets the item amount. + * + * @param item the item + * @param value the value + */ + public void setItemAmount(String item, int value) { + set("Items." + item + ".Amount", value); + } + + /** + * Sets the item data. + * + * @param item the item + * @param value the value + */ + public void setItemData(String item, int value) { + set("Items." + item + ".Data", value); + } + + /** + * Sets the item durability. + * + * @param item the item + * @param value the value + */ + public void setItemDurability(String item, int value) { + set("Items." + item + ".Durability", value); + } + + /** + * Sets the item enchant. + * + * @param item the item + * @param enchant the enchant + * @param value the value + */ + public void setItemEnchant(String item, String enchant, int value) { + set("Items." + item + ".Enchants." + enchant, value); + } + + /** + * Sets the item lore. + * + * @param item the item + * @param value the value + */ + public void setItemLore(String item, ArrayList value) { + set("Items." + item + ".Lore", value); + } + + /** + * Sets the item material. + * + * @param item the item + * @param value the value + */ + public void setItemMaterial(String item, String value) { + set("Items." + item + ".Material", value); + } + + /** + * Sets the item max amount. + * + * @param item the item + * @param value the value + */ + public void setItemMaxAmount(String item, int value) { + set("Items." + item + ".MaxAmount", value); + } + + /** + * Sets the item min amount. + * + * @param item the item + * @param value the value + */ + public void setItemMinAmount(String item, int value) { + set("Items." + item + ".MinAmount", value); + } + + /** + * Sets the item name. + * + * @param item the item + * @param value the value + */ + public void setItemName(String item, String value) { + set("Items." + item + ".Name", value); + } + + /** + * Sets the permission. + * + * @param perm the new permission + */ + public void setPermission(String perm) { + set("Permission", perm); + } + + /** + * Sets the require permission. + * + * @param value the new require permission + */ + public void setRequirePermission(boolean value) { + set("RequirePermission", value); + } + + /** + * Sets the reward type. + * + * @param value the new reward type + */ + public void setRewardType(String value) { + set("RewardType", value); + } + + /** + * Setup. + */ + public void setup() { + fileData = YamlConfiguration.loadConfiguration(dataFile); + + if (!dataFile.exists()) { + try { + fileData.save(dataFile); + } catch (IOException e) { + plugin.getLogger().severe(ChatColor.RED + "Could not create " + dataFile.getAbsolutePath()); + + } + } + configData = new CaseInsensitiveSection(fileData.getConfigurationSection("")); + } + + /** + * Sets the worlds. + * + * @param value the new worlds + */ + public void setWorlds(ArrayList value) { + set("Worlds", value); + } + +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index af28c08644..06b5aa6381 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -158,6 +158,29 @@ void generatedDotNameDoesNotSuppressManualUnderscoreName() throws Exception { assertFalse(suppressed.contains("foo_bar.yml")); } + @Test + void reusedGeneratedSnapshotDropsRemovedRewardKeys() throws IOException { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + AdvancedCorePlugin.setInstance(plugin); + + File directlyDefined = new File(tempDir, "Rewards/DirectlyDefined"); + assertTrue(directlyDefined.mkdirs()); + File snapshot = new File(directlyDefined, "Daily_AdvancedRewards_foo.yml"); + YamlConfiguration oldData = new YamlConfiguration(); + oldData.set("Commands", List.of("old command")); + oldData.set("Messages", List.of("old message")); + oldData.save(snapshot); + + Reward reward = new Reward(directlyDefined, "Daily_AdvancedRewards_foo"); + YamlConfiguration replacement = new YamlConfiguration(); + replacement.set("Messages", List.of("new message")); + reward.getConfig().setData(replacement); + + YamlConfiguration saved = YamlConfiguration.loadConfiguration(snapshot); + assertFalse(saved.contains("Commands")); + assertEquals(List.of("new message"), saved.getStringList("Messages")); + } + @Test void loadRewardsDoesNotSuppressNormalDirectFolderFileWithoutGeneratedMarker() throws IOException { AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); From 0d49297abe15a650645975d759a13edbf198a879 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 20:36:40 -0600 Subject: [PATCH 69/71] Strengthen reused snapshot regression --- .../advancedcore/api/rewards/RewardLoaderSecurityTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index 06b5aa6381..e77757c062 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -178,6 +178,7 @@ void reusedGeneratedSnapshotDropsRemovedRewardKeys() throws IOException { YamlConfiguration saved = YamlConfiguration.loadConfiguration(snapshot); assertFalse(saved.contains("Commands")); + assertFalse(saved.isConfigurationSection("Commands")); assertEquals(List.of("new message"), saved.getStringList("Messages")); } From 48d7e4d800c0e80719eeb9f53416c8d4055653db Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 20:38:24 -0600 Subject: [PATCH 70/71] Isolate snapshot replacement regression --- .../api/rewards/RewardLoaderSecurityTest.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java index e77757c062..1944e514df 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardLoaderSecurityTest.java @@ -174,7 +174,23 @@ void reusedGeneratedSnapshotDropsRemovedRewardKeys() throws IOException { Reward reward = new Reward(directlyDefined, "Daily_AdvancedRewards_foo"); YamlConfiguration replacement = new YamlConfiguration(); replacement.set("Messages", List.of("new message")); - reward.getConfig().setData(replacement); + + com.bencodez.advancedcore.api.misc.files.FilesManager filesManager = + mock(com.bencodez.advancedcore.api.misc.files.FilesManager.class); + org.mockito.Mockito.doAnswer(invocation -> { + File file = invocation.getArgument(0); + org.bukkit.configuration.file.FileConfiguration config = invocation.getArgument(1); + config.save(file); + return null; + }).when(filesManager).editFile( + org.mockito.ArgumentMatchers.any(File.class), + org.mockito.ArgumentMatchers.any(org.bukkit.configuration.file.FileConfiguration.class)); + + try (org.mockito.MockedStatic files = + org.mockito.Mockito.mockStatic(com.bencodez.advancedcore.api.misc.files.FilesManager.class)) { + files.when(com.bencodez.advancedcore.api.misc.files.FilesManager::getInstance).thenReturn(filesManager); + reward.getConfig().setData(replacement); + } YamlConfiguration saved = YamlConfiguration.loadConfiguration(snapshot); assertFalse(saved.contains("Commands")); From 9be2860aae9ca524b0f5904ed401e1eefb1aee74 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Aug 2026 20:39:09 -0600 Subject: [PATCH 71/71] Test generated snapshot replacement semantics --- .../rewards/RewardFileDataSnapshotTest.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardFileDataSnapshotTest.java diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardFileDataSnapshotTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardFileDataSnapshotTest.java new file mode 100644 index 0000000000..ffb523cffa --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/rewards/RewardFileDataSnapshotTest.java @@ -0,0 +1,65 @@ +package com.bencodez.advancedcore.api.rewards; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.util.List; + +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.misc.files.FilesManager; + +class RewardFileDataSnapshotTest { + + @TempDir + File tempDir; + + @AfterEach + void tearDown() { + AdvancedCorePlugin.setInstance(null); + } + + @Test + void snapshotReplacementDropsKeysRemovedFromSourceSection() throws Exception { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + AdvancedCorePlugin.setInstance(plugin); + + File target = new File(tempDir, "Queued.yml"); + YamlConfiguration existing = new YamlConfiguration(); + existing.set("Commands", List.of("old command")); + existing.set("Messages", List.of("old message")); + existing.save(target); + + Reward reward = mock(Reward.class); + when(reward.getFile()).thenReturn(target); + when(reward.getName()).thenReturn("Queued"); + RewardFileData rewardFileData = new RewardFileData(reward, tempDir); + + YamlConfiguration replacement = new YamlConfiguration(); + replacement.set("Messages", List.of("new message")); + + FilesManager filesManager = mock(FilesManager.class); + try (MockedStatic files = org.mockito.Mockito.mockStatic(FilesManager.class)) { + files.when(FilesManager::getInstance).thenReturn(filesManager); + rewardFileData.setData(replacement); + } + + ArgumentCaptor saved = ArgumentCaptor.forClass(FileConfiguration.class); + verify(filesManager).editFile(eq(target), saved.capture()); + assertFalse(saved.getValue().contains("Commands"), + "reused generated snapshots must not retain actions removed from the source reward"); + assertEquals(List.of("new message"), saved.getValue().getStringList("Messages")); + } +}