Skip to content

Commit 9c9f299

Browse files
committed
automatically escape JSON values, if renderend in a template for a JSON content-type; removed 'fn.' prefix from Groovy function calls
1 parent 34ee8c3 commit 9c9f299

29 files changed

Lines changed: 131 additions & 58 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,10 @@ api:
5757
htpasswdPath: .htpasswd
5858
- request:
5959
- template:
60+
contentType: application/json
6061
src: |
6162
{
62-
"sub": ${fn.toJSON(fn.user())}
63+
"sub": ${user()}
6364
}
6465
- jwtSign:
6566
jwk:
@@ -722,7 +723,7 @@ api:
722723
contentType: application/json
723724
src: |
724725
{
725-
"destination": ${fn.toJSON(json.city)}
726+
"destination": ${json.city}
726727
}
727728
- return:
728729
status: 200
@@ -756,7 +757,7 @@ api:
756757
- request:
757758
- template:
758759
src: |
759-
Buenas noches, ${fn.xpath('/person/@firstname')}
760+
Buenas noches, ${xpath('/person/@firstname')}
760761
- return:
761762
status: 200
762763
```

core/src/main/java/com/predic8/membrane/core/interceptor/templating/TemplateInterceptor.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,17 @@ protected byte[] getContent(Exchange exchange, Flow flow) {
121121
}
122122

123123
private @NotNull Map<String, Object> getVariableBinding(Exchange exc, Flow flow) {
124-
return createParameterBindings(router, exc, flow, scriptAccessesJson && isJsonMessage(exc, flow));
124+
return createParameterBindings(router, exc, flow, scriptAccessesJson && isJsonMessage(exc, flow), producesJSON());
125125
}
126126

127127
private static boolean isJsonMessage(Exchange exc, Flow flow) {
128128
return exc.getMessage(flow).isJSON();
129129
}
130130

131+
private boolean producesJSON() {
132+
return contentType != null && contentType.contains("json");
133+
}
134+
131135
private Template createTemplate() {
132136
if (src == null)
133137
throw new ConfigurationException("No template content provided via 'location' or inline text (%s).".formatted(getTemplateLocation()));

core/src/main/java/com/predic8/membrane/core/lang/AbstractScriptInterceptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ protected void handleScriptExecutionException(Exchange exc, Exception e) {
207207
}
208208

209209
private Map<String, Object> getParameterBindings(Exchange exc, Flow flow, Message msg) {
210-
var binding = createParameterBindings(router, exc, flow, scriptAccessesJson && msg.isJSON());
210+
var binding = createParameterBindings(router, exc, flow, scriptAccessesJson && msg.isJSON(), false);
211211
addOutcomeObjects(binding);
212212
return binding;
213213
}

core/src/main/java/com/predic8/membrane/core/lang/ScriptingUtils.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public class ScriptingUtils {
4242

4343
private static final ObjectMapper om = new ObjectMapper();
4444

45-
public static Map<String, Object> createParameterBindings(Router router, Exchange exc, Flow flow, boolean includeJsonObject) {
45+
public static Map<String, Object> createParameterBindings(Router router, Exchange exc, Flow flow, boolean includeJsonObject, boolean isJSON) {
4646

4747
var msg = exc.getMessage(flow);
4848

@@ -112,8 +112,15 @@ public static Map<String, Object> createParameterBindings(Router router, Exchang
112112

113113
params.put("pathParam", new PathParametersMap(exc));
114114

115-
// Allow invoking functions in Groovy with ${fn.functionname()}
116-
params.put("fn", new GroovyBuiltInFunctions(exc, flow, router));
115+
// Allow invoking functions in Groovy with ${functionname()}
116+
params.put("fn", new GroovyBuiltInFunctions(exc, flow, router, params) {
117+
@Override
118+
public Object escape(Object o) {
119+
if (isJSON)
120+
return toJSON(o);
121+
return o;
122+
}
123+
});
117124

118125
return params;
119126
}

core/src/main/java/com/predic8/membrane/core/lang/groovy/GroovyBuiltInFunctions.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,24 @@
1919
import com.predic8.membrane.core.interceptor.Interceptor.Flow;
2020
import com.predic8.membrane.core.lang.CommonBuiltInFunctions;
2121
import com.predic8.membrane.core.router.*;
22-
import groovy.lang.GroovyObjectSupport;
22+
import groovy.lang.Binding;
2323

2424
import java.util.List;
25+
import java.util.Map;
2526

2627
/**
2728
* Helper class for built-in functions that delegates to the implementation CommonBuiltInFunctions.
28-
* Difference to SpEL:
29-
* The functions are called with ${fn.functionname()} instead of ${functionname()} in the template interceptor
3029
*/
3130
@SuppressWarnings("unused")
32-
public class GroovyBuiltInFunctions extends GroovyObjectSupport {
31+
public class GroovyBuiltInFunctions extends Binding {
3332

3433
private final Exchange exchange;
3534
private final Flow flow;
3635
private final Router router;
3736
private XmlConfig xmlConfig;
3837

39-
public GroovyBuiltInFunctions(Exchange exchange, Flow flow, Router router) {
38+
public GroovyBuiltInFunctions(Exchange exchange, Flow flow, Router router, Map<String, Object> params) {
39+
super(params);
4040
this.exchange = exchange;
4141
this.flow = flow;
4242
this.router = router;
@@ -118,4 +118,13 @@ public long getDefaultSessionLifetime(String beanName) {
118118
public String env(String s) {
119119
return CommonBuiltInFunctions.env(s);
120120
}
121+
122+
/**
123+
* Post-Process values before they are written to the output.
124+
*
125+
* This gives subclasses the option to perform escaping (e.g. JSON/XML).
126+
*/
127+
public Object escape(Object o) {
128+
return o;
129+
}
121130
}

core/src/main/java/com/predic8/membrane/core/lang/groovy/GroovyExchangeExpression.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public GroovyExchangeExpression(String source, Router router) {
4747
public <T> T evaluate(Exchange exchange, Interceptor.Flow flow, Class<T> type) {
4848
Object o = null;
4949
try {
50-
o = script.apply(createParameterBindings(router, exchange, flow, false));
50+
o = script.apply(createParameterBindings(router, exchange, flow, false, false));
5151
} catch (MissingPropertyException mpe) {
5252
log.info("Expression {} tries to access non existing property {}",expression,mpe.getMessage());
5353
if (type.getName().equals(Object.class.getName())) {

core/src/main/java/com/predic8/membrane/core/lang/groovy/adapted/StreamingTemplateEngine.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
* methods.
5656
*
5757
* We therefore decided to copy the code of the inner class here (see first commit) and then made
58-
* some adjustments (the following commits).
58+
* some adjustments (the following commits, see "adjustment" comments).
5959
*/
6060
public class StreamingTemplateEngine extends groovy.text.StreamingTemplateEngine {
6161
@Override
@@ -86,7 +86,11 @@ private static class StreamingTemplate implements Template {
8686
+ "return { _p, _s, _b, out -> "
8787
+ "int _i = 0;"
8888
+ "try {"
89-
+ "delegate = new Binding(_b);";
89+
// adjustment: using the GroovyBuiltInFunctions binding implementation
90+
// provided from the outside (via the 'fn' map entry) allows the script
91+
// to directly call Membrane functions. This enables templating syntax like
92+
// ${xpath('/root/element')} .
93+
+ "delegate = _b.get('fn');";
9094

9195
/**
9296
* The 'footer' we use for the resulting groovy script source
@@ -514,7 +518,7 @@ private int parseDollarIdentifier(int c,
514518
final StringBuilder target,
515519
final Position sourcePosition,
516520
final Position targetPosition) throws IOException, FinishedReadingException {
517-
append(target, targetPosition, "out<<");
521+
append(target, targetPosition, "out<<escape("); // adjustment: escape the output
518522
append(target, targetPosition, (char) c);
519523

520524
while (true) {
@@ -525,7 +529,7 @@ private int parseDollarIdentifier(int c,
525529
append(target, targetPosition, (char) c);
526530
}
527531

528-
append(target, targetPosition, ";");
532+
append(target, targetPosition, ");");
529533

530534
return c;
531535
}
@@ -562,10 +566,13 @@ private void parseDollarCurlyIdentifier(final Reader reader,
562566
final StringBuilder target,
563567
final Position sourcePosition,
564568
final Position targetPosition) throws IOException, FinishedReadingException {
565-
append(target, targetPosition, "out<<\"\"\"${");
569+
append(target, targetPosition, "out<<\"\"\"${escape("); // adjustment: escape the output
566570

567571
while (true) {
568572
int c = read(reader, sourcePosition);
573+
// adjustment: escape the output
574+
if (c == '}')
575+
append(target, targetPosition, ')');
569576
append(target, targetPosition, (char) c);
570577
if (c == '}') break;
571578
}
@@ -612,11 +619,11 @@ private void parseExpression(final Reader reader,
612619
final StringBuilder target,
613620
final Position sourcePosition,
614621
final Position targetPosition) throws IOException, FinishedReadingException {
615-
append(target, targetPosition, "out<<\"\"\"${");
622+
append(target, targetPosition, "out<<\"\"\"${escape("); // adjustment: escape the output
616623

617624
readAndAppend(reader, target, sourcePosition, targetPosition);
618625

619-
append(target, targetPosition, "}\"\"\";");
626+
append(target, targetPosition, ")}\"\"\";");
620627
}
621628

622629
@Override
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.predic8.membrane.core.interceptor.templating;
2+
3+
import com.predic8.membrane.core.exchange.Exchange;
4+
import com.predic8.membrane.core.http.Request;
5+
import com.predic8.membrane.core.lang.groovy.adapted.StreamingTemplateEngine;
6+
import com.predic8.membrane.core.router.TestRouter;
7+
import groovy.text.Template;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
11+
import java.io.IOException;
12+
import java.io.StringReader;
13+
import java.net.URISyntaxException;
14+
15+
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.REQUEST;
16+
import static com.predic8.membrane.core.lang.ScriptingUtils.createParameterBindings;
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
19+
public class GroovyTemplateEscapingTest {
20+
private TestRouter r;
21+
private Exchange e;
22+
23+
@BeforeEach
24+
public void setUp() throws URISyntaxException {
25+
r = new TestRouter();
26+
e = Request.get("http://localhost/foo").buildExchange();
27+
}
28+
29+
@Test
30+
public void noJSON() throws IOException, ClassNotFoundException, URISyntaxException {
31+
Template t = new StreamingTemplateEngine().createTemplate(new StringReader(
32+
"${flow} $flow <%= flow %> ${hasScope('foo')}"));
33+
34+
assertEquals("REQUEST REQUEST REQUEST false",
35+
t.make(createParameterBindings(r, e, REQUEST, false, false))
36+
.toString());
37+
}
38+
39+
@Test
40+
public void JSON() throws IOException, ClassNotFoundException, URISyntaxException {
41+
Template t = new StreamingTemplateEngine().createTemplate(new StringReader(
42+
"${flow} $flow <%= flow %> ${hasScope('foo')}"));
43+
44+
assertEquals("\"REQUEST\" \"REQUEST\" \"REQUEST\" false",
45+
t.make(createParameterBindings(r, e, REQUEST, false, true))
46+
.toString());
47+
}
48+
49+
}

core/src/test/java/com/predic8/membrane/core/interceptor/templating/TemplateInterceptorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ void builtInFunctions() {
248248
exc.setProperty(SECURITY_SCHEMES, List.of(new BasicHttpSecurityScheme().username("alice")));
249249
ti.setContentType(APPLICATION_JSON);
250250
ti.setSrc("""
251-
{ "foo": "${fn.user()}" }
251+
{ "foo": ${user()} }
252252
""");
253253
ti.init(router);
254254
ti.handleRequest(exc);

core/src/test/java/com/predic8/membrane/core/lang/groovy/GroovyBuiltInFunctionsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class GroovyBuiltInFunctionsTest {
3434

3535
@BeforeEach
3636
void setUp() throws URISyntaxException {
37-
functions = new GroovyBuiltInFunctions(post("/foo").xml("<person name='Fritz'/>").buildExchange(), REQUEST, new DummyTestRouter());
37+
functions = new GroovyBuiltInFunctions(post("/foo").xml("<person name='Fritz'/>").buildExchange(), REQUEST, new DummyTestRouter(), new HashMap<>());
3838
}
3939

4040
@Test

0 commit comments

Comments
 (0)