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 ba62bd67a8..ff9639b069 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 @@ -345,7 +345,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 void 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 bbc478ad7d..e422baa340 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 %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 1752560bdb..3939118ff6 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/user/VotingPluginUser.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java index 148eca1b6f..095eac7ea2 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java @@ -2652,7 +2652,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); @@ -2670,7 +2670,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 d28caa2bd9..44110fcdd4 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java @@ -7,6 +7,7 @@ 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() { } @@ -16,7 +17,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 structural formatting delimiters or control characters */ public static boolean isValid(String serviceSite) { if (serviceSite == null || serviceSite.length() > MAX_LENGTH) { @@ -65,9 +66,27 @@ 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 == '\\') { + || 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 d67ec83cad..903dfecebb 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,17 @@ 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 com.bencodez.votingplugin.util.ServiceSiteValidator; import lombok.Getter; import lombok.Setter; public class VoteSite { - @Getter - @Setter - private String displayName; + @Getter + private String displayName; + private boolean displayNameFallback; @Getter @Setter @@ -125,16 +126,10 @@ public String getVoteURLJsonStrip() { return url; } - public void giveRewards(VotingPluginUser user, boolean online, boolean bungee) { - new RewardBuilder(plugin.getConfigVoteSites().getData(), plugin.getConfigVoteSites().getEverySiteRewardPath()) - .setOnline(online).withPlaceHolder("ServiceSite", getServiceSite()) - .withPlaceHolder("SiteName", getDisplayName()).withPlaceHolder("VoteDelay", "" + getVoteDelay()) - .withPlaceHolder("VoteURL", getVoteURL()).setServer(bungee).send(user); - - new RewardBuilder(plugin.getConfigVoteSites().getData(), plugin.getConfigVoteSites().getRewardsPath(key)) - .setOnline(online).withPlaceHolder("ServiceSite", getServiceSite()) - .withPlaceHolder("SiteName", getDisplayName()).withPlaceHolder("VoteDelay", "" + getVoteDelay()) - .withPlaceHolder("VoteURL", getVoteURL()).setServer(bungee).send(user); + public void giveRewards(VotingPluginUser user, boolean online, boolean bungee) { + createRewardBuilder(plugin.getConfigVoteSites().getEverySiteRewardPath(), online, bungee).send(user); + + createRewardBuilder(plugin.getConfigVoteSites().getRewardsPath(key), online, bungee).send(user); } @@ -145,19 +140,43 @@ public void giveRewards(VotingPluginUser user, boolean online, boolean bungee) { * @param online whether the player was online when the vote was received * @param bungee whether the vote came through the proxy */ - public void giveWaitUntilVoteDelayRewards(VotingPluginUser user, boolean online, boolean bungee) { - new RewardBuilder(plugin.getConfigVoteSites().getData(), - plugin.getConfigVoteSites().getWaitUntilVoteDelayRewardsPath(key)).setOnline(online) - .withPlaceHolder("ServiceSite", getServiceSite()) - .withPlaceHolder("SiteName", getDisplayName()) - .withPlaceHolder("VoteDelay", "" + getVoteDelay()) - .withPlaceHolder("VoteURL", getVoteURL()).setServer(bungee).send(user); - } - - public boolean hasRewards() { + public void giveWaitUntilVoteDelayRewards(VotingPluginUser user, boolean online, boolean bungee) { + createRewardBuilder(plugin.getConfigVoteSites().getWaitUntilVoteDelayRewardsPath(key), online, bungee) + .send(user); + } + + 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); + } + + public boolean hasRewards() { return plugin.getRewardHandler().hasRewards(plugin.getConfigVoteSites().getData(), 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. @@ -168,10 +187,11 @@ 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); + displayNameFallback = displayName == null || displayName.equals(""); + if (displayNameFallback) { + 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 c2f74ee44d..0b5aa35ba7 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -1,5 +1,6 @@ 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; @@ -13,8 +14,14 @@ 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", - "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" }) { + "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" }) { assertTrue(ServiceSiteValidator.isValid(serviceSite), serviceSite); } } @@ -22,11 +29,25 @@ 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", + "{player}", "\u00A7kSpoofed", "\u00A7aGreen" }) { 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 new file mode 100644 index 0000000000..cbd73c802a --- /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("\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 9ae77591de..7700bf23c2 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,6 +15,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; @@ -65,6 +67,48 @@ 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");