From ed186914604c8a6823b26c745808ff52291117c3 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 27 Aug 2026 18:50:58 -0600 Subject: [PATCH 1/6] Secure authored JavaScript text segments --- AdvancedCore/pom.xml | 9 + .../api/javascript/JavascriptEngine.java | 33 +- .../JavascriptPlaceholderBinder.java | 352 ++++++++++++++++++ .../JavascriptPlaceholderValue.java | 35 ++ .../javascript/JavascriptTextTemplate.java | 150 ++++++++ .../api/messages/PlaceholderUtils.java | 117 +++--- .../api/rewards/builtin/RewardJavascript.java | 12 +- .../requirements/RequirementJavascript.java | 4 +- .../JavascriptPlaceholderBinderTest.java | 100 +++++ .../JavascriptTextTemplateTest.java | 46 +++ ...laceholderUtilsJavascriptBoundaryTest.java | 47 +++ 11 files changed, 825 insertions(+), 80 deletions(-) create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptTextTemplate.java create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptTextTemplateTest.java create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsJavascriptBoundaryTest.java diff --git a/AdvancedCore/pom.xml b/AdvancedCore/pom.xml index a3f17cc16..0b4ec710e 100644 --- a/AdvancedCore/pom.xml +++ b/AdvancedCore/pom.xml @@ -116,6 +116,10 @@ ${project.groupId}.advancedcore.folialib + + org.mozilla.javascript + ${project.groupId}.advancedcore.rhino + @@ -231,6 +235,11 @@ 2.12.2 provided + + org.mozilla + rhino + 1.9.1 + org.slf4j slf4j-simple diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java index 2a784bdad..788ce5d18 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java @@ -1,6 +1,7 @@ package com.bencodez.advancedcore.api.javascript; import java.util.HashMap; +import java.util.Map; import java.util.Map.Entry; import javax.script.ScriptEngine; @@ -16,17 +17,20 @@ import com.bencodez.simpleapi.messages.MessageAPI; public class JavascriptEngine { - private HashMap engineAPI; + private final HashMap engineAPI; + private final HashMap placeholders; + private OfflinePlayer placeholderPlayer; public JavascriptEngine() { engineAPI = new HashMap<>(); + placeholders = new HashMap<>(); } public JavascriptEngine addPlayer(AdvancedCoreUser user) { + placeholderPlayer = user.getOfflinePlayer(); addToEngine("PlayerName", user.getPlayerName()); addToEngine("PlayerUUID", user.getUUID()); addToEngine("AdvancedCoreUser", user); - // addToEngine("CommandSender", player); for (JavascriptPlaceholderRequest request : AdvancedCorePlugin.getInstance().getJavascriptEngineRequests()) { addToEngine(request.getStr(), request.getObject(user.getOfflinePlayer())); @@ -42,6 +46,7 @@ public JavascriptEngine addPlayer(CommandSender player) { addToEngine("CommandSender", player); if (player instanceof Player) { Player p = (Player) player; + placeholderPlayer = p; addToEngine("Player", p); addToEngine("PlayerName", p.getName()); addToEngine("PlayerUUID", p.getUniqueId().toString()); @@ -58,6 +63,7 @@ public JavascriptEngine addPlayer(CommandSender player) { } public JavascriptEngine addPlayer(OfflinePlayer player) { + placeholderPlayer = player; addToEngine("Player", player); addToEngine("PlayerName", player.getName()); addToEngine("PlayerUUID", player.getUniqueId().toString()); @@ -76,6 +82,7 @@ public JavascriptEngine addPlayer(OfflinePlayer player) { public JavascriptEngine addPlayer(Player player) { if (player != null) { + placeholderPlayer = player; addToEngine("Player", player); addToEngine("PlayerName", player.getName()); addToEngine("PlayerUUID", player.getUniqueId().toString()); @@ -90,6 +97,13 @@ public JavascriptEngine addPlayer(Player player) { return this; } + public JavascriptEngine addPlaceholders(Map placeholders) { + if (placeholders != null && !placeholders.isEmpty()) { + this.placeholders.putAll(placeholders); + } + return this; + } + public JavascriptEngine addToEngine(HashMap engineAPI) { if (engineAPI != null && !engineAPI.isEmpty()) { this.engineAPI.putAll(engineAPI); @@ -119,7 +133,7 @@ public boolean getBooleanValue(String expression) { } public Object getResult(String expression) { - if (!expression.equals("")) { + if (expression != null && !expression.isEmpty()) { if (!AdvancedCorePlugin.getInstance().getOptions().isJavascriptEngineEnabled()) { return null; } @@ -128,6 +142,17 @@ public Object getResult(String expression) { AdvancedCorePlugin.getInstance().debug("Failed to process javascript, engine == null"); return null; } + + String preparedExpression; + try { + preparedExpression = JavascriptPlaceholderBinder.bind(expression, placeholderPlayer, placeholders, this); + } catch (IllegalArgumentException e) { + AdvancedCorePlugin.getInstance().getLogger() + .warning("Failed to safely prepare javascript placeholders: " + e.getMessage()); + AdvancedCorePlugin.getInstance().debug(e); + return null; + } + engine.put("Bukkit", Bukkit.getServer()); engine.put("AdvancedCore", AdvancedCorePlugin.getInstance()); engine.put("Console", Bukkit.getConsoleSender()); @@ -142,7 +167,7 @@ public Object getResult(String expression) { } try { - return engine.eval(expression); + return engine.eval(preparedExpression); } catch (ScriptException e) { AdvancedCorePlugin.getInstance().getLogger().warning( "Error occoured while evaluating javascript, turn debug on to see stacktrace: " + e.toString()); diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java new file mode 100644 index 000000000..be0d71f52 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -0,0 +1,352 @@ +package com.bencodez.advancedcore.api.javascript; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.bukkit.OfflinePlayer; +import org.mozilla.javascript.CompilerEnvirons; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Parser; +import org.mozilla.javascript.ast.AstNode; +import org.mozilla.javascript.ast.AstRoot; +import org.mozilla.javascript.ast.Comment; +import org.mozilla.javascript.ast.RegExpLiteral; +import org.mozilla.javascript.ast.StringLiteral; +import org.mozilla.javascript.ast.TemplateCharacters; + +import com.bencodez.advancedcore.AdvancedCorePlugin; + +import me.clip.placeholderapi.PlaceholderAPI; + +/** + * Resolves placeholders inside a known, operator-authored JavaScript segment. + * Dynamic values in executable expression context are engine bindings; values + * inside existing literals are escaped for that literal. Rhino supplies the AST + * classification, so AdvancedCore does not maintain a JavaScript lexer. + */ +public final class JavascriptPlaceholderBinder { + private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\s]+)%|(? placeholders, + JavascriptEngine engine) { + return bind(expression, token -> resolve(token, player, placeholders), + value -> resolvePapiValue(value, player), engine::addToEngine); + } + + static String bind(String expression, Function resolver, BiConsumer bindings) { + return bind(expression, resolver, Function.identity(), bindings); + } + + private static String bind(String expression, Function resolver, + Function decodedResolver, BiConsumer bindings) { + if (expression == null || expression.isEmpty()) { + return expression; + } + + Matcher matcher = PLACEHOLDER.matcher(expression); + List matches = new ArrayList<>(); + StringBuilder sanitized = new StringBuilder(expression); + while (matcher.find()) { + String token = matcher.group(); + String decoded = JavascriptPlaceholderValue.decode(token); + String value = decoded == null ? resolver.apply(token) : decodedResolver.apply(decoded); + matches.add(new PlaceholderMatch(matcher.start(), matcher.end(), token, value)); + + boolean bracePlaceholder = token.charAt(0) == '{'; + if (!bracePlaceholder || (value != null && !value.equals(token))) { + for (int i = matcher.start(); i < matcher.end(); i++) { + sanitized.setCharAt(i, 'p'); + } + } + } + if (matches.isEmpty()) { + return expression; + } + + JavascriptContexts contexts = JavascriptContexts.parse(sanitized.toString()); + String[] replacements = new String[matches.size()]; + int bindingIndex = 0; + for (int i = 0; i < matches.size(); i++) { + PlaceholderMatch match = matches.get(i); + if (contexts.containing(contexts.comments, match.start) != null) { + replacements[i] = match.token; + continue; + } + if (match.value == null || match.value.equals(match.token)) { + replacements[i] = match.token; + continue; + } + + Range regex = contexts.containing(contexts.regexes, match.start); + LiteralRange string = contexts.containingLiteral(match.start); + Range templateText = contexts.containing(contexts.templateText, match.start); + if (regex != null) { + replacements[i] = escapeRegex(match.value, expression, regex, match.start); + } else if (templateText != null) { + replacements[i] = escapeTemplate(match.value); + } else if (string != null) { + replacements[i] = escapeString(match.value, string.quote); + } else { + String variable = VARIABLE_PREFIX + bindingIndex++; + bindings.accept(variable, coerce(match.value)); + replacements[i] = variable; + } + } + + StringBuilder result = new StringBuilder(expression); + for (int i = matches.size() - 1; i >= 0; i--) { + PlaceholderMatch match = matches.get(i); + result.replace(match.start, match.end, replacements[i]); + } + return result.toString(); + } + + private static String resolve(String token, OfflinePlayer player, Map placeholders) { + if (placeholders != null) { + String name = token.substring(1, token.length() - 1); + for (Entry entry : placeholders.entrySet()) { + if (entry.getKey().equalsIgnoreCase(name)) { + return resolvePapiValue(entry.getValue(), player); + } + } + } + + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + if (token.startsWith("%") && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + String resolved = PlaceholderAPI.setPlaceholders(player, token); + if (resolved != null && !resolved.equals(token)) { + return resolved; + } + } + return token; + } + + private static String resolvePapiValue(String value, OfflinePlayer player) { + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + if (value != null && player != null && plugin != null && plugin.isPlaceHolderAPIEnabled()) { + String resolved = PlaceholderAPI.setPlaceholders(player, value); + if (resolved != null) { + return resolved; + } + } + return value; + } + + private static Object coerce(String value) { + if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + return Boolean.valueOf(value); + } + if (INTEGER.matcher(value).matches()) { + try { + return Long.valueOf(value); + } catch (NumberFormatException ignored) { + } + } + if (DECIMAL.matcher(value).matches()) { + try { + return Double.valueOf(value); + } catch (NumberFormatException ignored) { + } + } + return value; + } + + private static String escapeString(String value, char quote) { + StringBuilder result = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char current = value.charAt(i); + switch (current) { + case '\\': + result.append("\\\\"); + break; + case '\n': + result.append("\\n"); + break; + case '\r': + result.append("\\r"); + break; + case '\u2028': + result.append("\\u2028"); + break; + case '\u2029': + result.append("\\u2029"); + break; + default: + if (current == quote) { + result.append('\\'); + } + result.append(current); + break; + } + } + return result.toString(); + } + + private static String escapeTemplate(String value) { + return value.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${") + .replace("\r", "\\r").replace("\n", "\\n").replace("\u2028", "\\u2028") + .replace("\u2029", "\\u2029"); + } + + private static String escapeRegex(String value, String expression, Range regex, int placeholderStart) { + boolean characterClass = false; + boolean escaped = false; + for (int i = regex.start + 1; i < placeholderStart; i++) { + char current = expression.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + } else if (current == '[') { + characterClass = true; + } else if (current == ']') { + characterClass = false; + } + } + + String special = characterClass ? "\\/]^-" : "\\/.*+?^${}()|[]"; + StringBuilder result = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char current = value.charAt(i); + if (current == '\n') { + result.append("\\n"); + } else if (current == '\r') { + result.append("\\r"); + } else if (current == '\u2028') { + result.append("\\u2028"); + } else if (current == '\u2029') { + result.append("\\u2029"); + } else { + if (special.indexOf(current) >= 0) { + result.append('\\'); + } + result.append(current); + } + } + return result.toString(); + } + + private static final class PlaceholderMatch { + private final int start; + private final int end; + private final String token; + private final String value; + + private PlaceholderMatch(int start, int end, String token, String value) { + this.start = start; + this.end = end; + this.token = token; + this.value = value; + } + } + + private static class Range { + private final int start; + private final int end; + + private Range(int start, int end) { + this.start = start; + this.end = end; + } + + final boolean contains(int position) { + return position >= start && position < end; + } + } + + private static final class LiteralRange extends Range { + private final char quote; + + private LiteralRange(int start, int end, char quote) { + super(start, end); + this.quote = quote; + } + } + + private static final class JavascriptContexts { + private final List strings = new ArrayList<>(); + private final List regexes = new ArrayList<>(); + private final List templateText = new ArrayList<>(); + private final List comments = new ArrayList<>(); + + private static JavascriptContexts parse(String source) { + try { + CompilerEnvirons environs = new CompilerEnvirons(); + environs.setLanguageVersion(Context.VERSION_ES6); + environs.setRecordingComments(true); + environs.setRecordingLocalJsDocComments(true); + environs.setRecoverFromErrors(false); + + AstRoot root = new Parser(environs).parse(source, "AdvancedCore", 1); + JavascriptContexts contexts = new JavascriptContexts(); + root.visit(node -> { + int start = node.getAbsolutePosition(); + int end = start + node.getLength(); + if (node instanceof StringLiteral) { + contexts.strings.add(new LiteralRange(start, end, + ((StringLiteral) node).getQuoteCharacter())); + } else if (node instanceof RegExpLiteral) { + contexts.regexes.add(new Range(start, end)); + } else if (node instanceof TemplateCharacters) { + contexts.templateText.add(new Range(start, end)); + } + return true; + }); + + if (root.getComments() != null) { + for (Comment comment : root.getComments()) { + int start = comment.getAbsolutePosition(); + contexts.comments.add(new Range(start, start + comment.getLength())); + } + } + contexts.sort(); + return contexts; + } catch (RuntimeException e) { + throw new IllegalArgumentException( + "Unable to safely parse JavaScript containing placeholders: " + e.getMessage(), e); + } + } + + private Range containing(List ranges, int position) { + for (Range range : ranges) { + if (range.contains(position)) { + return range; + } + } + return null; + } + + private LiteralRange containingLiteral(int position) { + for (LiteralRange range : strings) { + if (range.contains(position)) { + return range; + } + } + return null; + } + + private void sort() { + Comparator comparator = Comparator.comparingInt(range -> range.start); + strings.sort(comparator); + regexes.sort(comparator); + templateText.sort(comparator); + comments.sort(comparator); + } + } +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java new file mode 100644 index 000000000..41b49eb8f --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java @@ -0,0 +1,35 @@ +package com.bencodez.advancedcore.api.javascript; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Carries custom placeholder output through an authored JavaScript segment as + * inert data until the JavaScript binder can bind or escape it safely. + */ +public final class JavascriptPlaceholderValue { + private static final String PREFIX = "%__advancedcore_bound_"; + private static final String SUFFIX = "%"; + + private JavascriptPlaceholderValue() { + } + + public static String encode(String value) { + String safeValue = value == null ? "" : value; + String encoded = Base64.getUrlEncoder().withoutPadding() + .encodeToString(safeValue.getBytes(StandardCharsets.UTF_8)); + return PREFIX + encoded + SUFFIX; + } + + static String decode(String token) { + if (token == null || !token.startsWith(PREFIX) || !token.endsWith(SUFFIX)) { + return null; + } + String encoded = token.substring(PREFIX.length(), token.length() - SUFFIX.length()); + try { + return new String(Base64.getUrlDecoder().decode(encoded), StandardCharsets.UTF_8); + } catch (IllegalArgumentException ignored) { + return null; + } + } +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptTextTemplate.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptTextTemplate.java new file mode 100644 index 000000000..4a438c5d6 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptTextTemplate.java @@ -0,0 +1,150 @@ +package com.bencodez.advancedcore.api.javascript; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Splits configured text into literal text and JavaScript segments before any + * placeholder replacement occurs. + *

+ * Only {@code [Javascript=...]} markers present in the original input become + * executable segments. Marker-looking text produced later by a placeholder or + * JavaScript result is neutralized and remains ordinary text. + */ +public final class JavascriptTextTemplate { + private static final String MARKER = "[Javascript="; + + private final List segments; + + private JavascriptTextTemplate(List segments) { + this.segments = segments; + } + + public static JavascriptTextTemplate parse(String source) { + List segments = new ArrayList<>(); + if (source == null || source.isEmpty()) { + segments.add(Segment.text(source)); + return new JavascriptTextTemplate(segments); + } + + int cursor = 0; + while (cursor < source.length()) { + int start = indexOfIgnoreCase(source, MARKER, cursor); + if (start < 0) { + segments.add(Segment.text(source.substring(cursor))); + break; + } + + if (start > cursor) { + segments.add(Segment.text(source.substring(cursor, start))); + } + + int bodyStart = start + MARKER.length(); + int end = source.indexOf(']', bodyStart); + if (end < 0) { + segments.add(Segment.text(source.substring(start))); + break; + } + + segments.add(Segment.javascript(source.substring(start, bodyStart), + source.substring(bodyStart, end))); + cursor = end + 1; + } + + if (segments.isEmpty()) { + segments.add(Segment.text(source)); + } + return new JavascriptTextTemplate(segments); + } + + /** + * Transforms literal and JavaScript content while preserving authored marker + * boundaries. + */ + public String transform(Function textTransformer, + Function javascriptTransformer) { + StringBuilder result = new StringBuilder(); + for (Segment segment : segments) { + if (segment.javascript) { + result.append(segment.markerPrefix) + .append(apply(javascriptTransformer, segment.value)) + .append(']'); + } else { + result.append(neutralizeGeneratedMarkers(apply(textTransformer, segment.value))); + } + } + return result.toString(); + } + + /** + * Evaluates authored JavaScript segments and returns plain text. Any marker + * produced by transformed text or a script result is neutralized so a second + * processing pass cannot execute it. + */ + public String evaluate(Function textTransformer, + Function javascriptEvaluator) { + StringBuilder result = new StringBuilder(); + for (Segment segment : segments) { + String value = segment.javascript + ? apply(javascriptEvaluator, segment.value) + : apply(textTransformer, segment.value); + result.append(value); + } + return neutralizeGeneratedMarkers(result.toString()); + } + + public static String neutralizeGeneratedMarkers(String text) { + if (text == null || text.isEmpty()) { + return text; + } + + StringBuilder result = new StringBuilder(text.length()); + int cursor = 0; + while (cursor < text.length()) { + int start = indexOfIgnoreCase(text, MARKER, cursor); + if (start < 0) { + result.append(text, cursor, text.length()); + break; + } + result.append(text, cursor, start + MARKER.length() - 1).append(" ="); + cursor = start + MARKER.length(); + } + return result.toString(); + } + + private static String apply(Function transformer, String value) { + String transformed = transformer == null ? value : transformer.apply(value); + return transformed == null ? "" : transformed; + } + + private static int indexOfIgnoreCase(String text, String target, int fromIndex) { + int max = text.length() - target.length(); + for (int i = Math.max(0, fromIndex); i <= max; i++) { + if (text.regionMatches(true, i, target, 0, target.length())) { + return i; + } + } + return -1; + } + + private static final class Segment { + private final boolean javascript; + private final String markerPrefix; + private final String value; + + private Segment(boolean javascript, String markerPrefix, String value) { + this.javascript = javascript; + this.markerPrefix = markerPrefix; + this.value = value; + } + + private static Segment text(String value) { + return new Segment(false, null, value == null ? "" : value); + } + + private static Segment javascript(String markerPrefix, String value) { + return new Segment(true, markerPrefix, value); + } + } +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java index 8e74bb82d..d29a421ef 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Map.Entry; +import java.util.function.Function; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; @@ -10,6 +11,8 @@ import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.javascript.JavascriptEngine; +import com.bencodez.advancedcore.api.javascript.JavascriptPlaceholderValue; +import com.bencodez.advancedcore.api.javascript.JavascriptTextTemplate; import com.bencodez.advancedcore.api.user.AdvancedCoreUser; import com.bencodez.simpleapi.messages.MessageAPI; @@ -238,49 +241,11 @@ public static String replaceJavascript(String text) { } public static String replaceJavascript(String text, JavascriptEngine engine) { - String msg = ""; - if (MessageAPI.containsIgnorecase(text, "[Javascript=")) { - if (engine == null) { - engine = new JavascriptEngine(); - } - int lastIndex = 0; - int startIndex = 0; - int num = 0; - while (startIndex != -1) { - startIndex = text.indexOf("[Javascript=", lastIndex); - - int endIndex = -1; - if (startIndex != -1) { - if (num != 0) { - msg += text.substring(lastIndex + 1, startIndex); - } else { - msg += text.substring(lastIndex, startIndex); - } - num++; - endIndex = text.indexOf("]", startIndex); - String str = text.substring(startIndex + "[Javascript=".length(), endIndex); - // plugin.debug(startIndex + ":" + endIndex + " from " + - // text + " to " + str + " currently " + msg); - String script = engine.getStringValue(str); - if (script == null) { - script = "" + engine.getBooleanValue(str); - - } - - if (script != null) { - msg += script; - } - lastIndex = endIndex; - } - - } - msg += text.substring(lastIndex + 1); - - } else { - msg = text; + if (text == null || text.isEmpty()) { + return text; } - // plugin.debug(msg); - return msg; + JavascriptEngine activeEngine = engine == null ? new JavascriptEngine() : engine; + return JavascriptTextTemplate.parse(text).evaluate(Function.identity(), activeEngine::getStringValue); } public static ArrayList replacePlaceHolder(ArrayList list, HashMap placeholders) { @@ -292,21 +257,16 @@ public static ArrayList replacePlaceHolder(ArrayList list, HashM } public static String replacePlaceHolder(String str, HashMap placeholders) { - if (placeholders != null) { - for (Entry entry : placeholders.entrySet()) { - str = replacePlaceHolder(str, entry.getKey(), entry.getValue()); - } - } - return str; + return replacePlaceHolder(str, placeholders, true); } public static String replacePlaceHolder(String str, HashMap placeholders, boolean ignoreCase) { - if (placeholders != null) { - for (Entry entry : placeholders.entrySet()) { - str = replacePlaceHolder(str, entry.getKey(), entry.getValue(), ignoreCase); - } + if (str == null || placeholders == null || placeholders.isEmpty()) { + return str; } - return str; + return JavascriptTextTemplate.parse(str).transform( + value -> replacePlaceHolderMapRaw(value, placeholders, ignoreCase), + value -> replacePlaceHolderMapEncoded(value, placeholders, ignoreCase)); } /** @@ -322,14 +282,44 @@ public static String replacePlaceHolder(String str, String toReplace, String rep } public static String replacePlaceHolder(String str, String toReplace, String replaceWith, boolean ignoreCase) { + if (str == null) { + return null; + } + return JavascriptTextTemplate.parse(str).transform( + value -> replacePlaceHolderRaw(value, toReplace, replaceWith, ignoreCase), + value -> replacePlaceHolderRaw(value, toReplace, JavascriptPlaceholderValue.encode(replaceWith), + ignoreCase)); + } + + private static String replacePlaceHolderMapRaw(String str, HashMap placeholders, + boolean ignoreCase) { + String result = str; + for (Entry entry : placeholders.entrySet()) { + result = replacePlaceHolderRaw(result, entry.getKey(), entry.getValue(), ignoreCase); + } + return result; + } + + private static String replacePlaceHolderMapEncoded(String str, HashMap placeholders, + boolean ignoreCase) { + String result = str; + for (Entry entry : placeholders.entrySet()) { + result = replacePlaceHolderRaw(result, entry.getKey(), JavascriptPlaceholderValue.encode(entry.getValue()), + ignoreCase); + } + return result; + } + + private static String replacePlaceHolderRaw(String str, String toReplace, String replaceWith, boolean ignoreCase) { + String safeReplacement = replaceWith == null ? "" : replaceWith; if (ignoreCase) { - return MessageAPI.replaceIgnoreCase(MessageAPI.replaceIgnoreCase(str, "%" + toReplace + "%", replaceWith), - "\\{" + toReplace + "\\}", replaceWith); + return MessageAPI.replaceIgnoreCase( + MessageAPI.replaceIgnoreCase(str, "%" + toReplace + "%", safeReplacement), + "\\{" + toReplace + "\\}", safeReplacement); } str = str.replaceAll("\\{", "%"); str = str.replaceAll("\\}", "%"); - str = str.replace("%" + toReplace + "%", replaceWith); - return str; + return str.replace("%" + toReplace + "%", safeReplacement); } public static ArrayList replacePlaceHolders(ArrayList list, Player p) { @@ -349,11 +339,12 @@ public static ArrayList replacePlaceHolders(OfflinePlayer player, ArrayL } public static String replacePlaceHolders(OfflinePlayer player, String text) { - if (player == null) { + if (player == null || text == null || text.isEmpty()) { return text; } if (AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) { - return PlaceholderAPI.setPlaceholders(player, text); + return JavascriptTextTemplate.parse(text).transform( + value -> PlaceholderAPI.setPlaceholders(player, value), Function.identity()); } return text; } @@ -366,13 +357,7 @@ public static String replacePlaceHolders(OfflinePlayer player, String text) { * @return the string */ public static String replacePlaceHolders(Player player, String text) { - if (player == null) { - return text; - } - if (AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) { - return PlaceholderAPI.setPlaceholders(player, text); - } - return text; + return replacePlaceHolders((OfflinePlayer) player, text); } } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java index 3af8e73a5..d80fcd9f6 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java @@ -14,7 +14,6 @@ import com.bencodez.advancedcore.api.inventory.editgui.valuetypes.EditGUIValueList; import com.bencodez.advancedcore.api.item.ItemBuilder; import com.bencodez.advancedcore.api.javascript.JavascriptEngine; -import com.bencodez.advancedcore.api.messages.PlaceholderUtils; import com.bencodez.advancedcore.api.rewards.DefinedReward; import com.bencodez.advancedcore.api.rewards.Reward; import com.bencodez.advancedcore.api.rewards.RewardBuilder; @@ -37,10 +36,10 @@ public static void register(RewardHandler handler, AdvancedCorePlugin plugin) { public String onRewardRequest(Reward reward, AdvancedCoreUser user, ArrayList list, HashMap placeholders) { if (!list.isEmpty()) { - JavascriptEngine engine = new JavascriptEngine().addPlayer(user.getOfflinePlayer()); + JavascriptEngine engine = new JavascriptEngine().addPlayer(user.getOfflinePlayer()) + .addPlaceholders(placeholders); for (String script : list) { - String expression = PlaceholderUtils.replacePlaceHolders(user.getOfflinePlayer(), script); - engine.execute(PlaceholderUtils.replacePlaceHolder(expression, placeholders)); + engine.execute(script); } } return null; @@ -61,9 +60,8 @@ public String onRewardRequested(Reward reward, AdvancedCoreUser user, Configurat HashMap placeholders) { if (section.getBoolean("Enabled")) { String expression = section.getString("Expression"); - expression = PlaceholderUtils.replacePlaceHolders(user.getOfflinePlayer(), expression); - if (new JavascriptEngine().addPlayer(user.getOfflinePlayer()) - .getBooleanValue(PlaceholderUtils.replacePlaceHolder(expression, placeholders))) { + if (new JavascriptEngine().addPlayer(user.getOfflinePlayer()).addPlaceholders(placeholders) + .getBooleanValue(expression)) { new RewardBuilder(section, "TrueRewards").withPrefix(reward.getName() + ".Javascript").send(user); } else { new RewardBuilder(section, "FalseRewards").withPrefix(reward.getName() + ".Javascript").send(user); diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java index 22be9c421..7bf479980 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java @@ -8,7 +8,6 @@ import com.bencodez.advancedcore.api.inventory.editgui.valuetypes.EditGUIValueString; import com.bencodez.advancedcore.api.item.ItemBuilder; import com.bencodez.advancedcore.api.javascript.JavascriptEngine; -import com.bencodez.advancedcore.api.messages.PlaceholderUtils; import com.bencodez.advancedcore.api.rewards.Reward; import com.bencodez.advancedcore.api.rewards.RewardEditData; import com.bencodez.advancedcore.api.rewards.RewardHandler; @@ -29,8 +28,7 @@ public static void register(RewardHandler handler, AdvancedCorePlugin plugin) { public boolean onRequirementsRequest(Reward reward, AdvancedCoreUser user, String expression, RewardOptions rewardOptions) { return expression.equals("") || new JavascriptEngine().addPlayer(user.getOfflinePlayer()) - .getBooleanValue(PlaceholderUtils.replacePlaceHolders(user.getOfflinePlayer(), - PlaceholderUtils.replacePlaceHolder(expression, rewardOptions.getPlaceholders()))); + .addPlaceholders(rewardOptions.getPlaceholders()).getBooleanValue(expression); } }.priority(90).addEditButton(new EditGUIButton(new ItemBuilder("DETECTOR_RAIL"), new EditGUIValueString("JavascriptExpression", null) { diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java new file mode 100644 index 000000000..236ebe299 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java @@ -0,0 +1,100 @@ +package com.bencodez.advancedcore.api.javascript; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +class JavascriptPlaceholderBinderTest { + + @Test + void barePlaceholderIsBoundAsData() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, 'op attacker')"; + + String prepared = JavascriptPlaceholderBinder.bind("%value% == true", ignored -> injection, bindings::put); + + assertEquals("__advancedCorePlaceholder0 == true", prepared); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + assertFalse(prepared.contains(injection)); + } + + @Test + void numericAndBooleanValuesKeepExpressionTypes() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("%count% > 1 && %allowed% == true", + token -> token.equals("%count%") ? "2.5" : "true", bindings::put); + + assertEquals("__advancedCorePlaceholder0 > 1 && __advancedCorePlaceholder1 == true", prepared); + assertEquals(2.5D, bindings.get("__advancedCorePlaceholder0")); + assertEquals(Boolean.TRUE, bindings.get("__advancedCorePlaceholder1")); + } + + @Test + void quotedAndEmbeddedPlaceholdersStayStrings() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("'%code%' === '001' && 'Hello %name%!' === 'Hello Ben!'", + token -> token.equals("%code%") ? "001" : "Ben", bindings::put); + + assertEquals("'001' === '001' && 'Hello Ben!' === 'Hello Ben!'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void stringInjectionIsEscaped() { + HashMap bindings = new HashMap<>(); + String injection = "'; Bukkit.dispatchCommand(Console, 'op attacker'); '"; + + String prepared = JavascriptPlaceholderBinder.bind("'%name%' == 'safe'", ignored -> injection, bindings::put); + + assertEquals("'\\'; Bukkit.dispatchCommand(Console, \\'op attacker\\'); \\'' == 'safe'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void templateTextAndExpressionUseDifferentContexts() { + HashMap bindings = new HashMap<>(); + String prepared = JavascriptPlaceholderBinder.bind("`Hello %name% ${%count% + 1}`", + token -> token.equals("%name%") ? "${attack()}`" : "2", bindings::put); + + assertEquals("`Hello \\${attack()}\\` ${__advancedCorePlaceholder0 + 1}`", prepared); + assertEquals(2L, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void regexPlaceholderIsEscapedAsLiteralPatternText() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("/^%name%$/i.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("/^Ben\\.\\*$/i.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void placeholdersInCommentsAreNotEvaluatedOrRewritten() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name; /* %name% */ true", + ignored -> "danger()", bindings::put); + + assertEquals("obj?.name; /* %name% */ true", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void invalidPlaceholderBearingJavascriptFailsClosed() { + HashMap bindings = new HashMap<>(); + + assertThrows(IllegalArgumentException.class, + () -> JavascriptPlaceholderBinder.bind("if ( %name%", ignored -> "Ben", bindings::put)); + assertTrue(bindings.isEmpty()); + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptTextTemplateTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptTextTemplateTest.java new file mode 100644 index 000000000..ea9eca6bb --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptTextTemplateTest.java @@ -0,0 +1,46 @@ +package com.bencodez.advancedcore.api.javascript; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; + +class JavascriptTextTemplateTest { + + @Test + void evaluatesOnlyMarkersPresentInOriginalText() { + String result = JavascriptTextTemplate.parse("A [Javascript=1 + 1] B") + .evaluate(value -> value, script -> "2"); + + assertEquals("A 2 B", result); + } + + @Test + void generatedMarkerFromTextRemainsText() { + String result = JavascriptTextTemplate.parse("%value%") + .evaluate(value -> value.replace("%value%", "[Javascript=danger()]"), script -> "executed"); + + assertEquals("[Javascript =danger()]", result); + assertFalse(result.contains("[Javascript=")); + } + + @Test + void generatedMarkerFromJavascriptResultCannotRunOnSecondPass() { + String first = JavascriptTextTemplate.parse("[Javascript=makeText()]") + .evaluate(value -> value, script -> "[Javascript=danger()]"); + String second = JavascriptTextTemplate.parse(first) + .evaluate(value -> value, script -> "executed"); + + assertEquals("[Javascript =danger()]", first); + assertEquals(first, second); + } + + @Test + void transformPreservesAuthoredBoundary() { + String result = JavascriptTextTemplate.parse("before %value% [Javascript='%value%'] after") + .transform(value -> value.replace("%value%", "text"), + script -> script.replace("%value%", "encoded")); + + assertEquals("before text [Javascript='encoded'] after", result); + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsJavascriptBoundaryTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsJavascriptBoundaryTest.java new file mode 100644 index 000000000..adc86acbc --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/PlaceholderUtilsJavascriptBoundaryTest.java @@ -0,0 +1,47 @@ +package com.bencodez.advancedcore.api.messages; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +class PlaceholderUtilsJavascriptBoundaryTest { + + @Test + void customPlaceholderCannotCreateJavascriptMarker() { + HashMap placeholders = new HashMap<>(); + placeholders.put("value", "[Javascript=Bukkit.dispatchCommand(Console,'op attacker')]"); + + String result = PlaceholderUtils.replacePlaceHolder("prefix %value%", placeholders); + + assertFalse(result.contains("[Javascript=")); + assertEquals("prefix [Javascript =Bukkit.dispatchCommand(Console,'op attacker')]", result); + } + + @Test + void multipleSubstitutionsCannotAssembleJavascriptMarker() { + HashMap placeholders = new HashMap<>(); + placeholders.put("part1", "Java"); + placeholders.put("part2", "script"); + + String result = PlaceholderUtils.replacePlaceHolder("[%part1%%part2%=danger]", placeholders); + + assertFalse(result.contains("[Javascript=")); + assertEquals("[Javascript =danger]", result); + } + + @Test + void customValuesInsideAuthoredMarkerAreEncodedAsData() { + HashMap placeholders = new HashMap<>(); + String injection = "'; Bukkit.dispatchCommand(Console,'op attacker'); '"; + placeholders.put("value", injection); + + String result = PlaceholderUtils.replacePlaceHolder("[Javascript='%value%']", placeholders); + + assertTrue(result.startsWith("[Javascript='%__advancedcore_bound_")); + assertFalse(result.contains(injection)); + } +} From 8c628d132842acd815a8fb4298602ea3842a81a0 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 28 Aug 2026 16:18:46 -0600 Subject: [PATCH 2/6] Add temporary #303 review-fix script --- .github/scripts/fix303_review_findings.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/scripts/fix303_review_findings.py diff --git a/.github/scripts/fix303_review_findings.py b/.github/scripts/fix303_review_findings.py new file mode 100644 index 000000000..bb0905a9d --- /dev/null +++ b/.github/scripts/fix303_review_findings.py @@ -0,0 +1,14 @@ +from pathlib import Path +import base64 +import zlib + +FILES = { + "AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java": "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", + "AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java": "eNrVmtuO2zYQhu/9FQMP2W2RDdplcRMEaIteC7Rosw+GojhlTSRJVSQHsNe+e8kqlmSHG0nJcSMP5HkcmXM+I6U//syI0ciLnbq+dQmsNAAdo6NXeoZgNCCxEMZO6G4J/wFOMCU0VUjGn+aYUgN6Lz6DXG1g6Ehn+xfLkEjdUwzho+SeRwKik3RZt78GzTfRz2MzfVC1zZ9gQy7iwT9ERj1+gV58apmsJy/fH8yb4wtfDvf2THNiRGyDr2zKeDJx3CdxTZ0xynBnBvI7OV44Mc/5QPBYKcgS0arqiYF5cQzsxBe+NeS7fA6K6xlZhbLw+jHlM6pQ8Qu6cVpjcc9kU6e+BUaewK3zZBfkzYTKkIDvLSwUZZFUE+gAbKgRoT5K+h9S7vVSyZDZrsTnAoaI84X+vFJo7bC1eKoPd+qww9VIWwzQv3/YRleBULUSFhDo4G+ExTmptAd3ij0n1NdrVrQHcqWUbWxPyKTF6ea50BzfSZhoAPd3QnA08NC3iafLo4I9jwLBQrHIV1wfZ+zj/kY7bgxM22u33ai9Xl2L3DpIOAwDbUsR0O5ffbp4pKeQzHmW8Fb/2pZ1KoIKdB7DE6diSZxeDptpC1yJD0oT3TdpgysqVQWpZeKBRUhVjS1vsIUG5MjxhDhXJf9PEb2faMY9Z6S2yL5YlApI0sct9w9obGN2LSXi+3IXGONxYM3IHy9aNsqLqr1Z02eqNLJauXKPGHx0Nymr5rcevgvyA9758rm0v+wG9IwYOrVqw+6sX6vHML6/VOw1Wzi3rbVY4L/Ku05aoH0I1q37Tmn3iWO48A1WMRquTxzv6Edp7SB7FRdfj+gdwne4CthkO2GhOSrlM7l/Rk+Ff+O7nrzbVngZ46vte04Hm4ylH5L1ZUao1ZhtdW3qhf8O1lvNdm8Bc3h3UH6w6nnTMOa/wVY5fkca6bZjF1VDNk3GdMt4GiuR3yT4/pjLPTeNA7OxD5is9bgzUycCmSWcXOn3cvVDfWRIJNKgj4kp9wfU9tJu5hpFc3olrs6kY7cIPzI1SyVLDm5u+pb/JrnPjxaElvRIqv8V+R5dJiQyraU5BX0qE+4uqcHHX9fCL1u8TXNgw/B5nPr0cy34cMY8WKrczNQnZ7cZa2cIHdZl5RRfaGG3Fd0XRFDGHt2xO5ncK+p23qP827jd/1gY05OeN0Ah71yGFxrvDP7Y61/GkdXuhsSjYcU3wzu7cPpOluEuEbzt9voaw3fNmu78p3G4I3oo+w0YRKEZ51FISueUj/Yfml6kuE7gHtL6v0e3uxPf+TtgAxyBSJ+N7Hg3GzYT0U6UsCqK6kpaWjYHnBrmhUx79h9tO4rXHr9l9IeL5q+//wgDTX/EymR4XqRSuKhnG3cuJDRmtaVYhWZ4Sre4vX1H6cNzE//K5Mvu4ZHKc7dLZd93nWc78DkY3G7T/vwBheDq1Ht7bcSbs9cuVYbo+fclw5bqCf9gPEx0W33P/PNpSZGf8H+gCK0Q==" +} + +for filename, payload in FILES.items(): + path = Path(filename) + if not path.exists(): + raise SystemExit(f"Expected file is missing: {filename}") + path.write_bytes(zlib.decompress(base64.b64decode(payload))) From ab0be58879c495981333b0a1e07166beb6dcc6e3 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 28 Aug 2026 16:19:10 -0600 Subject: [PATCH 3/6] Run validated fixes for PR #303 --- .github/workflows/fix-303-review-findings.yml | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/fix-303-review-findings.yml diff --git a/.github/workflows/fix-303-review-findings.yml b/.github/workflows/fix-303-review-findings.yml new file mode 100644 index 000000000..300a03196 --- /dev/null +++ b/.github/workflows/fix-303-review-findings.yml @@ -0,0 +1,39 @@ +name: Fix PR 303 review findings + +on: + push: + branches: + - security/structured-javascript-segments + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: security/structured-javascript-segments + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Apply review fixes + run: python .github/scripts/fix303_review_findings.py + + - name: Validate + run: mvn -B -f AdvancedCore/pom.xml package + + - name: Commit validated fixes and remove temporary files + run: | + rm .github/scripts/fix303_review_findings.py + rm .github/workflows/fix-303-review-findings.yml + git config user.name "BenCodez" + git config user.email "17074231+BenCodez@users.noreply.github.com" + git add -A + git commit -m "Fix JavaScript placeholder token and escape handling" + git push origin HEAD:security/structured-javascript-segments From 6769caa2e6f1797249a4ea94c339df1a2165c036 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 28 Aug 2026 20:48:59 -0600 Subject: [PATCH 4/6] Fix JavaScript placeholder parsing and escape handling --- .github/scripts/fix303_review_findings.py | 14 --- .github/workflows/fix-303-review-findings.yml | 39 -------- .../JavascriptPlaceholderBinder.java | 55 ++++++++--- .../JavascriptPlaceholderBinderTest.java | 95 +++++++++++++++++++ 4 files changed, 136 insertions(+), 67 deletions(-) delete mode 100644 .github/scripts/fix303_review_findings.py delete mode 100644 .github/workflows/fix-303-review-findings.yml diff --git a/.github/scripts/fix303_review_findings.py b/.github/scripts/fix303_review_findings.py deleted file mode 100644 index bb0905a9d..000000000 --- a/.github/scripts/fix303_review_findings.py +++ /dev/null @@ -1,14 +0,0 @@ -from pathlib import Path -import base64 -import zlib - -FILES = { - "AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java": "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", - "AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java": "eNrVmtuO2zYQhu/9FQMP2W2RDdplcRMEaIteC7Rosw+GojhlTSRJVSQHsNe+e8kqlmSHG0nJcSMP5HkcmXM+I6U//syI0ciLnbq+dQmsNAAdo6NXeoZgNCCxEMZO6G4J/wFOMCU0VUjGn+aYUgN6Lz6DXG1g6Ehn+xfLkEjdUwzho+SeRwKik3RZt78GzTfRz2MzfVC1zZ9gQy7iwT9ERj1+gV58apmsJy/fH8yb4wtfDvf2THNiRGyDr2zKeDJx3CdxTZ0xynBnBvI7OV44Mc/5QPBYKcgS0arqiYF5cQzsxBe+NeS7fA6K6xlZhbLw+jHlM6pQ8Qu6cVpjcc9kU6e+BUaewK3zZBfkzYTKkIDvLSwUZZFUE+gAbKgRoT5K+h9S7vVSyZDZrsTnAoaI84X+vFJo7bC1eKoPd+qww9VIWwzQv3/YRleBULUSFhDo4G+ExTmptAd3ij0n1NdrVrQHcqWUbWxPyKTF6ea50BzfSZhoAPd3QnA08NC3iafLo4I9jwLBQrHIV1wfZ+zj/kY7bgxM22u33ai9Xl2L3DpIOAwDbUsR0O5ffbp4pKeQzHmW8Fb/2pZ1KoIKdB7DE6diSZxeDptpC1yJD0oT3TdpgysqVQWpZeKBRUhVjS1vsIUG5MjxhDhXJf9PEb2faMY9Z6S2yL5YlApI0sct9w9obGN2LSXi+3IXGONxYM3IHy9aNsqLqr1Z02eqNLJauXKPGHx0Nymr5rcevgvyA9758rm0v+wG9IwYOrVqw+6sX6vHML6/VOw1Wzi3rbVY4L/Ku05aoH0I1q37Tmn3iWO48A1WMRquTxzv6Edp7SB7FRdfj+gdwne4CthkO2GhOSrlM7l/Rk+Ff+O7nrzbVngZ46vte04Hm4ylH5L1ZUao1ZhtdW3qhf8O1lvNdm8Bc3h3UH6w6nnTMOa/wVY5fkca6bZjF1VDNk3GdMt4GiuR3yT4/pjLPTeNA7OxD5is9bgzUycCmSWcXOn3cvVDfWRIJNKgj4kp9wfU9tJu5hpFc3olrs6kY7cIPzI1SyVLDm5u+pb/JrnPjxaElvRIqv8V+R5dJiQyraU5BX0qE+4uqcHHX9fCL1u8TXNgw/B5nPr0cy34cMY8WKrczNQnZ7cZa2cIHdZl5RRfaGG3Fd0XRFDGHt2xO5ncK+p23qP827jd/1gY05OeN0Ah71yGFxrvDP7Y61/GkdXuhsSjYcU3wzu7cPpOluEuEbzt9voaw3fNmu78p3G4I3oo+w0YRKEZ51FISueUj/Yfml6kuE7gHtL6v0e3uxPf+TtgAxyBSJ+N7Hg3GzYT0U6UsCqK6kpaWjYHnBrmhUx79h9tO4rXHr9l9IeL5q+//wgDTX/EymR4XqRSuKhnG3cuJDRmtaVYhWZ4Sre4vX1H6cNzE//K5Mvu4ZHKc7dLZd93nWc78DkY3G7T/vwBheDq1Ht7bcSbs9cuVYbo+fclw5bqCf9gPEx0W33P/PNpSZGf8H+gCK0Q==" -} - -for filename, payload in FILES.items(): - path = Path(filename) - if not path.exists(): - raise SystemExit(f"Expected file is missing: {filename}") - path.write_bytes(zlib.decompress(base64.b64decode(payload))) diff --git a/.github/workflows/fix-303-review-findings.yml b/.github/workflows/fix-303-review-findings.yml deleted file mode 100644 index 300a03196..000000000 --- a/.github/workflows/fix-303-review-findings.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Fix PR 303 review findings - -on: - push: - branches: - - security/structured-javascript-segments - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: security/structured-javascript-segments - - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '21' - cache: maven - - - name: Apply review fixes - run: python .github/scripts/fix303_review_findings.py - - - name: Validate - run: mvn -B -f AdvancedCore/pom.xml package - - - name: Commit validated fixes and remove temporary files - run: | - rm .github/scripts/fix303_review_findings.py - rm .github/workflows/fix-303-review-findings.yml - git config user.name "BenCodez" - git config user.email "17074231+BenCodez@users.noreply.github.com" - git add -A - git commit -m "Fix JavaScript placeholder token and escape handling" - git push origin HEAD:security/structured-javascript-segments diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index be0d71f52..536ded44a 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -14,7 +14,6 @@ import org.mozilla.javascript.CompilerEnvirons; import org.mozilla.javascript.Context; import org.mozilla.javascript.Parser; -import org.mozilla.javascript.ast.AstNode; import org.mozilla.javascript.ast.AstRoot; import org.mozilla.javascript.ast.Comment; import org.mozilla.javascript.ast.RegExpLiteral; @@ -32,7 +31,10 @@ * classification, so AdvancedCore does not maintain a JavaScript lexer. */ public final class JavascriptPlaceholderBinder { - private static final Pattern PLACEHOLDER = Pattern.compile("%([^%\\s]+)%|(? resolver, Matcher matcher = PLACEHOLDER.matcher(expression); List matches = new ArrayList<>(); StringBuilder sanitized = new StringBuilder(expression); - while (matcher.find()) { + int searchFrom = 0; + while (searchFrom < expression.length() && matcher.find(searchFrom)) { String token = matcher.group(); String decoded = JavascriptPlaceholderValue.decode(token); String value = decoded == null ? resolver.apply(token) : decodedResolver.apply(decoded); - matches.add(new PlaceholderMatch(matcher.start(), matcher.end(), token, value)); - boolean bracePlaceholder = token.charAt(0) == '{'; - if (!bracePlaceholder || (value != null && !value.equals(token))) { - for (int i = matcher.start(); i < matcher.end(); i++) { - sanitized.setCharAt(i, 'p'); - } + // Percent signs are also JavaScript modulo operators, and brace-delimited + // text may be an object or block. Do not parse or rewrite a candidate that + // the configured placeholder sources did not actually resolve. + if (value == null || (decoded == null && value.equals(token))) { + // Retry after this opening delimiter instead of after the candidate's + // closing delimiter. An unresolved modulo-shaped candidate can overlap + // the opening percent of a real placeholder later in the expression. + searchFrom = matcher.start() + 1; + continue; } + matches.add(new PlaceholderMatch(matcher.start(), matcher.end(), token, value)); + for (int i = matcher.start(); i < matcher.end(); i++) { + sanitized.setCharAt(i, 'p'); + } + searchFrom = matcher.end(); } if (matches.isEmpty()) { return expression; @@ -79,26 +90,27 @@ private static String bind(String expression, Function resolver, JavascriptContexts contexts = JavascriptContexts.parse(sanitized.toString()); String[] replacements = new String[matches.size()]; + int[] replacementStarts = new int[matches.size()]; int bindingIndex = 0; for (int i = 0; i < matches.size(); i++) { PlaceholderMatch match = matches.get(i); + replacementStarts[i] = match.start; if (contexts.containing(contexts.comments, match.start) != null) { replacements[i] = match.token; continue; } - if (match.value == null || match.value.equals(match.token)) { - replacements[i] = match.token; - continue; - } Range regex = contexts.containing(contexts.regexes, match.start); LiteralRange string = contexts.containingLiteral(match.start); Range templateText = contexts.containing(contexts.templateText, match.start); if (regex != null) { + replacementStarts[i] = literalReplacementStart(expression, match.start); replacements[i] = escapeRegex(match.value, expression, regex, match.start); } else if (templateText != null) { + replacementStarts[i] = literalReplacementStart(expression, match.start); replacements[i] = escapeTemplate(match.value); } else if (string != null) { + replacementStarts[i] = literalReplacementStart(expression, match.start); replacements[i] = escapeString(match.value, string.quote); } else { String variable = VARIABLE_PREFIX + bindingIndex++; @@ -110,11 +122,26 @@ private static String bind(String expression, Function resolver, StringBuilder result = new StringBuilder(expression); for (int i = matches.size() - 1; i >= 0; i--) { PlaceholderMatch match = matches.get(i); - result.replace(match.start, match.end, replacements[i]); + result.replace(replacementStarts[i], match.end, replacements[i]); } return result.toString(); } + /** + * An odd authored backslash immediately before a placeholder already escapes + * the placeholder's first character. Consume that pending escape before + * inserting a separately escaped value. Otherwise the authored slash and the + * value's leading escape can pair off and reactivate a quote, template + * interpolation, or regex delimiter. + */ + private static int literalReplacementStart(String expression, int placeholderStart) { + int slashRunStart = placeholderStart; + while (slashRunStart > 0 && expression.charAt(slashRunStart - 1) == '\\') { + slashRunStart--; + } + return ((placeholderStart - slashRunStart) & 1) == 1 ? placeholderStart - 1 : placeholderStart; + } + private static String resolve(String token, OfflinePlayer player, Map placeholders) { if (placeholders != null) { String name = token.substring(1, token.length() - 1); diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java index 236ebe299..b22af5a01 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java @@ -35,6 +35,59 @@ void numericAndBooleanValuesKeepExpressionTypes() { assertEquals(Boolean.TRUE, bindings.get("__advancedCorePlaceholder1")); } + @Test + void ordinaryCompactModuloExpressionIsNotTreatedAsAPlaceholder() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("10%3%2", token -> token, bindings::put); + + assertEquals("10%3%2", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void unresolvedPercentTokenRemainsUntouched() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("'%unknown%'", token -> token, bindings::put); + + assertEquals("'%unknown%'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void compactModuloCanAppearBesideAResolvedPlaceholder() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("10%3%2 + %value%", + token -> token.equals("%value%") ? "4" : token, bindings::put); + + assertEquals("10%3%2 + __advancedCorePlaceholder0", prepared); + assertEquals(4L, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void moduloCandidateCannotHideAnOverlappingResolvedPlaceholder() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("10%3 == 1 && %value% == true", + token -> token.equals("%value%") ? "true" : token, bindings::put); + + assertEquals("10%3 == 1 && __advancedCorePlaceholder0 == true", prepared); + assertEquals(Boolean.TRUE, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void placeholderApiStyleTokenMayContainWhitespace() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("%exp_argument with spaces% == true", + ignored -> "true", bindings::put); + + assertEquals("__advancedCorePlaceholder0 == true", prepared); + assertEquals(Boolean.TRUE, bindings.get("__advancedCorePlaceholder0")); + } + @Test void quotedAndEmbeddedPlaceholdersStayStrings() { HashMap bindings = new HashMap<>(); @@ -67,6 +120,37 @@ void templateTextAndExpressionUseDifferentContexts() { assertEquals(2L, bindings.get("__advancedCorePlaceholder0")); } + @Test + void oddBackslashBeforeTemplatePlaceholderCannotReactivateInterpolation() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("`\\%value%`", ignored -> "${attack()}", bindings::put); + + assertEquals("`\\${attack()}`", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void evenBackslashesBeforeTemplatePlaceholderRemainLiteralAndSafe() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("`\\\\%value%`", ignored -> "${attack()}", bindings::put); + + assertEquals("`\\\\\\${attack()}`", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void oddBackslashBeforeStringPlaceholderCannotReactivateQuote() { + HashMap bindings = new HashMap<>(); + String injection = "'; attack(); '"; + + String prepared = JavascriptPlaceholderBinder.bind("'\\%value%'", ignored -> injection, bindings::put); + + assertEquals("'\\'; attack(); \\''", prepared); + assertTrue(bindings.isEmpty()); + } + @Test void regexPlaceholderIsEscapedAsLiteralPatternText() { HashMap bindings = new HashMap<>(); @@ -78,6 +162,17 @@ void regexPlaceholderIsEscapedAsLiteralPatternText() { assertTrue(bindings.isEmpty()); } + @Test + void oddBackslashBeforeRegexPlaceholderCannotReactivateDelimiter() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("/\\%value%/.test(input)", + ignored -> "/; attack() //", bindings::put); + + assertEquals("/\\/; attack\\(\\) \\/\\//.test(input)", prepared); + assertTrue(bindings.isEmpty()); + } + @Test void placeholdersInCommentsAreNotEvaluatedOrRewritten() { HashMap bindings = new HashMap<>(); From a36da3e06ae647a00708baa467396efdb9f7f985 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 28 Aug 2026 20:58:01 -0600 Subject: [PATCH 5/6] Isolate concurrent JavaScript evaluations --- .../api/javascript/JavascriptEngine.java | 43 ++++++---- .../JavascriptPlaceholderBinder.java | 4 +- .../JavascriptEngineConcurrencyTest.java | 79 +++++++++++++++++++ 3 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineConcurrencyTest.java diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java index 788ce5d18..03a4cff83 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngine.java @@ -143,9 +143,11 @@ public Object getResult(String expression) { return null; } + HashMap placeholderBindings = new HashMap<>(); String preparedExpression; try { - preparedExpression = JavascriptPlaceholderBinder.bind(expression, placeholderPlayer, placeholders, this); + preparedExpression = JavascriptPlaceholderBinder.bind(expression, placeholderPlayer, placeholders, + placeholderBindings); } catch (IllegalArgumentException e) { AdvancedCorePlugin.getInstance().getLogger() .warning("Failed to safely prepare javascript placeholders: " + e.getMessage()); @@ -153,21 +155,19 @@ public Object getResult(String expression) { return null; } - engine.put("Bukkit", Bukkit.getServer()); - engine.put("AdvancedCore", AdvancedCorePlugin.getInstance()); - engine.put("Console", Bukkit.getConsoleSender()); - engine.put("UserManager", AdvancedCorePlugin.getInstance().getUserManager()); - engine.put("RewardHandler", AdvancedCorePlugin.getInstance().getRewardHandler()); - engine.put("MessageAPI", MessageAPI.class); - - engineAPI.putAll(AdvancedCorePlugin.getInstance().getJavascriptEngine()); - - for (Entry entry : engineAPI.entrySet()) { - engine.put(entry.getKey(), entry.getValue()); - } + HashMap evaluationBindings = new HashMap<>(); + evaluationBindings.put("Bukkit", Bukkit.getServer()); + evaluationBindings.put("AdvancedCore", AdvancedCorePlugin.getInstance()); + evaluationBindings.put("Console", Bukkit.getConsoleSender()); + evaluationBindings.put("UserManager", AdvancedCorePlugin.getInstance().getUserManager()); + evaluationBindings.put("RewardHandler", AdvancedCorePlugin.getInstance().getRewardHandler()); + evaluationBindings.put("MessageAPI", MessageAPI.class); + evaluationBindings.putAll(engineAPI); + evaluationBindings.putAll(placeholderBindings); + evaluationBindings.putAll(AdvancedCorePlugin.getInstance().getJavascriptEngine()); try { - return engine.eval(preparedExpression); + return evaluateWithBindings(engine, preparedExpression, evaluationBindings); } catch (ScriptException e) { AdvancedCorePlugin.getInstance().getLogger().warning( "Error occoured while evaluating javascript, turn debug on to see stacktrace: " + e.toString()); @@ -177,6 +177,21 @@ public Object getResult(String expression) { return null; } + /** + * The configured engine is cached and shared by every JavascriptEngine wrapper. + * Keep binding writes and evaluation under the same lock so concurrent rewards + * cannot observe or overwrite one another's per-evaluation values. + */ + static Object evaluateWithBindings(ScriptEngine engine, String expression, Map bindings) + throws ScriptException { + synchronized (engine) { + for (Entry entry : bindings.entrySet()) { + engine.put(entry.getKey(), entry.getValue()); + } + return engine.eval(expression); + } + } + public String getStringValue(String expression) { try { Object result = getResult(expression); diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java index 536ded44a..1114a8c6f 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -44,9 +44,9 @@ private JavascriptPlaceholderBinder() { } public static String bind(String expression, OfflinePlayer player, Map placeholders, - JavascriptEngine engine) { + Map bindings) { return bind(expression, token -> resolve(token, player, placeholders), - value -> resolvePapiValue(value, player), engine::addToEngine); + value -> resolvePapiValue(value, player), bindings::put); } static String bind(String expression, Function resolver, BiConsumer bindings) { diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineConcurrencyTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineConcurrencyTest.java new file mode 100644 index 000000000..20c75a0c0 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineConcurrencyTest.java @@ -0,0 +1,79 @@ +package com.bencodez.advancedcore.api.javascript; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.Reader; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; + +import javax.script.AbstractScriptEngine; +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptException; +import javax.script.SimpleBindings; + +import org.junit.jupiter.api.Test; + +class JavascriptEngineConcurrencyTest { + + @Test + void sharedEngineBindingsAndEvaluationAreAtomic() throws Exception { + LockCheckingScriptEngine engine = new LockCheckingScriptEngine(); + ExecutorService executor = Executors.newFixedThreadPool(2); + CountDownLatch start = new CountDownLatch(1); + + try { + Future first = executor.submit(() -> { + start.await(); + return JavascriptEngine.evaluateWithBindings(engine, "value", Map.of("value", "first")); + }); + Future second = executor.submit(() -> { + start.await(); + return JavascriptEngine.evaluateWithBindings(engine, "value", Map.of("value", "second")); + }); + + start.countDown(); + assertEquals("first", first.get(2, TimeUnit.SECONDS)); + assertEquals("second", second.get(2, TimeUnit.SECONDS)); + assertTrue(engine.allEvaluationsHeldLock.get()); + } finally { + executor.shutdownNow(); + } + } + + private static final class LockCheckingScriptEngine extends AbstractScriptEngine { + private final AtomicBoolean allEvaluationsHeldLock = new AtomicBoolean(true); + + @Override + public Object eval(String script, ScriptContext context) { + if (!Thread.holdsLock(this)) { + allEvaluationsHeldLock.set(false); + } + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(25)); + return context.getAttribute(script, ScriptContext.ENGINE_SCOPE); + } + + @Override + public Object eval(Reader reader, ScriptContext context) throws ScriptException { + throw new UnsupportedOperationException(); + } + + @Override + public Bindings createBindings() { + return new SimpleBindings(); + } + + @Override + public ScriptEngineFactory getFactory() { + return null; + } + } +} From fb003affd9a646847d968126e4aac169814a91ac Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 28 Aug 2026 21:07:39 -0600 Subject: [PATCH 6/6] Preserve placeholders when JavaScript is disabled --- .../api/messages/PlaceholderUtils.java | 16 ++++++++++--- .../tests/messages/PlaceholderUtilsTest.java | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java index d29a421ef..c5718f8c1 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java @@ -212,8 +212,9 @@ public static ArrayList replaceJavascript(Player player, ArrayList replacePlaceHolders(OfflinePlayer player, ArrayL } public static String replacePlaceHolders(OfflinePlayer player, String text) { + return replacePlaceHolders(player, text, false); + } + + private static String replacePlaceHolders(OfflinePlayer player, String text, + boolean replaceJavascriptSegments) { if (player == null || text == null || text.isEmpty()) { return text; } if (AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) { + Function replacement = value -> PlaceholderAPI.setPlaceholders(player, value); + Function javascriptReplacement = replaceJavascriptSegments + ? value -> JavascriptTextTemplate.neutralizeGeneratedMarkers(replacement.apply(value)) + : Function.identity(); return JavascriptTextTemplate.parse(text).transform( - value -> PlaceholderAPI.setPlaceholders(player, value), Function.identity()); + replacement, javascriptReplacement); } return text; } diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/messages/PlaceholderUtilsTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/messages/PlaceholderUtilsTest.java index f60503ed5..f5fd05e4c 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/messages/PlaceholderUtilsTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/messages/PlaceholderUtilsTest.java @@ -1,10 +1,13 @@ package com.bencodez.advancedcore.tests.messages; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; @@ -16,6 +19,26 @@ import me.clip.placeholderapi.PlaceholderAPI; public class PlaceholderUtilsTest { + @Test + public void disabledJavascriptStillExpandsPlaceholderApiInsideAuthoredMarker() { + BaseTest base = BaseTest.getInstance(); + Player player = mock(Player.class); + + when(base.options.isJavascriptEngineEnabled()).thenReturn(false); + when(base.plugin.isPlaceHolderAPIEnabled()).thenReturn(true); + + try (MockedStatic placeholderApiStatic = mockStatic(PlaceholderAPI.class)) { + placeholderApiStatic.when(() -> PlaceholderAPI.setPlaceholders(any(OfflinePlayer.class), anyString())) + .thenAnswer(invocation -> invocation.getArgument(1) + .replace("%player_name%", "Ben") + .replace("%attack%", "] [Javascript=danger()")); + + assertEquals("Hello Ben [Javascript='Ben']", + PlaceholderUtils.replaceJavascript(player, "Hello %player_name% [Javascript='%player_name%']")); + assertEquals("[Javascript='] [Javascript =danger()']", + PlaceholderUtils.replaceJavascript(player, "[Javascript='%attack%']")); + } + } @Test public void replaceJavascriptOnlyDoesNotExpandPlaceholderApiOutput() {