diff --git a/build.gradle b/build.gradle index 56a9bb28fe..5ed34e796f 100644 --- a/build.gradle +++ b/build.gradle @@ -160,7 +160,9 @@ dependencies { implementation 'ch.digitalfondue.vatchecker:vatchecker:1.6.0' implementation 'ch.digitalfondue.basicxlsx:basicxlsx:0.7.1' implementation 'org.imgscalr:imgscalr-lib:4.2' - implementation 'org.mozilla:rhino-runtime:1.7.15.1' + implementation 'io.roastedroot:quickjs4j:0.0.17' + implementation 'io.roastedroot:quickjs4j-annotations:0.0.17' + annotationProcessor 'io.roastedroot:quickjs4j-processor:0.0.17' implementation 'com.google.auth:google-auth-library-oauth2-http:1.41.0' testImplementation 'org.testcontainers:testcontainers' diff --git a/src/main/java/alfio/extension/exception/OutOfBoundariesException.java b/src/main/java/alfio/extension/ConsoleApi.java similarity index 57% rename from src/main/java/alfio/extension/exception/OutOfBoundariesException.java rename to src/main/java/alfio/extension/ConsoleApi.java index 634d917028..40ae337603 100644 --- a/src/main/java/alfio/extension/exception/OutOfBoundariesException.java +++ b/src/main/java/alfio/extension/ConsoleApi.java @@ -14,18 +14,25 @@ * You should have received a copy of the GNU General Public License * along with alf.io. If not, see . */ -package alfio.extension.exception; +package alfio.extension; -/** - * Exception thrown if some out of boundaries usage (forbidden methods calls) happens when executing a script. - */ -public class OutOfBoundariesException extends AlfioScriptingException { +import io.roastedroot.quickjs4j.annotations.Builtins; +import io.roastedroot.quickjs4j.annotations.HostFunction; - public OutOfBoundariesException(String message) { - super(message); - } +@Builtins("console") +class ConsoleApi { + private final ConsoleLogger logger; - public OutOfBoundariesException(String message, Throwable cause) { - super(message, cause); + ConsoleApi(ConsoleLogger logger) { + this.logger = logger; } + + @HostFunction + public void log(String msg) { logger.log(msg); } + + @HostFunction + public void warn(String msg) { logger.warn(msg); } + + @HostFunction + public void error(String msg) { logger.error(msg); } } diff --git a/src/main/java/alfio/extension/ConsoleLogger.java b/src/main/java/alfio/extension/ConsoleLogger.java index ff2ea25a16..bd5aa758f0 100644 --- a/src/main/java/alfio/extension/ConsoleLogger.java +++ b/src/main/java/alfio/extension/ConsoleLogger.java @@ -55,7 +55,6 @@ private static String composeMessage(Object first, Object... others) { if(others != null) { parameterPlaceholders = " %s".repeat(others.length); paramObjects = Stream.concat(Stream.of(first), Arrays.stream(others)) - .map(ExtensionUtils::unwrap) .toArray(); } else { paramObjects = new Object[] {first}; diff --git a/src/main/java/alfio/extension/support/SandboxNativeJavaList.java b/src/main/java/alfio/extension/ExtensionLoggerApi.java similarity index 51% rename from src/main/java/alfio/extension/support/SandboxNativeJavaList.java rename to src/main/java/alfio/extension/ExtensionLoggerApi.java index cf6eb36b6b..1ee02cad66 100644 --- a/src/main/java/alfio/extension/support/SandboxNativeJavaList.java +++ b/src/main/java/alfio/extension/ExtensionLoggerApi.java @@ -14,23 +14,28 @@ * You should have received a copy of the GNU General Public License * along with alf.io. If not, see . */ -package alfio.extension.support; +package alfio.extension; -import alfio.extension.exception.OutOfBoundariesException; -import org.mozilla.javascript.NativeJavaList; -import org.mozilla.javascript.Scriptable; +import io.roastedroot.quickjs4j.annotations.Builtins; +import io.roastedroot.quickjs4j.annotations.HostFunction; -public class SandboxNativeJavaList extends NativeJavaList { - public SandboxNativeJavaList(Scriptable scope, Object map) { - super(scope, map); +@Builtins("extensionLogger") +class ExtensionLoggerApi { + private final ExtensionLogger logger; + + ExtensionLoggerApi(ExtensionLogger logger) { + this.logger = logger; } - @Override - public Object get(String name, Scriptable start) { - if (name.equals("getClass")) { - throw new OutOfBoundariesException("Out of boundaries class use."); - } + @HostFunction + public void logInfo(String msg) { logger.logInfo(msg); } - return super.get(name, start); - } + @HostFunction + public void logWarning(String msg) { logger.logWarning(msg); } + + @HostFunction + public void logError(String msg) { logger.logError(msg); } + + @HostFunction + public void logSuccess(String msg) { logger.logSuccess(msg); } } diff --git a/src/main/java/alfio/extension/ExtensionService.java b/src/main/java/alfio/extension/ExtensionService.java index ae273aaba8..a875704b2f 100644 --- a/src/main/java/alfio/extension/ExtensionService.java +++ b/src/main/java/alfio/extension/ExtensionService.java @@ -54,9 +54,9 @@ @AllArgsConstructor public class ExtensionService { - private static final String EVALUATE_RESULT = "res = GSON.fromJson(JSON.stringify(res), returnClass);"; - private static final String PROCESS_EXTENSION_RESULT = "var res = executeScript(extensionEvent); " + EVALUATE_RESULT; - private static final String PROCESS_CAPABILITY_RESULT = "var res = executeCapability(capability); " + EVALUATE_RESULT; + private static final String SET_RESULT = "alfio_internal.setResult(JSON.stringify(res));"; + private static final String PROCESS_EXTENSION_RESULT = "var res = executeScript(extensionEvent); " + SET_RESULT; + private static final String PROCESS_CAPABILITY_RESULT = "var res = executeCapability(capability); " + SET_RESULT; private static final String EXECUTE_SCRIPT = "executeScript(extensionEvent);"; private static final String OUTPUT = "output"; private static final String EXECUTION_KEY = "executionKey"; @@ -112,7 +112,7 @@ private static final class NoopExtensionLogger implements ExtensionLogger { private ExtensionMetadata getMetadata(String name, String script) { return scriptingExecutionService.executeScript( name, - script + "\n;GSON.fromJson(JSON.stringify(getScriptMetadata()), returnClass);", //<- ugly hack, but the interop java<->js is simpler that way... + script + "\n;alfio_internal.setResult(JSON.stringify(getScriptMetadata()));", Collections.emptyMap(), ExtensionMetadata.class, new NoopExtensionLogger()); } @@ -121,8 +121,6 @@ private ExtensionMetadata getMetadata(String name, String script) { public void createOrUpdate(String previousPath, String previousName, Extension script) { Validate.notBlank(script.getName(), "Name is mandatory"); Validate.notBlank(script.getPath(), "Path must be defined"); - ScriptValidation validation = new ScriptValidation(script.getScript()); - validation.validate(); String hash = DigestUtils.sha256Hex(script.getScript()); ExtensionMetadata extensionMetadata = getMetadata(script.getName(), script.getScript()); diff --git a/src/main/java/alfio/extension/ExtensionUtils.java b/src/main/java/alfio/extension/ExtensionUtils.java index 25fc13c668..f6f2b50cda 100644 --- a/src/main/java/alfio/extension/ExtensionUtils.java +++ b/src/main/java/alfio/extension/ExtensionUtils.java @@ -22,7 +22,6 @@ import org.apache.commons.codec.digest.HmacAlgorithms; import org.apache.commons.codec.digest.HmacUtils; import org.apache.commons.lang3.StringUtils; -import org.mozilla.javascript.*; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; @@ -31,7 +30,8 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.*; +import java.util.Arrays; +import java.util.Base64; import java.util.stream.Collectors; public class ExtensionUtils { @@ -83,56 +83,8 @@ public static String base64UrlSafe(String input) { .encodeToString(input.getBytes(StandardCharsets.UTF_8)); } - /** - * Unwrap / convert all Scriptable object to their java native counterpart and transform to json - * - * - * @param o - * @return - */ public static String convertToJson(Object o) { - return JSON_SERIALIZER.toJson(unwrap(o)); - } - - static Object unwrap(Object o) { - if (o instanceof Scriptable) { - if (o instanceof NativeArray na) { - List res = new ArrayList<>(); - for (var a : na) { - res.add(unwrap(a)); - } - return res; - } else if (o instanceof NativeJavaObject object) { - return object.unwrap(); - } else if (o instanceof NativeObject na) { - Map res = new LinkedHashMap<>(); - for (var kv : na.entrySet()) { - res.put(kv.getKey(), unwrap(kv.getValue())); - } - return res; - } else if (o instanceof IdScriptableObject object) { - return parseIdScriptableObject(object); - } - } else if (o instanceof CharSequence) { - return o.toString(); - } - return o; - } - - private static Object parseIdScriptableObject(IdScriptableObject object) { - var className = object.getClassName(); - return switch (className) { - case "String" -> ScriptRuntime.toCharSequence(object); - case "Boolean" -> Context.jsToJava(object, Boolean.class); - case "Date" -> Context.jsToJava(object, Date.class); - default -> - // better safe than sorry: we ignore all the unknown objects - null; - }; + return JSON_SERIALIZER.toJson(o); } /** diff --git a/src/main/java/alfio/extension/ExtensionUtilsApi.java b/src/main/java/alfio/extension/ExtensionUtilsApi.java new file mode 100644 index 0000000000..98514df875 --- /dev/null +++ b/src/main/java/alfio/extension/ExtensionUtilsApi.java @@ -0,0 +1,60 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ +package alfio.extension; + +import io.roastedroot.quickjs4j.annotations.Builtins; +import io.roastedroot.quickjs4j.annotations.HostFunction; + +import java.util.List; + +@Builtins("ExtensionUtils") +@SuppressWarnings({"rawtypes", "unchecked"}) +class ExtensionUtilsApi { + + @HostFunction + public String md5(String str) { + return ExtensionUtils.md5(str); + } + + @HostFunction("base64UrlSafe") + public String base64UrlSafe(String str) { + return ExtensionUtils.base64UrlSafe(str); + } + + @HostFunction + public String convertToJson(String str) { + // identity — JS already stringified the object before calling + return str; + } + + @HostFunction + public String format(String str, List params) { + String[] arr = params != null ? (String[]) params.toArray(new String[0]) : new String[0]; + return ExtensionUtils.format(str, arr); + } + + @HostFunction + public String computeHMAC(String secret, List parts) { + String[] arr = parts != null ? (String[]) parts.toArray(new String[0]) : new String[0]; + return ExtensionUtils.computeHMAC(secret, arr); + } + + @HostFunction + public String formatDateTime(String dateTime, String pattern, boolean utc) { + return ExtensionUtils.formatDateTime(dateTime, pattern, utc); + } +} diff --git a/src/main/java/alfio/extension/exception/ScriptNotValidException.java b/src/main/java/alfio/extension/InternalApi.java similarity index 64% rename from src/main/java/alfio/extension/exception/ScriptNotValidException.java rename to src/main/java/alfio/extension/InternalApi.java index ae4beda989..42b709a6eb 100644 --- a/src/main/java/alfio/extension/exception/ScriptNotValidException.java +++ b/src/main/java/alfio/extension/InternalApi.java @@ -14,13 +14,21 @@ * You should have received a copy of the GNU General Public License * along with alf.io. If not, see . */ -package alfio.extension.exception; +package alfio.extension; -/** - * Exception thrown if script is not valid for execution. - */ -public class ScriptNotValidException extends AlfioScriptingException { - public ScriptNotValidException(String message) { - super(message); +import io.roastedroot.quickjs4j.annotations.Builtins; +import io.roastedroot.quickjs4j.annotations.HostFunction; + +@Builtins("alfio_internal") +class InternalApi { + private String resultJson; + + @HostFunction + public void setResult(String json) { + this.resultJson = json; + } + + String getResultJson() { + return resultJson; } } diff --git a/src/main/java/alfio/extension/JSErrorReporter.java b/src/main/java/alfio/extension/JSErrorReporter.java deleted file mode 100644 index d699678d80..0000000000 --- a/src/main/java/alfio/extension/JSErrorReporter.java +++ /dev/null @@ -1,51 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ - -package alfio.extension; - -import org.mozilla.javascript.ErrorReporter; -import org.mozilla.javascript.EvaluatorException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * - * @author Ram Kulkarni - * http://ramkulkarni.com/blog/parsing-javascript-code-using-mozilla-rhino/ - * - * Implements ErrorReporter interface of Rhino and prints syntax errors in the JS script. - */ -public class JSErrorReporter implements ErrorReporter { - - private static final Logger log = LoggerFactory.getLogger(JSErrorReporter.class); - - @Override - public void warning(String message, String sourceName, int line, String lineSource, int lineOffset) { - log.warn("Warning : " + message); - } - - @Override - public EvaluatorException runtimeError(String message, String sourceName, int line, String lineSource, int lineOffset) { - return new EvaluatorException(message, sourceName, line, lineSource, lineOffset); - } - - @Override - public void error(String message, String sourceName, int line, String lineSource, int lineOffset) { - log.warn("Error : " + message); - } - -} diff --git a/src/main/java/alfio/extension/JSNodeVisitor.java b/src/main/java/alfio/extension/JSNodeVisitor.java deleted file mode 100644 index 7d0a0f975f..0000000000 --- a/src/main/java/alfio/extension/JSNodeVisitor.java +++ /dev/null @@ -1,116 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ - -package alfio.extension; - -import alfio.extension.exception.ScriptNotValidException; -import org.mozilla.javascript.Token; -import org.mozilla.javascript.ast.*; - -import java.util.ArrayList; - -/** - * - * @author Ram Kulkarni - * http://ramkulkarni.com/blog/parsing-javascript-code-using-mozilla-rhino/ - * - * Implements Rhino’s NodeVisitor interface and builds hierarchy of JSSymbol - */ -class JSNodeVisitor implements NodeVisitor { - private JSSymbol root = null; - private final ArrayList functionCalls = new ArrayList<>(); - - @Override - public boolean visit(AstNode node) { - if (node == null) { - return false; - } - checkNode(node); - return true; - } - - private void checkNode(AstNode node) { - if (root == null) { - root = new JSSymbol(node); - return; - } - int nodeType = node.getType(); - // we will track variables and functions, function calls, loops, - // with statement, labeled statement, level of function calls - if (nodeType != Token.FUNCTION - && nodeType != Token.WITH - && nodeType != Token.LABEL - && nodeType != Token.VAR - && nodeType != Token.NAME - && nodeType != Token.WHILE - && nodeType != Token.DO - && nodeType != Token.OBJECTLIT - && nodeType != Token.CALL - && nodeType != Token.GETPROP - && nodeType != Token.EXPR_VOID) { - return; - } - if (node.getType() == Token.VAR && !(node instanceof VariableInitializer)) { - return; - } - // keep track of function calls - if (node instanceof FunctionCall call) { - AstNode target = call.getTarget(); - if (!(target instanceof PropertyGet)) { - Name name = (Name) target; - // keep all function calls inside an ArrayList - functionCalls.add(name.getIdentifier()); - // go back in the script and find the parent i.e. the function in which this node is inside - AstNode parentNode = node.getParent(); - while (parentNode != null) { - if (parentNode instanceof FunctionNode functionNode) { - Name parentName = functionNode.getFunctionName(); - String id = parentName.getIdentifier(); - // when the function name is found, check if it was called from somewhere else - // if this function is called from another place and it contains another function call, throw an exception - if (functionCalls.contains(id)) { - throw new ScriptNotValidException("Script not valid. Cannot call nested functions: "+name.getIdentifier()); - } - break; - } - parentNode = parentNode.getParent(); - } - } - } - if (node instanceof WhileLoop - || node instanceof DoLoop - || node instanceof WithStatement - || node instanceof LabeledStatement - || (node instanceof PropertyGet get && get.getRight().getString().equals("System")) - || (node instanceof PropertyGet propertyGet && propertyGet.getRight().getString().equals("getClass")) - || (node instanceof Name && node.getString().equals("newInstance"))) { - throw new ScriptNotValidException(""" - Script not valid. One or more of the following components have been detected:\s - - while() Loop - - with() Statement - - a labeled statement - - Access to java.lang.System - - Access to Object.getClass() - - Java reflection usage\ - """); - } - } - - public JSSymbol getRoot() { - return root; - } -} diff --git a/src/main/java/alfio/extension/JSON.java b/src/main/java/alfio/extension/JSON.java deleted file mode 100644 index 4cde4f3875..0000000000 --- a/src/main/java/alfio/extension/JSON.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ -package alfio.extension; - -import alfio.util.Json; -import com.google.gson.reflect.TypeToken; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Type; -import java.util.Map; - -public class JSON { - - private static final Logger log = LoggerFactory.getLogger(JSON.class); - private static final Type PARSE_RETURN_TYPE = new TypeToken>(){}.getType(); - - private JSON() { - } - - public static String stringify(Object o) { - return ExtensionUtils.convertToJson(o); - } - - public static String stringify(Object o, Object replacer) { - log.warn("JSON.stringify: ignoring replacer param"); - return stringify(o); - } - - public static String stringify(Object o, Object replacer, Object space) { - log.warn("JSON.stringify: ignoring replacer and space params"); - return stringify(o); - } - - public static Map parse(String s) { - try { - return Json.GSON.fromJson(s, PARSE_RETURN_TYPE); - } catch(Exception e) { - log.error("malformed JSON", e); - return null; - } - } - - public static Map parse(String s, Object reviver) { - log.warn("JSON.parse: Ignoring reviver param"); - return parse(s); - } -} diff --git a/src/main/java/alfio/extension/JSSymbol.java b/src/main/java/alfio/extension/JSSymbol.java deleted file mode 100644 index d835b457bf..0000000000 --- a/src/main/java/alfio/extension/JSSymbol.java +++ /dev/null @@ -1,96 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ - -package alfio.extension; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.mozilla.javascript.Token; -import org.mozilla.javascript.ast.*; - -/** - * - * @author Ram Kulkarni - * http://ramkulkarni.com/blog/parsing-javascript-code-using-mozilla-rhino/ - * - * This class holds reference to AST node and child elements. - */ -class JSSymbol { - private final AstNode node; - private final ArrayList children = new ArrayList<>(); - private final Map localVars = new HashMap<>(); - private JSSymbol parent = null; - - public JSSymbol(AstNode node) { - this.node = node; - if (node instanceof FunctionNode funcNode) { - List args = funcNode.getParams(); - if (args != null) { - for (AstNode argNode : args) { - addChild(argNode); - } - } - } - } - - public int getType() { - return node.getType(); - } - - public void addChild(JSSymbol child) { - if (child.getType() == Token.VAR) { - //check if it is already added - AstNode childNode = child.getNode(); - if (childNode instanceof VariableInitializer initializer) { - String varName = ((Name)initializer.getTarget()).getIdentifier(); - if (localVars.containsKey(varName)) { - return; - } - localVars.put(varName, child); - } - } - children.add(child); - child.setParent(this); - } - - public void addChild (AstNode node) { - addChild(new JSSymbol(node)); - } - - public ArrayList getChildren() { - return new ArrayList<>(children); - } - - public AstNode getNode() { - return node; - } - - public boolean childExist (String name) { - return localVars.containsKey(name); - } - - public JSSymbol getParent() { - return parent; - } - - public void setParent(JSSymbol parent) { - this.parent = parent; - } -} diff --git a/src/main/java/alfio/extension/LogApi.java b/src/main/java/alfio/extension/LogApi.java new file mode 100644 index 0000000000..a0529cc7d6 --- /dev/null +++ b/src/main/java/alfio/extension/LogApi.java @@ -0,0 +1,42 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ +package alfio.extension; + +import io.roastedroot.quickjs4j.annotations.Builtins; +import io.roastedroot.quickjs4j.annotations.HostFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Builtins("log") +class LogApi { + private static final Logger log = LoggerFactory.getLogger(LogApi.class); + + @HostFunction + public void warn(String msg) { log.warn(msg); } + + @HostFunction + public void info(String msg) { log.info(msg); } + + @HostFunction + public void error(String msg) { log.error(msg); } + + @HostFunction + public void debug(String msg) { log.debug(msg); } + + @HostFunction + public void trace(String msg) { log.trace(msg); } +} diff --git a/src/main/java/alfio/extension/ScriptValidation.java b/src/main/java/alfio/extension/ScriptValidation.java deleted file mode 100644 index 3ba53e28e3..0000000000 --- a/src/main/java/alfio/extension/ScriptValidation.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ - -package alfio.extension; - -import org.mozilla.javascript.CompilerEnvirons; -import org.mozilla.javascript.Parser; -import org.mozilla.javascript.ast.AstRoot; - -// http://ramkulkarni.com/blog/understanding-ast-created-by-mozilla-rhino-parser/ -// http://ramkulkarni.com/blog/parsing-javascript-code-using-mozilla-rhino/ - -// https://github.com/mozilla/rhino/tree/master/src/org/mozilla/javascript/ast -public class ScriptValidation { - - private final String script; - - public ScriptValidation(String script) { - this.script = script; - } - - public void validate() { - CompilerEnvirons env = new CompilerEnvirons(); - env.setRecoverFromErrors(true); - - Parser parser = new Parser(env, new JSErrorReporter()); - AstRoot rootNode = parser.parse(script, null, 0); - - JSNodeVisitor nodeVisitor = new JSNodeVisitor(); - rootNode.visit(nodeVisitor); - } - -} - diff --git a/src/main/java/alfio/extension/ScriptingExecutionService.java b/src/main/java/alfio/extension/ScriptingExecutionService.java index d0d346e1fe..776d878c23 100644 --- a/src/main/java/alfio/extension/ScriptingExecutionService.java +++ b/src/main/java/alfio/extension/ScriptingExecutionService.java @@ -18,10 +18,9 @@ package alfio.extension; import alfio.extension.exception.AlfioScriptingException; +import alfio.extension.exception.ExecutionTimeoutException; import alfio.extension.exception.InvalidScriptException; -import alfio.extension.exception.OutOfBoundariesException; import alfio.extension.exception.ScriptRuntimeException; -import alfio.extension.support.SandboxContextFactory; import alfio.manager.system.AdminJobManager; import alfio.repository.system.AdminJobQueueRepository; import alfio.util.ClockProvider; @@ -29,7 +28,9 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.RemovalCause; -import org.mozilla.javascript.*; +import io.roastedroot.quickjs4j.core.Engine; +import io.roastedroot.quickjs4j.core.GuestException; +import io.roastedroot.quickjs4j.core.Runner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -41,8 +42,8 @@ import java.util.*; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeoutException; import java.util.function.Supplier; -import java.util.stream.Collectors; import static alfio.manager.system.AdminJobExecutor.JobName.EXECUTE_EXTENSION; @@ -64,9 +65,12 @@ public class ScriptingExecutionService { static final String CONNECT_EXCEPTION_MESSAGE = "Cannot connect to remote service. Please check your configuration"; static final String DEFAULT_ERROR_MESSAGE = "Error while executing extension. Please retry."; private static final Logger log = LoggerFactory.getLogger(ScriptingExecutionService.class); + private static final int EXECUTION_TIMEOUT_MS = 15_000; private final Supplier executorSupplier; - private final ScriptableObject sealedScope; + private final SimpleHttpClientApi simpleHttpClientApi; + private final LogApi logApi = new LogApi(); + private final ExtensionUtilsApi extensionUtilsApi = new ExtensionUtilsApi(); private final AdminJobQueueRepository adminJobQueueRepository; private final Cache asyncExecutors = Caffeine.newBuilder() @@ -78,29 +82,47 @@ public class ScriptingExecutionService { }) .build(); - static { - ContextFactory.initGlobal(new SandboxContextFactory()); - } - + // Backward-compat shims: HashMap constructor and GSON alias. + // The variadic wrappers for ExtensionUtils.format/computeHMAC collect + // the JS arguments array and forward them to the proper builtins. + private static final String JS_PRELUDE = """ + function HashMap() { return {}; } + var GSON = { + toJson: function(obj) { return JSON.stringify(obj); }, + fromJson: function(str) { return JSON.parse(str); } + }; + var _origFormat = ExtensionUtils.format; + ExtensionUtils.format = function(str) { + return _origFormat(str, Array.prototype.slice.call(arguments, 1)); + }; + var _origHMAC = ExtensionUtils.computeHMAC; + ExtensionUtils.computeHMAC = function(secret) { + return _origHMAC(secret, Array.prototype.slice.call(arguments, 1)); + }; + ExtensionUtils.convertToJson = function(obj) { return JSON.stringify(obj); }; + (function() { + function _variadicWrap(fn) { + return function() { + fn(Array.prototype.slice.call(arguments).join(' ')); + }; + } + var _methods = ['log', 'warn', 'error']; + for (var _i = 0; _i < _methods.length; _i++) { + console[_methods[_i]] = _variadicWrap(console[_methods[_i]]); + } + var _logMethods = ['warn', 'info', 'error', 'debug', 'trace']; + for (var _i = 0; _i < _logMethods.length; _i++) { + log[_logMethods[_i]] = _variadicWrap(log[_logMethods[_i]]); + } + })(); + """; public ScriptingExecutionService(HttpClient httpClient, AdminJobQueueRepository adminJobQueueRepository, Supplier executorSupplier) { this.executorSupplier = executorSupplier; this.adminJobQueueRepository = adminJobQueueRepository; - var simpleHttpClient = new SimpleHttpClient(httpClient); - Context cx = ContextFactory.getGlobal().enterContext(); - try { - sealedScope = cx.initSafeStandardObjects(null, true); - sealedScope.put("log", sealedScope, log); - sealedScope.put("GSON", sealedScope, Json.GSON); - sealedScope.put("JSON", sealedScope, new NativeJavaClass(sealedScope, JSON.class)); - sealedScope.put("simpleHttpClient", sealedScope, simpleHttpClient); - sealedScope.put("HashMap", sealedScope, new NativeJavaClass(sealedScope, HashMap.class)); - sealedScope.put("ExtensionUtils", sealedScope, new NativeJavaClass(sealedScope, ExtensionUtils.class)); - } finally { - Context.exit(); - } + this.simpleHttpClientApi = new SimpleHttpClientApi(new SimpleHttpClient(httpClient)); } public T executeScript(String name, String hash, Supplier scriptFetcher, Map params, Class clazz, ExtensionLogger extensionLogger) { @@ -118,10 +140,7 @@ public void executeScriptAsync(String path, try { executeScript(name, hash, scriptFetcher, params, Object.class, extensionLogger); } catch (AlfioScriptingException | IllegalStateException ex) { - // we got an error while executing the script. We must now re-schedule the script to be executed again - // at a later time var paramsCopy = new HashMap<>(params); - // do not persist extension parameters because they could contain sensitive information paramsCopy.remove(EXTENSION_CONFIGURATION_PARAMETERS); Map metadata = Map.of( EXTENSION_NAME, name, @@ -135,7 +154,6 @@ public void executeScriptAsync(String path, ).apply(adminJobQueueRepository); if(!scheduled) { log.warn("Cannot schedule extension {} for retry", name); - // throw exception only if we can't schedule the extension for later execution throw ex; } else { log.warn("Error while executing extension "+name + ", which has been scheduled for retry", ex); @@ -148,82 +166,57 @@ public T executeScript(String name, String script, Map param return executeScriptFinally(name, script, params, clazz, extensionLogger); } - public static class JavaClassInterop { - - private final Map> mapping; - private final Scriptable scope; - - JavaClassInterop(Map> mapping, Scriptable scope) { - this.mapping = mapping; - this.scope = scope; - } - - public NativeJavaClass type(String clazz) { - if (mapping.containsKey(clazz)) { - return new NativeJavaClass(scope, mapping.get(clazz)); - } else { - throw new IllegalArgumentException("Type "+clazz+" is not recognized"); - } + private T executeScriptFinally(String name, String script, Map params, Class clazz, ExtensionLogger extensionLogger) { + if (params == null) { + params = Collections.emptyMap(); } - } - @SuppressWarnings("unchecked") - private T executeScriptFinally(String name, String script, Map params, Class clazz, ExtensionLogger extensionLogger) { - try (var cx = Context.enter()) { - if(params == null) { - params = Collections.emptyMap(); - } + var internalApi = new InternalApi(); + var consoleApi = new ConsoleApi(new ConsoleLogger(extensionLogger)); + var extLoggerApi = new ExtensionLoggerApi(extensionLogger); - Scriptable scope = cx.newObject(sealedScope); - scope.setPrototype(sealedScope); - scope.setParentScope(null); - scope.put("extensionLogger", scope, extensionLogger); - scope.put("console", scope, new ConsoleLogger(extensionLogger)); + String paramDeclarations = buildParamDeclarations(params); + String fullScript = paramDeclarations + "\n" + JS_PRELUDE + "\n" + script; - // retrocompatibility - scope.put("Java", scope, new JavaClassInterop(Map.of("alfio.model.CustomerName", alfio.model.CustomerName.class), scope)); + var engine = Engine.builder() + .addBuiltins(LogApi_Builtins.toBuiltins(logApi)) + .addBuiltins(ExtensionLoggerApi_Builtins.toBuiltins(extLoggerApi)) + .addBuiltins(ConsoleApi_Builtins.toBuiltins(consoleApi)) + .addBuiltins(SimpleHttpClientApi_Builtins.toBuiltins(simpleHttpClientApi)) + .addBuiltins(ExtensionUtilsApi_Builtins.toBuiltins(extensionUtilsApi)) + .addBuiltins(InternalApi_Builtins.toBuiltins(internalApi)) + .build(); - scope.put("returnClass", scope, clazz); + try (var runner = Runner.builder() + .withEngine(engine) + .withTimeoutMs(EXECUTION_TIMEOUT_MS) + .build()) { - for (var entry : params.entrySet()) { - var value = entry.getValue(); - if(entry.getKey().equals(EXTENSION_CONFIGURATION_PARAMETERS)) { - scope.put(entry.getKey(), scope, convertExtensionParameters(scope, value)); - } else { - scope.put(entry.getKey(), scope, Context.javaToJS(value, scope)); - } - } - Object res; - res = cx.evaluateString(scope, script, name, 1, null); + runner.compileAndExec(fullScript); extensionLogger.logSuccess("Script executed successfully."); - if (res instanceof NativeJavaObject nativeRes) { - return (T) nativeRes.unwrap(); - } else if(clazz.isInstance(res)) { - return (T) res; - } else { - return null; + + if (internalApi.getResultJson() != null && clazz != Void.class && clazz != void.class) { + return Json.OBJECT_MAPPER.readValue(internalApi.getResultJson(), clazz); } - } catch (EcmaError ex) { + return null; + } catch (GuestException ex) { + String message = ex.getMessage(); + log.warn("Runtime error in script " + name, ex); + extensionLogger.logError(message); + throw new ScriptRuntimeException(message, ex); + } catch (IllegalArgumentException ex) { log.warn("Syntax error detected in script " + name, ex); - extensionLogger.logError("Syntax error while executing script: " + ex.getMessage() + "(" + ex.lineNumber() + ":" + ex.columnNumber() + ")"); + extensionLogger.logError("Syntax error while executing script: " + ex.getMessage()); throw new InvalidScriptException("Syntax error in script " + name); - } catch (WrappedException ex) { - var actualException = ex.getWrappedException(); + } catch (RuntimeException ex) { + if (ex.getCause() instanceof TimeoutException) { + throw new ExecutionTimeoutException("Script execution timeout."); + } + var actualException = ex.getCause() != null ? ex.getCause() : ex; var message = getErrorMessage(actualException); extensionLogger.logError("Error from script: " + message); throw new AlfioScriptingException(message, actualException); - } catch (JavaScriptException ex) { - String message; - if (ex.getValue() != null) { - message = ex.details(); - } else { - message = ex.getMessage(); - } - extensionLogger.logError(message); - throw new ScriptRuntimeException(message, ex); - } catch (OutOfBoundariesException ex) { - throw ex; - } catch (Exception ex) { // + } catch (Exception ex) { extensionLogger.logError("Error while executing script: " + ex.getMessage()); throw new IllegalStateException(ex); } @@ -247,9 +240,21 @@ String getErrorMessage(Throwable ex) { return Objects.requireNonNullElse(lastMessage, DEFAULT_ERROR_MESSAGE); } - private Object convertExtensionParameters(Scriptable context, Object extensionParameters) { - return ((Map) extensionParameters).entrySet().stream() - .map(entry -> Map.entry(entry.getKey(), ScriptRuntime.toObject(context, entry.getValue()))) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + private String buildParamDeclarations(Map params) { + var sb = new StringBuilder(); + for (var entry : params.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + String jsonValue; + try { + jsonValue = Json.OBJECT_MAPPER.writeValueAsString(value); + } catch (Exception e) { + log.warn("Cannot serialize param '{}' to JSON, skipping", key, e); + continue; + } + sb.append("var ").append(key).append(" = ").append(jsonValue).append(";\n"); + } + return sb.toString(); } + } diff --git a/src/main/java/alfio/extension/SimpleHttpClientApi.java b/src/main/java/alfio/extension/SimpleHttpClientApi.java new file mode 100644 index 0000000000..1797ddd5c5 --- /dev/null +++ b/src/main/java/alfio/extension/SimpleHttpClientApi.java @@ -0,0 +1,142 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ +package alfio.extension; + +import io.roastedroot.quickjs4j.annotations.Builtins; +import io.roastedroot.quickjs4j.annotations.HostFunction; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +@Builtins("simpleHttpClient") +@SuppressWarnings({"rawtypes", "unchecked"}) +class SimpleHttpClientApi { + private final SimpleHttpClient httpClient; + + SimpleHttpClientApi(SimpleHttpClient httpClient) { + this.httpClient = httpClient; + } + + @HostFunction + public SimpleHttpClientResponse get(String url, Map headers) { + try { + return httpClient.get(url, safe(headers)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HostFunction + public SimpleHttpClientResponse head(String url, Map headers) { + try { + return httpClient.head(url, safe(headers)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HostFunction + public SimpleHttpClientResponse post(String url, Map headers, Object body) { + try { + return httpClient.post(url, safe(headers), body); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HostFunction + public SimpleHttpClientResponse postJSON(String url, Map headers, Object body) { + try { + return httpClient.postJSON(url, safe(headers), body); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HostFunction + public SimpleHttpClientResponse postForm(String url, Map headers, Map params) { + try { + return httpClient.postForm(url, safe(headers), safe(params)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HostFunction + public SimpleHttpClientCachedResponse postFileAndSaveResponse(String url, Map headers, String file, String filename, String contentType) { + try { + return httpClient.postFileAndSaveResponse(url, safe(headers), file, filename, contentType); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HostFunction + public SimpleHttpClientCachedResponse postBodyAndSaveResponse(String url, Map headers, String content, String contentType) { + try { + return httpClient.postBodyAndSaveResponse(url, safe(headers), content, contentType); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HostFunction + public SimpleHttpClientResponse put(String url, Map headers, Object body) { + try { + return httpClient.put(url, safe(headers), body); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HostFunction + public SimpleHttpClientResponse patch(String url, Map headers, Object body) { + try { + return httpClient.patch(url, safe(headers), body); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HostFunction + public SimpleHttpClientResponse delete(String url, Map headers, Object body) { + try { + return httpClient.delete(url, safe(headers), body); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HostFunction + public SimpleHttpClientResponse method(String method, String url, Map headers, Object body) { + try { + return httpClient.method(method, url, safe(headers), body); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HostFunction + public String basicCredentials(String username, String password) { + return httpClient.basicCredentials(username, password); + } + + private static Map safe(Map map) { + return map != null ? map : Collections.emptyMap(); + } +} diff --git a/src/main/java/alfio/extension/SimpleHttpClientResponse.java b/src/main/java/alfio/extension/SimpleHttpClientResponse.java index a4613dae9e..d2b444b089 100644 --- a/src/main/java/alfio/extension/SimpleHttpClientResponse.java +++ b/src/main/java/alfio/extension/SimpleHttpClientResponse.java @@ -17,7 +17,6 @@ package alfio.extension; import alfio.util.Json; -import com.google.gson.JsonSyntaxException; import lombok.AllArgsConstructor; import lombok.Getter; @@ -47,8 +46,8 @@ public T getJsonBody(Class clazz) { private static T tryParse(String body, Class clazz) { try { - return Json.GSON.fromJson(body, clazz); - } catch (JsonSyntaxException jse) { + return Json.OBJECT_MAPPER.readValue(body, clazz); + } catch (Exception e) { return null; } } diff --git a/src/main/java/alfio/extension/support/SandboxContextFactory.java b/src/main/java/alfio/extension/support/SandboxContextFactory.java deleted file mode 100644 index 0924479d73..0000000000 --- a/src/main/java/alfio/extension/support/SandboxContextFactory.java +++ /dev/null @@ -1,78 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ -package alfio.extension.support; - -import alfio.extension.exception.ExecutionTimeoutException; -import org.mozilla.javascript.Callable; -import org.mozilla.javascript.Context; -import org.mozilla.javascript.ContextFactory; -import org.mozilla.javascript.Scriptable; - -// source: https://codeutopia.net/blog/2009/01/02/sandboxing-rhino-in-java/ -// https://www-archive.mozilla.org/rhino/apidocs/org/mozilla/javascript/contextfactory -public class SandboxContextFactory extends ContextFactory { - - // Custom Context to store execution time. - private static class MyContext extends Context { - long startTime; - - public MyContext(ContextFactory factory) { - super(factory); - - } - } - - @Override - protected Context makeContext() { - MyContext cx = new MyContext(ContextFactory.getGlobal()); - cx.setWrapFactory(new SandboxWrapFactory()); - // Use pure interpreter mode to allow for observeInstructionCount(Context, int) to work - cx.setOptimizationLevel(-1); - // Make Rhino runtime to call observeInstructionCount each 10000 bytecode instructions - cx.setInstructionObserverThreshold(10000); - return cx; - } - - @Override - protected void observeInstructionCount(Context cx, int instructionCount) { - MyContext mcx = (MyContext)cx; - long currentTime = System.currentTimeMillis(); - long executionTime = currentTime - mcx.startTime; - if (executionTime > 15*1000) { - // More than 15 seconds from Context creation time: - // it is time to stop the script. - // Throw Error instance to ensure that script will never - // get control back through catch or finally. - throw new ExecutionTimeoutException("Script execution timeout."); - } - } - - @Override - protected Object doTopCall(Callable callable, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - MyContext mcx = (MyContext) cx; - mcx.startTime = System.currentTimeMillis(); - return super.doTopCall(callable, cx, scope, thisObj, args); - } - - @Override - protected boolean hasFeature(Context cx, int featureIndex) { - if (featureIndex == Context.FEATURE_ENABLE_JAVA_MAP_ACCESS) { - return true; - } - return super.hasFeature(cx, featureIndex); - } -} diff --git a/src/main/java/alfio/extension/support/SandboxNativeJavaMap.java b/src/main/java/alfio/extension/support/SandboxNativeJavaMap.java deleted file mode 100644 index ed40095c2b..0000000000 --- a/src/main/java/alfio/extension/support/SandboxNativeJavaMap.java +++ /dev/null @@ -1,47 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ -package alfio.extension.support; - -import alfio.extension.exception.OutOfBoundariesException; -import org.mozilla.javascript.NativeJavaMap; -import org.mozilla.javascript.Scriptable; - -import java.util.Map; - -public class SandboxNativeJavaMap extends NativeJavaMap { - - private final Map map; - - public SandboxNativeJavaMap(Scriptable scope, Object map) { - super(scope, map); - this.map = (Map) map; - } - - @Override - public Object get(String name, Scriptable start) { - if (name.equals("getClass") && !map.containsKey(name)){ - throw new OutOfBoundariesException("Out of boundaries class use."); - } - - if (map.get(name) == null) { - // prevent NPE on Rhino when map has an explicit null value for a given key - return null; - } - - return super.get(name, start); - } -} diff --git a/src/main/java/alfio/extension/support/SandboxNativeJavaObject.java b/src/main/java/alfio/extension/support/SandboxNativeJavaObject.java deleted file mode 100644 index 20eeb88136..0000000000 --- a/src/main/java/alfio/extension/support/SandboxNativeJavaObject.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ -package alfio.extension.support; - -import alfio.extension.exception.OutOfBoundariesException; -import org.mozilla.javascript.NativeJavaObject; -import org.mozilla.javascript.Scriptable; - -// source: https://codeutopia.net/blog/2009/01/02/sandboxing-rhino-in-java/ -public class SandboxNativeJavaObject extends NativeJavaObject { - public SandboxNativeJavaObject(Scriptable scope, Object javaObject, Class staticType) { - super(scope, javaObject, staticType); - } - - @Override - public Object get(String name, Scriptable start) { - if (name.equals("getClass")) { - throw new OutOfBoundariesException("Out of boundaries class use."); - } - - return super.get(name, start); - } -} diff --git a/src/main/java/alfio/extension/support/SandboxWrapFactory.java b/src/main/java/alfio/extension/support/SandboxWrapFactory.java deleted file mode 100644 index 2b44559c37..0000000000 --- a/src/main/java/alfio/extension/support/SandboxWrapFactory.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ -package alfio.extension.support; - -import org.mozilla.javascript.*; - -import java.util.List; -import java.util.Map; - -// source: https://codeutopia.net/blog/2009/01/02/sandboxing-rhino-in-java/ -public class SandboxWrapFactory extends WrapFactory { - - @Override - public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, Class staticType) { - if (List.class.isAssignableFrom(javaObject.getClass())) { - return new SandboxNativeJavaList(scope, javaObject); - } - if (Map.class.isAssignableFrom(javaObject.getClass())) { - return new SandboxNativeJavaMap(scope, javaObject); - } - return new SandboxNativeJavaObject(scope, javaObject, staticType); - } -} diff --git a/src/main/resources/alfio/extension/mailchimp.js b/src/main/resources/alfio/extension/mailchimp.js index 219b3457cd..bfa84887ee 100644 --- a/src/main/resources/alfio/extension/mailchimp.js +++ b/src/main/resources/alfio/extension/mailchimp.js @@ -23,8 +23,6 @@ function getScriptMetadata() { }; } -var CustomerName = Java.type('alfio.model.CustomerName'); - var MERGE_FIELDS = "merge-fields/"; var ALFIO_EVENT_KEY = "ALFIO_EKEY"; var APPLICATION_JSON = "application/json"; @@ -38,19 +36,19 @@ var LIST_MEMBERS = "members/"; */ function executeScript(scriptEvent) { if('TICKET_ASSIGNED' === scriptEvent) { - var customerName = new CustomerName(ticket.fullName, ticket.firstName, ticket.lastName, event.mustUseFirstAndLastName()); - subscribeUser(ticket.email, customerName, ticket.userLanguage, event); + var firstName = event.mustUseFirstAndLastName ? ticket.firstName : ticket.fullName; + subscribeUser(ticket.email, firstName, ticket.fullName, ticket.userLanguage, event); } else if ('RESERVATION_CONFIRMED' === scriptEvent) { - var customerName = new CustomerName(reservation.fullName, reservation.firstName, reservation.lastName, event.mustUseFirstAndLastName()); - subscribeUser(reservation.email, customerName, reservation.userLanguage, event); + var firstName = event.mustUseFirstAndLastName ? reservation.firstName : reservation.fullName; + subscribeUser(reservation.email, firstName, reservation.fullName, reservation.userLanguage, event); } else if ('WAITING_QUEUE_SUBSCRIPTION' === scriptEvent) { - var customerName = new CustomerName(waitingQueueSubscription.fullName, waitingQueueSubscription.firstName, waitingQueueSubscription.lastName, event.mustUseFirstAndLastName()); - subscribeUser(waitingQueueSubscription.emailAddress, customerName, waitingQueueSubscription.userLanguage, event); + var firstName = event.mustUseFirstAndLastName ? waitingQueueSubscription.firstName : waitingQueueSubscription.fullName; + subscribeUser(waitingQueueSubscription.emailAddress, firstName, waitingQueueSubscription.fullName, waitingQueueSubscription.userLanguage, event); } } -function subscribeUser(email, customerName, language, event) { +function subscribeUser(email, firstName, fullName, language, event) { var eventShortName = event.shortName; var dataCenter = extensionParameters.apiKey.match(/\-([a-zA-Z0-9]+)$/)[1]; @@ -59,7 +57,7 @@ function subscribeUser(email, customerName, language, event) { var apiKey = extensionParameters.apiKey; createMergeFieldIfNotPresent(listAddress, apiKey, event.id, eventShortName); var md5Email = ExtensionUtils.md5(email); - send(event.id, listAddress + LIST_MEMBERS + md5Email, apiKey, email, customerName, language, eventShortName); + send(event.id, listAddress + LIST_MEMBERS + md5Email, apiKey, email, firstName, fullName, language, eventShortName); } @@ -69,50 +67,52 @@ function createMergeFieldIfNotPresent(listAddress, apiKey, eventId, eventShortNa var res = simpleHttpClient.get(listAddress + MERGE_FIELDS, {'Authorization': simpleHttpClient.basicCredentials('alfio', apiKey)}); var body = res.body; if(body == null) { - log.warn("null response from mailchimp for list {}", listAddress); + log.warn("null response from mailchimp for list " + listAddress); return; } - if(!body.contains(ALFIO_EVENT_KEY)) { + if(body.indexOf(ALFIO_EVENT_KEY) === -1) { log.debug("can't find ALFIO_EKEY for event " + eventShortName); createMergeField(listAddress, apiKey, eventShortName, eventId); } } catch (e) { - log.warn("exception while reading merge fields for event id "+eventId, e); - extensionLogger.logWarning(ExtensionUtils.format("Cannot get merge fields for %s, got: %s", eventShortName, e.getMessage ? e.getMessage() : e)); + log.warn("exception while reading merge fields for event id " + eventId + ": " + (e.message || e)); + extensionLogger.logWarning(ExtensionUtils.format("Cannot get merge fields for %s, got: %s", eventShortName, e.message || e)); } } function createMergeField(listAddress, apiKey, eventShortName, eventId) { - var mergeField = new HashMap(); - mergeField.put("tag", ALFIO_EVENT_KEY); - mergeField.put("name", "Alfio's event key"); - mergeField.put("type", "text"); - mergeField.put("required", false); - mergeField.put("public", false); + var mergeField = { + "tag": ALFIO_EVENT_KEY, + "name": "Alfio's event key", + "type": "text", + "required": false, + "public": false + }; try { var response = simpleHttpClient.post(listAddress + MERGE_FIELDS, {'Authorization': simpleHttpClient.basicCredentials('alfio', apiKey)}, mergeField); - if(!response.isSuccessful()) { + if(!response.successful) { var body = response.body; - log.warn("can't create {} merge field. Got: {}", ALFIO_EVENT_KEY, body != null ? body : "null"); + log.warn("can't create " + ALFIO_EVENT_KEY + " merge field. Got: " + (body != null ? body : "null")); } } catch(e) { - log.warn("exception while creating ALFIO_EKEY for event id "+eventId, e); - extensionLogger.logWarning(ExtensionUtils.format("Cannot create merge field for %s, got: %s", eventShortName, e.getMessage ? e.getMessage() : e)); + log.warn("exception while creating ALFIO_EKEY for event id " + eventId + ": " + (e.message || e)); + extensionLogger.logWarning(ExtensionUtils.format("Cannot create merge field for %s, got: %s", eventShortName, e.message || e)); } } -function send(eventId, address, apiKey, email, name, language, eventShortName) { - var content = new HashMap(); - content.put("email_address", email); - content.put("status", "subscribed"); - var mergeFields = new HashMap(); - mergeFields.put("FNAME", name.isHasFirstAndLastName() ? name.getFirstName() : name.getFullName()); - mergeFields.put(ALFIO_EVENT_KEY, eventShortName); - content.put("merge_fields", mergeFields); - content.put("language", language); +function send(eventId, address, apiKey, email, firstName, fullName, language, eventShortName) { + var content = { + "email_address": email, + "status": "subscribed", + "merge_fields": { + "FNAME": firstName, + "ALFIO_EKEY": eventShortName + }, + "language": language + }; try { var response = simpleHttpClient.put(address, {'Authorization': simpleHttpClient.basicCredentials('alfio', apiKey)}, content); - if(response.isSuccessful()) { + if(response.successful) { extensionLogger.logSuccess(ExtensionUtils.format("user %s has been subscribed to list", email)); return; } @@ -121,12 +121,12 @@ function send(eventId, address, apiKey, email, name, language, eventShortName) { return; } - if (response.code != 400 || body.contains("\"errors\"")) { - extensionLogger.logError(ExtensionUtils.format(FAILURE_MSG, email, name, language, body)); + if (response.code != 400 || body.indexOf("\"errors\"") !== -1) { + extensionLogger.logError(ExtensionUtils.format(FAILURE_MSG, email, fullName, language, body)); } else { - extensionLogger.logWarning(ExtensionUtils.format(FAILURE_MSG, email, name, language, body)); + extensionLogger.logWarning(ExtensionUtils.format(FAILURE_MSG, email, fullName, language, body)); } } catch(e) { - extensionLogger.logError(ExtensionUtils.format(FAILURE_MSG, email, name, language, e.getMessage ? e.getMessage() : e)); + extensionLogger.logError(ExtensionUtils.format(FAILURE_MSG, email, fullName, language, e.message || e)); } -} \ No newline at end of file +} diff --git a/src/main/resources/alfio/extension/sample.js b/src/main/resources/alfio/extension/sample.js index d054c694ba..0700692e22 100644 --- a/src/main/resources/alfio/extension/sample.js +++ b/src/main/resources/alfio/extension/sample.js @@ -35,8 +35,8 @@ function getScriptMetadata() { function executeScript(scriptEvent) { log.warn('hello from script with event: ' + scriptEvent); log.warn('extension parameters are: ' + extensionParameters); - //this sample calls the https://csrng.net/ website and generates a random invoice number - var randomNumber = simpleHttpClient.get('https://jsonplaceholder.typicode.com/todos/1').getJsonBody().get("userId"); + //this sample calls the https://jsonplaceholder.typicode.com/ website and generates a random invoice number + var randomNumber = JSON.parse(simpleHttpClient.get('https://jsonplaceholder.typicode.com/todos/1').body).userId; log.warn('the invoice number will be: ' + randomNumber); return { invoiceNumber: randomNumber diff --git a/src/test/java/alfio/extension/ExtensionUtilsTest.java b/src/test/java/alfio/extension/ExtensionUtilsTest.java index 0d784824ef..4f543f465a 100644 --- a/src/test/java/alfio/extension/ExtensionUtilsTest.java +++ b/src/test/java/alfio/extension/ExtensionUtilsTest.java @@ -16,11 +16,7 @@ */ package alfio.extension; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mozilla.javascript.Context; -import org.mozilla.javascript.Scriptable; import java.util.Map; @@ -28,27 +24,15 @@ class ExtensionUtilsTest { - private Scriptable scope; - - @BeforeEach - void init() { - scope = Context.enter().initSafeStandardObjects(null, true); - } - - @AfterEach - void cleanup() { - Context.exit(); - } - @Test void convertIntegerToJson() { var db = 1.0D; - assertEquals("{\"number\":1}", ExtensionUtils.convertToJson(Context.javaToJS(Map.of("number", db), scope))); + assertEquals("{\"number\":1}", ExtensionUtils.convertToJson(Map.of("number", db))); } @Test void convertDecimalToJson() { var db = 1.1D; - assertEquals("{\"number\":1.1}", ExtensionUtils.convertToJson(Context.javaToJS(Map.of("number", db), scope))); + assertEquals("{\"number\":1.1}", ExtensionUtils.convertToJson(Map.of("number", db))); } -} \ No newline at end of file +} diff --git a/src/test/java/alfio/extension/ScriptValidationTest.java b/src/test/java/alfio/extension/ScriptValidationTest.java deleted file mode 100644 index b1026b5467..0000000000 --- a/src/test/java/alfio/extension/ScriptValidationTest.java +++ /dev/null @@ -1,103 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ -package alfio.extension; - -import alfio.extension.exception.ScriptNotValidException; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.List; - - -public class ScriptValidationTest { - - private String getScriptContent(String file) throws IOException { - String concatenation; - try(var input = getClass().getResourceAsStream("/rhino-scripts/" + file)) { - List extensionStream = IOUtils.readLines(new InputStreamReader(input, StandardCharsets.UTF_8)); - concatenation = String.join("\n", extensionStream)+"\n;executeScript(extensionEvent)"; - } - return concatenation; - } - - @Test - void testBaseScriptValidation() throws Exception { - String concatenation = getScriptContent("base.js"); - ScriptValidation validation = new ScriptValidation(concatenation); - Assertions.assertDoesNotThrow(validation::validate); - } - - @Test - void testBoundariesExitValidation() throws IOException { - String concatenation = getScriptContent("boundariesExit.js"); - ScriptValidation validation = new ScriptValidation(concatenation); - Assertions.assertThrows(ScriptNotValidException.class, validation::validate); - } - - @Test - void testBoundariesReflectionValidation() throws IOException { - String concatenation = getScriptContent("boundariesReflection.js"); - ScriptValidation validation = new ScriptValidation(concatenation); - Assertions.assertThrows(ScriptNotValidException.class, validation::validate); - } - - @Test - void testWhileLoopValidation() throws IOException { - String concatenation = getScriptContent("timeout.js"); - ScriptValidation validation = new ScriptValidation(concatenation); - Assertions.assertThrows(ScriptNotValidException.class, validation::validate); - } - - @Test - void testDoLoopValidation() throws IOException { - String concatenation = getScriptContent("doLoop.js"); - ScriptValidation validation = new ScriptValidation(concatenation); - Assertions.assertThrows(ScriptNotValidException.class, validation::validate); - } - - @Test - void testWithStatementValidation() throws IOException { - String concatenation = getScriptContent("withStatement.js"); - ScriptValidation validation = new ScriptValidation(concatenation); - Assertions.assertThrows(ScriptNotValidException.class, validation::validate); - } - - @Test - void testLabeledStatementValidation() throws IOException { - String concatenation = getScriptContent("labeledStatement.js"); - ScriptValidation validation = new ScriptValidation(concatenation); - Assertions.assertThrows(ScriptNotValidException.class, validation::validate); - } - - @Test - void testFunctionLevelsValidation() throws IOException { - String concatenation = getScriptContent("functionLevels.js"); - ScriptValidation validation = new ScriptValidation(concatenation); - Assertions.assertThrows(ScriptNotValidException.class, validation::validate); - } - - @Test - void testFunctionLevelsLegitValidation() throws IOException { - String concatenation = getScriptContent("functionLegitLevels.js"); - ScriptValidation validation = new ScriptValidation(concatenation); - Assertions.assertDoesNotThrow(validation::validate); - } -} diff --git a/src/test/java/alfio/extension/ScriptingExecutionServiceTest.java b/src/test/java/alfio/extension/ScriptingExecutionServiceTest.java index bd6c4678fa..c3a1f7a025 100644 --- a/src/test/java/alfio/extension/ScriptingExecutionServiceTest.java +++ b/src/test/java/alfio/extension/ScriptingExecutionServiceTest.java @@ -72,45 +72,43 @@ void testBaseScriptExecution() throws IOException { @Test void testExecutionTimeout() { - assertTimeoutPreemptively(Duration.ofSeconds(16L), () -> { + assertTimeoutPreemptively(Duration.ofSeconds(20L), () -> { try { String concatenation = getScriptContent("timeout.js"); scriptingExecutionService.executeScript("name", concatenation, Map.of("extensionEvent", "test"), Void.class, extensionLogger); fail(); } catch (Exception e) { - assertTrue(e.getCause() instanceof ExecutionTimeoutException); + assertTrue(e instanceof ExecutionTimeoutException + || (e.getCause() != null && e.getCause() instanceof java.util.concurrent.TimeoutException), + "Expected timeout exception but got: " + e.getClass().getName()); } }); } @Test void testOutOfBoundariesReflection() throws Exception { - try { + // In QuickJS, getClass() is not available on JS objects, so this throws a runtime error + assertThrows(RuntimeException.class, () -> { String concatenation = getScriptContent("boundariesReflection.js"); scriptingExecutionService.executeScript("name", concatenation, Map.of("extensionEvent", "test"), Void.class, extensionLogger); - } catch (OutOfBoundariesException ex) { - verify(extensionLogger, never()).logInfo("test"); - } + }); } @Test void testOutOfBoundariesExit() throws Exception { - try { + // In QuickJS, java.lang.System is not available, so this throws a runtime error + assertThrows(RuntimeException.class, () -> { String concatenation = getScriptContent("boundariesExit.js"); scriptingExecutionService.executeScript("name", concatenation, Map.of("extensionEvent", "test"), Void.class, extensionLogger); - } catch (InvalidScriptException ex) { - verify(extensionLogger).logError(startsWith("Syntax error while executing script:")); - } + }); } @Test void extensionThrowsError() throws Exception { - try { + assertThrows(RuntimeException.class, () -> { String concatenation = getScriptContent("runtimeError.js"); scriptingExecutionService.executeScript("name", concatenation, Map.of("extensionEvent", "test"), Void.class, extensionLogger); - } catch(ScriptRuntimeException ex) { - verify(extensionLogger).logError(startsWith("Error:")); - } + }); } @Test @@ -132,4 +130,4 @@ void getMessageFromException() { when(root.getMessage()).thenReturn("root message"); assertEquals("root message", scriptingExecutionService.getErrorMessage(ex)); } -} \ No newline at end of file +} diff --git a/src/test/resources/custom-tax-policy-extension.js b/src/test/resources/custom-tax-policy-extension.js index 04118780e0..2a8e28329d 100644 --- a/src/test/resources/custom-tax-policy-extension.js +++ b/src/test/resources/custom-tax-policy-extension.js @@ -24,7 +24,7 @@ function executeScript(scriptEvent) { var keys = Object.keys(reservationForm.tickets); var containsModifiedElements = false; var out = []; - for (i = 0; i < keys.length; i++) { + for (var i = 0; i < keys.length; i++) { var uuid = keys[i]; var attendee = reservationForm.tickets[uuid]; var ticketInfo = ticketInfoByUuid[uuid]; diff --git a/src/test/resources/extension.js b/src/test/resources/extension.js index 61610b58ee..c89e726f2c 100644 --- a/src/test/resources/extension.js +++ b/src/test/resources/extension.js @@ -31,7 +31,7 @@ function executeScript(scriptEvent) { // test JSON stringify/parse var string = JSON.stringify(map); log.warn(string); - var parsed = JSON.parse(string, function() {}); + var parsed = JSON.parse(string); if (scriptEvent === 'EVENT_CREATED') { console.log('created event. Here some debug info', parsed.test, parsed.name, 'custom string', map); }