From ccc1052272a27a01b775f125326e545d7e1c1b4a Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:56:02 -0600 Subject: [PATCH 1/6] Keep external vote service identifiers inert --- .../util/ServiceSiteValidator.java | 22 +++++++++++++++++-- .../tests/ServiceSiteValidatorTest.java | 5 +++-- 2 files changed, 23 insertions(+), 4 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 d28caa2bd9..827ff9b722 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,8 @@ 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 formatting or placeholder delimiters. Percent-encoded URL + * octets remain supported. */ public static boolean isValid(String serviceSite) { if (serviceSite == null || serviceSite.length() > MAX_LENGTH) { @@ -26,6 +27,14 @@ public static boolean isValid(String serviceSite) { boolean hasVisibleCharacter = false; for (int offset = 0; offset < serviceSite.length();) { int codePoint = serviceSite.codePointAt(offset); + if (codePoint == '%') { + if (!isPercentEncodedOctet(serviceSite, offset)) { + return false; + } + hasVisibleCharacter = true; + offset += 3; + continue; + } if (isDisallowed(codePoint)) { return false; } @@ -67,7 +76,7 @@ public static String sanitizeForLog(String value) { private static boolean isDisallowed(int codePoint) { if (codePoint == '[' || codePoint == ']' || codePoint == '\'' || codePoint == '"' || codePoint == '`' - || codePoint == '\\') { + || codePoint == '\\' || codePoint == '{' || codePoint == '}') { return true; } @@ -76,6 +85,15 @@ private static boolean isDisallowed(int codePoint) { || type == Character.PARAGRAPH_SEPARATOR || type == Character.SURROGATE; } + private static boolean isPercentEncodedOctet(String value, int offset) { + return offset + 2 < value.length() && isHexDigit(value.charAt(offset + 1)) + && isHexDigit(value.charAt(offset + 2)); + } + + private static boolean isHexDigit(char value) { + return value >= '0' && value <= '9' || value >= 'a' && value <= 'f' || value >= 'A' && value <= 'F'; + } + private static boolean isVisibleBaseCharacter(int codePoint) { if (Character.isWhitespace(codePoint) || Character.isSpaceChar(codePoint) || isDefaultIgnorable(codePoint)) { 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..8d150f8faf 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -13,7 +13,7 @@ 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!", + "https://example.com/search?q=site%20name%2Bnetwork", "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,8 @@ 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_name%", "%javascript_vote%", "{player}", "site%", "site%2", "site%GG" }) { assertFalse(ServiceSiteValidator.isValid(serviceSite), serviceSite); } } From 3b078dab3e46bda084be8474168ed8f4c408ce59 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:11:42 -0600 Subject: [PATCH 2/6] Reject ambiguous encoded placeholder delimiters --- .../votingplugin/util/ServiceSiteValidator.java | 13 ++++++++++--- .../tests/ServiceSiteValidatorTest.java | 5 +++-- 2 files changed, 13 insertions(+), 5 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 827ff9b722..1c5e89c835 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java @@ -16,8 +16,8 @@ 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 formatting or placeholder delimiters. Percent-encoded URL - * octets remain supported. + * contain formatting or placeholder delimiters. One percent-encoded + * URL octet remains supported where it cannot form a delimiter pair. */ public static boolean isValid(String serviceSite) { if (serviceSite == null || serviceSite.length() > MAX_LENGTH) { @@ -25,12 +25,19 @@ public static boolean isValid(String serviceSite) { } boolean hasVisibleCharacter = false; + boolean sawPercentEncodedOctet = false; for (int offset = 0; offset < serviceSite.length();) { int codePoint = serviceSite.codePointAt(offset); if (codePoint == '%') { - if (!isPercentEncodedOctet(serviceSite, offset)) { + // PlaceholderAPI treats any pair of percent signs as a possible + // placeholder boundary. Even individually valid URL octets such as + // "%be_secret%20" can therefore select an expansion after this value + // is inserted into a configured message. A single encoded octet is + // unambiguous; a second one is not safe at this trust boundary. + if (sawPercentEncodedOctet || !isPercentEncodedOctet(serviceSite, offset)) { return false; } + sawPercentEncodedOctet = true; hasVisibleCharacter = true; offset += 3; continue; 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 8d150f8faf..4c1cb1a0a5 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -13,7 +13,7 @@ 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%2Bnetwork", "site_name-2", "Site, Other; Network!", + "https://example.com/search?q=site%20name", "site_name-2", "Site, Other; Network!", "Serviço de votação", "Site\u00A0Name", "Site\uFE0F", "Cafe\u0301", "Site\u3164Name" }) { assertTrue(ServiceSiteValidator.isValid(serviceSite), serviceSite); } @@ -23,7 +23,8 @@ void acceptsCommonServiceSiteNames() { 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_name%", "%javascript_vote%", "{player}", "site%", "site%2", "site%GG" }) { + "%player_name%", "%javascript_vote%", "%be_secret%20", "%20name%2Bnetwork", + "%25player_name%25", "{player}", "site%", "site%2", "site%GG" }) { assertFalse(ServiceSiteValidator.isValid(serviceSite), serviceSite); } } From 574fabbb4fea2276717fb0b6a9fef984a056a622 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:51:51 -0600 Subject: [PATCH 3/6] Reject executable service formatting tokens --- .../util/ServiceSiteValidator.java | 35 +++++++------------ .../tests/ServiceSiteValidatorTest.java | 5 +-- 2 files changed, 16 insertions(+), 24 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 1c5e89c835..693cc5dad9 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java @@ -16,8 +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 formatting or placeholder delimiters. One percent-encoded - * URL octet remains supported where it cannot form a delimiter pair. + * contain formatting or placeholder delimiters */ public static boolean isValid(String serviceSite) { if (serviceSite == null || serviceSite.length() > MAX_LENGTH) { @@ -25,22 +24,16 @@ public static boolean isValid(String serviceSite) { } boolean hasVisibleCharacter = false; - boolean sawPercentEncodedOctet = false; for (int offset = 0; offset < serviceSite.length();) { int codePoint = serviceSite.codePointAt(offset); if (codePoint == '%') { - // PlaceholderAPI treats any pair of percent signs as a possible - // placeholder boundary. Even individually valid URL octets such as - // "%be_secret%20" can therefore select an expansion after this value - // is inserted into a configured message. A single encoded octet is - // unambiguous; a second one is not safe at this trust boundary. - if (sawPercentEncodedOctet || !isPercentEncodedOctet(serviceSite, offset)) { - return false; - } - sawPercentEncodedOctet = true; - hasVisibleCharacter = true; - offset += 3; - continue; + // The surrounding administrator template can supply the other percent + // delimiter, so even one encoded URL octet is unsafe here. + return false; + } + if (codePoint == '&' && offset + 1 < serviceSite.length() + && isLegacyColorCode(serviceSite.charAt(offset + 1))) { + return false; } if (isDisallowed(codePoint)) { return false; @@ -83,6 +76,7 @@ public static String sanitizeForLog(String value) { private static boolean isDisallowed(int codePoint) { if (codePoint == '[' || codePoint == ']' || codePoint == '\'' || codePoint == '"' || codePoint == '`' + || codePoint == '\u00A7' || codePoint == '\\' || codePoint == '{' || codePoint == '}') { return true; } @@ -92,13 +86,10 @@ private static boolean isDisallowed(int codePoint) { || type == Character.PARAGRAPH_SEPARATOR || type == Character.SURROGATE; } - private static boolean isPercentEncodedOctet(String value, int offset) { - return offset + 2 < value.length() && isHexDigit(value.charAt(offset + 1)) - && isHexDigit(value.charAt(offset + 2)); - } - - private static boolean isHexDigit(char value) { - return value >= '0' && value <= '9' || value >= 'a' && value <= 'f' || value >= 'A' && value <= 'F'; + private static boolean isLegacyColorCode(char value) { + char normalized = Character.toLowerCase(value); + return normalized >= '0' && normalized <= '9' || normalized >= 'a' && normalized <= 'f' + || normalized >= 'k' && normalized <= 'o' || normalized == 'r' || normalized == 'x'; } private static boolean isVisibleBaseCharacter(int codePoint) { 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 4c1cb1a0a5..6a57cf0182 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -13,7 +13,7 @@ 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", "site_name-2", "Site, Other; Network!", + "Research & Development", "site_name-2", "Site, Other; Network!", "Serviço de votação", "Site\u00A0Name", "Site\uFE0F", "Cafe\u0301", "Site\u3164Name" }) { assertTrue(ServiceSiteValidator.isValid(serviceSite), serviceSite); } @@ -24,7 +24,8 @@ 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_name%", "%javascript_vote%", "%be_secret%20", "%20name%2Bnetwork", - "%25player_name%25", "{player}", "site%", "site%2", "site%GG" }) { + "%25player_name%25", "{player}", "site%", "site%2", "site%GG", "site%20name", + "&kSpoofed", "&aGreen", "&xHex", "\u00A7kSpoofed", "\u00A7aGreen" }) { assertFalse(ServiceSiteValidator.isValid(serviceSite), serviceSite); } } From 6d55b77e411fad484526eee987c229672d5ccb4f Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:25:19 -0600 Subject: [PATCH 4/6] Preserve safe encoded service identifiers --- .../util/ServiceSiteValidator.java | 28 ++++++++++++++++--- .../tests/ServiceSiteValidatorTest.java | 11 +++++--- 2 files changed, 31 insertions(+), 8 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 693cc5dad9..3cfd4f103d 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java @@ -24,15 +24,22 @@ public static boolean isValid(String serviceSite) { } boolean hasVisibleCharacter = false; + int previousPercent = -1; for (int offset = 0; offset < serviceSite.length();) { int codePoint = serviceSite.codePointAt(offset); if (codePoint == '%') { - // The surrounding administrator template can supply the other percent - // delimiter, so even one encoded URL octet is unsafe here. - return false; + if (offset + 2 >= serviceSite.length() || !isHex(serviceSite.charAt(offset + 1)) + || !isHex(serviceSite.charAt(offset + 2))) return false; + // PlaceholderAPI identifiers require an expansion/parameter separator. + // Reject that form between otherwise valid URL escapes while keeping + // ordinary names with multiple encoded octets usable. + if (previousPercent >= 0 + && serviceSite.substring(previousPercent + 1, offset).indexOf('_') >= 0) return false; + previousPercent = offset; } if (codePoint == '&' && offset + 1 < serviceSite.length() - && isLegacyColorCode(serviceSite.charAt(offset + 1))) { + && (isLegacyColorCode(serviceSite.charAt(offset + 1)) + || isHexColor(serviceSite, offset))) { return false; } if (isDisallowed(codePoint)) { @@ -92,6 +99,19 @@ private static boolean isLegacyColorCode(char value) { || normalized >= 'k' && normalized <= 'o' || normalized == 'r' || normalized == 'x'; } + private static boolean isHexColor(String value, int ampersandOffset) { + if (ampersandOffset + 7 >= value.length() || value.charAt(ampersandOffset + 1) != '#') return false; + for (int index = ampersandOffset + 2; index < ampersandOffset + 8; index++) { + if (!isHex(value.charAt(index))) return false; + } + return true; + } + + private static boolean isHex(char value) { + return value >= '0' && value <= '9' || value >= 'a' && value <= 'f' + || value >= 'A' && value <= 'F'; + } + private static boolean isVisibleBaseCharacter(int codePoint) { if (Character.isWhitespace(codePoint) || Character.isSpaceChar(codePoint) || isDefaultIgnorable(codePoint)) { 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 6a57cf0182..a0764504ca 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -14,7 +14,9 @@ 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", "site_name-2", "Site, Other; Network!", - "Serviço de votação", "Site\u00A0Name", "Site\uFE0F", "Cafe\u0301", "Site\u3164Name" }) { + "site%20name", "site%2Bname", "site%20%2Bname", "site%20name%2Bnetwork", + "Serviço de votação", + "Site\u00A0Name", "Site\uFE0F", "Cafe\u0301", "Site\u3164Name" }) { assertTrue(ServiceSiteValidator.isValid(serviceSite), serviceSite); } } @@ -23,9 +25,10 @@ void acceptsCommonServiceSiteNames() { 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_name%", "%javascript_vote%", "%be_secret%20", "%20name%2Bnetwork", - "%25player_name%25", "{player}", "site%", "site%2", "site%GG", "site%20name", - "&kSpoofed", "&aGreen", "&xHex", "\u00A7kSpoofed", "\u00A7aGreen" }) { + "%player_name%", "%javascript_vote%", "%be_secret%20", + "%25player_name%25", "{player}", "site%", "site%2", "site%GG", + "&kSpoofed", "&aGreen", "&xHex", "&#ff0000Spoofed", "&#ABCDEFText", + "\u00A7kSpoofed", "\u00A7aGreen" }) { assertFalse(ServiceSiteValidator.isValid(serviceSite), serviceSite); } } From 1f988459cb253b80aa88649d46d79f2a24de54e6 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:46:20 -0600 Subject: [PATCH 5/6] Guard service names at formatting boundaries --- .../commands/gui/player/VoteURL.java | 8 +++---- .../commands/gui/player/VoteURLVoteSite.java | 3 ++- .../votingplugin/config/ConfigVoteSites.java | 3 ++- .../util/ServiceSiteValidator.java | 11 ++++++++++ .../votingplugin/votesites/VoteSite.java | 22 ++++++++++++------- .../tests/ServiceSiteValidatorTest.java | 10 +++++++++ .../tests/votesite/VoteSiteTest.java | 8 +++++++ 7 files changed, 51 insertions(+), 14 deletions(-) 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..94b56a5117 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 @@ -152,8 +152,8 @@ public void onDialog(Player player) { dialog.placeholder("sitename", voteSite.getDisplayName()) .placeholder("SiteName", voteSite.getDisplayName()) - .placeholder("servicesite", voteSite.getServiceSite()) - .placeholder("ServiceSite", voteSite.getServiceSite()) + .placeholder("servicesite", voteSite.getServiceSiteForFormatting()) + .placeholder("ServiceSite", voteSite.getServiceSiteForFormatting()) .placeholder("VoteDelay", "" + voteSite.getVoteDelay()) .placeholder("VoteHour", "" + voteSite.getVoteDelayDailyHour()) .placeholder("Next", finalUser.voteCommandNextInfo(voteSite)) @@ -168,7 +168,7 @@ public void onDialog(Player player) { placeholders.put("voteurl", voteSite.getVoteURL()); placeholders.put("sitename", voteSite.getDisplayName()); 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); @@ -344,7 +344,7 @@ public void onClick(ClickEvent event) { placeholders.put("voteurl", voteSite.getVoteURL()); placeholders.put("sitename", voteSite.getDisplayName()); 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..985158bd06 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 @@ -119,7 +119,8 @@ public void onDialog(Player player) { 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("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..4340ca32c3 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java @@ -83,11 +83,12 @@ private boolean tryGenerateVoteSite(String siteName, boolean automatic) { plugin.getLogger().warning("Creating VoteSite " + siteName + " for the service site '" + org + "'"); setEnabled(siteName, true); setServiceSite(siteName, org); + setDisplayName(siteName, ServiceSiteValidator.inertForFormatting(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/util/ServiceSiteValidator.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java index 3cfd4f103d..45702ec226 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() { } @@ -81,6 +82,16 @@ 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; + return FORMATTING_BOUNDARY + value + FORMATTING_BOUNDARY; + } + private static boolean isDisallowed(int codePoint) { if (codePoint == '[' || codePoint == ']' || codePoint == '\'' || codePoint == '"' || codePoint == '`' || codePoint == '\u00A7' 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..09c5ddbf12 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java @@ -10,8 +10,9 @@ 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; @@ -127,12 +128,12 @@ public String getVoteURLJsonStrip() { public void giveRewards(VotingPluginUser user, boolean online, boolean bungee) { new RewardBuilder(plugin.getConfigVoteSites().getData(), plugin.getConfigVoteSites().getEverySiteRewardPath()) - .setOnline(online).withPlaceHolder("ServiceSite", getServiceSite()) + .setOnline(online).withPlaceHolder("ServiceSite", getServiceSiteForFormatting()) .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()) + .setOnline(online).withPlaceHolder("ServiceSite", getServiceSiteForFormatting()) .withPlaceHolder("SiteName", getDisplayName()).withPlaceHolder("VoteDelay", "" + getVoteDelay()) .withPlaceHolder("VoteURL", getVoteURL()).setServer(bungee).send(user); @@ -148,16 +149,21 @@ public void giveRewards(VotingPluginUser user, boolean online, boolean bungee) { public void giveWaitUntilVoteDelayRewards(VotingPluginUser user, boolean online, boolean bungee) { new RewardBuilder(plugin.getConfigVoteSites().getData(), plugin.getConfigVoteSites().getWaitUntilVoteDelayRewardsPath(key)).setOnline(online) - .withPlaceHolder("ServiceSite", getServiceSite()) + .withPlaceHolder("ServiceSite", getServiceSiteForFormatting()) .withPlaceHolder("SiteName", getDisplayName()) .withPlaceHolder("VoteDelay", "" + getVoteDelay()) .withPlaceHolder("VoteURL", getVoteURL()).setServer(bungee).send(user); } - public boolean hasRewards() { + 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()); + } /** * Inits the. @@ -170,7 +176,7 @@ public void init() { setPriority(plugin.getConfigVoteSites().getPriority(key)); displayName = plugin.getConfigVoteSites().getDisplayName(key); if (displayName == null || displayName.equals("")) { - displayName = key; + displayName = ServiceSiteValidator.inertForFormatting(key); } item = plugin.getConfigVoteSites().getItem(key); voteDelayDaily = plugin.getConfigVoteSites().getVoteSiteResetVoteDelayDaily(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 a0764504ca..1af77b9198 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; @@ -33,6 +34,15 @@ void rejectsUnsupportedCharacters() { } } + @Test + void formattingGuardPreventsTokensAcrossTemplateBoundaries() { + String guarded = ServiceSiteValidator.inertForFormatting("kSpoofed&"); + + assertFalse(("&" + guarded + "a").contains("&k")); + assertFalse(("%" + guarded + "player_name%").contains("%kSpoofed&player_name%")); + assertEquals("kSpoofed&", guarded.replace("\u2060", "")); + } + @Test void rejectsMissingAndOversizedNames() { assertFalse(ServiceSiteValidator.isValid(null)); 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..6c6d02aeb1 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,14 @@ 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 testGetVoteURLJson() { VoteSite voteSite = new VoteSite(plugin, "site.test"); From 0000118d6ca9a85abe9f12d6c0fd02b0f647d2d4 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sat, 26 Sep 2026 23:11:45 -0600 Subject: [PATCH 6/6] Preserve service identifiers in reward actions --- .../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 | 18 ++--- .../commands/gui/player/VoteURLVoteSite.java | 6 +- .../votingplugin/config/ConfigVoteSites.java | 9 ++- .../votingplugin/cooldown/CoolDownCheck.java | 1 + .../listeners/PlayerVoteListener.java | 2 +- .../votingplugin/user/VotingPluginUser.java | 4 +- .../util/ServiceSiteValidator.java | 46 +++---------- .../votereminding/VoteRemindersListener.java | 5 +- .../votereminding/VoteRemindersManager.java | 8 ++- .../votingplugin/votesites/VoteSite.java | 64 ++++++++++------- .../tests/ServiceSiteValidatorTest.java | 19 ++++-- .../reminders/VoteRemindersListenerTest.java | 68 +++++++++++++++++++ .../tests/votesite/VoteSiteTest.java | 36 ++++++++++ 17 files changed, 215 insertions(+), 111 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 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 94b56a5117..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()) + 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,7 +166,7 @@ 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.getServiceSiteForFormatting()); placeholders.put("VoteDelay", "" + voteSite.getVoteDelay()); @@ -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,7 +342,7 @@ 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.getServiceSiteForFormatting()); placeholders.put("VoteDelay", "" + voteSite.getVoteDelay()); 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 985158bd06..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,8 +117,8 @@ 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("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()) 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 4340ca32c3..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,11 +80,10 @@ 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); - setDisplayName(siteName, ServiceSiteValidator.inertForFormatting(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); 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 45702ec226..44110fcdd4 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/util/ServiceSiteValidator.java @@ -17,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 formatting or placeholder delimiters + * contain structural formatting delimiters or control characters */ public static boolean isValid(String serviceSite) { if (serviceSite == null || serviceSite.length() > MAX_LENGTH) { @@ -25,24 +25,8 @@ public static boolean isValid(String serviceSite) { } boolean hasVisibleCharacter = false; - int previousPercent = -1; for (int offset = 0; offset < serviceSite.length();) { int codePoint = serviceSite.codePointAt(offset); - if (codePoint == '%') { - if (offset + 2 >= serviceSite.length() || !isHex(serviceSite.charAt(offset + 1)) - || !isHex(serviceSite.charAt(offset + 2))) return false; - // PlaceholderAPI identifiers require an expansion/parameter separator. - // Reject that form between otherwise valid URL escapes while keeping - // ordinary names with multiple encoded octets usable. - if (previousPercent >= 0 - && serviceSite.substring(previousPercent + 1, offset).indexOf('_') >= 0) return false; - previousPercent = offset; - } - if (codePoint == '&' && offset + 1 < serviceSite.length() - && (isLegacyColorCode(serviceSite.charAt(offset + 1)) - || isHexColor(serviceSite, offset))) { - return false; - } if (isDisallowed(codePoint)) { return false; } @@ -89,7 +73,14 @@ public static String sanitizeForLog(String value) { */ public static String inertForFormatting(String value) { if (value == null || value.isEmpty()) return value == null ? "" : value; - return FORMATTING_BOUNDARY + value + FORMATTING_BOUNDARY; + 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) { @@ -104,25 +95,6 @@ private static boolean isDisallowed(int codePoint) { || type == Character.PARAGRAPH_SEPARATOR || type == Character.SURROGATE; } - private static boolean isLegacyColorCode(char value) { - char normalized = Character.toLowerCase(value); - return normalized >= '0' && normalized <= '9' || normalized >= 'a' && normalized <= 'f' - || normalized >= 'k' && normalized <= 'o' || normalized == 'r' || normalized == 'x'; - } - - private static boolean isHexColor(String value, int ampersandOffset) { - if (ampersandOffset + 7 >= value.length() || value.charAt(ampersandOffset + 1) != '#') return false; - for (int index = ampersandOffset + 2; index < ampersandOffset + 8; index++) { - if (!isHex(value.charAt(index))) return false; - } - return true; - } - - private static boolean isHex(char value) { - return value >= '0' && value <= '9' || value >= 'a' && value <= 'f' - || value >= 'A' && value <= 'F'; - } - private static boolean isVisibleBaseCharacter(int codePoint) { if (Character.isWhitespace(codePoint) || Character.isSpaceChar(codePoint) || isDefaultIgnorable(codePoint)) { 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 09c5ddbf12..903dfecebb 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSite.java @@ -18,9 +18,9 @@ import lombok.Setter; public class VoteSite { - @Getter - @Setter - private String displayName; + @Getter + private String displayName; + private boolean displayNameFallback; @Getter @Setter @@ -126,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", getServiceSiteForFormatting()) - .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", getServiceSiteForFormatting()) - .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); } @@ -146,14 +140,22 @@ 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", getServiceSiteForFormatting()) - .withPlaceHolder("SiteName", getDisplayName()) - .withPlaceHolder("VoteDelay", "" + getVoteDelay()) - .withPlaceHolder("VoteURL", getVoteURL()).setServer(bungee).send(user); - } + 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(), @@ -164,6 +166,17 @@ public boolean hasRewards() { 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. @@ -174,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 = ServiceSiteValidator.inertForFormatting(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 1af77b9198..0b5aa35ba7 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/ServiceSiteValidatorTest.java @@ -14,8 +14,12 @@ 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", "site_name-2", "Site, Other; Network!", + "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); @@ -26,21 +30,22 @@ void acceptsCommonServiceSiteNames() { 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_name%", "%javascript_vote%", "%be_secret%20", - "%25player_name%25", "{player}", "site%", "site%2", "site%GG", - "&kSpoofed", "&aGreen", "&xHex", "&#ff0000Spoofed", "&#ABCDEFText", - "\u00A7kSpoofed", "\u00A7aGreen" }) { + "{player}", "\u00A7kSpoofed", "\u00A7aGreen" }) { assertFalse(ServiceSiteValidator.isValid(serviceSite), serviceSite); } } @Test void formattingGuardPreventsTokensAcrossTemplateBoundaries() { - String guarded = ServiceSiteValidator.inertForFormatting("kSpoofed&"); + 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%")); - assertEquals("kSpoofed&", guarded.replace("\u2060", "")); + assertFalse(guarded.contains("%player_name%")); + assertFalse(guarded.contains("&a")); + assertFalse(guarded.contains("&#ff0000")); + assertEquals(value, guarded.replace("\u2060", "")); } @Test 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 6c6d02aeb1..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; @@ -73,6 +75,40 @@ public void serviceSiteFormattingCannotJoinSurroundingColorCodes() { 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");