Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheException;
import com.github.mustachejava.MustacheVisitor;
import com.github.mustachejava.SafeMustacheFactory;
import com.github.mustachejava.TemplateContext;
import com.github.mustachejava.TemplateFunction;
import com.github.mustachejava.codes.DefaultMustache;
import com.github.mustachejava.codes.IterableCode;
import com.github.mustachejava.codes.WriteCode;
Expand All @@ -34,12 +36,11 @@
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CustomMustacheFactory extends DefaultMustacheFactory {
public class CustomMustacheFactory extends SafeMustacheFactory {
static final String V7_JSON_MEDIA_TYPE_WITH_CHARSET = "application/json; charset=UTF-8";
static final String JSON_MEDIA_TYPE_WITH_CHARSET = "application/json;charset=utf-8";
static final String JSON_MEDIA_TYPE = "application/json";
Expand All @@ -64,7 +65,7 @@ public class CustomMustacheFactory extends DefaultMustacheFactory {
private final Encoder encoder;

public CustomMustacheFactory(String mediaType) {
super();
super(Collections.emptySet(), ".");
setObjectHandler(new CustomReflectionObjectHandler());
this.encoder = createEncoder(mediaType);
}
Expand Down Expand Up @@ -145,7 +146,7 @@ protected void tag(Writer writer, String tag) throws IOException {
writer.write(tc.endChars());
}

protected abstract Function<String, String> createFunction(Object resolved);
protected abstract TemplateFunction createFunction(Object resolved);

/**
* At compile time, this function extracts the name of the variable:
Expand Down Expand Up @@ -188,7 +189,7 @@ static class ToJsonCode extends CustomCode {

@Override
@SuppressWarnings("unchecked")
protected Function<String, String> createFunction(Object resolved) {
protected TemplateFunction createFunction(Object resolved) {
return s -> {
if (resolved == null) {
return null;
Expand Down Expand Up @@ -238,7 +239,7 @@ static class JoinerCode extends CustomCode {
}

@Override
protected Function<String, String> createFunction(Object resolved) {
protected TemplateFunction createFunction(Object resolved) {
return s -> {
if (s == null) {
return null;
Expand All @@ -260,7 +261,7 @@ static boolean match(String variable) {

static class CustomJoinerCode extends JoinerCode {

private static final Pattern PATTERN = Pattern.compile("^(?:" + CODE + " delimiter='(.*)')$");
private static final Pattern PATTERN = Pattern.compile("^" + CODE + " delimiter='(.*)'$");
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern has been changed to remove the non-capturing group (?:...), which may affect pattern matching behavior. The original pattern ^(?:" + CODE + " delimiter='(.*)')$ had specific grouping logic that is now removed. Verify that the pattern still correctly matches the expected input format and that any code relying on capture group indices is updated accordingly.

Suggested change
private static final Pattern PATTERN = Pattern.compile("^" + CODE + " delimiter='(.*)'$");
private static final Pattern PATTERN = Pattern.compile("^(?:" + CODE + " delimiter='(.*)')$");

Copilot uses AI. Check for mistakes.

CustomJoinerCode(TemplateContext tc, DefaultMustacheFactory df, Mustache mustache, String variable) {
super(tc, df, mustache, extractDelimiter(variable));
Expand Down Expand Up @@ -357,7 +358,7 @@ static class UrlEncoder implements Encoder {

@Override
public void encode(String s, Writer writer) throws IOException {
writer.write(URLEncoder.encode(s, StandardCharsets.UTF_8.name()));
writer.write(URLEncoder.encode(s, StandardCharsets.UTF_8));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

import com.github.mustachejava.reflect.ReflectionObjectHandler;

import org.elasticsearch.common.util.CollectionUtils;
import org.elasticsearch.common.util.Maps;
import org.elasticsearch.common.util.iterable.Iterables;

Expand Down Expand Up @@ -144,10 +143,4 @@ public Iterator<Object> iterator() {
return col.iterator();
}
}

@Override
public String stringify(Object object) {
CollectionUtils.ensureNoSelfReferences(object, "CustomReflectionObjectHandler stringify");
return super.stringify(object);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package org.elasticsearch.xpack.core.watcher.watch;

import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.util.CollectionUtils;
import org.elasticsearch.xcontent.ToXContentObject;
import org.elasticsearch.xcontent.XContentBuilder;

Expand Down Expand Up @@ -37,6 +38,7 @@ public Simple(String key, Object value) {
}

public Simple(Map<String, Object> data) {
CollectionUtils.ensureNoSelfReferences(data, "watcher action payload");
this.data = data;
}

Expand Down
1 change: 1 addition & 0 deletions x-pack/plugin/watcher/qa/rest/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ tasks.named("yamlRestTestV7CompatTransform").configure{ task ->
task.skipTest("mustache/30_search_input/Test search input mustache integration (using request body and rest_total_hits_as_int)", "remove JodaCompatibleDateTime -- ZonedDateTime doesn't output millis/nanos if they're 0 (#78417)")
task.skipTest("mustache/30_search_input/Test search input mustache integration (using request body)", "remove JodaCompatibleDateTime -- ZonedDateTime doesn't output millis/nanos if they're 0 (#78417)")
task.skipTest("mustache/40_search_transform/Test search transform mustache integration (using request body)", "remove JodaCompatibleDateTime -- ZonedDateTime doesn't output millis/nanos if they're 0 (#78417)")
task.skipTest("painless/40_exception/Test painless exceptions are returned when logging a broken response", "Exceptions are no longer thrown from Mustache, but from the transform action itself")
task.replaceKeyInDo("watcher.ack_watch", "xpack-watcher.ack_watch")
task.replaceKeyInDo("watcher.activate_watch", "xpack-watcher.activate_watch")
task.replaceKeyInDo("watcher.deactivate_watch", "xpack-watcher.deactivate_watch")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@

---
"Test painless exceptions are returned when logging a broken response":
- skip:
version: " - 8.6.99"
reason: "self-referencing objects were fixed to be in Painless instead of Mustache in 8.7"

- do:
cluster.health:
wait_for_status: green
Expand Down Expand Up @@ -75,36 +79,5 @@
- match: { watch_record.trigger_event.type: "manual" }
- match: { watch_record.state: "executed" }
- match: { watch_record.result.actions.0.status: "failure" }
- match: { watch_record.result.actions.0.error.caused_by.caused_by.type: "illegal_argument_exception" }
- match: { watch_record.result.actions.0.error.caused_by.caused_by.reason: "Iterable object is self-referencing itself (CustomReflectionObjectHandler stringify)" }

- do:
catch: bad_request
watcher.execute_watch:
body: >
{
"watch": {
"trigger": {
"schedule": {
"interval": "10s"
}
},
"input": {
"simple": {
"foo": "bar"
}
},
"actions": {
"my-logging": {
"transform": {
"script": {
"source": "def x = [:] ; def y = [:] ; x.a = y ; y.a = x ; return x"
}
},
"logging": {
"text": "{{#join}}ctx.payload{{/join}}"
}
}
}
}
}
- match: { watch_record.result.actions.0.reason: "Failed to transform payload" }
- match: { watch_record.result.actions.0.transform.error.reason: "Iterable object is self-referencing itself (watcher action payload)" }