From c3d7d1131062e6d3ebdec4bf0b88c3e4b410c690 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 11 Oct 2026 11:09:03 -0600 Subject: [PATCH 1/7] Revert "Keep external vote service identifiers inert" (#1643) Revert the player-facing WORD JOINER formatting guard introduced by VotingPlugin #1643 and its display-only reward placeholder changes. Preserve unrelated asynchronous reward work. Keep NeoForge's later-added player-message isolation private to its own adapter, without relying on the reverted common formatting utility or altering its reward behavior. As requested, this restores the prior Bukkit external identifier rendering/parsing, removing #1643's hardening on those paths. --- .../messaging/BackendProxyMessageRouter.java | 2 +- .../commands/gui/player/VoteLast.java | 10 +-- .../commands/gui/player/VoteNext.java | 12 ++-- .../commands/gui/player/VoteToday.java | 16 ++--- .../commands/gui/player/VoteURL.java | 26 +++---- .../commands/gui/player/VoteURLVoteSite.java | 9 ++- .../votingplugin/config/ConfigVoteSites.java | 10 +-- .../votingplugin/cooldown/CoolDownCheck.java | 1 - .../listeners/PlayerVoteListener.java | 2 +- .../neoforge/NeoForgeRewardConfiguration.java | 25 ++++++- .../votingplugin/user/VotingPluginUser.java | 4 +- .../util/ServiceSiteValidator.java | 23 +------ .../votereminding/VoteRemindersListener.java | 5 +- .../votereminding/VoteRemindersManager.java | 8 +-- .../votingplugin/votesites/VoteSite.java | 41 +++-------- .../tests/ServiceSiteValidatorTest.java | 27 +------- .../reminders/VoteRemindersListenerTest.java | 68 ------------------- .../tests/votesite/VoteSiteTest.java | 44 ------------ 18 files changed, 83 insertions(+), 250 deletions(-) delete mode 100644 VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/reminders/VoteRemindersListenerTest.java diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java index 436d6f1924..33ee20d5ba 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java @@ -595,7 +595,7 @@ private void handleVoteBroadcast(JsonEnvelope msg) { boolean online = fields.containsKey(VotingPluginWire.K_WAS_ONLINE) ? Boolean.parseBoolean(fields.get(VotingPluginWire.K_WAS_ONLINE)) : user.isOnline(); plugin.getBroadcastHandler().broadcastVote(user.getJavaUUID(), user.getPlayerName(), - voteSite.getDisplayNameForFormatting(), online, totals); + voteSite.getDisplayName(), online, totals); } private WireVoteResult handleWireVoteDelayRejected(JsonEnvelope msg) { diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteLast.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteLast.java index 437bea8643..34a696b8eb 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteLast.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteLast.java @@ -71,7 +71,7 @@ public void onDialog(Player player) { continue; } - dialog.placeholder("sitename", site.getDisplayNameForFormatting()).button(site.getDisplayNameForFormatting(), + dialog.placeholder("sitename", site.getDisplayName()).button(site.getDisplayName(), user.voteCommandLastLine(site), payload -> { if (plugin.getGui().isChestVoteLastClickableLinks()) { @@ -83,7 +83,7 @@ public void onDialog(Player player) { HashMap placeholders = new HashMap(); placeholders.put("voteurl", site.getVoteURL()); - placeholders.put("sitename", site.getDisplayNameForFormatting()); + placeholders.put("sitename", site.getDisplayName()); placeholders.put("player", clicked.getName()); clickedUser.sendMessage(plugin.getGui().getChestVoteURLURLText(), placeholders); @@ -121,7 +121,7 @@ public void onChest(Player player) { for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { if (!site.isHidden()) { inv.addButton(inv.getNextSlot(), - new UpdatingBInventoryButton(plugin, site.getItem().setName(site.getDisplayNameForFormatting()) + new UpdatingBInventoryButton(plugin, site.getItem().setName(site.getDisplayName()) .setLore(user.voteCommandLastGUILine(site)).setAmountNone(1), 1000, 1000) { @Override @@ -139,7 +139,7 @@ public void onClick(ClickEvent event) { plugin.getGui() .getChestVoteURLURLText(), "voteurl", site.getVoteURL()), - "sitename", site.getDisplayNameForFormatting()), + "sitename", site.getDisplayName()), "player", player.getName())); } @@ -148,7 +148,7 @@ public void onClick(ClickEvent event) { @Override public ItemBuilder onUpdate(Player p) { - return site.getItem().setName(site.getDisplayNameForFormatting()) + return site.getItem().setName(site.getDisplayName()) .setLore(user.voteCommandLastGUILine(site)).setAmountNone(1); } }); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteNext.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteNext.java index 3d4a93256d..008ed73bda 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteNext.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteNext.java @@ -60,7 +60,7 @@ public ArrayList getChat(CommandSender sender) { msgLine = MessageAPI.replaceIgnoreCase(msgLine, "%info%", user.voteCommandNextInfo(voteSite)); - msgLine = MessageAPI.replaceIgnoreCase(msgLine, "%SiteName%", voteSite.getDisplayNameForFormatting()); + msgLine = MessageAPI.replaceIgnoreCase(msgLine, "%SiteName%", voteSite.getDisplayName()); msg.add(MessageAPI.colorize(msgLine)); } } @@ -81,10 +81,10 @@ public void onDialog(Player player) { String siteNameDisplay = plugin.getGui().getChestVoteNextCustomSiteNamesDisplays(site.getKey()); if (siteNameDisplay.isEmpty()) { - siteNameDisplay = site.getDisplayNameForFormatting(); + siteNameDisplay = site.getDisplayName(); } - dialog.placeholder("sitename", site.getDisplayNameForFormatting()).placeholder("SiteName", site.getDisplayNameForFormatting()) + dialog.placeholder("sitename", site.getDisplayName()).placeholder("SiteName", site.getDisplayName()) .placeholder("info", user.voteCommandNextInfo(site)) .button(siteNameDisplay, user.voteCommandNextInfo(site), payload -> { @@ -117,7 +117,7 @@ public void onBook(Player player) { if (!site.isHidden()) { Layout nextLayout = new Layout(new ArrayList<>(Arrays.asList("[Json]"))); nextLayout.replaceTextComponent("[Json]", - BookUtil.TextBuilder.of(book.colorize(site.getDisplayNameForFormatting())) + BookUtil.TextBuilder.of(book.colorize(site.getDisplayName())) .onClick(BookUtil.ClickAction.openUrl(site.getVoteURLJsonStrip())) .onHover(BookUtil.HoverAction.showText(user.voteCommandNextInfo(site))).build()); book.addLayout(nextLayout); @@ -141,7 +141,7 @@ public void onChest(Player player) { if (!site.isHidden()) { String siteNameDisplay = plugin.getGui().getChestVoteNextCustomSiteNamesDisplays(site.getKey()); if (siteNameDisplay.isEmpty()) { - siteNameDisplay = site.getDisplayNameForFormatting(); + siteNameDisplay = site.getDisplayName(); } inv.addButton(inv.getNextSlot(), new UpdatingBInventoryButton(plugin, @@ -159,7 +159,7 @@ public void onClick(ClickEvent clickEvent) { @Override public ItemBuilder onUpdate(Player player) { - return site.getItem().setName(site.getDisplayNameForFormatting()) + return site.getItem().setName(site.getDisplayName()) .setLore(user.voteCommandNextInfo(site)).setAmountNone(1) .addPlaceholder("player", user.getPlayerName()); } diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteToday.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteToday.java index 88fa311a3b..accbd1b3aa 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteToday.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteToday.java @@ -52,9 +52,9 @@ public void onDialog(Player player) { final UUID uuid = topPlayer.getUuid(); dialog.placeholder("player", topPlayer.getPlayerName()).placeholder("votesite", voteSite.getKey()) - .placeholder("sitename", voteSite.getDisplayNameForFormatting()) - .placeholder("SiteName", voteSite.getDisplayNameForFormatting()) - .placeholder("VoteSite", voteSite.getDisplayNameForFormatting()).placeholder("time", timeString) + .placeholder("sitename", voteSite.getDisplayName()) + .placeholder("SiteName", voteSite.getDisplayName()) + .placeholder("VoteSite", voteSite.getDisplayName()).placeholder("time", timeString) .placeholder("Time", timeString).button("&e" + topPlayer.getPlayerName(), plugin.getConfigFile().getFormatCommandsVoteTodayLine(), payload -> { Player clicked = player.getServer().getPlayer(payload.owner()); @@ -136,9 +136,9 @@ public void onChest(Player player) { HashMap placeholders = new HashMap<>(); placeholders.put("player", user.getPlayerName()); placeholders.put("votesite", voteSite.getKey()); - placeholders.put("sitename", voteSite.getDisplayNameForFormatting()); - placeholders.put("SiteName", voteSite.getDisplayNameForFormatting()); - placeholders.put("VoteSite", voteSite.getDisplayNameForFormatting()); + placeholders.put("sitename", voteSite.getDisplayName()); + placeholders.put("SiteName", voteSite.getDisplayName()); + placeholders.put("VoteSite", voteSite.getDisplayName()); placeholders.put("time", timeString); placeholders.put("Time", timeString); @@ -204,7 +204,7 @@ public String[] voteToday() { HashMap placeholders = new HashMap<>(); placeholders.put("player", user.getPlayerName()); placeholders.put("votesite", voteSite.getKey()); - placeholders.put("sitename", voteSite.getDisplayNameForFormatting()); + placeholders.put("sitename", voteSite.getDisplayName()); placeholders.put("time", timeString); msg.add(PlaceholderUtils.replacePlaceHolder(plugin.getConfigFile().getFormatCommandsVoteTodayLine(), placeholders)); @@ -234,7 +234,7 @@ public String[] voteTodayGUI() { HashMap placeholders = new HashMap<>(); placeholders.put("player", user.getPlayerName()); placeholders.put("votesite", mostRecentSite.getKey()); - placeholders.put("sitename", mostRecentSite.getDisplayNameForFormatting()); + placeholders.put("sitename", mostRecentSite.getDisplayName()); placeholders.put("time", timeString); msg.add(PlaceholderUtils.replacePlaceHolder(plugin.getConfigFile().getFormatCommandsVoteTodayLine(), placeholders)); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURL.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURL.java index 36a7e2c2e5..61ea8b80ee 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURL.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURL.java @@ -71,7 +71,7 @@ public ArrayList getChat(CommandSender sender, boolean bypassCanVote) { plugin.getConfigFile().getFormatCommandsVoteURLS()); message.replacePlaceholder("num", Integer.toString(counter)) .replacePlaceholder("url", voteURL) - .replacePlaceholder("SiteName", voteSite.getDisplayNameForFormatting()); + .replacePlaceholder("SiteName", voteSite.getDisplayName()); if (user != null && user.getPlayerName() != null) { message.replacePlaceholder("player", "" + user.getPlayerName()) .replacePlaceholder("Next", "" + user.voteCommandNextInfo(voteSite)); @@ -150,14 +150,14 @@ public void onDialog(Player player) { boolean canVote = finalUser.canVoteSite(voteSite); String color = canVote ? "&a" : "&c"; - dialog.placeholder("sitename", voteSite.getDisplayNameForFormatting()) - .placeholder("SiteName", voteSite.getDisplayNameForFormatting()) - .placeholder("servicesite", voteSite.getServiceSiteForFormatting()) - .placeholder("ServiceSite", voteSite.getServiceSiteForFormatting()) + dialog.placeholder("sitename", voteSite.getDisplayName()) + .placeholder("SiteName", voteSite.getDisplayName()) + .placeholder("servicesite", voteSite.getServiceSite()) + .placeholder("ServiceSite", voteSite.getServiceSite()) .placeholder("VoteDelay", "" + voteSite.getVoteDelay()) .placeholder("VoteHour", "" + voteSite.getVoteDelayDailyHour()) .placeholder("Next", finalUser.voteCommandNextInfo(voteSite)) - .button(color + voteSite.getDisplayNameForFormatting(), + .button(color + voteSite.getDisplayName(), canVote ? "&aClick to view vote link" : "&c" + finalUser.voteCommandNextInfo(voteSite), payload -> { Player clicked = player.getServer().getPlayer(payload.owner()); @@ -166,9 +166,9 @@ public void onDialog(Player player) { .getVotingPluginUser(clicked); HashMap placeholders = new HashMap(); placeholders.put("voteurl", voteSite.getVoteURL()); - placeholders.put("sitename", voteSite.getDisplayNameForFormatting()); + placeholders.put("sitename", voteSite.getDisplayName()); placeholders.put("player", clicked.getName()); - placeholders.put("servicesite", voteSite.getServiceSiteForFormatting()); + placeholders.put("servicesite", voteSite.getServiceSite()); placeholders.put("VoteDelay", "" + voteSite.getVoteDelay()); placeholders.put("VoteHour", "" + voteSite.getVoteDelayDailyHour()); clickedUser.sendMessage(plugin.getGui().getChestVoteURLURLText(), placeholders); @@ -218,8 +218,8 @@ private ItemBuilder getItemVoteSite(VoteSite voteSite) { builder.setName(plugin.getGui().getChestVoteURLGUISiteName()); builder.addLoreLine(plugin.getGui().getChestVoteURLNextVote()); } - builder.addPlaceholder("ServiceSite", voteSite.getServiceSiteForFormatting()); - builder.addPlaceholder("Name", voteSite.getDisplayNameForFormatting()); + builder.addPlaceholder("ServiceSite", voteSite.getServiceSite()); + builder.addPlaceholder("Name", voteSite.getDisplayName()); builder.addPlaceholder("VoteDelay", "" + voteSite.getVoteDelay()); builder.addPlaceholder("VoteHour", "" + voteSite.getVoteDelayDailyHour()); builder.addPlaceholder("Info", user.voteCommandNextInfo(voteSite)); @@ -240,7 +240,7 @@ public void onBook(Player player) { if (!site.isHidden()) { if (site.getPermissionToView().isEmpty() || player.hasPermission(site.getPermissionToView())) { Layout layout = new Layout(plugin.getGui().getBookVoteURLBookGUILayout()) - .addPlaceholder("sitename", site.getDisplayNameForFormatting()).addPlaceholder("num", "" + i); + .addPlaceholder("sitename", site.getDisplayName()).addPlaceholder("num", "" + i); String text = plugin.getGui().getBookVoteURLBookGUIAlreadyVotedText(); ChatColor color = ChatColor.valueOf(plugin.getGui().getBookVoteURLBookGUIAlreadyVotedColor()); if (user.canVoteSite(site)) { @@ -342,9 +342,9 @@ public void onClick(ClickEvent event) { VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(player); HashMap placeholders = new HashMap<>(); placeholders.put("voteurl", voteSite.getVoteURL()); - placeholders.put("sitename", voteSite.getDisplayNameForFormatting()); + placeholders.put("sitename", voteSite.getDisplayName()); placeholders.put("player", player.getName()); - placeholders.put("servicesite", voteSite.getServiceSiteForFormatting()); + placeholders.put("servicesite", voteSite.getServiceSite()); placeholders.put("VoteDelay", "" + voteSite.getVoteDelay()); placeholders.put("VoteHour", "" + voteSite.getVoteDelayDailyHour()); user.sendMessage(plugin.getGui().getChestVoteURLURLText(), placeholders); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURLVoteSite.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURLVoteSite.java index 779227cc6b..94e0e51e3d 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURLVoteSite.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURLVoteSite.java @@ -53,7 +53,7 @@ public void onChest(Player player) { } VoteSite site = plugin.getVoteSiteManager().getVoteSite(voteSite, true); BInventory inv = new BInventory(plugin.getGui().getChestVoteURLSiteName()); - inv.addPlaceholder("site", site.getDisplayNameForFormatting()); + inv.addPlaceholder("site", site.getDisplayName()); inv.setMeta(player, "VoteSite", site); if (!plugin.getConfigFile().isAlwaysCloseInventory()) { inv.dontClose(); @@ -117,10 +117,9 @@ public void onDialog(Player player) { VoteSite site = plugin.getVoteSiteManager().getVoteSite(voteSite, true); com.bencodez.simpleapi.dialog.MultiActionDialogBuilder dialog = plugin.getDialogService().multiAction(player) - .placeholder("player", user.getPlayerName()).placeholder("site", site.getDisplayNameForFormatting()) - .placeholder("sitename", site.getDisplayNameForFormatting()).placeholder("SiteName", site.getDisplayNameForFormatting()) - .placeholder("servicesite", site.getServiceSiteForFormatting()) - .placeholder("ServiceSite", site.getServiceSiteForFormatting()) + .placeholder("player", user.getPlayerName()).placeholder("site", site.getDisplayName()) + .placeholder("sitename", site.getDisplayName()).placeholder("SiteName", site.getDisplayName()) + .placeholder("servicesite", site.getServiceSite()).placeholder("ServiceSite", site.getServiceSite()) .placeholder("VoteDelay", "" + site.getVoteDelay()) .placeholder("VoteHour", "" + site.getVoteDelayDailyHour()) .placeholder("Next", user.voteCommandNextInfo(site)).placeholder("Last", user.voteCommandLastLine(site)) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java index e422baa340..bbc478ad7d 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java @@ -80,14 +80,14 @@ private boolean tryGenerateVoteSite(String siteName, boolean automatic) { String org = siteName; siteName = siteName.replaceAll("[\\.\\s]+", "_"); - plugin.getLogger().warning("Creating VoteSite " + siteName + " for the service site '" + org + "'"); - setEnabled(siteName, true); - setServiceSite(siteName, org); - setVoteURL(siteName, "VoteURL"); + plugin.getLogger().warning("Creating VoteSite " + siteName + " for the service site '" + org + "'"); + setEnabled(siteName, true); + setServiceSite(siteName, org); + setVoteURL(siteName, "VoteURL"); setVoteDelay(siteName, "24h"); set(siteName, "DisplayItem.Material", "STONE"); set(siteName, "DisplayItem.Amount", 1); - set(siteName, "Rewards.Messages.Player", "&aThanks for voting on %SiteName%!"); + set(siteName, "Rewards.Messages.Player", "&aThanks for voting on %ServiceSite%!"); set(siteName, "WaitUntilVoteDelayRewards", Collections.emptyMap()); plugin.loadVoteSites(); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/cooldown/CoolDownCheck.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/cooldown/CoolDownCheck.java index 2e370fe37c..de0140bc09 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/cooldown/CoolDownCheck.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/cooldown/CoolDownCheck.java @@ -280,7 +280,6 @@ public void onCoolDownEnd(PlayerVoteCoolDownEndEvent event) { public void onCoolDownEnd(PlayerVoteSiteCoolDownEndEvent event) { plugin.getRewardHandler().giveReward(event.getPlayer(), event.getSite().getSiteData(), "CoolDownEndRewards", new RewardOptions().addPlaceholder("sitename", event.getSite().getDisplayName()) - .addDisplayPlaceholder("sitename", event.getSite().getDisplayNameForFormatting()) .addPlaceholder("url", event.getSite().getVoteURL(false))); } diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/PlayerVoteListener.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/PlayerVoteListener.java index 6b4d816681..7c573a7608 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/PlayerVoteListener.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/PlayerVoteListener.java @@ -95,7 +95,7 @@ private BukkitOperations(VotingPluginMain plugin, PlayerVoteEvent event) { } @Override public boolean siteEnabled(VoteSite site) { return site.isEnabled(); } @Override public String siteKey(VoteSite site) { return site.getKey(); } - @Override public String siteDisplayName(VoteSite site) { return site.getDisplayNameForFormatting(); } + @Override public String siteDisplayName(VoteSite site) { return site.getDisplayName(); } @Override public VotingPluginUser resolveUser(String name) { if (event.getVotingPluginUser() != null) return event.getVotingPluginUser(); Player player = Bukkit.getPlayerExact(name); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardConfiguration.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardConfiguration.java index e5de5e9d53..3e873f69ef 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardConfiguration.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardConfiguration.java @@ -8,7 +8,6 @@ import org.spongepowered.configurate.ConfigurationNode; -import com.bencodez.votingplugin.util.ServiceSiteValidator; /** Parses the first NeoForge-safe subset of existing VotingPlugin reward YAML. */ final class NeoForgeRewardConfiguration { @@ -72,16 +71,36 @@ NeoForgeRewardPlan plan(NeoForgeDeferredVote vote, NeoForgeVoteSite site, boolea private static NeoForgeRewardPlan.Action expand(NeoForgeRewardPlan.Action action, NeoForgeDeferredVote vote, NeoForgeVoteSite site) { + // NeoForge's message isolation was added after #1643. Preserve that + // independent safety contract when reverting the Bukkit formatting + // change; command arguments still receive the raw identifier. String serviceSite = action.type() == NeoForgeRewardPlan.ActionType.PLAYER_MESSAGE - ? ServiceSiteValidator.inertForFormatting(vote.serviceSite()) : vote.serviceSite(); + ? inertMessageIdentifier(vote.serviceSite()) : vote.serviceSite(); String siteName = action.type() == NeoForgeRewardPlan.ActionType.PLAYER_MESSAGE && site.displayNameFallback() - ? ServiceSiteValidator.inertForFormatting(site.displayName()) : site.displayName(); + ? inertMessageIdentifier(site.displayName()) : site.displayName(); return new NeoForgeRewardPlan.Action(action.type(), action.value() .replace("%SiteName%", siteName).replace("%sitename%", siteName) .replace("%ServiceSite%", serviceSite).replace("%servicesite%", serviceSite)); } + /** + * Prevents interpreted formatting and placeholders in NeoForge player + * messages. This later-added adapter intentionally retains its own + * untrusted-message guards independently of Bukkit's reverted UI path. + */ + private static String inertMessageIdentifier(String value) { + if (value == null || value.isEmpty()) return value == null ? "" : value; + StringBuilder inert = new StringBuilder(value.length() + 2).append('\u2060'); + for (int offset = 0; offset < value.length();) { + int codePoint = value.codePointAt(offset); + inert.appendCodePoint(codePoint); + if (codePoint == '%' || codePoint == '&') inert.append('\u2060'); + offset += Character.charCount(codePoint); + } + return inert.append('\u2060').toString(); + } + private String unsupportedGlobalBehavior() { if (node(config, ignoreCase, "CloseInventoryOnVote").getBoolean(true)) { return "CloseInventoryOnVote is not supported by NeoForge replay yet"; diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java index d958a71dfc..0c3c44aa2d 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java @@ -2976,7 +2976,7 @@ public String voteCommandLastGUILine(VoteSite voteSite) { HashMap placeholders = new HashMap<>(); placeholders.put("time", timeString); - placeholders.put("SiteName", voteSite.getDisplayNameForFormatting()); + placeholders.put("SiteName", voteSite.getDisplayName()); placeholders.put("timesince", timeSince); return PlaceholderUtils.replacePlaceHolder(plugin.getGui().getChestVoteLastLine(), placeholders); @@ -2994,7 +2994,7 @@ public String voteCommandLastLine(VoteSite voteSite) { HashMap placeholders = new HashMap<>(); placeholders.put("time", timeString); - placeholders.put("SiteName", voteSite.getDisplayNameForFormatting()); + placeholders.put("SiteName", voteSite.getDisplayName()); placeholders.put("timesince", timeSince); return PlaceholderUtils.replacePlaceHolder(plugin.getConfigFile().getFormatCommandsVoteLastLine(), diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java index 44110fcdd4..d28caa2bd9 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java @@ -7,7 +7,6 @@ public final class ServiceSiteValidator { /** Maximum accepted UTF-16 length for a service-site identifier. */ public static final int MAX_LENGTH = 2048; private static final int MAX_LOG_LENGTH = 128; - private static final String FORMATTING_BOUNDARY = "\u2060"; private ServiceSiteValidator() { } @@ -17,7 +16,7 @@ private ServiceSiteValidator() { * * @param serviceSite service-site name supplied by a vote source * @return {@code true} for a bounded, visible service-site name that does not - * contain structural formatting delimiters or control characters + * contain unsupported delimiters */ public static boolean isValid(String serviceSite) { if (serviceSite == null || serviceSite.length() > MAX_LENGTH) { @@ -66,27 +65,9 @@ public static String sanitizeForLog(String value) { return sanitized.toString(); } - /** - * Keeps a validated external identifier visually unchanged while preventing a - * surrounding trusted template from completing a formatting token across a - * substitution boundary. - */ - public static String inertForFormatting(String value) { - if (value == null || value.isEmpty()) return value == null ? "" : value; - StringBuilder inert = new StringBuilder(value.length() + 2).append(FORMATTING_BOUNDARY); - for (int offset = 0; offset < value.length();) { - int codePoint = value.codePointAt(offset); - inert.appendCodePoint(codePoint); - if (codePoint == '%' || codePoint == '&') inert.append(FORMATTING_BOUNDARY); - offset += Character.charCount(codePoint); - } - return inert.append(FORMATTING_BOUNDARY).toString(); - } - private static boolean isDisallowed(int codePoint) { if (codePoint == '[' || codePoint == ']' || codePoint == '\'' || codePoint == '"' || codePoint == '`' - || codePoint == '\u00A7' - || codePoint == '\\' || codePoint == '{' || codePoint == '}') { + || codePoint == '\\') { return true; } diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersListener.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersListener.java index ca775c6ce7..eb5cca3c57 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersListener.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersListener.java @@ -7,7 +7,6 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; -import com.bencodez.advancedcore.api.rewards.RewardDisplayPlaceholders; import com.bencodez.advancedcore.listeners.AdvancedCoreLoginEvent; import com.bencodez.votingplugin.VotingPluginMain; import com.bencodez.votingplugin.events.PlayerPostVoteEvent; @@ -38,8 +37,7 @@ public VoteRemindersListener(VotingPluginMain plugin) { */ @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onPostVote(PlayerPostVoteEvent event) { - plugin.getVoteRemindersManager().onVoteCast(event.getUser(), event.getVoteSite().getDisplayName(), - event.getVoteSite().getDisplayNameForFormatting()); + plugin.getVoteRemindersManager().onVoteCast(event.getUser(), event.getVoteSite().getDisplayName()); } /** @@ -84,7 +82,6 @@ public void onCoolDownEnd(PlayerVoteCoolDownEndEvent event) { public void onCoolDownEnd(PlayerVoteSiteCoolDownEndEvent event) { HashMap placeholders = new HashMap<>(); placeholders.put("votesite", event.getSite().getDisplayName()); - RewardDisplayPlaceholders.put(placeholders, "votesite", event.getSite().getDisplayNameForFormatting()); placeholders.put("votesite_id", event.getSite().getKey()); plugin.getVoteRemindersManager().onCooldownTrigger(event.getPlayer(), VoteReminderType.COOLDOWN_END_ANY_SITE, placeholders); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersManager.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersManager.java index 6a4ab5a1be..9f224d98a2 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersManager.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersManager.java @@ -26,7 +26,6 @@ import org.bukkit.entity.Player; import com.bencodez.advancedcore.api.rewards.RewardBuilder; -import com.bencodez.advancedcore.api.rewards.RewardDisplayPlaceholders; import com.bencodez.simpleapi.time.ParsedDuration; import com.bencodez.votingplugin.VotingPluginMain; import com.bencodez.votingplugin.user.VotingPluginUser; @@ -488,10 +487,6 @@ public void onQuit(Player player) { } public void onVoteCast(VotingPluginUser user, String siteKey) { - onVoteCast(user, siteKey, siteKey); - } - - public void onVoteCast(VotingPluginUser user, String siteKey, String displaySite) { if (!isEnabled() || user == null) { return; } @@ -504,10 +499,9 @@ public void onVoteCast(VotingPluginUser user, String siteKey, String displaySite return; } - HashMap ph = new HashMap<>(); + Map ph = new HashMap<>(); if (siteKey != null && !siteKey.isEmpty()) { ph.put("site", siteKey); - RewardDisplayPlaceholders.put(ph, "site", displaySite); } queueTrigger(user, VoteReminderType.VOTE_CAST, ph); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java index ac30171a5e..ba6e0a6f6a 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java @@ -10,17 +10,16 @@ import com.bencodez.simpleapi.array.ArrayUtils; import com.bencodez.simpleapi.messages.MessageAPI; import com.bencodez.simpleapi.time.ParsedDuration; -import com.bencodez.votingplugin.VotingPluginMain; -import com.bencodez.votingplugin.user.VotingPluginUser; -import com.bencodez.votingplugin.util.ServiceSiteValidator; +import com.bencodez.votingplugin.VotingPluginMain; +import com.bencodez.votingplugin.user.VotingPluginUser; import lombok.Getter; import lombok.Setter; public class VoteSite { - @Getter - private String displayName; - private boolean displayNameFallback; + @Getter + @Setter + private String displayName; @Getter @Setter @@ -158,13 +157,8 @@ public java.util.concurrent.CompletionStage giveRewardsAsync(VotingPluginU } private RewardBuilder createRewardBuilder(String path, boolean online, boolean bungee) { - // Reward placeholders also feed commands and other exact-value actions. The - // service identifier is already validated at ingress, so preserve it here; - // display-only callers use getServiceSiteForFormatting() instead. return new RewardBuilder(plugin.getConfigVoteSites().getData(), path).setOnline(online) .withPlaceHolder("ServiceSite", getServiceSite()).withPlaceHolder("SiteName", getDisplayName()) - .withDisplayPlaceHolder("ServiceSite", getServiceSiteForFormatting()) - .withDisplayPlaceHolder("SiteName", getDisplayNameForFormatting()) .withPlaceHolder("VoteDelay", "" + getVoteDelay()).withPlaceHolder("VoteURL", getVoteURL()) .setServer(bungee); } @@ -174,22 +168,6 @@ public boolean hasRewards() { plugin.getConfigVoteSites().getRewardsPath(key)); } - /** Returns the configured service identifier guarded for trusted templates. */ - public String getServiceSiteForFormatting() { - return ServiceSiteValidator.inertForFormatting(getServiceSite()); - } - - /** Sets an administrator-controlled display name. */ - public void setDisplayName(String displayName) { - this.displayName = displayName; - this.displayNameFallback = false; - } - - /** Returns the site label guarded only when it falls back to an external site key. */ - public String getDisplayNameForFormatting() { - return displayNameFallback ? ServiceSiteValidator.inertForFormatting(getDisplayName()) : getDisplayName(); - } - /** * Inits the. */ @@ -199,11 +177,10 @@ public void init() { this.voteDelay = plugin.getConfigVoteSites().getVoteDelay(key); setEnabled(plugin.getConfigVoteSites().getVoteSiteEnabled(key)); setPriority(plugin.getConfigVoteSites().getPriority(key)); - displayName = plugin.getConfigVoteSites().getDisplayName(key); - displayNameFallback = displayName == null || displayName.equals(""); - if (displayNameFallback) { - displayName = key; - } + displayName = plugin.getConfigVoteSites().getDisplayName(key); + if (displayName == null || displayName.equals("")) { + displayName = key; + } item = plugin.getConfigVoteSites().getItem(key); voteDelayDaily = plugin.getConfigVoteSites().getVoteSiteResetVoteDelayDaily(key); giveOffline = plugin.getConfigVoteSites().getVoteSiteGiveOffline(key); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java index 0b5aa35ba7..c2f74ee44d 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -1,6 +1,5 @@ package com.bencodez.votingplugin.tests; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -14,14 +13,8 @@ class ServiceSiteValidatorTest { void acceptsCommonServiceSiteNames() { for (String serviceSite : new String[] { "PlanetMinecraft.com", "Minecraft Server List", "Crafty.gg", "https://example.com/vote", "https://list.example/vote?id=1&source=proxy#top", - "Research & Development", "R&D", "site_name-2", "Site, Other; Network!", - "site%20name", "site%2Bname", "site%20%2Bname", "site%20name%2Bnetwork", - "site%20name_with%20spaces", "Top 100% Servers", "%player_name%", "%javascript_vote%", - "%be_secret%20", "%25player_name%25", "site%", "site%2", "site%GG", - "&kSpoofed", "&aGreen", "&rReset", - "&xHex", "&#ff0000Spoofed", "&#ABCDEFText", - "Serviço de votação", - "Site\u00A0Name", "Site\uFE0F", "Cafe\u0301", "Site\u3164Name" }) { + "https://example.com/search?q=site%20name+network", "site_name-2", "Site, Other; Network!", + "Serviço de votação", "Site\u00A0Name", "Site\uFE0F", "Cafe\u0301", "Site\u3164Name" }) { assertTrue(ServiceSiteValidator.isValid(serviceSite), serviceSite); } } @@ -29,25 +22,11 @@ void acceptsCommonServiceSiteNames() { @Test void rejectsUnsupportedCharacters() { for (String serviceSite : new String[] { "[Javascript=1]", "Site's", "\"Site\"", "Site`Name", - "Site\\Name", "Site\nName", "Site\tName", "Site\u0000Name", "Site\u200BName", - "{player}", "\u00A7kSpoofed", "\u00A7aGreen" }) { + "Site\\Name", "Site\nName", "Site\tName", "Site\u0000Name", "Site\u200BName" }) { assertFalse(ServiceSiteValidator.isValid(serviceSite), serviceSite); } } - @Test - void formattingGuardPreventsTokensAcrossTemplateBoundaries() { - String value = "%player_name% &aGreen &#ff0000Red kSpoofed&"; - String guarded = ServiceSiteValidator.inertForFormatting(value); - - assertFalse(("&" + guarded + "a").contains("&k")); - assertFalse(("%" + guarded + "player_name%").contains("%kSpoofed&player_name%")); - assertFalse(guarded.contains("%player_name%")); - assertFalse(guarded.contains("&a")); - assertFalse(guarded.contains("&#ff0000")); - assertEquals(value, guarded.replace("\u2060", "")); - } - @Test void rejectsMissingAndOversizedNames() { assertFalse(ServiceSiteValidator.isValid(null)); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/reminders/VoteRemindersListenerTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/reminders/VoteRemindersListenerTest.java deleted file mode 100644 index cbd73c802a..0000000000 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/reminders/VoteRemindersListenerTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.bencodez.votingplugin.tests.reminders; - -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.verify; -import static org.mockito.Mockito.when; - -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import com.bencodez.advancedcore.api.rewards.RewardDisplayPlaceholders; -import com.bencodez.votingplugin.VotingPluginMain; -import com.bencodez.votingplugin.events.PlayerPostVoteEvent; -import com.bencodez.votingplugin.events.PlayerVoteSiteCoolDownEndEvent; -import com.bencodez.votingplugin.user.VotingPluginUser; -import com.bencodez.votingplugin.votereminding.VoteRemindersListener; -import com.bencodez.votingplugin.votereminding.VoteRemindersManager; -import com.bencodez.votingplugin.votereminding.VoteRemindersManager.VoteReminderType; -import com.bencodez.votingplugin.votesites.VoteSite; - -class VoteRemindersListenerTest { - - @Test - void postVoteKeepsRawActionValueAndSeparateDisplayValue() { - VotingPluginMain plugin = mock(VotingPluginMain.class); - VoteRemindersManager manager = mock(VoteRemindersManager.class); - VoteSite site = mock(VoteSite.class); - VotingPluginUser user = mock(VotingPluginUser.class); - PlayerPostVoteEvent event = mock(PlayerPostVoteEvent.class); - when(plugin.getVoteRemindersManager()).thenReturn(manager); - when(event.getUser()).thenReturn(user); - when(event.getVoteSite()).thenReturn(site); - when(site.getDisplayName()).thenReturn("site_key"); - when(site.getDisplayNameForFormatting()).thenReturn("\u2060site_key\u2060"); - - new VoteRemindersListener(plugin).onPostVote(event); - - verify(manager).onVoteCast(user, "site_key", "\u2060site_key\u2060"); - } - - @Test - void cooldownKeepsRawActionValueAndSeparateDisplayValue() { - VotingPluginMain plugin = mock(VotingPluginMain.class); - VoteRemindersManager manager = mock(VoteRemindersManager.class); - VoteSite site = mock(VoteSite.class); - VotingPluginUser user = mock(VotingPluginUser.class); - PlayerVoteSiteCoolDownEndEvent event = mock(PlayerVoteSiteCoolDownEndEvent.class); - when(plugin.getVoteRemindersManager()).thenReturn(manager); - when(event.getPlayer()).thenReturn(user); - when(event.getSite()).thenReturn(site); - when(site.getDisplayName()).thenReturn("site_key"); - when(site.getDisplayNameForFormatting()).thenReturn("\u2060site_key\u2060"); - when(site.getKey()).thenReturn("site_key"); - - new VoteRemindersListener(plugin).onCoolDownEnd(event); - - @SuppressWarnings("unchecked") - ArgumentCaptor> placeholders = ArgumentCaptor.forClass(Map.class); - verify(manager).onCooldownTrigger(eq(user), eq(VoteReminderType.COOLDOWN_END_ANY_SITE), placeholders.capture()); - HashMap values = new HashMap<>(placeholders.getValue()); - assertEquals("site_key", values.get("votesite")); - assertEquals("\u2060site_key\u2060", RewardDisplayPlaceholders.forDisplay(values).get("votesite")); - } -} diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java index 7700bf23c2..9ae77591de 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java @@ -15,8 +15,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.bencodez.advancedcore.api.rewards.RewardBuilder; -import com.bencodez.advancedcore.api.rewards.RewardDisplayPlaceholders; import com.bencodez.simpleapi.time.ParsedDuration; import com.bencodez.votingplugin.VotingPluginMain; import com.bencodez.votingplugin.config.Config; @@ -67,48 +65,6 @@ public void testInitSetsValuesCorrectly() { assertEquals(12L * 60L * 60L * 1000L, d.getMillis(), "VoteDelay millis should be 12h"); } - @Test - public void serviceSiteFormattingCannotJoinSurroundingColorCodes() { - VoteSite voteSite = new VoteSite(plugin, "site.test"); - - assertEquals("ServiceSite", voteSite.getServiceSiteForFormatting().replace("\u2060", "")); - assertTrue(("&" + voteSite.getServiceSiteForFormatting()).indexOf("&S") < 0); - } - - @Test - public void rewardActionsReceiveTheExactValidatedServiceSite() throws Exception { - VoteSite voteSite = new VoteSite(plugin, "site.test"); - java.lang.reflect.Method factory = VoteSite.class.getDeclaredMethod("createRewardBuilder", String.class, - boolean.class, boolean.class); - factory.setAccessible(true); - - RewardBuilder builder = (RewardBuilder) factory.invoke(voteSite, "Rewards", true, false); - - assertEquals("ServiceSite", builder.getRewardOptions().getPlaceholders().get("ServiceSite")); - assertEquals("ServiceSite", RewardDisplayPlaceholders - .forDisplay(builder.getRewardOptions().getPlaceholders()).get("ServiceSite").replace("\u2060", "")); - } - - @Test - public void fallbackDisplayNameStaysRawForActionsAndGuardedForFormatting() throws Exception { - when(plugin.getConfigVoteSites().getDisplayName(anyString())).thenReturn(""); - VoteSite voteSite = new VoteSite(plugin, "site.test"); - java.lang.reflect.Method factory = VoteSite.class.getDeclaredMethod("createRewardBuilder", String.class, - boolean.class, boolean.class); - factory.setAccessible(true); - RewardBuilder builder = (RewardBuilder) factory.invoke(voteSite, "Rewards", true, false); - - assertEquals("site_test", voteSite.getDisplayName()); - assertEquals("site_test", builder.getRewardOptions().getPlaceholders().get("SiteName")); - assertEquals("site_test", voteSite.getDisplayNameForFormatting().replace("\u2060", "")); - assertTrue(voteSite.getDisplayNameForFormatting().startsWith("\u2060")); - assertEquals(voteSite.getDisplayNameForFormatting(), RewardDisplayPlaceholders - .forDisplay(builder.getRewardOptions().getPlaceholders()).get("SiteName")); - - voteSite.setDisplayName("Configured %server_name%"); - assertEquals("Configured %server_name%", voteSite.getDisplayNameForFormatting()); - } - @Test public void testGetVoteURLJson() { VoteSite voteSite = new VoteSite(plugin, "site.test"); From 1cda20842d3471bb205753f8e9b6c9a8a74e5d9c Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:37:55 -0600 Subject: [PATCH 2/7] Reject interpreter delimiters before vote reward admission --- .../messaging/BackendProxyMessageRouter.java | 4 +++ .../votingplugin/config/ConfigVoteSites.java | 10 +++---- .../votingplugin/user/VotingPluginUser.java | 4 +-- .../util/ServiceSiteValidator.java | 5 ++-- .../votingplugin/votesites/VoteSite.java | 18 ++++++------- .../BackendProxyMessageRouterTest.java | 23 ++++++++++++++++ .../NeoForgeRewardReplayServiceTest.java | 26 +++++++++--------- .../tests/ServiceSiteValidatorTest.java | 8 +++--- .../VotiferEventDisabledVoteSiteTest.java | 12 +++++++++ .../tests/votesite/VoteSiteTest.java | 15 +++++++++++ docs/service-site-rendering.md | 27 +++++++++++++++++++ 11 files changed, 119 insertions(+), 33 deletions(-) create mode 100644 docs/service-site-rendering.md diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java index 33ee20d5ba..49026a8b9b 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java @@ -612,6 +612,10 @@ private WireVoteResult handleWireVoteDelayRejected(JsonEnvelope msg) { if (rejected.uuid.isEmpty() || rejected.service.isEmpty()) { return new WireVoteResult(rejected.voteId, true); } + if (!ServiceSiteValidator.isValid(rejected.service)) { + plugin.getLogger().warning("Rejected VoteDelayRejected with invalid service site"); + return new WireVoteResult(rejected.voteId, true); + } UUID javaUuid; try { javaUuid = UUID.fromString(rejected.uuid); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java index bbc478ad7d..a4432b010f 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java @@ -80,14 +80,14 @@ private boolean tryGenerateVoteSite(String siteName, boolean automatic) { String org = siteName; siteName = siteName.replaceAll("[\\.\\s]+", "_"); - plugin.getLogger().warning("Creating VoteSite " + siteName + " for the service site '" + org + "'"); - setEnabled(siteName, true); - setServiceSite(siteName, org); - setVoteURL(siteName, "VoteURL"); + plugin.getLogger().warning("Creating VoteSite " + siteName + " for the service site '" + org + "'"); + setEnabled(siteName, true); + setServiceSite(siteName, org); + setVoteURL(siteName, "VoteURL"); setVoteDelay(siteName, "24h"); set(siteName, "DisplayItem.Material", "STONE"); set(siteName, "DisplayItem.Amount", 1); - set(siteName, "Rewards.Messages.Player", "&aThanks for voting on %ServiceSite%!"); + set(siteName, "Rewards.Messages.Player", "&aThanks for voting on %ServiceSite%!"); set(siteName, "WaitUntilVoteDelayRewards", Collections.emptyMap()); plugin.loadVoteSites(); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java index 0c3c44aa2d..5cb2c79bfd 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java @@ -2976,7 +2976,7 @@ public String voteCommandLastGUILine(VoteSite voteSite) { HashMap placeholders = new HashMap<>(); placeholders.put("time", timeString); - placeholders.put("SiteName", voteSite.getDisplayName()); + placeholders.put("SiteName", voteSite.getDisplayName()); placeholders.put("timesince", timeSince); return PlaceholderUtils.replacePlaceHolder(plugin.getGui().getChestVoteLastLine(), placeholders); @@ -2994,7 +2994,7 @@ public String voteCommandLastLine(VoteSite voteSite) { HashMap placeholders = new HashMap<>(); placeholders.put("time", timeString); - placeholders.put("SiteName", voteSite.getDisplayName()); + placeholders.put("SiteName", voteSite.getDisplayName()); placeholders.put("timesince", timeSince); return PlaceholderUtils.replacePlaceHolder(plugin.getConfigFile().getFormatCommandsVoteLastLine(), diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java index d28caa2bd9..88a8fa88c3 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java @@ -16,7 +16,7 @@ private ServiceSiteValidator() { * * @param serviceSite service-site name supplied by a vote source * @return {@code true} for a bounded, visible service-site name that does not - * contain unsupported delimiters + * contain interpreter delimiters */ public static boolean isValid(String serviceSite) { if (serviceSite == null || serviceSite.length() > MAX_LENGTH) { @@ -67,7 +67,8 @@ public static String sanitizeForLog(String value) { private static boolean isDisallowed(int codePoint) { if (codePoint == '[' || codePoint == ']' || codePoint == '\'' || codePoint == '"' || codePoint == '`' - || codePoint == '\\') { + || codePoint == '\\' || codePoint == '&' || codePoint == '%' || codePoint == '\u00A7' + || codePoint == '{' || codePoint == '}') { return true; } diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java index ba6e0a6f6a..6c61aa27c3 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java @@ -10,16 +10,16 @@ import com.bencodez.simpleapi.array.ArrayUtils; import com.bencodez.simpleapi.messages.MessageAPI; import com.bencodez.simpleapi.time.ParsedDuration; -import com.bencodez.votingplugin.VotingPluginMain; -import com.bencodez.votingplugin.user.VotingPluginUser; +import com.bencodez.votingplugin.VotingPluginMain; +import com.bencodez.votingplugin.user.VotingPluginUser; import lombok.Getter; import lombok.Setter; public class VoteSite { - @Getter - @Setter - private String displayName; + @Getter + @Setter + private String displayName; @Getter @Setter @@ -177,10 +177,10 @@ public void init() { this.voteDelay = plugin.getConfigVoteSites().getVoteDelay(key); setEnabled(plugin.getConfigVoteSites().getVoteSiteEnabled(key)); setPriority(plugin.getConfigVoteSites().getPriority(key)); - displayName = plugin.getConfigVoteSites().getDisplayName(key); - if (displayName == null || displayName.equals("")) { - displayName = key; - } + displayName = plugin.getConfigVoteSites().getDisplayName(key); + if (displayName == null || displayName.equals("")) { + displayName = key; + } item = plugin.getConfigVoteSites().getItem(key); voteDelayDaily = plugin.getConfigVoteSites().getVoteSiteResetVoteDelayDaily(key); giveOffline = plugin.getConfigVoteSites().getVoteSiteGiveOffline(key); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java index 07ccdb127e..2eb0d385ea 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java @@ -1066,6 +1066,29 @@ void receiptReleaseBypassesOrderingOnlyAfterItsReceiptIsDurable() { assertTrue(voteRouter.hasDurableReceiptForRelease(release)); } + @Test + void delayRejectionRejectsInterpreterDelimitersBeforeResolvingUsersOrSites() { + ProcessedVoteCache cache = mock(ProcessedVoteCache.class); + when(cache.complete(any(UUID.class))).thenReturn(true); + BackendProxyMessageRouter voteRouter = new BackendProxyMessageRouter(plugin, + mock(BackendPresenceManager.class), mock(BackendGlobalDataSync.class), + mock(BackendVotePartySync.class), cache); + for (String service : new String[] { "&kSpoofed", "%player_name%", "§kSpoofed", "{RED}" }) { + for (boolean reliable : new boolean[] { false, true }) { + configureDelayRejection(true); + JsonEnvelope rejection = VotingPluginWire.voteDelayRejected("Player", PLAYER_UUID.toString(), + service, true, UUID.randomUUID()); + if (reliable) rejection = VotingPluginWire.requestVoteDeliveryAcknowledgement(rejection); + AtomicReference outcome = new AtomicReference<>(); + voteRouter.handleOrderedVote(rejection, outcome::set); + assertEquals(OrderedVoteOutcome.COMPLETE, outcome.get()); + verify(voteSiteManager, never()).getVoteSiteName(true, service); + } + } + verify(plugin.getVotingPluginUserManager(), never()).getVotingPluginUser(PLAYER_UUID, "Player"); + verify(user, never()).cache(); + } + @Test void reliableDelayRejectionExecutesOnceAndPersistsItsVoteId() { UUID voteId = UUID.randomUUID(); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java index aeeed44eb4..a6916cdfdd 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java @@ -862,7 +862,7 @@ void playerMessageIntroducedLegacyFormattingRemainsPending() throws Exception { } @Test - void playerMessageKeepsConfiguredServiceFormattingLiteral() throws Exception { + void configuredServiceFormattingIsRejectedBeforeRewards() throws Exception { writeConfiguration(false); String sites = Files.readString(directory.resolve("VoteSites.yml")); Files.writeString(directory.resolve("VoteSites.yml"), sites @@ -874,17 +874,18 @@ void playerMessageKeepsConfiguredServiceFormattingLiteral() throws Exception { try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory); NeoForgeRewardReplayService replay = service(runtime, actions)) { runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true)); - retain(runtime, UUID.randomUUID(), playerId, "&aService"); - - assertEquals(NeoForgeRewardReplayService.Status.COMPLETED, - runOne(runtime, replay, actions).status()); - assertEquals(List.of("Thanks Alex from \u2060&\u2060aService\u2060"), actions.rendered); + assertEquals(NeoForgeVoteResult.Status.UNKNOWN_SITE, + runtime.voteProcessor().process(new NeoForgeVoteRequest(UUID.randomUUID(), playerId, + "Alex", "&aService", 100L, true, true, true, + NeoForgeVoteRequest.Scope.COMPLETE)).status()); + assertEquals(0, actions.calls.get()); + assertTrue(actions.rendered.isEmpty()); assertTrue(runtime.deferredVotes().pending(playerId).isEmpty()); } } @Test - void playerMessageKeepsServicePlaceholderTextInert() throws Exception { + void configuredServicePlaceholderIsRejectedBeforeRewards() throws Exception { writeConfiguration(false); String sites = Files.readString(directory.resolve("VoteSites.yml")); Files.writeString(directory.resolve("VoteSites.yml"), sites @@ -896,11 +897,12 @@ void playerMessageKeepsServicePlaceholderTextInert() throws Exception { try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory); NeoForgeRewardReplayService replay = service(runtime, actions)) { runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true)); - retain(runtime, UUID.randomUUID(), playerId, "%player%"); - - assertEquals(NeoForgeRewardReplayService.Status.COMPLETED, - runOne(runtime, replay, actions).status()); - assertEquals(List.of("Thanks Alex from \u2060%\u2060player%\u2060\u2060"), actions.rendered); + assertEquals(NeoForgeVoteResult.Status.UNKNOWN_SITE, + runtime.voteProcessor().process(new NeoForgeVoteRequest(UUID.randomUUID(), playerId, + "Alex", "%player%", 100L, true, true, true, + NeoForgeVoteRequest.Scope.COMPLETE)).status()); + assertEquals(0, actions.calls.get()); + assertTrue(actions.rendered.isEmpty()); assertTrue(runtime.deferredVotes().pending(playerId).isEmpty()); } } diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java index c2f74ee44d..fad8b2d7fe 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -12,8 +12,8 @@ class ServiceSiteValidatorTest { @Test void acceptsCommonServiceSiteNames() { for (String serviceSite : new String[] { "PlanetMinecraft.com", "Minecraft Server List", "Crafty.gg", - "https://example.com/vote", "https://list.example/vote?id=1&source=proxy#top", - "https://example.com/search?q=site%20name+network", "site_name-2", "Site, Other; Network!", + "https://example.com/vote", "https://list.example/vote?id=1#top", + "https://example.com/search?q=site+name+network", "site_name-2", "Site, Other; Network!", "Serviço de votação", "Site\u00A0Name", "Site\uFE0F", "Cafe\u0301", "Site\u3164Name" }) { assertTrue(ServiceSiteValidator.isValid(serviceSite), serviceSite); } @@ -22,7 +22,9 @@ void acceptsCommonServiceSiteNames() { @Test void rejectsUnsupportedCharacters() { for (String serviceSite : new String[] { "[Javascript=1]", "Site's", "\"Site\"", "Site`Name", - "Site\\Name", "Site\nName", "Site\tName", "Site\u0000Name", "Site\u200BName" }) { + "Site\\Name", "Site\nName", "Site\tName", "Site\u0000Name", "Site\u200BName", "&kSpoofed", "&rReset", "&#abcdef", + "%player_name%", "unfinished%", "trailing&", "\u00A7kSpoofed", "{RED}", "Site}", + "https://list.example/vote?id=1&source=proxy", "https://example.com/site%20name" }) { assertFalse(ServiceSiteValidator.isValid(serviceSite), serviceSite); } } diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java index b667f52987..3783cf1e00 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java @@ -103,6 +103,18 @@ private com.vexsoftware.votifier.model.VotifierEvent createVoteEvent(String serv return event; } + @Test + void rejectsInterpreterServiceNamesBeforeSchedulingOrAutomaticCreation() { + when(plugin.getConfigFile().isAutoCreateVoteSites()).thenReturn(true); + for (String service : new String[] { "&kSpoofed", "%player_name%", "{RED}", "\u00A7rSpoofed", "unfinished%", "trailing&" }) { + listener.onVotiferEvent(createVoteEvent(service)); + } + verify(voteTimer, never()).submit(any(Runnable.class)); + verify(overflowQueue, never()).enqueue(anyString(), anyString()); + verify(configVoteSites, never()).tryAutoGenerateVoteSite(anyString()); + verify(pluginManager, never()).callEvent(any(PlayerVoteEvent.class)); + } + @Test public void testDisabledConfiguredSiteIsNotGeneratedByVotifierPath() { when(plugin.getConfigFile().isAutoCreateVoteSites()).thenReturn(true); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java index 9ae77591de..6c896e2baa 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java @@ -65,6 +65,21 @@ public void testInitSetsValuesCorrectly() { assertEquals(12L * 60L * 60L * 1000L, d.getMillis(), "VoteDelay millis should be 12h"); } + @Test + void ordinaryFallbackNameContainsNoFormattingGuardsAndIdentifierRemainsExact() { + when(plugin.getConfigVoteSites().getDisplayName(anyString())).thenReturn(""); + when(plugin.getConfigVoteSites().getServiceSite(anyString())).thenReturn("PlanetMinecraft.com"); + VoteSite site = new VoteSite(plugin, "PlanetMinecraft_com"); + assertEquals("PlanetMinecraft_com", site.getDisplayName()); + assertEquals("PlanetMinecraft.com", site.getServiceSite()); + } + @Test + void ownerConfiguredDisplayColorRemainsSupported() { + when(plugin.getConfigVoteSites().getDisplayName(anyString())).thenReturn("&aConfigured Name"); + VoteSite site = new VoteSite(plugin, "configured"); + assertEquals("&aConfigured Name", site.getDisplayName()); + } + @Test public void testGetVoteURLJson() { VoteSite voteSite = new VoteSite(plugin, "site.test"); diff --git a/docs/service-site-rendering.md b/docs/service-site-rendering.md new file mode 100644 index 0000000000..34be666770 --- /dev/null +++ b/docs/service-site-rendering.md @@ -0,0 +1,27 @@ +# Voting-service identifiers and displayed names + +VotingPlugin displays ordinary configured names directly, without WORD JOINER or +other invisible formatting guards. Existing administrator-authored `DisplayName` +colors remain supported. The exact accepted `ServiceSite` is still used for vote +matching; no escaping or rewriting is applied to that identifier. + +External vote-service identifiers must be bounded, visible names without +interpreter delimiters. `&`, `%`, the section-sign color character, braces, +quotes, brackets, backticks, backslashes and control/format characters are +rejected by the shared validator before vote scheduling or automatic site +creation. This applies to Bukkit Votifier ingress, backend/proxy messages, +NeoForge ingress and the existing validation of persisted ingress queues. +A rejected service does not receive vote credit or rewards. + +Use a plain service identifier such as `PlanetMinecraft.com` or +`Minecraft Server List`. The service field is not the voting URL: encoded URLs +or query strings containing `%` or `&` are rejected too. Keep the clickable +voting URL in the site's `VoteURL` setting, whose URL behavior is unchanged. +These restrictions do not rewrite or migrate configured sites or saved player +records. A previously accepted delimiter-bearing service must be corrected at +its vote source before new votes from that source can be accepted. Malformed +persisted ingress queues retain their existing fail-closed recovery behavior; +no unsafe records are silently awarded or reinterpreted. + +This is an intentional ingress compatibility restriction instead of the old +invisible display guards. Live client rendering has not been visually verified. From 6aa5acb99d5e2bcd664eda1f839a92f230ed5676 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:49:34 -0600 Subject: [PATCH 3/7] Preserve legacy vote recovery with printable display guards --- .../messaging/BackendProxyMessageRouter.java | 2 +- .../commands/gui/player/VoteLast.java | 10 +-- .../commands/gui/player/VoteNext.java | 12 ++-- .../commands/gui/player/VoteToday.java | 16 ++--- .../commands/gui/player/VoteURL.java | 26 +++---- .../commands/gui/player/VoteURLVoteSite.java | 9 +-- .../votingplugin/config/ConfigVoteSites.java | 2 +- .../votingplugin/cooldown/CoolDownCheck.java | 1 + .../listeners/PlayerVoteListener.java | 2 +- .../neoforge/NeoForgeRewardConfiguration.java | 25 +------ .../votingplugin/user/VotingPluginUser.java | 4 +- .../util/ServiceSiteValidator.java | 13 +++- .../votereminding/VoteRemindersListener.java | 5 +- .../votereminding/VoteRemindersManager.java | 8 ++- .../votingplugin/votesites/VoteSite.java | 27 ++++++- .../BackendProxyMessageRouterTest.java | 2 +- .../NeoForgeRewardReplayServiceTest.java | 30 ++++---- .../proxy/PendingIncomingVoteJournalTest.java | 14 ++++ .../tests/ServiceSiteValidatorTest.java | 20 ++++-- .../VotiferEventDisabledVoteSiteTest.java | 4 +- .../reminders/VoteRemindersListenerTest.java | 68 ++++++++++++++++++ .../tests/votesite/VoteSiteTest.java | 72 +++++++++++++++---- docs/service-site-rendering.md | 43 ++++++----- 23 files changed, 285 insertions(+), 130 deletions(-) create mode 100644 VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/reminders/VoteRemindersListenerTest.java diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java index 49026a8b9b..491eb36a0d 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java @@ -595,7 +595,7 @@ private void handleVoteBroadcast(JsonEnvelope msg) { boolean online = fields.containsKey(VotingPluginWire.K_WAS_ONLINE) ? Boolean.parseBoolean(fields.get(VotingPluginWire.K_WAS_ONLINE)) : user.isOnline(); plugin.getBroadcastHandler().broadcastVote(user.getJavaUUID(), user.getPlayerName(), - voteSite.getDisplayName(), online, totals); + voteSite.getDisplayNameForFormatting(), online, totals); } private WireVoteResult handleWireVoteDelayRejected(JsonEnvelope msg) { diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteLast.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteLast.java index 34a696b8eb..437bea8643 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteLast.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteLast.java @@ -71,7 +71,7 @@ public void onDialog(Player player) { continue; } - dialog.placeholder("sitename", site.getDisplayName()).button(site.getDisplayName(), + dialog.placeholder("sitename", site.getDisplayNameForFormatting()).button(site.getDisplayNameForFormatting(), user.voteCommandLastLine(site), payload -> { if (plugin.getGui().isChestVoteLastClickableLinks()) { @@ -83,7 +83,7 @@ public void onDialog(Player player) { HashMap placeholders = new HashMap(); placeholders.put("voteurl", site.getVoteURL()); - placeholders.put("sitename", site.getDisplayName()); + placeholders.put("sitename", site.getDisplayNameForFormatting()); placeholders.put("player", clicked.getName()); clickedUser.sendMessage(plugin.getGui().getChestVoteURLURLText(), placeholders); @@ -121,7 +121,7 @@ public void onChest(Player player) { for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { if (!site.isHidden()) { inv.addButton(inv.getNextSlot(), - new UpdatingBInventoryButton(plugin, site.getItem().setName(site.getDisplayName()) + new UpdatingBInventoryButton(plugin, site.getItem().setName(site.getDisplayNameForFormatting()) .setLore(user.voteCommandLastGUILine(site)).setAmountNone(1), 1000, 1000) { @Override @@ -139,7 +139,7 @@ public void onClick(ClickEvent event) { plugin.getGui() .getChestVoteURLURLText(), "voteurl", site.getVoteURL()), - "sitename", site.getDisplayName()), + "sitename", site.getDisplayNameForFormatting()), "player", player.getName())); } @@ -148,7 +148,7 @@ public void onClick(ClickEvent event) { @Override public ItemBuilder onUpdate(Player p) { - return site.getItem().setName(site.getDisplayName()) + return site.getItem().setName(site.getDisplayNameForFormatting()) .setLore(user.voteCommandLastGUILine(site)).setAmountNone(1); } }); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteNext.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteNext.java index 008ed73bda..3d4a93256d 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteNext.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteNext.java @@ -60,7 +60,7 @@ public ArrayList getChat(CommandSender sender) { msgLine = MessageAPI.replaceIgnoreCase(msgLine, "%info%", user.voteCommandNextInfo(voteSite)); - msgLine = MessageAPI.replaceIgnoreCase(msgLine, "%SiteName%", voteSite.getDisplayName()); + msgLine = MessageAPI.replaceIgnoreCase(msgLine, "%SiteName%", voteSite.getDisplayNameForFormatting()); msg.add(MessageAPI.colorize(msgLine)); } } @@ -81,10 +81,10 @@ public void onDialog(Player player) { String siteNameDisplay = plugin.getGui().getChestVoteNextCustomSiteNamesDisplays(site.getKey()); if (siteNameDisplay.isEmpty()) { - siteNameDisplay = site.getDisplayName(); + siteNameDisplay = site.getDisplayNameForFormatting(); } - dialog.placeholder("sitename", site.getDisplayName()).placeholder("SiteName", site.getDisplayName()) + dialog.placeholder("sitename", site.getDisplayNameForFormatting()).placeholder("SiteName", site.getDisplayNameForFormatting()) .placeholder("info", user.voteCommandNextInfo(site)) .button(siteNameDisplay, user.voteCommandNextInfo(site), payload -> { @@ -117,7 +117,7 @@ public void onBook(Player player) { if (!site.isHidden()) { Layout nextLayout = new Layout(new ArrayList<>(Arrays.asList("[Json]"))); nextLayout.replaceTextComponent("[Json]", - BookUtil.TextBuilder.of(book.colorize(site.getDisplayName())) + BookUtil.TextBuilder.of(book.colorize(site.getDisplayNameForFormatting())) .onClick(BookUtil.ClickAction.openUrl(site.getVoteURLJsonStrip())) .onHover(BookUtil.HoverAction.showText(user.voteCommandNextInfo(site))).build()); book.addLayout(nextLayout); @@ -141,7 +141,7 @@ public void onChest(Player player) { if (!site.isHidden()) { String siteNameDisplay = plugin.getGui().getChestVoteNextCustomSiteNamesDisplays(site.getKey()); if (siteNameDisplay.isEmpty()) { - siteNameDisplay = site.getDisplayName(); + siteNameDisplay = site.getDisplayNameForFormatting(); } inv.addButton(inv.getNextSlot(), new UpdatingBInventoryButton(plugin, @@ -159,7 +159,7 @@ public void onClick(ClickEvent clickEvent) { @Override public ItemBuilder onUpdate(Player player) { - return site.getItem().setName(site.getDisplayName()) + return site.getItem().setName(site.getDisplayNameForFormatting()) .setLore(user.voteCommandNextInfo(site)).setAmountNone(1) .addPlaceholder("player", user.getPlayerName()); } diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteToday.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteToday.java index accbd1b3aa..88fa311a3b 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteToday.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteToday.java @@ -52,9 +52,9 @@ public void onDialog(Player player) { final UUID uuid = topPlayer.getUuid(); dialog.placeholder("player", topPlayer.getPlayerName()).placeholder("votesite", voteSite.getKey()) - .placeholder("sitename", voteSite.getDisplayName()) - .placeholder("SiteName", voteSite.getDisplayName()) - .placeholder("VoteSite", voteSite.getDisplayName()).placeholder("time", timeString) + .placeholder("sitename", voteSite.getDisplayNameForFormatting()) + .placeholder("SiteName", voteSite.getDisplayNameForFormatting()) + .placeholder("VoteSite", voteSite.getDisplayNameForFormatting()).placeholder("time", timeString) .placeholder("Time", timeString).button("&e" + topPlayer.getPlayerName(), plugin.getConfigFile().getFormatCommandsVoteTodayLine(), payload -> { Player clicked = player.getServer().getPlayer(payload.owner()); @@ -136,9 +136,9 @@ public void onChest(Player player) { HashMap placeholders = new HashMap<>(); placeholders.put("player", user.getPlayerName()); placeholders.put("votesite", voteSite.getKey()); - placeholders.put("sitename", voteSite.getDisplayName()); - placeholders.put("SiteName", voteSite.getDisplayName()); - placeholders.put("VoteSite", voteSite.getDisplayName()); + placeholders.put("sitename", voteSite.getDisplayNameForFormatting()); + placeholders.put("SiteName", voteSite.getDisplayNameForFormatting()); + placeholders.put("VoteSite", voteSite.getDisplayNameForFormatting()); placeholders.put("time", timeString); placeholders.put("Time", timeString); @@ -204,7 +204,7 @@ public String[] voteToday() { HashMap placeholders = new HashMap<>(); placeholders.put("player", user.getPlayerName()); placeholders.put("votesite", voteSite.getKey()); - placeholders.put("sitename", voteSite.getDisplayName()); + placeholders.put("sitename", voteSite.getDisplayNameForFormatting()); placeholders.put("time", timeString); msg.add(PlaceholderUtils.replacePlaceHolder(plugin.getConfigFile().getFormatCommandsVoteTodayLine(), placeholders)); @@ -234,7 +234,7 @@ public String[] voteTodayGUI() { HashMap placeholders = new HashMap<>(); placeholders.put("player", user.getPlayerName()); placeholders.put("votesite", mostRecentSite.getKey()); - placeholders.put("sitename", mostRecentSite.getDisplayName()); + placeholders.put("sitename", mostRecentSite.getDisplayNameForFormatting()); placeholders.put("time", timeString); msg.add(PlaceholderUtils.replacePlaceHolder(plugin.getConfigFile().getFormatCommandsVoteTodayLine(), placeholders)); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURL.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURL.java index 61ea8b80ee..36a7e2c2e5 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURL.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURL.java @@ -71,7 +71,7 @@ public ArrayList getChat(CommandSender sender, boolean bypassCanVote) { plugin.getConfigFile().getFormatCommandsVoteURLS()); message.replacePlaceholder("num", Integer.toString(counter)) .replacePlaceholder("url", voteURL) - .replacePlaceholder("SiteName", voteSite.getDisplayName()); + .replacePlaceholder("SiteName", voteSite.getDisplayNameForFormatting()); if (user != null && user.getPlayerName() != null) { message.replacePlaceholder("player", "" + user.getPlayerName()) .replacePlaceholder("Next", "" + user.voteCommandNextInfo(voteSite)); @@ -150,14 +150,14 @@ public void onDialog(Player player) { boolean canVote = finalUser.canVoteSite(voteSite); String color = canVote ? "&a" : "&c"; - dialog.placeholder("sitename", voteSite.getDisplayName()) - .placeholder("SiteName", voteSite.getDisplayName()) - .placeholder("servicesite", voteSite.getServiceSite()) - .placeholder("ServiceSite", voteSite.getServiceSite()) + dialog.placeholder("sitename", voteSite.getDisplayNameForFormatting()) + .placeholder("SiteName", voteSite.getDisplayNameForFormatting()) + .placeholder("servicesite", voteSite.getServiceSiteForFormatting()) + .placeholder("ServiceSite", voteSite.getServiceSiteForFormatting()) .placeholder("VoteDelay", "" + voteSite.getVoteDelay()) .placeholder("VoteHour", "" + voteSite.getVoteDelayDailyHour()) .placeholder("Next", finalUser.voteCommandNextInfo(voteSite)) - .button(color + voteSite.getDisplayName(), + .button(color + voteSite.getDisplayNameForFormatting(), canVote ? "&aClick to view vote link" : "&c" + finalUser.voteCommandNextInfo(voteSite), payload -> { Player clicked = player.getServer().getPlayer(payload.owner()); @@ -166,9 +166,9 @@ public void onDialog(Player player) { .getVotingPluginUser(clicked); HashMap placeholders = new HashMap(); placeholders.put("voteurl", voteSite.getVoteURL()); - placeholders.put("sitename", voteSite.getDisplayName()); + placeholders.put("sitename", voteSite.getDisplayNameForFormatting()); placeholders.put("player", clicked.getName()); - placeholders.put("servicesite", voteSite.getServiceSite()); + placeholders.put("servicesite", voteSite.getServiceSiteForFormatting()); placeholders.put("VoteDelay", "" + voteSite.getVoteDelay()); placeholders.put("VoteHour", "" + voteSite.getVoteDelayDailyHour()); clickedUser.sendMessage(plugin.getGui().getChestVoteURLURLText(), placeholders); @@ -218,8 +218,8 @@ private ItemBuilder getItemVoteSite(VoteSite voteSite) { builder.setName(plugin.getGui().getChestVoteURLGUISiteName()); builder.addLoreLine(plugin.getGui().getChestVoteURLNextVote()); } - builder.addPlaceholder("ServiceSite", voteSite.getServiceSite()); - builder.addPlaceholder("Name", voteSite.getDisplayName()); + builder.addPlaceholder("ServiceSite", voteSite.getServiceSiteForFormatting()); + builder.addPlaceholder("Name", voteSite.getDisplayNameForFormatting()); builder.addPlaceholder("VoteDelay", "" + voteSite.getVoteDelay()); builder.addPlaceholder("VoteHour", "" + voteSite.getVoteDelayDailyHour()); builder.addPlaceholder("Info", user.voteCommandNextInfo(voteSite)); @@ -240,7 +240,7 @@ public void onBook(Player player) { if (!site.isHidden()) { if (site.getPermissionToView().isEmpty() || player.hasPermission(site.getPermissionToView())) { Layout layout = new Layout(plugin.getGui().getBookVoteURLBookGUILayout()) - .addPlaceholder("sitename", site.getDisplayName()).addPlaceholder("num", "" + i); + .addPlaceholder("sitename", site.getDisplayNameForFormatting()).addPlaceholder("num", "" + i); String text = plugin.getGui().getBookVoteURLBookGUIAlreadyVotedText(); ChatColor color = ChatColor.valueOf(plugin.getGui().getBookVoteURLBookGUIAlreadyVotedColor()); if (user.canVoteSite(site)) { @@ -342,9 +342,9 @@ public void onClick(ClickEvent event) { VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(player); HashMap placeholders = new HashMap<>(); placeholders.put("voteurl", voteSite.getVoteURL()); - placeholders.put("sitename", voteSite.getDisplayName()); + placeholders.put("sitename", voteSite.getDisplayNameForFormatting()); placeholders.put("player", player.getName()); - placeholders.put("servicesite", voteSite.getServiceSite()); + placeholders.put("servicesite", voteSite.getServiceSiteForFormatting()); placeholders.put("VoteDelay", "" + voteSite.getVoteDelay()); placeholders.put("VoteHour", "" + voteSite.getVoteDelayDailyHour()); user.sendMessage(plugin.getGui().getChestVoteURLURLText(), placeholders); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURLVoteSite.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURLVoteSite.java index 94e0e51e3d..779227cc6b 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURLVoteSite.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/commands/gui/player/VoteURLVoteSite.java @@ -53,7 +53,7 @@ public void onChest(Player player) { } VoteSite site = plugin.getVoteSiteManager().getVoteSite(voteSite, true); BInventory inv = new BInventory(plugin.getGui().getChestVoteURLSiteName()); - inv.addPlaceholder("site", site.getDisplayName()); + inv.addPlaceholder("site", site.getDisplayNameForFormatting()); inv.setMeta(player, "VoteSite", site); if (!plugin.getConfigFile().isAlwaysCloseInventory()) { inv.dontClose(); @@ -117,9 +117,10 @@ public void onDialog(Player player) { VoteSite site = plugin.getVoteSiteManager().getVoteSite(voteSite, true); com.bencodez.simpleapi.dialog.MultiActionDialogBuilder dialog = plugin.getDialogService().multiAction(player) - .placeholder("player", user.getPlayerName()).placeholder("site", site.getDisplayName()) - .placeholder("sitename", site.getDisplayName()).placeholder("SiteName", site.getDisplayName()) - .placeholder("servicesite", site.getServiceSite()).placeholder("ServiceSite", site.getServiceSite()) + .placeholder("player", user.getPlayerName()).placeholder("site", site.getDisplayNameForFormatting()) + .placeholder("sitename", site.getDisplayNameForFormatting()).placeholder("SiteName", site.getDisplayNameForFormatting()) + .placeholder("servicesite", site.getServiceSiteForFormatting()) + .placeholder("ServiceSite", site.getServiceSiteForFormatting()) .placeholder("VoteDelay", "" + site.getVoteDelay()) .placeholder("VoteHour", "" + site.getVoteDelayDailyHour()) .placeholder("Next", user.voteCommandNextInfo(site)).placeholder("Last", user.voteCommandLastLine(site)) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java index a4432b010f..e422baa340 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java @@ -87,7 +87,7 @@ private boolean tryGenerateVoteSite(String siteName, boolean automatic) { setVoteDelay(siteName, "24h"); set(siteName, "DisplayItem.Material", "STONE"); set(siteName, "DisplayItem.Amount", 1); - set(siteName, "Rewards.Messages.Player", "&aThanks for voting on %ServiceSite%!"); + set(siteName, "Rewards.Messages.Player", "&aThanks for voting on %SiteName%!"); set(siteName, "WaitUntilVoteDelayRewards", Collections.emptyMap()); plugin.loadVoteSites(); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/cooldown/CoolDownCheck.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/cooldown/CoolDownCheck.java index de0140bc09..2e370fe37c 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/cooldown/CoolDownCheck.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/cooldown/CoolDownCheck.java @@ -280,6 +280,7 @@ public void onCoolDownEnd(PlayerVoteCoolDownEndEvent event) { public void onCoolDownEnd(PlayerVoteSiteCoolDownEndEvent event) { plugin.getRewardHandler().giveReward(event.getPlayer(), event.getSite().getSiteData(), "CoolDownEndRewards", new RewardOptions().addPlaceholder("sitename", event.getSite().getDisplayName()) + .addDisplayPlaceholder("sitename", event.getSite().getDisplayNameForFormatting()) .addPlaceholder("url", event.getSite().getVoteURL(false))); } diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/PlayerVoteListener.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/PlayerVoteListener.java index 7c573a7608..6b4d816681 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/PlayerVoteListener.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/PlayerVoteListener.java @@ -95,7 +95,7 @@ private BukkitOperations(VotingPluginMain plugin, PlayerVoteEvent event) { } @Override public boolean siteEnabled(VoteSite site) { return site.isEnabled(); } @Override public String siteKey(VoteSite site) { return site.getKey(); } - @Override public String siteDisplayName(VoteSite site) { return site.getDisplayName(); } + @Override public String siteDisplayName(VoteSite site) { return site.getDisplayNameForFormatting(); } @Override public VotingPluginUser resolveUser(String name) { if (event.getVotingPluginUser() != null) return event.getVotingPluginUser(); Player player = Bukkit.getPlayerExact(name); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardConfiguration.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardConfiguration.java index 3e873f69ef..e5de5e9d53 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardConfiguration.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardConfiguration.java @@ -8,6 +8,7 @@ import org.spongepowered.configurate.ConfigurationNode; +import com.bencodez.votingplugin.util.ServiceSiteValidator; /** Parses the first NeoForge-safe subset of existing VotingPlugin reward YAML. */ final class NeoForgeRewardConfiguration { @@ -71,36 +72,16 @@ NeoForgeRewardPlan plan(NeoForgeDeferredVote vote, NeoForgeVoteSite site, boolea private static NeoForgeRewardPlan.Action expand(NeoForgeRewardPlan.Action action, NeoForgeDeferredVote vote, NeoForgeVoteSite site) { - // NeoForge's message isolation was added after #1643. Preserve that - // independent safety contract when reverting the Bukkit formatting - // change; command arguments still receive the raw identifier. String serviceSite = action.type() == NeoForgeRewardPlan.ActionType.PLAYER_MESSAGE - ? inertMessageIdentifier(vote.serviceSite()) : vote.serviceSite(); + ? ServiceSiteValidator.inertForFormatting(vote.serviceSite()) : vote.serviceSite(); String siteName = action.type() == NeoForgeRewardPlan.ActionType.PLAYER_MESSAGE && site.displayNameFallback() - ? inertMessageIdentifier(site.displayName()) : site.displayName(); + ? ServiceSiteValidator.inertForFormatting(site.displayName()) : site.displayName(); return new NeoForgeRewardPlan.Action(action.type(), action.value() .replace("%SiteName%", siteName).replace("%sitename%", siteName) .replace("%ServiceSite%", serviceSite).replace("%servicesite%", serviceSite)); } - /** - * Prevents interpreted formatting and placeholders in NeoForge player - * messages. This later-added adapter intentionally retains its own - * untrusted-message guards independently of Bukkit's reverted UI path. - */ - private static String inertMessageIdentifier(String value) { - if (value == null || value.isEmpty()) return value == null ? "" : value; - StringBuilder inert = new StringBuilder(value.length() + 2).append('\u2060'); - for (int offset = 0; offset < value.length();) { - int codePoint = value.codePointAt(offset); - inert.appendCodePoint(codePoint); - if (codePoint == '%' || codePoint == '&') inert.append('\u2060'); - offset += Character.charCount(codePoint); - } - return inert.append('\u2060').toString(); - } - private String unsupportedGlobalBehavior() { if (node(config, ignoreCase, "CloseInventoryOnVote").getBoolean(true)) { return "CloseInventoryOnVote is not supported by NeoForge replay yet"; diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java index 5cb2c79bfd..d958a71dfc 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java @@ -2976,7 +2976,7 @@ public String voteCommandLastGUILine(VoteSite voteSite) { HashMap placeholders = new HashMap<>(); placeholders.put("time", timeString); - placeholders.put("SiteName", voteSite.getDisplayName()); + placeholders.put("SiteName", voteSite.getDisplayNameForFormatting()); placeholders.put("timesince", timeSince); return PlaceholderUtils.replacePlaceHolder(plugin.getGui().getChestVoteLastLine(), placeholders); @@ -2994,7 +2994,7 @@ public String voteCommandLastLine(VoteSite voteSite) { HashMap placeholders = new HashMap<>(); placeholders.put("time", timeString); - placeholders.put("SiteName", voteSite.getDisplayName()); + placeholders.put("SiteName", voteSite.getDisplayNameForFormatting()); placeholders.put("timesince", timeSince); return PlaceholderUtils.replacePlaceHolder(plugin.getConfigFile().getFormatCommandsVoteLastLine(), diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java index 88a8fa88c3..e6765bec66 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java @@ -16,7 +16,7 @@ private ServiceSiteValidator() { * * @param serviceSite service-site name supplied by a vote source * @return {@code true} for a bounded, visible service-site name that does not - * contain interpreter delimiters + * contain unsupported delimiters */ public static boolean isValid(String serviceSite) { if (serviceSite == null || serviceSite.length() > MAX_LENGTH) { @@ -65,10 +65,17 @@ public static String sanitizeForLog(String value) { return sanitized.toString(); } + /** Display-only representation; raw identifiers remain authoritative for matching/actions. + * Uses printable ASCII labels rather than invisible or font-dependent characters. */ + public static String inertForFormatting(String value) { + if (value == null) return ""; + return value.replace("&", "(ampersand)").replace("%", "(percent)") + .replace("§", "(section)").replace("{", "(open brace)").replace("}", "(close brace)"); + } + private static boolean isDisallowed(int codePoint) { if (codePoint == '[' || codePoint == ']' || codePoint == '\'' || codePoint == '"' || codePoint == '`' - || codePoint == '\\' || codePoint == '&' || codePoint == '%' || codePoint == '\u00A7' - || codePoint == '{' || codePoint == '}') { + || codePoint == '\\') { return true; } diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersListener.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersListener.java index eb5cca3c57..ca775c6ce7 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersListener.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersListener.java @@ -7,6 +7,7 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; +import com.bencodez.advancedcore.api.rewards.RewardDisplayPlaceholders; import com.bencodez.advancedcore.listeners.AdvancedCoreLoginEvent; import com.bencodez.votingplugin.VotingPluginMain; import com.bencodez.votingplugin.events.PlayerPostVoteEvent; @@ -37,7 +38,8 @@ public VoteRemindersListener(VotingPluginMain plugin) { */ @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onPostVote(PlayerPostVoteEvent event) { - plugin.getVoteRemindersManager().onVoteCast(event.getUser(), event.getVoteSite().getDisplayName()); + plugin.getVoteRemindersManager().onVoteCast(event.getUser(), event.getVoteSite().getDisplayName(), + event.getVoteSite().getDisplayNameForFormatting()); } /** @@ -82,6 +84,7 @@ public void onCoolDownEnd(PlayerVoteCoolDownEndEvent event) { public void onCoolDownEnd(PlayerVoteSiteCoolDownEndEvent event) { HashMap placeholders = new HashMap<>(); placeholders.put("votesite", event.getSite().getDisplayName()); + RewardDisplayPlaceholders.put(placeholders, "votesite", event.getSite().getDisplayNameForFormatting()); placeholders.put("votesite_id", event.getSite().getKey()); plugin.getVoteRemindersManager().onCooldownTrigger(event.getPlayer(), VoteReminderType.COOLDOWN_END_ANY_SITE, placeholders); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersManager.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersManager.java index 9f224d98a2..6a4ab5a1be 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersManager.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votereminding/VoteRemindersManager.java @@ -26,6 +26,7 @@ import org.bukkit.entity.Player; import com.bencodez.advancedcore.api.rewards.RewardBuilder; +import com.bencodez.advancedcore.api.rewards.RewardDisplayPlaceholders; import com.bencodez.simpleapi.time.ParsedDuration; import com.bencodez.votingplugin.VotingPluginMain; import com.bencodez.votingplugin.user.VotingPluginUser; @@ -487,6 +488,10 @@ public void onQuit(Player player) { } public void onVoteCast(VotingPluginUser user, String siteKey) { + onVoteCast(user, siteKey, siteKey); + } + + public void onVoteCast(VotingPluginUser user, String siteKey, String displaySite) { if (!isEnabled() || user == null) { return; } @@ -499,9 +504,10 @@ public void onVoteCast(VotingPluginUser user, String siteKey) { return; } - Map ph = new HashMap<>(); + HashMap ph = new HashMap<>(); if (siteKey != null && !siteKey.isEmpty()) { ph.put("site", siteKey); + RewardDisplayPlaceholders.put(ph, "site", displaySite); } queueTrigger(user, VoteReminderType.VOTE_CAST, ph); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java index 6c61aa27c3..654cee7db2 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java @@ -12,14 +12,15 @@ import com.bencodez.simpleapi.time.ParsedDuration; import com.bencodez.votingplugin.VotingPluginMain; import com.bencodez.votingplugin.user.VotingPluginUser; +import com.bencodez.votingplugin.util.ServiceSiteValidator; import lombok.Getter; import lombok.Setter; public class VoteSite { @Getter - @Setter private String displayName; + private boolean displayNameFallback; @Getter @Setter @@ -157,8 +158,13 @@ public java.util.concurrent.CompletionStage giveRewardsAsync(VotingPluginU } private RewardBuilder createRewardBuilder(String path, boolean online, boolean bungee) { + // Reward placeholders also feed commands and other exact-value actions. The + // service identifier is already validated at ingress, so preserve it here; + // display-only callers use getServiceSiteForFormatting() instead. return new RewardBuilder(plugin.getConfigVoteSites().getData(), path).setOnline(online) .withPlaceHolder("ServiceSite", getServiceSite()).withPlaceHolder("SiteName", getDisplayName()) + .withDisplayPlaceHolder("ServiceSite", getServiceSiteForFormatting()) + .withDisplayPlaceHolder("SiteName", getDisplayNameForFormatting()) .withPlaceHolder("VoteDelay", "" + getVoteDelay()).withPlaceHolder("VoteURL", getVoteURL()) .setServer(bungee); } @@ -168,6 +174,22 @@ public boolean hasRewards() { plugin.getConfigVoteSites().getRewardsPath(key)); } + /** Returns the configured service identifier guarded for trusted templates. */ + public String getServiceSiteForFormatting() { + return ServiceSiteValidator.inertForFormatting(getServiceSite()); + } + + /** Sets an administrator-controlled display name. */ + public void setDisplayName(String displayName) { + this.displayName = displayName; + this.displayNameFallback = false; + } + + /** Returns the site label guarded only when it falls back to an external site key. */ + public String getDisplayNameForFormatting() { + return displayNameFallback ? ServiceSiteValidator.inertForFormatting(getDisplayName()) : getDisplayName(); + } + /** * Inits the. */ @@ -178,7 +200,8 @@ public void init() { setEnabled(plugin.getConfigVoteSites().getVoteSiteEnabled(key)); setPriority(plugin.getConfigVoteSites().getPriority(key)); displayName = plugin.getConfigVoteSites().getDisplayName(key); - if (displayName == null || displayName.equals("")) { + displayNameFallback = displayName == null || displayName.equals(""); + if (displayNameFallback) { displayName = key; } item = plugin.getConfigVoteSites().getItem(key); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java index 2eb0d385ea..d70194e865 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java @@ -1073,7 +1073,7 @@ void delayRejectionRejectsInterpreterDelimitersBeforeResolvingUsersOrSites() { BackendProxyMessageRouter voteRouter = new BackendProxyMessageRouter(plugin, mock(BackendPresenceManager.class), mock(BackendGlobalDataSync.class), mock(BackendVotePartySync.class), cache); - for (String service : new String[] { "&kSpoofed", "%player_name%", "§kSpoofed", "{RED}" }) { + for (String service : new String[] { "[Spoofed]", "bad\\name", "bad\nname", "bad\u2060name" }) { for (boolean reliable : new boolean[] { false, true }) { configureDelayRejection(true); JsonEnvelope rejection = VotingPluginWire.voteDelayRejected("Player", PLAYER_UUID.toString(), diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java index a6916cdfdd..38dbff04ed 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java @@ -834,7 +834,7 @@ void playerMessageOnlyRewardIsSupported() throws Exception { runtime.scheduler().onServerTick(); assertEquals(NeoForgeRewardReplayService.Status.COMPLETED, completion.get(5, TimeUnit.SECONDS).get(0).status()); - assertEquals(List.of("Thanks Alex from \u2060Service\u2060 at Supported Display"), actions.rendered); + assertEquals(List.of("Thanks Alex from Service at Supported Display"), actions.rendered); } } @@ -862,7 +862,7 @@ void playerMessageIntroducedLegacyFormattingRemainsPending() throws Exception { } @Test - void configuredServiceFormattingIsRejectedBeforeRewards() throws Exception { + void playerMessageKeepsConfiguredServiceFormattingLiteral() throws Exception { writeConfiguration(false); String sites = Files.readString(directory.resolve("VoteSites.yml")); Files.writeString(directory.resolve("VoteSites.yml"), sites @@ -874,18 +874,17 @@ void configuredServiceFormattingIsRejectedBeforeRewards() throws Exception { try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory); NeoForgeRewardReplayService replay = service(runtime, actions)) { runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true)); - assertEquals(NeoForgeVoteResult.Status.UNKNOWN_SITE, - runtime.voteProcessor().process(new NeoForgeVoteRequest(UUID.randomUUID(), playerId, - "Alex", "&aService", 100L, true, true, true, - NeoForgeVoteRequest.Scope.COMPLETE)).status()); - assertEquals(0, actions.calls.get()); - assertTrue(actions.rendered.isEmpty()); + retain(runtime, UUID.randomUUID(), playerId, "&aService"); + + assertEquals(NeoForgeRewardReplayService.Status.COMPLETED, + runOne(runtime, replay, actions).status()); + assertEquals(List.of("Thanks Alex from (ampersand)aService"), actions.rendered); assertTrue(runtime.deferredVotes().pending(playerId).isEmpty()); } } @Test - void configuredServicePlaceholderIsRejectedBeforeRewards() throws Exception { + void playerMessageKeepsServicePlaceholderTextInert() throws Exception { writeConfiguration(false); String sites = Files.readString(directory.resolve("VoteSites.yml")); Files.writeString(directory.resolve("VoteSites.yml"), sites @@ -897,12 +896,11 @@ void configuredServicePlaceholderIsRejectedBeforeRewards() throws Exception { try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory); NeoForgeRewardReplayService replay = service(runtime, actions)) { runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true)); - assertEquals(NeoForgeVoteResult.Status.UNKNOWN_SITE, - runtime.voteProcessor().process(new NeoForgeVoteRequest(UUID.randomUUID(), playerId, - "Alex", "%player%", 100L, true, true, true, - NeoForgeVoteRequest.Scope.COMPLETE)).status()); - assertEquals(0, actions.calls.get()); - assertTrue(actions.rendered.isEmpty()); + retain(runtime, UUID.randomUUID(), playerId, "%player%"); + + assertEquals(NeoForgeRewardReplayService.Status.COMPLETED, + runOne(runtime, replay, actions).status()); + assertEquals(List.of("Thanks Alex from (percent)player(percent)"), actions.rendered); assertTrue(runtime.deferredVotes().pending(playerId).isEmpty()); } } @@ -925,7 +923,7 @@ void playerMessageKeepsFallbackSiteNamePlaceholderTextInert() throws Exception { assertEquals(NeoForgeRewardReplayService.Status.COMPLETED, runOne(runtime, replay, actions).status()); - assertEquals(List.of("Thanks Alex from \u2060%\u2060player%\u2060\u2060"), actions.rendered); + assertEquals(List.of("Thanks Alex from (percent)player(percent)"), actions.rendered); assertTrue(runtime.deferredVotes().pending(playerId).isEmpty()); } } diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/proxy/PendingIncomingVoteJournalTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/proxy/PendingIncomingVoteJournalTest.java index c083e19b12..20fde41631 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/proxy/PendingIncomingVoteJournalTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/proxy/PendingIncomingVoteJournalTest.java @@ -19,6 +19,20 @@ class PendingIncomingVoteJournalTest { @TempDir Path temporaryDirectory; + @Test + void legacyDelimiterBearingVotesRemainReplayableWithOtherQueuedVotes() throws Exception { + var journal = new PendingIncomingVoteJournal(temporaryDirectory); + var votes = List.of(new VoteTimeQueue(UUID.randomUUID(), "Player", "R&D", 1L), + new VoteTimeQueue(UUID.randomUUID(), "Player", "https://example.com/site%20name", 2L), + new VoteTimeQueue(UUID.randomUUID(), "Player", "NormalSite", 3L)); + journal.merge(votes); + var recovered = journal.load(); + assertEquals(votes.stream().map(VoteTimeQueue::getService).toList(), + recovered.stream().map(VoteTimeQueue::getService).toList()); + assertEquals(votes.stream().map(VoteTimeQueue::getVoteId).toList(), + recovered.stream().map(VoteTimeQueue::getVoteId).toList()); + } + @Test void atomicJournalPreservesStableIdAndPartialEffectFences() throws Exception { PendingIncomingVoteJournal journal = new PendingIncomingVoteJournal(temporaryDirectory); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java index fad8b2d7fe..708df532bf 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -20,12 +20,22 @@ void acceptsCommonServiceSiteNames() { } @Test - void rejectsUnsupportedCharacters() { - for (String serviceSite : new String[] { "[Javascript=1]", "Site's", "\"Site\"", "Site`Name", - "Site\\Name", "Site\nName", "Site\tName", "Site\u0000Name", "Site\u200BName", "&kSpoofed", "&rReset", "&#abcdef", - "%player_name%", "unfinished%", "trailing&", "\u00A7kSpoofed", "{RED}", "Site}", + void acceptsLegacyFormattingIdentifiersWithoutChangingMatching() { + for (String value : new String[] { "R&D", "&kSpoofed", "%player_name%", "§kSpoofed", "{RED}", "https://list.example/vote?id=1&source=proxy", "https://example.com/site%20name" }) { - assertFalse(ServiceSiteValidator.isValid(serviceSite), serviceSite); + assertTrue(ServiceSiteValidator.isValid(value), value); + String display = ServiceSiteValidator.inertForFormatting(value); + assertFalse(display.contains("&") || display.contains("%") || display.contains("§") + || display.contains("{") || display.contains("}")); + assertFalse(display.contains("\u2060")); + } + } + + @Test + void rejectsUnsupportedCharacters() { + for (String value : new String[] { "[Javascript=1]", "Site's", "\"Site\"", "Site`Name", + "Site\\Name", "Site\nName", "Site\tName", "Site\u0000Name", "Site\u200BName" }) { + assertFalse(ServiceSiteValidator.isValid(value), value); } } diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java index 3783cf1e00..67fe593d55 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java @@ -104,9 +104,9 @@ private com.vexsoftware.votifier.model.VotifierEvent createVoteEvent(String serv } @Test - void rejectsInterpreterServiceNamesBeforeSchedulingOrAutomaticCreation() { + void rejectsStructurallyInvalidServiceNamesBeforeSchedulingOrAutomaticCreation() { when(plugin.getConfigFile().isAutoCreateVoteSites()).thenReturn(true); - for (String service : new String[] { "&kSpoofed", "%player_name%", "{RED}", "\u00A7rSpoofed", "unfinished%", "trailing&" }) { + for (String service : new String[] { "[Javascript=1]", "bad\\name", "bad\nname" }) { listener.onVotiferEvent(createVoteEvent(service)); } verify(voteTimer, never()).submit(any(Runnable.class)); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/reminders/VoteRemindersListenerTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/reminders/VoteRemindersListenerTest.java new file mode 100644 index 0000000000..25962f2e3d --- /dev/null +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/reminders/VoteRemindersListenerTest.java @@ -0,0 +1,68 @@ +package com.bencodez.votingplugin.tests.reminders; + +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.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.bencodez.advancedcore.api.rewards.RewardDisplayPlaceholders; +import com.bencodez.votingplugin.VotingPluginMain; +import com.bencodez.votingplugin.events.PlayerPostVoteEvent; +import com.bencodez.votingplugin.events.PlayerVoteSiteCoolDownEndEvent; +import com.bencodez.votingplugin.user.VotingPluginUser; +import com.bencodez.votingplugin.votereminding.VoteRemindersListener; +import com.bencodez.votingplugin.votereminding.VoteRemindersManager; +import com.bencodez.votingplugin.votereminding.VoteRemindersManager.VoteReminderType; +import com.bencodez.votingplugin.votesites.VoteSite; + +class VoteRemindersListenerTest { + + @Test + void postVoteKeepsRawActionValueAndSeparateDisplayValue() { + VotingPluginMain plugin = mock(VotingPluginMain.class); + VoteRemindersManager manager = mock(VoteRemindersManager.class); + VoteSite site = mock(VoteSite.class); + VotingPluginUser user = mock(VotingPluginUser.class); + PlayerPostVoteEvent event = mock(PlayerPostVoteEvent.class); + when(plugin.getVoteRemindersManager()).thenReturn(manager); + when(event.getUser()).thenReturn(user); + when(event.getVoteSite()).thenReturn(site); + when(site.getDisplayName()).thenReturn("site_key"); + when(site.getDisplayNameForFormatting()).thenReturn("site_key"); + + new VoteRemindersListener(plugin).onPostVote(event); + + verify(manager).onVoteCast(user, "site_key", "site_key"); + } + + @Test + void cooldownKeepsRawActionValueAndSeparateDisplayValue() { + VotingPluginMain plugin = mock(VotingPluginMain.class); + VoteRemindersManager manager = mock(VoteRemindersManager.class); + VoteSite site = mock(VoteSite.class); + VotingPluginUser user = mock(VotingPluginUser.class); + PlayerVoteSiteCoolDownEndEvent event = mock(PlayerVoteSiteCoolDownEndEvent.class); + when(plugin.getVoteRemindersManager()).thenReturn(manager); + when(event.getPlayer()).thenReturn(user); + when(event.getSite()).thenReturn(site); + when(site.getDisplayName()).thenReturn("site_key"); + when(site.getDisplayNameForFormatting()).thenReturn("site_key"); + when(site.getKey()).thenReturn("site_key"); + + new VoteRemindersListener(plugin).onCoolDownEnd(event); + + @SuppressWarnings("unchecked") + ArgumentCaptor> placeholders = ArgumentCaptor.forClass(Map.class); + verify(manager).onCooldownTrigger(eq(user), eq(VoteReminderType.COOLDOWN_END_ANY_SITE), placeholders.capture()); + HashMap values = new HashMap<>(placeholders.getValue()); + assertEquals("site_key", values.get("votesite")); + assertEquals("site_key", RewardDisplayPlaceholders.forDisplay(values).get("votesite")); + } +} diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java index 6c896e2baa..c9005e011c 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java @@ -1,6 +1,7 @@ package com.bencodez.votingplugin.tests.votesite; 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.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -15,6 +16,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.bencodez.advancedcore.api.rewards.RewardBuilder; +import com.bencodez.advancedcore.api.rewards.RewardDisplayPlaceholders; import com.bencodez.simpleapi.time.ParsedDuration; import com.bencodez.votingplugin.VotingPluginMain; import com.bencodez.votingplugin.config.Config; @@ -51,6 +54,20 @@ public void setUp() { when(plugin.getServerData()).thenReturn(dummyServerData); } + @Test + void legacyAutoCreatedFallbackIsVisibleAndInertWhileIdentityStaysExact() { + for (String name : new String[] { "&kSpoofed", "%player_name%", "R&D" }) { + when(plugin.getConfigVoteSites().getDisplayName(name)).thenReturn(""); + VoteSite site = new VoteSite(plugin, name); + site.setServiceSite(name); + assertEquals(name, site.getServiceSite()); + assertEquals(name, site.getDisplayName()); + assertFalse(site.getDisplayNameForFormatting().contains("&")); + assertFalse(site.getDisplayNameForFormatting().contains("%")); + assertFalse(site.getDisplayNameForFormatting().contains("\u2060")); + } + } + @Test public void testInitSetsValuesCorrectly() { VoteSite voteSite = new VoteSite(plugin, "site.test"); @@ -65,20 +82,47 @@ public void testInitSetsValuesCorrectly() { assertEquals(12L * 60L * 60L * 1000L, d.getMillis(), "VoteDelay millis should be 12h"); } - @Test - void ordinaryFallbackNameContainsNoFormattingGuardsAndIdentifierRemainsExact() { - when(plugin.getConfigVoteSites().getDisplayName(anyString())).thenReturn(""); - when(plugin.getConfigVoteSites().getServiceSite(anyString())).thenReturn("PlanetMinecraft.com"); - VoteSite site = new VoteSite(plugin, "PlanetMinecraft_com"); - assertEquals("PlanetMinecraft_com", site.getDisplayName()); - assertEquals("PlanetMinecraft.com", site.getServiceSite()); - } - @Test - void ownerConfiguredDisplayColorRemainsSupported() { - when(plugin.getConfigVoteSites().getDisplayName(anyString())).thenReturn("&aConfigured Name"); - VoteSite site = new VoteSite(plugin, "configured"); - assertEquals("&aConfigured Name", site.getDisplayName()); - } + @Test + public void serviceSiteFormattingCannotJoinSurroundingColorCodes() { + VoteSite voteSite = new VoteSite(plugin, "site.test"); + + assertEquals("ServiceSite", voteSite.getServiceSiteForFormatting().replace("\u2060", "")); + assertFalse(voteSite.getServiceSiteForFormatting().contains("\u2060")); + } + + @Test + public void rewardActionsReceiveTheExactValidatedServiceSite() throws Exception { + VoteSite voteSite = new VoteSite(plugin, "site.test"); + java.lang.reflect.Method factory = VoteSite.class.getDeclaredMethod("createRewardBuilder", String.class, + boolean.class, boolean.class); + factory.setAccessible(true); + + RewardBuilder builder = (RewardBuilder) factory.invoke(voteSite, "Rewards", true, false); + + assertEquals("ServiceSite", builder.getRewardOptions().getPlaceholders().get("ServiceSite")); + assertEquals("ServiceSite", RewardDisplayPlaceholders + .forDisplay(builder.getRewardOptions().getPlaceholders()).get("ServiceSite").replace("\u2060", "")); + } + + @Test + public void fallbackDisplayNameStaysRawForActionsAndGuardedForFormatting() throws Exception { + when(plugin.getConfigVoteSites().getDisplayName(anyString())).thenReturn(""); + VoteSite voteSite = new VoteSite(plugin, "site.test"); + java.lang.reflect.Method factory = VoteSite.class.getDeclaredMethod("createRewardBuilder", String.class, + boolean.class, boolean.class); + factory.setAccessible(true); + RewardBuilder builder = (RewardBuilder) factory.invoke(voteSite, "Rewards", true, false); + + assertEquals("site_test", voteSite.getDisplayName()); + assertEquals("site_test", builder.getRewardOptions().getPlaceholders().get("SiteName")); + assertEquals("site_test", voteSite.getDisplayNameForFormatting().replace("\u2060", "")); + assertFalse(voteSite.getDisplayNameForFormatting().contains("\u2060")); + assertEquals(voteSite.getDisplayNameForFormatting(), RewardDisplayPlaceholders + .forDisplay(builder.getRewardOptions().getPlaceholders()).get("SiteName")); + + voteSite.setDisplayName("Configured %server_name%"); + assertEquals("Configured %server_name%", voteSite.getDisplayNameForFormatting()); + } @Test public void testGetVoteURLJson() { diff --git a/docs/service-site-rendering.md b/docs/service-site-rendering.md index 34be666770..1e31de8521 100644 --- a/docs/service-site-rendering.md +++ b/docs/service-site-rendering.md @@ -1,27 +1,26 @@ # Voting-service identifiers and displayed names -VotingPlugin displays ordinary configured names directly, without WORD JOINER or -other invisible formatting guards. Existing administrator-authored `DisplayName` -colors remain supported. The exact accepted `ServiceSite` is still used for vote -matching; no escaping or rewriting is applied to that identifier. +Ordinary voting-site names display directly, without WORD JOINER or other +invisible characters. Administrator-authored display-name colors and placeholders +remain supported. The exact service identifier is preserved for matching, +action placeholders, and queued-vote recovery. -External vote-service identifiers must be bounded, visible names without -interpreter delimiters. `&`, `%`, the section-sign color character, braces, -quotes, brackets, backticks, backslashes and control/format characters are -rejected by the shared validator before vote scheduling or automatic site -creation. This applies to Bukkit Votifier ingress, backend/proxy messages, -NeoForge ingress and the existing validation of persisted ingress queues. -A rejected service does not receive vote credit or rewards. +Display-only values originating from external service identifiers or fallback +site keys use printable ASCII labels for interpreter delimiters: `&` becomes +`(ampersand)`, `%` becomes `(percent)`, `§` becomes `(section)`, and braces become +`(open brace)` / `(close brace)`. This avoids executing color/placeholder syntax +without relying on font support for hidden Unicode guards. For example, a legacy +`R&D` identifier matches exactly as before but its fallback display is +`R(ampersand)D`. An explicit trusted `DisplayName` can provide a preferred label. -Use a plain service identifier such as `PlanetMinecraft.com` or -`Minecraft Server List`. The service field is not the voting URL: encoded URLs -or query strings containing `%` or `&` are rejected too. Keep the clickable -voting URL in the site's `VoteURL` setting, whose URL behavior is unchanged. -These restrictions do not rewrite or migrate configured sites or saved player -records. A previously accepted delimiter-bearing service must be corrected at -its vote source before new votes from that source can be accepted. Malformed -persisted ingress queues retain their existing fail-closed recovery behavior; -no unsafe records are silently awarded or reinterpreted. +This also protects already-persisted auto-created sites. It does not rename site +keys or change reward action identifiers. Existing service-name acceptance and +persisted ingress journals remain compatible: names containing `&` or `%`, +including legacy URL-shaped identifiers, remain replayable. Structural delimiters +such as quotes, brackets, backticks and backslashes, control characters and +oversized identifiers retain their existing rejection rules. The same validation +applies before delayed-vote reward site/user lookup. `VoteURL` is unchanged. -This is an intentional ingress compatibility restriction instead of the old -invisible display guards. Live client rendering has not been visually verified. +No configuration or data migration is required. Minecraft client rendering of +the new printable labels has not been visually verified; automated tests cover +literal display values, trusted labels, queued-vote recovery and reward routing. From ee0110d665f71cfbeb0c27818b142865f0e38111 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:01:14 -0600 Subject: [PATCH 4/7] Preserve printable boundaries around external site display values --- .../util/ServiceSiteValidator.java | 9 ++++--- .../NeoForgeRewardReplayServiceTest.java | 8 +++--- .../tests/ServiceSiteValidatorTest.java | 27 +++++++++++++++++++ .../tests/votesite/VoteSiteTest.java | 8 +++--- docs/service-site-rendering.md | 7 +++-- 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java index e6765bec66..1e292fb8d3 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java @@ -66,11 +66,12 @@ public static String sanitizeForLog(String value) { } /** Display-only representation; raw identifiers remain authoritative for matching/actions. - * Uses printable ASCII labels rather than invisible or font-dependent characters. */ + * Uses printable ASCII labels and space boundaries rather than font-dependent guards. + * Boundaries prevent surrounding template text from completing interpreter tokens. */ public static String inertForFormatting(String value) { - if (value == null) return ""; - return value.replace("&", "(ampersand)").replace("%", "(percent)") - .replace("§", "(section)").replace("{", "(open brace)").replace("}", "(close brace)"); + if (value == null || value.isEmpty()) return ""; + return " " + value.replace("&", "(ampersand)").replace("%", "(percent)") + .replace("§", "(section)").replace("{", "(open brace)").replace("}", "(close brace)") + " "; } private static boolean isDisallowed(int codePoint) { diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java index 38dbff04ed..5b8c0a91d6 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/neoforge/NeoForgeRewardReplayServiceTest.java @@ -834,7 +834,7 @@ void playerMessageOnlyRewardIsSupported() throws Exception { runtime.scheduler().onServerTick(); assertEquals(NeoForgeRewardReplayService.Status.COMPLETED, completion.get(5, TimeUnit.SECONDS).get(0).status()); - assertEquals(List.of("Thanks Alex from Service at Supported Display"), actions.rendered); + assertEquals(List.of("Thanks Alex from Service at Supported Display"), actions.rendered); } } @@ -878,7 +878,7 @@ void playerMessageKeepsConfiguredServiceFormattingLiteral() throws Exception { assertEquals(NeoForgeRewardReplayService.Status.COMPLETED, runOne(runtime, replay, actions).status()); - assertEquals(List.of("Thanks Alex from (ampersand)aService"), actions.rendered); + assertEquals(List.of("Thanks Alex from (ampersand)aService "), actions.rendered); assertTrue(runtime.deferredVotes().pending(playerId).isEmpty()); } } @@ -900,7 +900,7 @@ void playerMessageKeepsServicePlaceholderTextInert() throws Exception { assertEquals(NeoForgeRewardReplayService.Status.COMPLETED, runOne(runtime, replay, actions).status()); - assertEquals(List.of("Thanks Alex from (percent)player(percent)"), actions.rendered); + assertEquals(List.of("Thanks Alex from (percent)player(percent) "), actions.rendered); assertTrue(runtime.deferredVotes().pending(playerId).isEmpty()); } } @@ -923,7 +923,7 @@ void playerMessageKeepsFallbackSiteNamePlaceholderTextInert() throws Exception { assertEquals(NeoForgeRewardReplayService.Status.COMPLETED, runOne(runtime, replay, actions).status()); - assertEquals(List.of("Thanks Alex from (percent)player(percent)"), actions.rendered); + assertEquals(List.of("Thanks Alex from (percent)player(percent) "), actions.rendered); assertTrue(runtime.deferredVotes().pending(playerId).isEmpty()); } } diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java index 708df532bf..1503be1233 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -1,10 +1,14 @@ package com.bencodez.votingplugin.tests; +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 java.util.concurrent.atomic.AtomicInteger; + import org.junit.jupiter.api.Test; +import com.bencodez.advancedcore.api.javascript.JavascriptTextTemplate; import com.bencodez.votingplugin.util.ServiceSiteValidator; class ServiceSiteValidatorTest { @@ -31,6 +35,29 @@ void acceptsLegacyFormattingIdentifiersWithoutChangingMatching() { } } + @Test + void formattingGuardPreventsTokensAcrossTemplateBoundaries() { + AtomicInteger executions = new AtomicInteger(); + for (String[] layout : new String[][] { + { "[%SiteName%]", "Javascript=1" }, + { "[Java%SiteName%]", "script=1" }, + { "[%SiteName%script=1]", "Java" } }) { + assertTrue(ServiceSiteValidator.isValid(layout[1])); + String rendered = layout[0].replace("%SiteName%", + ServiceSiteValidator.inertForFormatting(layout[1])); + assertEquals(rendered, JavascriptTextTemplate.parse(rendered).evaluate(text -> text, script -> { + executions.incrementAndGet(); + return "executed"; + })); + } + assertEquals(0, executions.get()); + assertEquals("& kSpoofed ", "&%SiteName%".replace("%SiteName%", + ServiceSiteValidator.inertForFormatting("kSpoofed"))); + assertEquals("% player_name %", "%" + ServiceSiteValidator.inertForFormatting("player_name") + "%"); + assertEquals("", ServiceSiteValidator.inertForFormatting(null)); + assertEquals("", ServiceSiteValidator.inertForFormatting("")); + } + @Test void rejectsUnsupportedCharacters() { for (String value : new String[] { "[Javascript=1]", "Site's", "\"Site\"", "Site`Name", diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java index c9005e011c..f0d92aabf2 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteTest.java @@ -86,7 +86,7 @@ public void testInitSetsValuesCorrectly() { public void serviceSiteFormattingCannotJoinSurroundingColorCodes() { VoteSite voteSite = new VoteSite(plugin, "site.test"); - assertEquals("ServiceSite", voteSite.getServiceSiteForFormatting().replace("\u2060", "")); + assertEquals(" ServiceSite ", voteSite.getServiceSiteForFormatting()); assertFalse(voteSite.getServiceSiteForFormatting().contains("\u2060")); } @@ -100,8 +100,8 @@ public void rewardActionsReceiveTheExactValidatedServiceSite() throws Exception RewardBuilder builder = (RewardBuilder) factory.invoke(voteSite, "Rewards", true, false); assertEquals("ServiceSite", builder.getRewardOptions().getPlaceholders().get("ServiceSite")); - assertEquals("ServiceSite", RewardDisplayPlaceholders - .forDisplay(builder.getRewardOptions().getPlaceholders()).get("ServiceSite").replace("\u2060", "")); + assertEquals(" ServiceSite ", RewardDisplayPlaceholders + .forDisplay(builder.getRewardOptions().getPlaceholders()).get("ServiceSite")); } @Test @@ -115,7 +115,7 @@ public void fallbackDisplayNameStaysRawForActionsAndGuardedForFormatting() throw assertEquals("site_test", voteSite.getDisplayName()); assertEquals("site_test", builder.getRewardOptions().getPlaceholders().get("SiteName")); - assertEquals("site_test", voteSite.getDisplayNameForFormatting().replace("\u2060", "")); + assertEquals(" site_test ", voteSite.getDisplayNameForFormatting()); assertFalse(voteSite.getDisplayNameForFormatting().contains("\u2060")); assertEquals(voteSite.getDisplayNameForFormatting(), RewardDisplayPlaceholders .forDisplay(builder.getRewardOptions().getPlaceholders()).get("SiteName")); diff --git a/docs/service-site-rendering.md b/docs/service-site-rendering.md index 1e31de8521..cecef9e003 100644 --- a/docs/service-site-rendering.md +++ b/docs/service-site-rendering.md @@ -1,7 +1,10 @@ # Voting-service identifiers and displayed names -Ordinary voting-site names display directly, without WORD JOINER or other -invisible characters. Administrator-authored display-name colors and placeholders +Ordinary voting-site names retain their text, without WORD JOINER or other +font-dependent guards. External display values have an ASCII space on each side +to prevent adjacent template text from completing a formatting token. For example, +`[%SiteName%]` renders `[ Javascript=1 ]` for the literal service `Javascript=1`, +rather than creating an executable rich-format token. Administrator-authored display-name colors and placeholders remain supported. The exact service identifier is preserved for matching, action placeholders, and queued-vote recovery. From 612f53f48d3811ab472ced97071db1efb8feca96 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:07:50 -0600 Subject: [PATCH 5/7] Keep configured legacy delay-rejection services rewardable --- .../messaging/BackendProxyMessageRouter.java | 11 +++++--- .../util/ServiceSiteValidator.java | 21 +++++++++++++-- .../BackendProxyMessageRouterTest.java | 27 +++++++++++++++++++ .../tests/ServiceSiteValidatorTest.java | 15 +++++++++++ docs/service-site-rendering.md | 6 +++-- 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java index 491eb36a0d..6a28d4f462 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java @@ -612,7 +612,10 @@ private WireVoteResult handleWireVoteDelayRejected(JsonEnvelope msg) { if (rejected.uuid.isEmpty() || rejected.service.isEmpty()) { return new WireVoteResult(rejected.voteId, true); } - if (!ServiceSiteValidator.isValid(rejected.service)) { + boolean configuredLegacy = !ServiceSiteValidator.isValid(rejected.service) + && ServiceSiteValidator.isValidConfiguredLegacyIdentifier(rejected.service) + && plugin.getVoteSiteManager().hasConfiguredVoteSite(rejected.service); + if (!ServiceSiteValidator.isValid(rejected.service) && !configuredLegacy) { plugin.getLogger().warning("Rejected VoteDelayRejected with invalid service site"); return new WireVoteResult(rejected.voteId, true); } @@ -624,8 +627,10 @@ private WireVoteResult handleWireVoteDelayRejected(JsonEnvelope msg) { + ServiceSiteValidator.sanitizeForLog(rejected.uuid)); return new WireVoteResult(rejected.voteId, true); } - VoteSite voteSite = plugin.getVoteSiteManager() - .getVoteSite(plugin.getVoteSiteManager().getVoteSiteName(true, rejected.service), true); + VoteSite voteSite = configuredLegacy + ? plugin.getVoteSiteManager().resolveVoteSite(rejected.service, true) + : plugin.getVoteSiteManager() + .getVoteSite(plugin.getVoteSiteManager().getVoteSiteName(true, rejected.service), true); if (voteSite == null) { plugin.getLogger().warning("No voting site with the service site: '" + ServiceSiteValidator.sanitizeForLog(rejected.service) + "'"); diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java index 1e292fb8d3..651e245523 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java @@ -19,6 +19,15 @@ private ServiceSiteValidator() { * contain unsupported delimiters */ public static boolean isValid(String serviceSite) { + return isValid(serviceSite, false); + } + + /** Bounded legacy identifier validation; only use for an already-configured site. */ + public static boolean isValidConfiguredLegacyIdentifier(String serviceSite) { + return isValid(serviceSite, true); + } + + private static boolean isValid(String serviceSite, boolean allowLegacyDelimiters) { if (serviceSite == null || serviceSite.length() > MAX_LENGTH) { return false; } @@ -26,7 +35,7 @@ public static boolean isValid(String serviceSite) { boolean hasVisibleCharacter = false; for (int offset = 0; offset < serviceSite.length();) { int codePoint = serviceSite.codePointAt(offset); - if (isDisallowed(codePoint)) { + if (isDisallowed(codePoint) && !(allowLegacyDelimiters && isLegacyDelimiter(codePoint))) { return false; } if (isVisibleBaseCharacter(codePoint)) { @@ -71,7 +80,15 @@ public static String sanitizeForLog(String value) { public static String inertForFormatting(String value) { if (value == null || value.isEmpty()) return ""; return " " + value.replace("&", "(ampersand)").replace("%", "(percent)") - .replace("§", "(section)").replace("{", "(open brace)").replace("}", "(close brace)") + " "; + .replace("§", "(section)").replace("{", "(open brace)").replace("}", "(close brace)") + .replace("[", "(open bracket)").replace("]", "(close bracket)") + .replace("\"", "(double quote)").replace("'", "(quote)") + .replace("`", "(backtick)").replace("\\", "(backslash)") + " "; + } + + private static boolean isLegacyDelimiter(int codePoint) { + return codePoint == '[' || codePoint == ']' || codePoint == '\'' || codePoint == '"' + || codePoint == '`' || codePoint == '\\'; } private static boolean isDisallowed(int codePoint) { diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java index d70194e865..7e00dc0457 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java @@ -1089,6 +1089,33 @@ void delayRejectionRejectsInterpreterDelimitersBeforeResolvingUsersOrSites() { verify(user, never()).cache(); } + @Test + void configuredLegacyDelayRejectionNamesKeepRewardsWithoutCreatingSites() { + for (String service : new String[] { "Old's Site", "[Old]", "Old`Site", "Old\\Site", "Old\"Site" }) { + for (boolean reliable : new boolean[] { false, true }) { + VoteSite site = configureDelayRejection(true); + when(voteSiteManager.hasConfiguredVoteSite(service)).thenReturn(true); + when(voteSiteManager.resolveVoteSite(service, true)).thenReturn(site); + UUID id = UUID.randomUUID(); + ProcessedVoteCache cache = mock(ProcessedVoteCache.class); + when(cache.reserveWithOutcome(id)).thenReturn(Reservation.RESERVED); + when(cache.complete(id)).thenReturn(true); + BackendProxyMessageRouter voteRouter = new BackendProxyMessageRouter(plugin, + mock(BackendPresenceManager.class), mock(BackendGlobalDataSync.class), + mock(BackendVotePartySync.class), cache); + JsonEnvelope message = VotingPluginWire.voteDelayRejected("Player", PLAYER_UUID.toString(), + service, true, id); + if (reliable) message = VotingPluginWire.requestVoteDeliveryAcknowledgement(message); + AtomicReference outcome = new AtomicReference<>(); + voteRouter.handleOrderedVote(message, outcome::set); + assertEquals(OrderedVoteOutcome.COMPLETE, outcome.get()); + verify(site).giveWaitUntilVoteDelayRewards(user, false, true); + verify(voteSiteManager, never()).getVoteSiteName(true, service); + verify(voteSiteManager, never()).getVoteSite(service, true); + } + } + } + @Test void reliableDelayRejectionExecutesOnceAndPersistsItsVoteId() { UUID voteId = UUID.randomUUID(); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java index 1503be1233..011227ddfc 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -58,6 +58,21 @@ void formattingGuardPreventsTokensAcrossTemplateBoundaries() { assertEquals("", ServiceSiteValidator.inertForFormatting("")); } + @Test + void legacyConfiguredValidationAllowsPunctuationButKeepsBoundsAndControlChecks() { + for (String service : new String[] { "Old's Site", "[Javascript=1]", "Old`Site", "Old\\Site" }) { + assertFalse(ServiceSiteValidator.isValid(service)); + assertTrue(ServiceSiteValidator.isValidConfiguredLegacyIdentifier(service)); + String display = ServiceSiteValidator.inertForFormatting(service); + assertFalse(display.contains("[") || display.contains("]") || display.contains("'") + || display.contains("`") || display.contains("\\")); + } + for (String service : new String[] { "bad\nsite", "bad\u2060site", "x".repeat(2049), " " }) { + assertFalse(ServiceSiteValidator.isValidConfiguredLegacyIdentifier(service)); + } + assertFalse(ServiceSiteValidator.isValidConfiguredLegacyIdentifier(null)); + } + @Test void rejectsUnsupportedCharacters() { for (String value : new String[] { "[Javascript=1]", "Site's", "\"Site\"", "Site`Name", diff --git a/docs/service-site-rendering.md b/docs/service-site-rendering.md index cecef9e003..92f6d51e52 100644 --- a/docs/service-site-rendering.md +++ b/docs/service-site-rendering.md @@ -21,8 +21,10 @@ keys or change reward action identifiers. Existing service-name acceptance and persisted ingress journals remain compatible: names containing `&` or `%`, including legacy URL-shaped identifiers, remain replayable. Structural delimiters such as quotes, brackets, backticks and backslashes, control characters and -oversized identifiers retain their existing rejection rules. The same validation -applies before delayed-vote reward site/user lookup. `VoteURL` is unchanged. +oversized identifiers retain their existing rejection rules. Delayed-vote rejection messages additionally accept bounded legacy punctuation +for an already-configured site, resolved without auto-creation; unknown malformed +names, control characters and oversized values remain rejected before user lookup. +Legacy brackets, quotes, backticks and backslashes use printable display labels too. `VoteURL` is unchanged. No configuration or data migration is required. Minecraft client rendering of the new printable labels has not been visually verified; automated tests cover From 67194213f931778733910eb56da6e682418e8879 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:14:53 -0600 Subject: [PATCH 6/7] Retain section-sign rejection for incoming service names --- .../bencodez/votingplugin/util/ServiceSiteValidator.java | 4 ++-- .../messaging/BackendProxyMessageRouterTest.java | 2 +- .../votingplugin/tests/ServiceSiteValidatorTest.java | 8 ++++---- .../tests/listeners/VotiferEventDisabledVoteSiteTest.java | 2 +- docs/service-site-rendering.md | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java index 651e245523..5515de38d4 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java @@ -88,12 +88,12 @@ public static String inertForFormatting(String value) { private static boolean isLegacyDelimiter(int codePoint) { return codePoint == '[' || codePoint == ']' || codePoint == '\'' || codePoint == '"' - || codePoint == '`' || codePoint == '\\'; + || codePoint == '`' || codePoint == '\\' || codePoint == '\u00A7'; } private static boolean isDisallowed(int codePoint) { if (codePoint == '[' || codePoint == ']' || codePoint == '\'' || codePoint == '"' || codePoint == '`' - || codePoint == '\\') { + || codePoint == '\\' || codePoint == '\u00A7') { return true; } diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java index 7e00dc0457..544ac90f80 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java @@ -1091,7 +1091,7 @@ void delayRejectionRejectsInterpreterDelimitersBeforeResolvingUsersOrSites() { @Test void configuredLegacyDelayRejectionNamesKeepRewardsWithoutCreatingSites() { - for (String service : new String[] { "Old's Site", "[Old]", "Old`Site", "Old\\Site", "Old\"Site" }) { + for (String service : new String[] { "Old's Site", "[Old]", "Old`Site", "Old\\Site", "Old\"Site", "§kOldSite" }) { for (boolean reliable : new boolean[] { false, true }) { VoteSite site = configureDelayRejection(true); when(voteSiteManager.hasConfiguredVoteSite(service)).thenReturn(true); diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java index 011227ddfc..30e67a9699 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -25,7 +25,7 @@ void acceptsCommonServiceSiteNames() { @Test void acceptsLegacyFormattingIdentifiersWithoutChangingMatching() { - for (String value : new String[] { "R&D", "&kSpoofed", "%player_name%", "§kSpoofed", "{RED}", + for (String value : new String[] { "R&D", "&kSpoofed", "%player_name%", "{RED}", "https://list.example/vote?id=1&source=proxy", "https://example.com/site%20name" }) { assertTrue(ServiceSiteValidator.isValid(value), value); String display = ServiceSiteValidator.inertForFormatting(value); @@ -60,12 +60,12 @@ void formattingGuardPreventsTokensAcrossTemplateBoundaries() { @Test void legacyConfiguredValidationAllowsPunctuationButKeepsBoundsAndControlChecks() { - for (String service : new String[] { "Old's Site", "[Javascript=1]", "Old`Site", "Old\\Site" }) { + for (String service : new String[] { "Old's Site", "[Javascript=1]", "Old`Site", "Old\\Site", "§kOldSite" }) { assertFalse(ServiceSiteValidator.isValid(service)); assertTrue(ServiceSiteValidator.isValidConfiguredLegacyIdentifier(service)); String display = ServiceSiteValidator.inertForFormatting(service); assertFalse(display.contains("[") || display.contains("]") || display.contains("'") - || display.contains("`") || display.contains("\\")); + || display.contains("`") || display.contains("\\") || display.contains("§")); } for (String service : new String[] { "bad\nsite", "bad\u2060site", "x".repeat(2049), " " }) { assertFalse(ServiceSiteValidator.isValidConfiguredLegacyIdentifier(service)); @@ -76,7 +76,7 @@ void legacyConfiguredValidationAllowsPunctuationButKeepsBoundsAndControlChecks() @Test void rejectsUnsupportedCharacters() { for (String value : new String[] { "[Javascript=1]", "Site's", "\"Site\"", "Site`Name", - "Site\\Name", "Site\nName", "Site\tName", "Site\u0000Name", "Site\u200BName" }) { + "§kSpoofed", "Site\\Name", "Site\nName", "Site\tName", "Site\u0000Name", "Site\u200BName" }) { assertFalse(ServiceSiteValidator.isValid(value), value); } } diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java index 67fe593d55..c43d21301e 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java @@ -106,7 +106,7 @@ private com.vexsoftware.votifier.model.VotifierEvent createVoteEvent(String serv @Test void rejectsStructurallyInvalidServiceNamesBeforeSchedulingOrAutomaticCreation() { when(plugin.getConfigFile().isAutoCreateVoteSites()).thenReturn(true); - for (String service : new String[] { "[Javascript=1]", "bad\\name", "bad\nname" }) { + for (String service : new String[] { "[Javascript=1]", "§kSpoofed", "bad\\name", "bad\nname" }) { listener.onVotiferEvent(createVoteEvent(service)); } verify(voteTimer, never()).submit(any(Runnable.class)); diff --git a/docs/service-site-rendering.md b/docs/service-site-rendering.md index 92f6d51e52..706511756a 100644 --- a/docs/service-site-rendering.md +++ b/docs/service-site-rendering.md @@ -20,7 +20,7 @@ This also protects already-persisted auto-created sites. It does not rename site keys or change reward action identifiers. Existing service-name acceptance and persisted ingress journals remain compatible: names containing `&` or `%`, including legacy URL-shaped identifiers, remain replayable. Structural delimiters -such as quotes, brackets, backticks and backslashes, control characters and +such as quotes, brackets, backticks and backslashes, section-sign formatting codes, control characters and oversized identifiers retain their existing rejection rules. Delayed-vote rejection messages additionally accept bounded legacy punctuation for an already-configured site, resolved without auto-creation; unknown malformed names, control characters and oversized values remain rejected before user lookup. From ca43cd2d3356e457f0798b09f8f5c47fa9dec8de Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:22:37 -0600 Subject: [PATCH 7/7] Match configured service identities for legacy delay rejection rewards --- .../messaging/BackendProxyMessageRouter.java | 16 +++++-- .../BackendProxyMessageRouterTest.java | 45 ++++++++++++++++++- docs/service-site-rendering.md | 4 +- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java index 6a28d4f462..ac24245af3 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouter.java @@ -612,9 +612,17 @@ private WireVoteResult handleWireVoteDelayRejected(JsonEnvelope msg) { if (rejected.uuid.isEmpty() || rejected.service.isEmpty()) { return new WireVoteResult(rejected.voteId, true); } - boolean configuredLegacy = !ServiceSiteValidator.isValid(rejected.service) - && ServiceSiteValidator.isValidConfiguredLegacyIdentifier(rejected.service) - && plugin.getVoteSiteManager().hasConfiguredVoteSite(rejected.service); + VoteSite legacySite = null; + if (!ServiceSiteValidator.isValid(rejected.service) + && ServiceSiteValidator.isValidConfiguredLegacyIdentifier(rejected.service)) { + for (VoteSite site : plugin.getVoteSiteManager().getVoteSites().toArray(new VoteSite[0])) { + if (site.isEnabled() && rejected.service.equalsIgnoreCase(site.getServiceSite())) { + legacySite = site; + break; + } + } + } + boolean configuredLegacy = legacySite != null; if (!ServiceSiteValidator.isValid(rejected.service) && !configuredLegacy) { plugin.getLogger().warning("Rejected VoteDelayRejected with invalid service site"); return new WireVoteResult(rejected.voteId, true); @@ -628,7 +636,7 @@ private WireVoteResult handleWireVoteDelayRejected(JsonEnvelope msg) { return new WireVoteResult(rejected.voteId, true); } VoteSite voteSite = configuredLegacy - ? plugin.getVoteSiteManager().resolveVoteSite(rejected.service, true) + ? legacySite : plugin.getVoteSiteManager() .getVoteSite(plugin.getVoteSiteManager().getVoteSiteName(true, rejected.service), true); if (voteSite == null) { diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java index 544ac90f80..a022fd41c1 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/backendproxy/messaging/BackendProxyMessageRouterTest.java @@ -1094,8 +1094,8 @@ void configuredLegacyDelayRejectionNamesKeepRewardsWithoutCreatingSites() { for (String service : new String[] { "Old's Site", "[Old]", "Old`Site", "Old\\Site", "Old\"Site", "§kOldSite" }) { for (boolean reliable : new boolean[] { false, true }) { VoteSite site = configureDelayRejection(true); - when(voteSiteManager.hasConfiguredVoteSite(service)).thenReturn(true); - when(voteSiteManager.resolveVoteSite(service, true)).thenReturn(site); + when(site.getServiceSite()).thenReturn(service); + when(voteSiteManager.getVoteSites()).thenReturn(java.util.List.of(site)); UUID id = UUID.randomUUID(); ProcessedVoteCache cache = mock(ProcessedVoteCache.class); when(cache.reserveWithOutcome(id)).thenReturn(Reservation.RESERVED); @@ -1116,6 +1116,47 @@ void configuredLegacyDelayRejectionNamesKeepRewardsWithoutCreatingSites() { } } + @Test + void legacyDelayRejectionIgnoresDisplayAliasesAndFindsTheActualService() { + for (boolean reliable : new boolean[] { false, true }) { + String service = "[Old]"; + VoteSite alias = configureDelayRejection(true); + when(alias.getDisplayName()).thenReturn(service); + when(alias.getServiceSite()).thenReturn("different.example"); + when(voteSiteManager.hasConfiguredVoteSite(service)).thenReturn(true); + when(voteSiteManager.resolveVoteSite(service, true)).thenReturn(alias); + when(voteSiteManager.getVoteSites()).thenReturn(java.util.List.of(alias)); + ProcessedVoteCache cache = mock(ProcessedVoteCache.class); + when(cache.complete(any(UUID.class))).thenReturn(true); + when(cache.reserveWithOutcome(any(UUID.class))).thenReturn(Reservation.RESERVED); + BackendProxyMessageRouter voteRouter = new BackendProxyMessageRouter(plugin, + mock(BackendPresenceManager.class), mock(BackendGlobalDataSync.class), + mock(BackendVotePartySync.class), cache); + JsonEnvelope message = VotingPluginWire.voteDelayRejected("Player", PLAYER_UUID.toString(), + service, true, UUID.randomUUID()); + if (reliable) message = VotingPluginWire.requestVoteDeliveryAcknowledgement(message); + AtomicReference outcome = new AtomicReference<>(); + voteRouter.handleOrderedVote(message, outcome::set); + assertEquals(OrderedVoteOutcome.COMPLETE, outcome.get()); + verify(alias, never()).giveWaitUntilVoteDelayRewards(user, false, true); + verify(user, never()).cache(); + + VoteSite actual = mock(VoteSite.class); + when(actual.isEnabled()).thenReturn(true); + when(actual.isWaitUntilVoteDelay()).thenReturn(true); + when(actual.getServiceSite()).thenReturn("[oLD]"); + when(voteSiteManager.getVoteSites()).thenReturn(java.util.List.of(alias, actual)); + JsonEnvelope actualMessage = VotingPluginWire.voteDelayRejected("Player", PLAYER_UUID.toString(), + service, true, UUID.randomUUID()); + if (reliable) actualMessage = VotingPluginWire.requestVoteDeliveryAcknowledgement(actualMessage); + voteRouter.handleOrderedVote(actualMessage, outcome::set); + assertEquals(OrderedVoteOutcome.COMPLETE, outcome.get()); + verify(actual).giveWaitUntilVoteDelayRewards(user, false, true); + verify(alias, never()).giveWaitUntilVoteDelayRewards(user, false, true); + org.mockito.Mockito.clearInvocations(user); + } + } + @Test void reliableDelayRejectionExecutesOnceAndPersistsItsVoteId() { UUID voteId = UUID.randomUUID(); diff --git a/docs/service-site-rendering.md b/docs/service-site-rendering.md index 706511756a..730a45e3f8 100644 --- a/docs/service-site-rendering.md +++ b/docs/service-site-rendering.md @@ -22,7 +22,9 @@ persisted ingress journals remain compatible: names containing `&` or `%`, including legacy URL-shaped identifiers, remain replayable. Structural delimiters such as quotes, brackets, backticks and backslashes, section-sign formatting codes, control characters and oversized identifiers retain their existing rejection rules. Delayed-vote rejection messages additionally accept bounded legacy punctuation -for an already-configured site, resolved without auto-creation; unknown malformed +for an enabled, loaded site whose configured `ServiceSite` matches (case-insensitively), +resolved without auto-creation. Display-name and YAML-key aliases alone cannot +authorize these legacy rejection rewards; unknown malformed names, control characters and oversized values remain rejected before user lookup. Legacy brackets, quotes, backticks and backslashes use printable display labels too. `VoteURL` is unchanged.