From f9c2975d9e58391bd2b175b9b8f3183e2ba3bfb5 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Mon, 26 Oct 2026 16:52:33 +0100 Subject: [PATCH] Disable the JUEL script engine lang:import function by default and allow configuring the script output writer * The lang:import function in the JUEL script engine is no longer registered by default, it can be enabled through JuelScriptEngineFactory#setImportFunctionEnabled * The JUEL out:print function now writes to the writer of the script context instead of always using System.out * Add JSR223FlowableScriptEngine#setScriptOutputWriter to configure the writer used for script output (e.g. to discard it or keep it in memory) --- .../scripting/JSR223FlowableScriptEngine.java | 41 ++++++++++- .../impl/scripting/JuelScriptEngine.java | 43 +++++++++++- .../scripting/JuelScriptEngineFactory.java | 16 ++++- .../impl/scripting/JuelScriptEngineTest.java | 68 +++++++++++++++++++ .../bpmn/scripttask/ScriptTaskTest.java | 18 +++++ ...TaskTest.testScriptOutputWriter.bpmn20.xml | 27 ++++++++ .../impl/scripting/JuelScriptEngine.java | 43 +++++++++++- .../scripting/JuelScriptEngineFactory.java | 16 ++++- 8 files changed, 263 insertions(+), 9 deletions(-) create mode 100644 modules/flowable-engine/src/test/java/org/flowable/engine/impl/scripting/JuelScriptEngineTest.java create mode 100644 modules/flowable-engine/src/test/resources/org/flowable/examples/bpmn/scripttask/ScriptTaskTest.testScriptOutputWriter.bpmn20.xml diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/scripting/JSR223FlowableScriptEngine.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/scripting/JSR223FlowableScriptEngine.java index 8d39036e315..1c481de38c2 100644 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/scripting/JSR223FlowableScriptEngine.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/scripting/JSR223FlowableScriptEngine.java @@ -12,6 +12,7 @@ */ package org.flowable.common.engine.impl.scripting; +import java.io.Writer; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -23,6 +24,7 @@ import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; import javax.script.ScriptException; +import javax.script.SimpleScriptContext; import org.apache.commons.lang3.StringUtils; import org.flowable.common.engine.api.FlowableException; @@ -47,6 +49,12 @@ public class JSR223FlowableScriptEngine implements FlowableScriptEngine { protected boolean cacheScriptingEngines = true; protected Map cachedEngines; + /** + * The writer used by the scripts for their output (e.g. {@code println} in Groovy, {@code out:print} in JUEL). + * When not set, the writer of the script engine is used, which is usually {@code System.out}. + */ + protected Writer scriptOutputWriter; + public JSR223FlowableScriptEngine() { this(new ScriptEngineManager()); } @@ -132,6 +140,18 @@ public ScriptEngineManager getScriptEngineManager() { return scriptEngineManager; } + public Writer getScriptOutputWriter() { + return scriptOutputWriter; + } + + /** + * Set the writer used by the scripts for their output. + * Use {@link Writer#nullWriter()} to discard the output, or e.g. a {@link java.io.StringWriter} to keep it in memory. + */ + public void setScriptOutputWriter(Writer scriptOutputWriter) { + this.scriptOutputWriter = scriptOutputWriter; + } + protected class JavaScriptingFlowableScriptEvaluationRequest implements FlowableScriptEvaluationRequest { @@ -197,7 +217,12 @@ public ScriptEvaluation evaluate() throws FlowableScriptException { ScriptEngine scriptEngine = getEngineByName(language); Bindings bindings = createBindings(); try { - Object result = scriptEngine.eval(script, bindings); + Object result; + if (scriptOutputWriter != null) { + result = scriptEngine.eval(script, createScriptContext(scriptEngine, bindings)); + } else { + result = scriptEngine.eval(script, bindings); + } return new ScriptEvaluationImpl(resolver, result); } catch (ScriptException e) { throw new FlowableScriptException(e.getMessage(), e); @@ -207,5 +232,19 @@ public ScriptEvaluation evaluate() throws FlowableScriptException { protected Bindings createBindings() { return new ScriptBindings(Collections.singletonList(resolver), scopeContainer, inputVariableContainer, storeScriptVariables); } + + /** + * Creates the same script context as {@link javax.script.AbstractScriptEngine#eval(String, Bindings)} would, but with the configured script output writer. + */ + protected ScriptContext createScriptContext(ScriptEngine scriptEngine, Bindings bindings) { + ScriptContext engineContext = scriptEngine.getContext(); + ScriptContext scriptContext = new SimpleScriptContext(); + scriptContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE); + scriptContext.setBindings(engineContext.getBindings(ScriptContext.GLOBAL_SCOPE), ScriptContext.GLOBAL_SCOPE); + scriptContext.setReader(engineContext.getReader()); + scriptContext.setWriter(scriptOutputWriter); + scriptContext.setErrorWriter(engineContext.getErrorWriter()); + return scriptContext; + } } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/scripting/JuelScriptEngine.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/scripting/JuelScriptEngine.java index 4bab25ed287..1c6c146dc5c 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/scripting/JuelScriptEngine.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/scripting/JuelScriptEngine.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.io.Reader; +import java.io.Writer; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -59,11 +60,28 @@ */ public class JuelScriptEngine extends AbstractScriptEngine implements Compilable { + /** + * The script context of the expression that is currently being evaluated on this thread. + * EL functions are static methods, so this is how {@link #print(Object)} gets access to the writer of the script context. + */ + protected static final ThreadLocal CURRENT_SCRIPT_CONTEXT = new ThreadLocal<>(); + private ScriptEngineFactory scriptEngineFactory; private ExpressionFactory expressionFactory; + /** + * Whether the {@code lang:import} function is available in scripts. + * It allows loading any class and calling any of its public static methods, so it is disabled by default. + */ + protected boolean importFunctionEnabled; + public JuelScriptEngine(ScriptEngineFactory scriptEngineFactory) { + this(scriptEngineFactory, false); + } + + public JuelScriptEngine(ScriptEngineFactory scriptEngineFactory, boolean importFunctionEnabled) { this.scriptEngineFactory = scriptEngineFactory; + this.importFunctionEnabled = importFunctionEnabled; // Resolve the ExpressionFactory expressionFactory = ExpressionFactoryResolver.resolveExpressionFactory(); } @@ -111,10 +129,18 @@ public Bindings createBindings() { } private Object evaluateExpression(ValueExpression expr, ScriptContext ctx) throws ScriptException { + ScriptContext previousScriptContext = CURRENT_SCRIPT_CONTEXT.get(); + CURRENT_SCRIPT_CONTEXT.set(ctx); try { return expr.getValue(createElContext(ctx)); } catch (ELException elexp) { throw new ScriptException(elexp); + } finally { + if (previousScriptContext != null) { + CURRENT_SCRIPT_CONTEXT.set(previousScriptContext); + } else { + CURRENT_SCRIPT_CONTEXT.remove(); + } } } @@ -165,8 +191,7 @@ private ELContext createElContext(final ScriptContext scriptCtx) { // Built-in function are added to ScriptCtx scriptCtx.setAttribute("out:print", getPrintMethod(), ScriptContext.ENGINE_SCOPE); - SecurityManager securityManager = System.getSecurityManager(); - if (securityManager == null) { + if (importFunctionEnabled) { scriptCtx.setAttribute("lang:import", getImportMethod(), ScriptContext.ENGINE_SCOPE); } @@ -206,7 +231,19 @@ private static Method getPrintMethod() { } public static void print(Object object) { - System.out.print(object); + ScriptContext scriptContext = CURRENT_SCRIPT_CONTEXT.get(); + Writer writer = scriptContext != null ? scriptContext.getWriter() : null; + if (writer == null) { + System.out.print(object); + return; + } + + try { + writer.write(String.valueOf(object)); + writer.flush(); + } catch (IOException e) { + throw new ELException(e); + } } private static Method getImportMethod() { diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/scripting/JuelScriptEngineFactory.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/scripting/JuelScriptEngineFactory.java index bbf8c000da2..5403320d490 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/scripting/JuelScriptEngineFactory.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/scripting/JuelScriptEngineFactory.java @@ -31,6 +31,12 @@ public class JuelScriptEngineFactory implements ScriptEngineFactory { private static List extensions; private static List mimeTypes; + /** + * Whether the {@code lang:import} function is available in the created engines. + * It allows loading any class and calling any of its public static methods, so it is disabled by default. + */ + protected boolean importFunctionEnabled; + static { names = Collections.unmodifiableList(Collections.singletonList("juel")); extensions = names; @@ -137,7 +143,15 @@ public String getProgram(String... statements) { @Override public ScriptEngine getScriptEngine() { - return new JuelScriptEngine(this); + return new JuelScriptEngine(this, importFunctionEnabled); + } + + public boolean isImportFunctionEnabled() { + return importFunctionEnabled; + } + + public void setImportFunctionEnabled(boolean importFunctionEnabled) { + this.importFunctionEnabled = importFunctionEnabled; } } diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/impl/scripting/JuelScriptEngineTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/impl/scripting/JuelScriptEngineTest.java new file mode 100644 index 00000000000..dfa080ceedc --- /dev/null +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/impl/scripting/JuelScriptEngineTest.java @@ -0,0 +1,68 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.scripting; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.StringWriter; + +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptException; +import javax.script.SimpleScriptContext; + +import org.junit.jupiter.api.Test; + +/** + * @author Filip Hrisafov + */ +class JuelScriptEngineTest { + + @Test + void importFunctionIsDisabledByDefault() { + ScriptEngine scriptEngine = new JuelScriptEngineFactory().getScriptEngine(); + ScriptContext scriptContext = new SimpleScriptContext(); + + assertThatThrownBy(() -> scriptEngine.eval("${lang:import(context, 'math', 'java.lang.Math')}", scriptContext)) + .isInstanceOf(ScriptException.class) + .hasMessageContaining("lang:import"); + assertThat(scriptContext.getAttribute("lang:import")).isNull(); + } + + @Test + void importFunctionCanBeEnabled() throws ScriptException { + JuelScriptEngineFactory scriptEngineFactory = new JuelScriptEngineFactory(); + scriptEngineFactory.setImportFunctionEnabled(true); + ScriptEngine scriptEngine = scriptEngineFactory.getScriptEngine(); + ScriptContext scriptContext = new SimpleScriptContext(); + + scriptEngine.eval("${lang:import(context, 'integer', 'java.lang.Integer')}", scriptContext); + + assertThat(scriptEngine.eval("${integer:toHexString(255)}", scriptContext)).isEqualTo("ff"); + } + + @Test + void printWritesToScriptContextWriter() throws ScriptException { + ScriptEngine scriptEngine = new JuelScriptEngineFactory().getScriptEngine(); + ScriptContext scriptContext = new SimpleScriptContext(); + StringWriter writer = new StringWriter(); + scriptContext.setWriter(writer); + scriptContext.setAttribute("name", "Kermit", ScriptContext.ENGINE_SCOPE); + + scriptEngine.eval("${out:print('Hello ')}${out:print(name)}", scriptContext); + + assertThat(writer).hasToString("Hello Kermit"); + } + +} diff --git a/modules/flowable-engine/src/test/java/org/flowable/examples/bpmn/scripttask/ScriptTaskTest.java b/modules/flowable-engine/src/test/java/org/flowable/examples/bpmn/scripttask/ScriptTaskTest.java index 5da2018fecc..0a4cc436b7a 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/examples/bpmn/scripttask/ScriptTaskTest.java +++ b/modules/flowable-engine/src/test/java/org/flowable/examples/bpmn/scripttask/ScriptTaskTest.java @@ -17,12 +17,15 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; +import java.io.StringWriter; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.flowable.common.engine.impl.scripting.FlowableScriptEvaluationException; +import org.flowable.common.engine.impl.scripting.JSR223FlowableScriptEngine; import org.flowable.common.engine.impl.util.CollectionUtil; import org.flowable.engine.impl.test.PluggableFlowableTestCase; import org.flowable.engine.repository.ProcessDefinition; @@ -246,4 +249,19 @@ public void testSkipExpressionDisabled() { .containsOnly(entry("persistentResult", "success")); } + @Test + @Deployment + public void testScriptOutputWriter() { + JSR223FlowableScriptEngine scriptEngine = (JSR223FlowableScriptEngine) processEngineConfiguration.getScriptEngine(); + StringWriter writer = new StringWriter(); + scriptEngine.setScriptOutputWriter(writer); + try { + runtimeService.startProcessInstanceByKey("scriptOutputWriter", Collections.singletonMap("name", "Kermit")); + + assertThat(writer.toString()).isEqualToNormalizingNewlines("Hello Kermit from Groovy\n"); + } finally { + scriptEngine.setScriptOutputWriter(null); + } + } + } diff --git a/modules/flowable-engine/src/test/resources/org/flowable/examples/bpmn/scripttask/ScriptTaskTest.testScriptOutputWriter.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/examples/bpmn/scripttask/ScriptTaskTest.testScriptOutputWriter.bpmn20.xml new file mode 100644 index 00000000000..7f36edfc738 --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/examples/bpmn/scripttask/ScriptTaskTest.testScriptOutputWriter.bpmn20.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/flowable5-engine/src/main/java/org/activiti/engine/impl/scripting/JuelScriptEngine.java b/modules/flowable5-engine/src/main/java/org/activiti/engine/impl/scripting/JuelScriptEngine.java index e07ebb40dbb..b6e2acad31b 100644 --- a/modules/flowable5-engine/src/main/java/org/activiti/engine/impl/scripting/JuelScriptEngine.java +++ b/modules/flowable5-engine/src/main/java/org/activiti/engine/impl/scripting/JuelScriptEngine.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.io.Reader; +import java.io.Writer; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -58,11 +59,28 @@ */ public class JuelScriptEngine extends AbstractScriptEngine implements Compilable { + /** + * The script context of the expression that is currently being evaluated on this thread. + * EL functions are static methods, so this is how {@link #print(Object)} gets access to the writer of the script context. + */ + protected static final ThreadLocal CURRENT_SCRIPT_CONTEXT = new ThreadLocal<>(); + private ScriptEngineFactory scriptEngineFactory; private ExpressionFactory expressionFactory; + /** + * Whether the {@code lang:import} function is available in scripts. + * It allows loading any class and calling any of its public static methods, so it is disabled by default. + */ + protected boolean importFunctionEnabled; + public JuelScriptEngine(ScriptEngineFactory scriptEngineFactory) { + this(scriptEngineFactory, false); + } + + public JuelScriptEngine(ScriptEngineFactory scriptEngineFactory, boolean importFunctionEnabled) { this.scriptEngineFactory = scriptEngineFactory; + this.importFunctionEnabled = importFunctionEnabled; // Resolve the ExpressionFactory expressionFactory = ExpressionFactoryResolver.resolveExpressionFactory(); } @@ -110,10 +128,18 @@ public Bindings createBindings() { } private Object evaluateExpression(ValueExpression expr, ScriptContext ctx) throws ScriptException { + ScriptContext previousScriptContext = CURRENT_SCRIPT_CONTEXT.get(); + CURRENT_SCRIPT_CONTEXT.set(ctx); try { return expr.getValue(createElContext(ctx)); } catch (ELException elexp) { throw new ScriptException(elexp); + } finally { + if (previousScriptContext != null) { + CURRENT_SCRIPT_CONTEXT.set(previousScriptContext); + } else { + CURRENT_SCRIPT_CONTEXT.remove(); + } } } @@ -163,8 +189,7 @@ private ELContext createElContext(final ScriptContext scriptCtx) { // Built-in function are added to ScriptCtx scriptCtx.setAttribute("out:print", getPrintMethod(), ScriptContext.ENGINE_SCOPE); - SecurityManager securityManager = System.getSecurityManager(); - if (securityManager == null) { + if (importFunctionEnabled) { scriptCtx.setAttribute("lang:import", getImportMethod(), ScriptContext.ENGINE_SCOPE); } @@ -204,7 +229,19 @@ private static Method getPrintMethod() { } public static void print(Object object) { - System.out.print(object); + ScriptContext scriptContext = CURRENT_SCRIPT_CONTEXT.get(); + Writer writer = scriptContext != null ? scriptContext.getWriter() : null; + if (writer == null) { + System.out.print(object); + return; + } + + try { + writer.write(String.valueOf(object)); + writer.flush(); + } catch (IOException e) { + throw new ELException(e); + } } private static Method getImportMethod() { diff --git a/modules/flowable5-engine/src/main/java/org/activiti/engine/impl/scripting/JuelScriptEngineFactory.java b/modules/flowable5-engine/src/main/java/org/activiti/engine/impl/scripting/JuelScriptEngineFactory.java index 6579a18de73..856fb2ca78b 100644 --- a/modules/flowable5-engine/src/main/java/org/activiti/engine/impl/scripting/JuelScriptEngineFactory.java +++ b/modules/flowable5-engine/src/main/java/org/activiti/engine/impl/scripting/JuelScriptEngineFactory.java @@ -31,6 +31,12 @@ public class JuelScriptEngineFactory implements ScriptEngineFactory { private static List extensions; private static List mimeTypes; + /** + * Whether the {@code lang:import} function is available in the created engines. + * It allows loading any class and calling any of its public static methods, so it is disabled by default. + */ + protected boolean importFunctionEnabled; + static { names = Collections.unmodifiableList(Collections.singletonList("juel")); extensions = names; @@ -137,7 +143,15 @@ public String getProgram(String... statements) { @Override public ScriptEngine getScriptEngine() { - return new JuelScriptEngine(this); + return new JuelScriptEngine(this, importFunctionEnabled); + } + + public boolean isImportFunctionEnabled() { + return importFunctionEnabled; + } + + public void setImportFunctionEnabled(boolean importFunctionEnabled) { + this.importFunctionEnabled = importFunctionEnabled; } }