diff --git a/AdvancedCore/pom.xml b/AdvancedCore/pom.xml index a3f17cc16..ae2ed4bc6 100644 --- a/AdvancedCore/pom.xml +++ b/AdvancedCore/pom.xml @@ -231,6 +231,12 @@ 2.12.2 provided + + org.openjdk.nashorn + nashorn-core + 15.7 + provided + 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..4f0a59352 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,13 +17,17 @@ 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); @@ -42,6 +47,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 +64,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 +83,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 +98,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 +134,7 @@ public boolean getBooleanValue(String expression) { } public Object getResult(String expression) { - if (!expression.equals("")) { + if (expression != null && !expression.equals("")) { if (!AdvancedCorePlugin.getInstance().getOptions().isJavascriptEngineEnabled()) { return null; } @@ -128,6 +143,9 @@ public Object getResult(String expression) { AdvancedCorePlugin.getInstance().debug("Failed to process javascript, engine == null"); return null; } + + String preparedExpression = JavascriptPlaceholderBinder.bind(expression, placeholderPlayer, placeholders, this); + engine.put("Bukkit", Bukkit.getServer()); engine.put("AdvancedCore", AdvancedCorePlugin.getInstance()); engine.put("Console", Bukkit.getConsoleSender()); @@ -142,7 +160,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/JavascriptEngineHandler.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineHandler.java index 5a1e709d0..0a75b059d 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineHandler.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineHandler.java @@ -274,6 +274,55 @@ private ScriptEngine getServerProvidedEngine() { } } + /** + * Gets or prepares a Nashorn classloader for parser-only use. This is separate + * from the active ScriptEngine so a server-provided Rhino/GraalJS engine can + * still use Nashorn's parser for safe placeholder context detection. + * + * @return a classloader containing Nashorn's parser API, or null if unavailable + */ + public ClassLoader getOrCreateNashornParserClassLoader() { + if (nashornClassLoader != null) { + try { + Class.forName("org.openjdk.nashorn.api.tree.Parser", false, nashornClassLoader); + return nashornClassLoader; + } catch (ClassNotFoundException | LinkageError ignored) { + } + } + if (plugin == null) { + return null; + } + + URLClassLoader loader = createParserClassLoader(PRIMARY_NASHORN_VERSION, ASM_VERSION_FOR_PRIMARY); + if (loader == null) { + loader = createParserClassLoader(FALLBACK_NASHORN_VERSION, ASM_VERSION_FOR_FALLBACK); + } + if (loader != null) { + nashornClassLoader = loader; + } + return loader; + } + + private URLClassLoader createParserClassLoader(String nashornVersion, String asmVersion) { + try { + List jars = getOrDownloadJars(nashornVersion, asmVersion); + if (jars.isEmpty()) { + return null; + } + URLClassLoader loader = createClassLoader(jars); + try { + Class.forName("org.openjdk.nashorn.api.tree.Parser", false, loader); + return loader; + } catch (ClassNotFoundException | LinkageError e) { + closeQuietly(loader); + return null; + } + } catch (IOException e) { + logDebug(e); + return null; + } + } + /** * Gets a downloaded Nashorn engine. * 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..afcb9b44f --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinder.java @@ -0,0 +1,1028 @@ +package com.bencodez.advancedcore.api.javascript; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.script.ScriptEngine; + +import org.bukkit.OfflinePlayer; + +import com.bencodez.advancedcore.AdvancedCorePlugin; + +import me.clip.placeholderapi.PlaceholderAPI; + +/** + * Resolves JavaScript placeholders without allowing placeholder output to become + * executable source. + *

+ * AdvancedCore asks the already-loaded Nashorn parser to identify whether each + * placeholder is in executable code, a string, template text, or a regular + * expression. This preserves existing JavaScript syntax without maintaining a + * second JavaScript lexer inside AdvancedCore. + */ +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 value = JavascriptPlaceholderValue.decode(token); + if (value == null) { + value = resolver.apply(token); + } else { + // Values encoded by PlaceholderUtils are already known to be data, but + // they may still contain PlaceholderAPI tokens from legacy custom -> PAPI + // replacement chains. Resolve those tokens before escaping/binding. + value = decodedResolver.apply(value); + } + matches.add(new PlaceholderMatch(matcher.start(), matcher.end(), token, value)); + // Keep all source offsets unchanged while making resolved placeholders parse + // as an ordinary identifier. Unresolved brace syntax may be valid JavaScript + // (for example an object/block), so only sanitize brace placeholders when + // they actually resolve as AdvancedCore custom data. + 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()); + if (!contexts.parsed) { + contexts = JavascriptContexts.fallback(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 (match.value == null || match.value.equals(match.token)) { + replacements[i] = match.token; + continue; + } + + Range regex = contexts.containing(contexts.regexes, match.start); + Range string = contexts.containing(contexts.strings, match.start); + Range template = contexts.containing(contexts.templates, match.start); + if (regex != null) { + replacements[i] = escapeRegex(match.value, expression, regex, match.start); + } else if (template != null && !contexts.insideTemplateExpression(match.start)) { + // Template text wins over quote-looking text inside the template. A value + // containing ${...} must never become a live interpolation. + replacements[i] = escapeTemplate(match.value); + } else if (string != null) { + char delimiter = literalDelimiter(expression, string); + if (delimiter == '`' && !contexts.insideTemplateExpression(match.start)) { + replacements[i] = escapeTemplate(match.value); + } else { + replacements[i] = escapeString(match.value, delimiter); + } + } else { + String variable = VARIABLE_PREFIX + bindingIndex++; + bindings.accept(variable, coerce(match.value)); + replacements[i] = variable; + } + } + + // Apply from right to left so source positions from the parser remain valid. + 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) { + AdvancedCorePlugin plugin = AdvancedCorePlugin.getInstance(); + + // Preserve the historical replacement order: AdvancedCore custom/reward + // placeholders win name collisions, then PlaceholderAPI is applied to the + // selected custom value so custom placeholders may themselves contain PAPI. + 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); + } + } + } + + // Only consult PlaceholderAPI for the original token when no custom + // placeholder with the same name was supplied. + 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 char literalDelimiter(String expression, Range range) { + int[] candidates = { range.start - 1, range.start, range.end, range.end - 1 }; + for (int candidate : candidates) { + if (candidate < 0 || candidate >= expression.length()) { + continue; + } + char value = expression.charAt(candidate); + if (value == '\'' || value == '"' || value == '`') { + return value; + } + } + for (int i = Math.max(0, range.start - 2); + i <= Math.min(expression.length() - 1, range.start + 1); i++) { + char value = expression.charAt(i); + if (value == '\'' || value == '"' || value == '`') { + return value; + } + } + return '\''; + } + + 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 final class Range { + private final int start; + private final int end; + + private Range(long start, long end) { + this.start = (int) start; + this.end = (int) end; + } + + private boolean contains(int position) { + return position >= start && position < end; + } + } + + /** + * Context ranges obtained from Nashorn's parser API. Parser classes are loaded + * reflectively because AdvancedCore can download Nashorn into its own + * URLClassLoader at runtime rather than putting it on the plugin classpath. + */ + private static final class JavascriptContexts { + private final List strings = new ArrayList<>(); + private final List regexes = new ArrayList<>(); + private final List templates = new ArrayList<>(); + private final List templateExpressions = new ArrayList<>(); + private final List comments = new ArrayList<>(); + private boolean parsed; + + private static JavascriptContexts parse(String source) { + JavascriptContexts contexts = new JavascriptContexts(); + try { + ClassLoader loader = parserClassLoader(); + if (loader == null) { + return contexts; + } + + Class parserClass = Class.forName(PARSER_CLASS, true, loader); + Class diagnosticClass = Class.forName(DIAGNOSTIC_LISTENER_CLASS, true, loader); + Class treeClass = Class.forName(TREE_CLASS, true, loader); + Object parser = createParser(parserClass); + boolean[] hadParseDiagnostic = new boolean[1]; + Object diagnostic = Proxy.newProxyInstance(loader, new Class[] { diagnosticClass }, + (proxy, method, args) -> { + if ("report".equals(method.getName())) { + hadParseDiagnostic[0] = true; + } + return null; + }); + Method parse = parserClass.getMethod("parse", String.class, String.class, diagnosticClass); + Object root = parse.invoke(parser, "AdvancedCore", source, diagnostic); + if (root != null && !hadParseDiagnostic[0]) { + contexts.parsed = true; + walk(root, treeClass, contexts, new IdentityHashMap<>()); + } + } catch (ReflectiveOperationException | RuntimeException ignored) { + // If a script cannot be parsed, expression placeholders still fall back to + // engine bindings below. Placeholder output is never copied into source code. + } + contexts.sort(); + return contexts; + } + + private static JavascriptContexts fallback(String source) { + JavascriptContexts contexts = new JavascriptContexts(); + + // First identify template text so comment delimiters inside template text are + // ignored. Then find comments, mask them with same-length whitespace, and + // rebuild every literal range from the masked source. This prevents quotes or + // backticks inside comments from manufacturing fake literal ranges around + // executable placeholders. + addFallbackTemplateRanges(source, 0, source.length(), contexts); + addFallbackCommentRanges(source, contexts); + String scanSource = maskRanges(source, contexts.comments); + + contexts.templates.clear(); + contexts.templateExpressions.clear(); + addFallbackTemplateRanges(scanSource, 0, scanSource.length(), contexts); + + // Quote-looking text is a string only outside template text. Strings inside + // ${...} remain ordinary JavaScript strings and are tracked normally. + addPatternRanges(scanSource, FALLBACK_STRING, contexts.strings, contexts); + addFallbackRegexRanges(scanSource, contexts); + contexts.sort(); + return contexts; + } + + private static void addFallbackCommentRanges(String source, JavascriptContexts contexts) { + for (int i = 0; i < source.length(); i++) { + if (contexts.isTemplateText(i)) { + continue; + } + + char current = source.charAt(i); + if (current == '\'' || current == '"') { + i = skipQuotedLiteral(source, i, source.length(), current); + continue; + } + if (current != '/' || i + 1 >= source.length()) { + continue; + } + + char next = source.charAt(i + 1); + if (next == '/') { + int end = i + 2; + while (end < source.length() && source.charAt(end) != '\n' && source.charAt(end) != '\r') { + end++; + } + addFallbackComment(contexts, new Range(i, end)); + i = end - 1; + continue; + } + if (next == '*') { + int end = i + 2; + while (end + 1 < source.length() + && !(source.charAt(end) == '*' && source.charAt(end + 1) == '/')) { + end++; + } + end = end + 1 < source.length() ? end + 2 : source.length(); + addFallbackComment(contexts, new Range(i, end)); + i = end - 1; + continue; + } + + if (canStartRegex(source, i)) { + int regexEnd = skipRegexLiteral(source, i, source.length()); + if (regexEnd > i) { + i = regexEnd; + } + } + } + } + + private static void addFallbackComment(JavascriptContexts contexts, Range comment) { + contexts.comments.add(comment); + // Initial template discovery is only used to distinguish template text from + // comments. A backtick inside a comment can create a false template range, so + // discard any such range as soon as the comment is known. + contexts.removeOverlapping(contexts.templates, comment); + contexts.removeOverlapping(contexts.templateExpressions, comment); + } + + private static String maskRanges(String source, List ranges) { + StringBuilder masked = new StringBuilder(source); + for (Range range : ranges) { + for (int i = Math.max(0, range.start); i < Math.min(masked.length(), range.end); i++) { + char current = masked.charAt(i); + if (current != '\n' && current != '\r') { + masked.setCharAt(i, ' '); + } + } + } + return masked.toString(); + } + + private static void addFallbackTemplateRanges(String source, int start, int limit, + JavascriptContexts contexts) { + for (int i = start; i < limit; i++) { + char current = source.charAt(i); + if (current == '\'' || current == '"') { + i = skipQuotedLiteral(source, i, limit, current); + continue; + } + if (current == '/' && canStartRegex(source, i)) { + int regexEnd = skipRegexLiteral(source, i, limit); + if (regexEnd > i) { + i = regexEnd; + continue; + } + } + if (current != '`') { + continue; + } + + int templateEnd = skipTemplateLiteral(source, i, limit); + if (templateEnd <= i || templateEnd >= source.length() || source.charAt(templateEnd) != '`') { + continue; + } + + Range template = new Range(i, templateEnd + 1); + contexts.templates.add(template); + List expressions = new ArrayList<>(); + addFallbackTemplateExpressions(source, template, expressions); + contexts.templateExpressions.addAll(expressions); + + // Nested templates live inside an outer ${...}. Scan each interpolation + // recursively so their text ranges override the enclosing expression. + for (Range expression : expressions) { + addFallbackTemplateRanges(source, expression.start, expression.end, contexts); + } + i = templateEnd; + } + } + + private static void addFallbackRegexRanges(String source, JavascriptContexts contexts) { + for (int i = 0; i < source.length(); i++) { + if (source.charAt(i) != '/' || contexts.containing(contexts.strings, i) != null + || contexts.isTemplateText(i) || !canStartRegex(source, i)) { + continue; + } + + boolean escaped = false; + boolean characterClass = false; + for (int j = i + 1; j < source.length(); j++) { + char current = source.charAt(j); + if (current == '\r' || current == '\n') { + break; + } + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == '[') { + characterClass = true; + continue; + } + if (current == ']') { + characterClass = false; + continue; + } + if (current != '/' || characterClass) { + continue; + } + + int end = j + 1; + while (end < source.length() && "dgimsuvy".indexOf(source.charAt(end)) >= 0) { + end++; + } + Range candidate = new Range(i, end); + // A regex literal may legitimately contain quote characters. Since the + // opening slash was already proven to be outside a string/template and + // can start a regex, discard fallback string ranges fully contained by + // this regex instead of letting quote-looking regex text win. + contexts.removeContained(contexts.strings, candidate); + contexts.regexes.add(candidate); + i = end - 1; + break; + } + } + } + + private static boolean canStartRegex(String source, int slashIndex) { + int previousIndex = slashIndex - 1; + while (previousIndex >= 0 && Character.isWhitespace(source.charAt(previousIndex))) { + previousIndex--; + } + if (previousIndex < 0) { + return true; + } + + char previous = source.charAt(previousIndex); + if ("([{:;,=!?&|+-*%^~<>".indexOf(previous) >= 0) { + return true; + } + if (previous == ')' && closesControlStatementHead(source, previousIndex)) { + return true; + } + + if (Character.isJavaIdentifierPart(previous)) { + int end = previousIndex + 1; + int start = previousIndex; + while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { + start--; + } + String word = source.substring(start + 1, end); + return word.equals("return") || word.equals("case") || word.equals("throw") + || word.equals("else") || word.equals("do") || word.equals("yield") + || word.equals("await") || word.equals("typeof") || word.equals("void") + || word.equals("delete") || word.equals("instanceof") || word.equals("in") + || word.equals("new"); + } + return false; + } + + private static boolean closesControlStatementHead(String source, int closeParen) { + List openingParens = new ArrayList<>(); + for (int i = 0; i <= closeParen; i++) { + char current = source.charAt(i); + if (current == '\'' || current == '"') { + i = skipQuotedLiteral(source, i, closeParen + 1, current); + continue; + } + if (current == '`') { + i = skipTemplateLiteral(source, i, closeParen + 1); + continue; + } + if (current == '/' && canStartRegex(source, i)) { + int regexEnd = skipRegexLiteral(source, i, closeParen + 1); + if (regexEnd > i) { + i = regexEnd; + continue; + } + } + if (current == '(') { + openingParens.add(i); + } else if (current == ')') { + if (openingParens.isEmpty()) { + return false; + } + int openingParen = openingParens.remove(openingParens.size() - 1); + if (i == closeParen) { + return isControlKeywordBefore(source, openingParen); + } + } + } + return false; + } + + private static boolean isControlKeywordBefore(String source, int openingParen) { + int end = openingParen - 1; + while (end >= 0 && Character.isWhitespace(source.charAt(end))) { + end--; + } + if (end < 0 || !Character.isJavaIdentifierPart(source.charAt(end))) { + return false; + } + + int start = end; + while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { + start--; + } + String word = source.substring(start + 1, end + 1); + if (word.equals("if") || word.equals("while") || word.equals("for") || word.equals("with") + || word.equals("switch") || word.equals("catch")) { + return true; + } + + // Modern JavaScript may use `for await (...)`. + if (!word.equals("await")) { + return false; + } + end = start; + while (end >= 0 && Character.isWhitespace(source.charAt(end))) { + end--; + } + start = end; + while (start >= 0 && Character.isJavaIdentifierPart(source.charAt(start))) { + start--; + } + return end >= 0 && source.substring(start + 1, end + 1).equals("for"); + } + + private static void addPatternRanges(String source, Pattern pattern, List target, + JavascriptContexts existing) { + Matcher matcher = pattern.matcher(source); + while (matcher.find()) { + Range candidate = new Range(matcher.start(), matcher.end()); + if (existing == null || !existing.isTemplateText(candidate.start)) { + target.add(candidate); + } + } + } + + private static void addFallbackTemplateExpressions(String source, Range template, List target) { + boolean escaped = false; + for (int i = template.start + 1; i < template.end - 1; i++) { + char current = source.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == '$' && i + 1 < template.end && source.charAt(i + 1) == '{') { + int expressionStart = i + 2; + int expressionEnd = findTemplateExpressionEnd(source, expressionStart, template.end - 1); + if (expressionEnd >= 0) { + target.add(new Range(expressionStart, expressionEnd)); + i = expressionEnd; + } + } + } + } + + private static int findTemplateExpressionEnd(String source, int start, int limit) { + int depth = 1; + for (int i = start; i < limit; i++) { + char current = source.charAt(i); + if (current == '\'' || current == '"') { + i = skipQuotedLiteral(source, i, limit, current); + continue; + } + if (current == '`') { + i = skipTemplateLiteral(source, i, limit); + continue; + } + if (current == '/' && canStartRegex(source, i)) { + int regexEnd = skipRegexLiteral(source, i, limit); + if (regexEnd > i) { + i = regexEnd; + continue; + } + } + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + return i; + } + } + } + return -1; + } + + private static int skipQuotedLiteral(String source, int start, int limit, char quote) { + boolean escaped = false; + for (int i = start + 1; i < limit; i++) { + char current = source.charAt(i); + if (escaped) { + escaped = false; + } else if (current == '\\') { + escaped = true; + } else if (current == quote) { + return i; + } + } + return limit - 1; + } + + private static int skipTemplateLiteral(String source, int start, int limit) { + boolean escaped = false; + for (int i = start + 1; i < limit; i++) { + char current = source.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == '$' && i + 1 < limit && source.charAt(i + 1) == '{') { + int expressionEnd = findTemplateExpressionEnd(source, i + 2, limit); + if (expressionEnd >= 0) { + i = expressionEnd; + continue; + } + } + if (current == '`') { + return i; + } + } + return limit - 1; + } + + private static int skipRegexLiteral(String source, int start, int limit) { + boolean escaped = false; + boolean characterClass = false; + for (int i = start + 1; i < limit; i++) { + char current = source.charAt(i); + if (current == '\r' || current == '\n') { + return start; + } + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == '[') { + characterClass = true; + } else if (current == ']') { + characterClass = false; + } else if (current == '/' && !characterClass) { + int end = i; + while (end + 1 < limit && "dgimsuvy".indexOf(source.charAt(end + 1)) >= 0) { + end++; + } + return end; + } + } + return start; + } + + private boolean overlapsLiteral(Range candidate) { + return overlaps(strings, candidate) || overlaps(templates, candidate); + } + + private boolean overlaps(List ranges, Range candidate) { + for (Range range : ranges) { + if (candidate.start < range.end && range.start < candidate.end) { + return true; + } + } + return false; + } + + private void removeContained(List ranges, Range container) { + ranges.removeIf(range -> range.start >= container.start && range.end <= container.end); + } + + private void removeOverlapping(List ranges, Range overlap) { + ranges.removeIf(range -> range.start < overlap.end && overlap.start < range.end); + } + + private static Object createParser(Class parserClass) throws ReflectiveOperationException { + for (Method method : parserClass.getMethods()) { + if (!method.getName().equals("create") || !Modifier.isStatic(method.getModifiers())) { + continue; + } + if (method.getParameterCount() == 0) { + return method.invoke(null); + } + if (method.getParameterCount() == 1 && method.getParameterTypes()[0].isArray() + && method.getParameterTypes()[0].getComponentType() == String.class) { + return method.invoke(null, (Object) new String[] { "--language=es6" }); + } + } + throw new NoSuchMethodException("Nashorn Parser.create"); + } + + private static ClassLoader parserClassLoader() { + JavascriptEngineHandler handler = JavascriptEngineHandler.getInstance(); + ClassLoader downloaded = handler.getNashornClassLoader(); + if (canLoadParser(downloaded)) { + return downloaded; + } + + // Use a parser already visible to AdvancedCore when available. If not, + // the handler can prepare a dedicated runtime Nashorn parser loader below. + ClassLoader own = JavascriptPlaceholderBinder.class.getClassLoader(); + if (canLoadParser(own)) { + return own; + } + + ScriptEngine cached = handler.getCachedEngine(); + ClassLoader cachedLoader = cached == null ? null : cached.getClass().getClassLoader(); + if (canLoadParser(cachedLoader)) { + return cachedLoader; + } + + ClassLoader prepared = handler.getOrCreateNashornParserClassLoader(); + return canLoadParser(prepared) ? prepared : null; + } + + private static boolean canLoadParser(ClassLoader loader) { + if (loader == null) { + return false; + } + try { + Class.forName(PARSER_CLASS, false, loader); + return true; + } catch (ClassNotFoundException | LinkageError ignored) { + return false; + } + } + + private static void walk(Object node, Class treeClass, JavascriptContexts contexts, + IdentityHashMap visited) { + if (node == null || !treeClass.isInstance(node) || visited.put(node, Boolean.TRUE) != null) { + return; + } + + String kind = stringValue(invokeTreeMethod(node, "getKind")); + long start = longValue(invokeTreeMethod(node, "getStartPosition")); + long end = longValue(invokeTreeMethod(node, "getEndPosition")); + if (start >= 0 && end >= start) { + if ("STRING_LITERAL".equals(kind)) { + contexts.strings.add(new Range(start, end)); + } else if (kind != null && kind.contains("REGEXP")) { + contexts.regexes.add(new Range(start, end)); + } else if ("TEMPLATE_LITERAL".equals(kind)) { + contexts.templates.add(new Range(start, end)); + Object expressions = invokeTreeMethod(node, "getExpressions"); + if (expressions instanceof Iterable) { + for (Object expression : (Iterable) expressions) { + long expressionStart = longValue(invokeTreeMethod(expression, "getStartPosition")); + long expressionEnd = longValue(invokeTreeMethod(expression, "getEndPosition")); + if (expressionStart >= 0 && expressionEnd >= expressionStart) { + contexts.templateExpressions.add(new Range(expressionStart, expressionEnd)); + } + } + } + } + } + + for (Method method : treeApiMethods(node.getClass())) { + if (method.getParameterCount() != 0 || Modifier.isStatic(method.getModifiers())) { + continue; + } + String name = method.getName(); + if (name.equals("getKind") || name.equals("getStartPosition") || name.equals("getEndPosition") + || name.equals("getSourceName") || name.equals("getClass")) { + continue; + } + try { + Object value = method.invoke(node); + walkValue(value, treeClass, contexts, visited); + } catch (ReflectiveOperationException | RuntimeException ignored) { + } + } + } + + private static void walkValue(Object value, Class treeClass, JavascriptContexts contexts, + IdentityHashMap visited) { + if (value == null) { + return; + } + if (treeClass.isInstance(value)) { + walk(value, treeClass, contexts, visited); + } else if (value instanceof Iterable) { + for (Object element : (Iterable) value) { + if (treeClass.isInstance(element)) { + walk(element, treeClass, contexts, visited); + } + } + } else if (value.getClass().isArray()) { + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + Object element = Array.get(value, i); + if (treeClass.isInstance(element)) { + walk(element, treeClass, contexts, visited); + } + } + } + } + + private static Set treeApiMethods(Class type) { + LinkedHashSet methods = new LinkedHashSet<>(); + collectTreeApiMethods(type, methods, new LinkedHashSet<>()); + return methods; + } + + private static void collectTreeApiMethods(Class type, Set methods, Set> visited) { + if (type == null || !visited.add(type)) { + return; + } + for (Class iface : type.getInterfaces()) { + Package pkg = iface.getPackage(); + if (pkg != null && TREE_PACKAGE.equals(pkg.getName())) { + for (Method method : iface.getMethods()) { + methods.add(method); + } + } + collectTreeApiMethods(iface, methods, visited); + } + collectTreeApiMethods(type.getSuperclass(), methods, visited); + } + + private static Object invokeTreeMethod(Object node, String methodName) { + if (node == null) { + return null; + } + for (Method method : treeApiMethods(node.getClass())) { + if (method.getName().equals(methodName) && method.getParameterCount() == 0) { + try { + return method.invoke(node); + } catch (ReflectiveOperationException | RuntimeException ignored) { + return null; + } + } + } + return null; + } + + private static String stringValue(Object value) { + return value == null ? null : value.toString(); + } + + private static long longValue(Object value) { + return value instanceof Number ? ((Number) value).longValue() : -1; + } + + private Range containing(List ranges, int position) { + for (Range range : ranges) { + if (range.contains(position)) { + return range; + } + } + return null; + } + + private boolean insideTemplateExpression(int position) { + Range expression = innermostContaining(templateExpressions, position); + if (expression == null) { + return false; + } + Range template = innermostContaining(templates, position); + // An enclosing template expression must not override the text context of a + // nested template literal that starts later inside that expression. + return template == null || expression.start > template.start; + } + + private boolean isTemplateText(int position) { + return innermostContaining(templates, position) != null && !insideTemplateExpression(position); + } + + private Range innermostContaining(List ranges, int position) { + Range best = null; + for (Range range : ranges) { + if (!range.contains(position)) { + continue; + } + if (best == null || (range.end - range.start) < (best.end - best.start)) { + best = range; + } + } + return best; + } + + private void sort() { + Comparator comparator = Comparator.comparingInt(range -> range.start); + strings.sort(comparator); + regexes.sort(comparator); + templates.sort(comparator); + templateExpressions.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..d9318d130 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java @@ -0,0 +1,34 @@ +package com.bencodez.advancedcore.api.javascript; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Encodes placeholder output so it can pass through normal text replacement + * without ever becoming JavaScript source. + */ +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 encoded = Base64.getUrlEncoder().withoutPadding() + .encodeToString(value.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/messages/PlaceholderUtils.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java index 8e74bb82d..eb6ad61d6 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,7 @@ import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.javascript.JavascriptEngine; +import com.bencodez.advancedcore.api.javascript.JavascriptPlaceholderValue; import com.bencodez.advancedcore.api.user.AdvancedCoreUser; import com.bencodez.simpleapi.messages.MessageAPI; @@ -39,7 +41,6 @@ public static TextComponent parseJson(String msg) { TextComponent t = new TextComponent(text); String typeMsg = msg; - // types boolean parsing = true; while (parsing) { int nextTypeIndex = typeMsg.indexOf("\","); @@ -69,11 +70,6 @@ public static TextComponent parseJson(String msg) { } } - /* - * int secondMiddle = msg.indexOf("=\"", middle); String type = - * msg.substring(middle + "\",".length(), secondMiddle); String typeData = - * msg.substring(secondMiddle + "=\"".length(), endIndex); - */ comp.addExtra(parseJson(preMessage)); @@ -126,7 +122,7 @@ public static ArrayList replaceJavascript(AdvancedCoreUser user, ArrayLi public static String replaceJavascript(AdvancedCoreUser user, String text) { if (user.getPlugin().getOptions().isJavascriptEngineEnabled()) { JavascriptEngine engine = new JavascriptEngine().addPlayer(user); - return replaceJavascript(text, engine); + return replaceJavascript(text, engine, user.getOfflinePlayer()); } return text; } @@ -157,7 +153,7 @@ public static String replaceJavascript(CommandSender player, String text) { return replaceJavascript((Player) player, text); } JavascriptEngine engine = new JavascriptEngine().addPlayer(player); - return replaceJavascript(text, engine); + return replaceJavascript(text, engine, null); } return text; } @@ -176,7 +172,7 @@ public static String replaceJavascript(OfflinePlayer player, String text) { return replaceJavascript(player.getPlayer(), text); } JavascriptEngine engine = new JavascriptEngine().addPlayer(player); - return replaceJavascript(text, engine); + return replaceJavascript(text, engine, player); } return text; } @@ -195,7 +191,7 @@ public static String replaceJavascriptOnly(OfflinePlayer player, String text) { return replaceJavascriptOnly(player.getPlayer(), text); } JavascriptEngine engine = new JavascriptEngine().addPlayer(player); - return replaceJavascript(text, engine); + return replaceJavascript(text, engine, player); } return text; } @@ -212,7 +208,7 @@ public static String replaceJavascript(Player player, String text) { String msg = replacePlaceHolders(player, text); if (AdvancedCorePlugin.getInstance().getOptions().isJavascriptEngineEnabled()) { JavascriptEngine engine = new JavascriptEngine().addPlayer(player); - return replaceJavascript(msg, engine); + return replaceJavascript(msg, engine, player); } return msg; } @@ -228,7 +224,7 @@ public static ArrayList replaceJavascriptOnly(Player player, 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()); - } + if (placeholders == null) { + return str; } - return str; + return transformJavascriptMarkers(str, value -> replacePlaceHolderMapRaw(value, placeholders, true), + value -> replacePlaceHolderMapEncoded(value, 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 (placeholders == null) { + return str; } - return str; + return transformJavascriptMarkers(str, value -> replacePlaceHolderMapRaw(value, placeholders, ignoreCase), + value -> replacePlaceHolderMapEncoded(value, placeholders, ignoreCase)); } /** @@ -322,6 +317,30 @@ public static String replacePlaceHolder(String str, String toReplace, String rep } public static String replacePlaceHolder(String str, String toReplace, String replaceWith, boolean ignoreCase) { + return transformJavascriptMarkers(str, + 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) { if (ignoreCase) { return MessageAPI.replaceIgnoreCase(MessageAPI.replaceIgnoreCase(str, "%" + toReplace + "%", replaceWith), "\\{" + toReplace + "\\}", replaceWith); @@ -353,7 +372,8 @@ public static String replacePlaceHolders(OfflinePlayer player, String text) { return text; } if (AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) { - return PlaceholderAPI.setPlaceholders(player, text); + return transformJavascriptMarkers(text, value -> PlaceholderAPI.setPlaceholders(player, value), + Function.identity()); } return text; } @@ -366,13 +386,60 @@ public static String replacePlaceHolders(OfflinePlayer player, String text) { * @return the string */ public static String replacePlaceHolders(Player player, String text) { - if (player == null) { + return replacePlaceHolders((OfflinePlayer) player, text); + } + + private static String transformJavascriptMarkers(String text, Function outsideTransform, + Function insideTransform) { + if (text == null || text.isEmpty()) { return text; } - if (AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) { - return PlaceholderAPI.setPlaceholders(player, text); + StringBuilder result = new StringBuilder(text.length()); + int cursor = 0; + while (cursor < text.length()) { + int start = indexOfIgnoreCase(text, "[Javascript=", cursor); + if (start < 0) { + result.append(neutralizeJavascriptMarkers(outsideTransform.apply(text.substring(cursor)))); + break; + } + int end = text.indexOf(']', start); + if (end < 0) { + result.append(neutralizeJavascriptMarkers(outsideTransform.apply(text.substring(cursor)))); + break; + } + result.append(neutralizeJavascriptMarkers(outsideTransform.apply(text.substring(cursor, start)))); + int bodyStart = start + "[Javascript=".length(); + result.append(text, start, bodyStart); + result.append(insideTransform.apply(text.substring(bodyStart, end))); + result.append(']'); + cursor = end + 1; } - return text; + return result.toString(); + } + + private static String neutralizeJavascriptMarkers(String text) { + StringBuilder result = new StringBuilder(text.length()); + int cursor = 0; + while (cursor < text.length()) { + int start = indexOfIgnoreCase(text, "[Javascript=", cursor); + if (start < 0) { + result.append(text.substring(cursor)); + break; + } + result.append(text, cursor, start).append("[Javascript ="); + cursor = start + "[Javascript=".length(); + } + return result.toString(); + } + + 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; } } 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..a58baa1a8 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,9 @@ 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))) { + JavascriptEngine engine = new JavascriptEngine().addPlayer(user.getOfflinePlayer()) + .addPlaceholders(placeholders); + if (engine.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..113055e75 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; @@ -28,9 +27,11 @@ public static void register(RewardHandler handler, AdvancedCorePlugin plugin) { @Override 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()))); + if (expression.equals("")) { + return true; + } + return new JavascriptEngine().addPlayer(user.getOfflinePlayer()) + .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..138578e21 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderBinderTest.java @@ -0,0 +1,242 @@ +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.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.util.HashMap; + +import org.bukkit.OfflinePlayer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import com.bencodez.advancedcore.tests.BaseTest; + +import me.clip.placeholderapi.PlaceholderAPI; + +class JavascriptPlaceholderBinderTest { + + @BeforeEach + void useTestClasspathNashornParser() { + JavascriptEngineHandler.getInstance().setNashornClassLoader(null); + JavascriptEngineHandler.getInstance().setCachedEngine(null); + } + + @Test + void ordinaryJavascriptWithoutPlaceholdersIsUntouched() { + HashMap bindings = new HashMap<>(); + String script = "Player.hasPermission(\"someper\") == true"; + + String prepared = JavascriptPlaceholderBinder.bind(script, token -> token, bindings::put); + + assertEquals(script, prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void placeholderExpressionIsAutomaticallyBound() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("%permission_result% == true", ignored -> "true", + bindings::put); + + assertEquals("__advancedCorePlaceholder0 == true", prepared); + assertEquals(Boolean.TRUE, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void placeholderOutputIsBoundInsteadOfCopiedIntoSource() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, 'op attacker')"; + + String prepared = JavascriptPlaceholderBinder.bind("%name% == 'safe'", ignored -> injection, bindings::put); + + assertEquals("__advancedCorePlaceholder0 == 'safe'", prepared); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + assertFalse(prepared.contains(injection)); + } + + @Test + void existingQuotedPlaceholderSyntaxIsPreserved() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("'%name%' == 'Ben'", ignored -> "Ben", bindings::put); + + assertEquals("'Ben' == 'Ben'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void placeholderInsideExistingStringNeedsNoMigration() { + HashMap bindings = new HashMap<>(); + String value = "Ben's \\ server"; + + String prepared = JavascriptPlaceholderBinder.bind("'Hello %name%!'", ignored -> value, bindings::put); + + assertEquals("'Hello Ben\\'s \\\\ server!'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void placeholderInsideTemplateTextIsEscapedAutomatically() { + HashMap bindings = new HashMap<>(); + String value = "${attack}`"; + + String prepared = JavascriptPlaceholderBinder.bind("`Hello %name%`", ignored -> value, bindings::put); + + assertEquals("`Hello \\${attack}\\``", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void placeholderInsideTemplateExpressionIsBound() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("`${%count% + 1}`", ignored -> "5", bindings::put); + + assertEquals("`${__advancedCorePlaceholder0 + 1}`", prepared); + assertEquals(Long.valueOf(5), bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void placeholderInsideRegexKeepsExistingSyntax() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("/^%name%$/.test(value)", ignored -> "Ben.* /admin", + bindings::put); + + assertEquals("/^Ben\\.\\* \\/admin$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void encodedCustomPlaceholderInsideStringCannotBreakOut() { + HashMap bindings = new HashMap<>(); + String injection = "'; Bukkit.dispatchCommand(Console, 'op attacker'); '"; + String encoded = JavascriptPlaceholderValue.encode(injection); + + String prepared = JavascriptPlaceholderBinder.bind("'" + encoded + "'", token -> token, bindings::put); + + assertEquals("'\\'; Bukkit.dispatchCommand(Console, \\'op attacker\\'); \\''", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void preservesPrimitiveTypesForExpressionPlaceholders() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("%allowed% && %count% > 2 && %ratio% < 2.0", token -> { + if (token.equals("%allowed%")) { + return "true"; + } + if (token.equals("%count%")) { + return "5"; + } + return "1.5"; + }, bindings::put); + + assertEquals("__advancedCorePlaceholder0 && __advancedCorePlaceholder1 > 2 && __advancedCorePlaceholder2 < 2.0", + prepared); + assertEquals(Boolean.TRUE, bindings.get("__advancedCorePlaceholder0")); + assertEquals(Long.valueOf(5), bindings.get("__advancedCorePlaceholder1")); + assertEquals(Double.valueOf(1.5), bindings.get("__advancedCorePlaceholder2")); + } + + @Test + void exactQuotedNumericLookingPlaceholderRemainsAString() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("'%code%' === '001'", ignored -> "001", bindings::put); + + assertEquals("'001' === '001'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void braceDelimitedCustomPlaceholderIsAutomaticallyBound() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("{count} > 0", + token -> token.equals("{count}") ? "5" : token, bindings::put); + + assertEquals("__advancedCorePlaceholder0 > 0", prepared); + assertEquals(Long.valueOf(5), bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void unresolvedBraceSyntaxRemainsOrdinaryJavascript() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("var value = {count: 1}; %name%", + token -> token.equals("%name%") ? "Ben" : token, bindings::put); + + assertEquals("var value = {count: 1}; __advancedCorePlaceholder0", prepared); + assertEquals("Ben", bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void parserFailurePreservesQuotedPlaceholderUnderModernSyntax() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && '%name%' === 'Ben'", + ignored -> "Ben", bindings::put); + + assertEquals("obj?.name && 'Ben' === 'Ben'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void parserFailurePreservesTemplateTextUnderModernSyntax() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && `Hello %name%`", + ignored -> "Ben` ${attack}", bindings::put); + + assertEquals("obj?.name && `Hello Ben\\` \\${attack}`", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void parserFailurePreservesRegexPlaceholderUnderModernSyntax() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && /^%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("obj?.name && /^Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void customPlaceholderValueStillExpandsPlaceholderApiTokens() { + BaseTest base = BaseTest.getInstance(); + when(base.plugin.isPlaceHolderAPIEnabled()).thenReturn(true); + OfflinePlayer player = mock(OfflinePlayer.class); + HashMap placeholders = new HashMap<>(); + placeholders.put("alias", "%player_name%"); + + try (MockedStatic placeholderApi = mockStatic(PlaceholderAPI.class)) { + placeholderApi.when(() -> PlaceholderAPI.setPlaceholders(player, "%alias%")).thenReturn("%alias%"); + placeholderApi.when(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%")).thenReturn("Ben"); + + String prepared = JavascriptPlaceholderBinder.bind("'%alias%' == 'Ben'", player, placeholders, + new JavascriptEngine()); + + assertEquals("'Ben' == 'Ben'", prepared); + } + } + + @Test + void unresolvedTokensRemainUntouched() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("%unknown% == true", token -> token, bindings::put); + + assertEquals("%unknown% == true", prepared); + assertTrue(bindings.isEmpty()); + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderControlRegexFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderControlRegexFallbackTest.java new file mode 100644 index 000000000..7bb81e30f --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderControlRegexFallbackTest.java @@ -0,0 +1,40 @@ +package com.bencodez.advancedcore.api.javascript; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JavascriptPlaceholderControlRegexFallbackTest { + + @BeforeEach + void forceFallbackCompatibleSetup() { + JavascriptEngineHandler.getInstance().setNashornClassLoader(null); + JavascriptEngineHandler.getInstance().setCachedEngine(null); + } + + @Test + void regexAfterIfControlHeadUsesRegexEscaping() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("if (obj?.name) /^%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("if (obj?.name) /^Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void regexAfterNestedWhileControlHeadUsesRegexEscaping() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("while ((obj?.name)) /^%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("while ((obj?.name)) /^Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java new file mode 100644 index 000000000..392f47812 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderModernSyntaxFallbackTest.java @@ -0,0 +1,184 @@ +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.assertTrue; + +import java.util.HashMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JavascriptPlaceholderModernSyntaxFallbackTest { + + @BeforeEach + void resetJavascriptHandler() { + JavascriptEngineHandler.getInstance().setNashornClassLoader(null); + JavascriptEngineHandler.getInstance().setCachedEngine(null); + } + + @Test + void newerEngineSyntaxStillPreservesQuotedPlaceholderSemantics() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && '%name%' === 'Ben'", + ignored -> "Ben", bindings::put); + + assertEquals("obj?.name && 'Ben' === 'Ben'", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void newerEngineSyntaxStillEscapesQuotedPlaceholderInjection() { + HashMap bindings = new HashMap<>(); + String injection = "'; Bukkit.dispatchCommand(Console, 'op attacker'); '"; + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && '%name%' === 'safe'", + ignored -> injection, bindings::put); + + assertEquals("obj?.name && '\\'; Bukkit.dispatchCommand(Console, \\'op attacker\\'); \\'' === 'safe'", + prepared); + assertFalse(prepared.contains("&& ''; Bukkit")); + assertTrue(bindings.isEmpty()); + } + + @Test + void newerEngineSyntaxStillPreservesRegexPlaceholderSemantics() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && /^%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("obj?.name && /^Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void newerEngineSyntaxRegexFallbackKeepsEscapedSlashSemantics() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && /^path\\/%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("obj?.name && /^path\\/Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void newerEngineSyntaxRegexFallbackKeepsCharacterClassSlashes() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && /^[a/b]%name%$/.test(value)", + ignored -> "Ben.*", bindings::put); + + assertEquals("obj?.name && /^[a/b]Ben\\.\\*$/.test(value)", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void regexFallbackHandlesLongOrdinaryRunsWithoutChangingPlaceholderSemantics() { + HashMap bindings = new HashMap<>(); + String dots = ".".repeat(10_000); + String script = "obj?.name && /^" + dots + "%name%$/.test(value)"; + + String prepared = JavascriptPlaceholderBinder.bind(script, ignored -> "Ben", bindings::put); + + assertTrue(prepared.startsWith("obj?.name && /^" + dots)); + assertTrue(prepared.endsWith("Ben$/.test(value)")); + assertTrue(bindings.isEmpty()); + } + + @Test + void newerEngineSyntaxStillPreservesTemplatePlaceholderSemantics() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name && `Hello %name%`", + ignored -> "Ben` ${attack}", bindings::put); + + assertEquals("obj?.name && `Hello Ben\\` \\${attack}`", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void quoteLookingTextInsideTemplateStillUsesTemplateEscaping() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name; `'%name%'`", + ignored -> "${attack()}", bindings::put); + + assertEquals("obj?.name; `'\\${attack()}'`", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void divisionOperatorsDoNotBecomeFallbackRegexRanges() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.value; 10 / %count% / 2", + ignored -> "2.5", bindings::put); + + assertEquals("obj?.value; 10 / __advancedCorePlaceholder0 / 2", prepared); + assertEquals(1, bindings.size()); + assertEquals(2.5D, bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void quotedClosingBraceInsideTemplateExpressionDoesNotEndExpression() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name; `${\"}\" + %name%}`", + ignored -> "Bukkit.dispatchCommand(Console, 'op attacker')", bindings::put); + + assertEquals("obj?.name; `${\"}\" + __advancedCorePlaceholder0}`", prepared); + assertEquals(1, bindings.size()); + assertEquals("Bukkit.dispatchCommand(Console, 'op attacker')", bindings.get("__advancedCorePlaceholder0")); + } + + @Test + void quoteCharactersInsideRegexStillUseRegexEscaping() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name; /^'%name%'$/", + ignored -> "Ben.*", bindings::put); + + assertEquals("obj?.name; /^'Ben\\.\\*'$/", prepared); + assertTrue(bindings.isEmpty()); + } + + @Test + void nestedTemplateLiteralTextRemainsDataInFallback() { + HashMap bindings = new HashMap<>(); + + String prepared = JavascriptPlaceholderBinder.bind("obj?.name; `${`inner %name%`}`", + ignored -> "${attack()}", bindings::put); + + assertEquals("obj?.name; `${`inner \\${attack()}`}`", prepared); + assertTrue(bindings.isEmpty()); + } + @Test + void commentsCannotCreateFakeStringRangeAroundExecutablePlaceholder() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, 'op attacker')"; + + String prepared = JavascriptPlaceholderBinder.bind("obj?.x; /* ' */ %name%; /* ' */", + ignored -> injection, bindings::put); + + assertEquals("obj?.x; /* ' */ __advancedCorePlaceholder0; /* ' */", prepared); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + assertFalse(prepared.contains(injection)); + } + + @Test + void lineCommentsCannotCreateFakeStringRangeAroundExecutablePlaceholder() { + HashMap bindings = new HashMap<>(); + String injection = "Bukkit.dispatchCommand(Console, 'op attacker')"; + + String prepared = JavascriptPlaceholderBinder.bind("obj?.x; // '\n%name%; // '\n", + ignored -> injection, bindings::put); + + assertEquals("obj?.x; // '\n__advancedCorePlaceholder0; // '\n", prepared); + assertEquals(injection, bindings.get("__advancedCorePlaceholder0")); + assertFalse(prepared.contains(injection)); + } + +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java new file mode 100644 index 000000000..72b54b42c --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderResolutionPriorityTest.java @@ -0,0 +1,67 @@ +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.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +import java.util.Map; + +import org.bukkit.OfflinePlayer; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.tests.BaseTest; + +import me.clip.placeholderapi.PlaceholderAPI; + +class JavascriptPlaceholderResolutionPriorityTest { + + @Test + void customPlaceholderWinsSameNamedPapiTokenThenExpandsPapiInsideCustomValue() { + AdvancedCorePlugin plugin = BaseTest.getInstance().plugin; + OfflinePlayer player = mock(OfflinePlayer.class); + JavascriptEngine engine = new JavascriptEngine(); + + when(plugin.isPlaceHolderAPIEnabled()).thenReturn(true); + + try (MockedStatic papiStatic = mockStatic(PlaceholderAPI.class)) { + papiStatic.when(() -> PlaceholderAPI.setPlaceholders(player, "%reward_alias%")) + .thenReturn("CustomName"); + papiStatic.when(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%")) + .thenReturn("PapiName"); + + String prepared = JavascriptPlaceholderBinder.bind("'%player_name%' == 'CustomName'", player, + Map.of("player_name", "%reward_alias%"), engine); + + assertEquals("'CustomName' == 'CustomName'", prepared); + papiStatic.verify(() -> PlaceholderAPI.setPlaceholders(player, "%reward_alias%")); + papiStatic.verify(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%"), never()); + } + } + + @Test + void decodedCustomValueStillExpandsNestedPapiToken() { + AdvancedCorePlugin plugin = BaseTest.getInstance().plugin; + OfflinePlayer player = mock(OfflinePlayer.class); + JavascriptEngine engine = new JavascriptEngine(); + + when(plugin.isPlaceHolderAPIEnabled()).thenReturn(true); + String encoded = JavascriptPlaceholderValue.encode("%player_name%"); + + try (MockedStatic papiStatic = mockStatic(PlaceholderAPI.class)) { + papiStatic.when(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%")) + .thenReturn("Ben"); + + String prepared = JavascriptPlaceholderBinder.bind("'" + encoded + "' == 'Ben'", player, + Map.of(), engine); + + assertEquals("'Ben' == 'Ben'", prepared); + assertFalse(prepared.contains("%player_name%")); + papiStatic.verify(() -> PlaceholderAPI.setPlaceholders(player, "%player_name%")); + } + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderRuntimeAvailabilityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderRuntimeAvailabilityTest.java new file mode 100644 index 000000000..c4b8ba6d4 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderRuntimeAvailabilityTest.java @@ -0,0 +1,14 @@ +package com.bencodez.advancedcore.api.javascript; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +class JavascriptPlaceholderRuntimeAvailabilityTest { + + @Test + void nashornParserIsAvailableToAdvancedCore() { + assertDoesNotThrow(() -> Class.forName("org.openjdk.nashorn.api.tree.Parser", false, + JavascriptPlaceholderBinder.class.getClassLoader())); + } +} 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)); + } +}