Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qute Character Escapes for Json Content #43900

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
19 changes: 19 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,25 @@ If you need to render the unescaped value:

TIP: By default, a template with one of the following content types is escaped: `text/html`, `text/xml`, `application/xml` and `application/xhtml+xml`. However, it's possible to extend this list via the `quarkus.qute.escape-content-types` configuration property.

For JSON templates the `"`, `\t`, `\n`, `\r`, `\` characters are escaped by default if template variant is set, for content type `application/json`

NOTE: In Quarkus, a variant is set automatically for templates located in the `src/main/resources/templates`. By default, the `java.net.URLConnection#getFileNameMap()` is used to determine the content-type of a template file. The additional map of suffixes to content types can be set via `quarkus.qute.content-types`.

If you need to render the unescaped value:

1. Either use the `raw` or `safe` properties implemented as extension methods of the `java.lang.Object`,
2. Or wrap the `String` value in a `io.quarkus.qute.RawString`.

[source,json]
----
{
"id": "{valueId.raw}", <1>
"name": "{valueName}" <2>
}
----
<1> `valueId` that resolves to `\nA12345` will be rendered as `\nA12345` that will result in the end with a unvalid JSON Object because of the new line inserted inside the stringify JSON for the attribute `id` value
<2> `valueName` that resolves to `\tExpressions \n Escapes` will be rendered as `\\tExpressions \\n Escapes`

[[virtual_methods]]
==== Virtual Methods

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import io.quarkus.qute.EvalContext;
import io.quarkus.qute.Expression;
import io.quarkus.qute.HtmlEscaper;
import io.quarkus.qute.JsonEscaper;
import io.quarkus.qute.NamespaceResolver;
import io.quarkus.qute.ParserHook;
import io.quarkus.qute.Qute;
Expand Down Expand Up @@ -157,6 +158,9 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
// Escape some characters for HTML/XML templates
builder.addResultMapper(new HtmlEscaper(List.copyOf(config.escapeContentTypes)));

// Escape some characters for JSON templates
builder.addResultMapper(new JsonEscaper());

// Fallback reflection resolver
builder.addValueResolver(new ReflectionValueResolver());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.quarkus.qute;

import java.util.Optional;

import io.quarkus.qute.TemplateNode.Origin;

public class JsonEscaper implements ResultMapper {

@Override
public boolean appliesTo(Origin origin, Object result) {
if (result instanceof RawString) {
return false;
}
Optional<Variant> variant = origin.getVariant();
if (variant.isPresent()) {
return variant.get().getContentType().startsWith("application/json");
}
return false;
}

@Override
public String map(Object result, Expression expression) {
return escapeJson(result.toString());
}

String escapeJson(String value) {
if (value == null)
return "";

StringBuilder b = new StringBuilder();
for (char c : value.toCharArray()) {
if (c == '\r')
b.append("\\r");
else if (c == '\n')
b.append("\\n");
else if (c == '\t')
b.append("\\t");
else if (c == '"')
b.append("\\\"");
else if (c == '\\')
b.append("\\\\");
else if (c == ' ')
b.append(" ");
else if (Character.isWhitespace(c)) {
b.append("\\u"+String.format("%4s", Integer.toHexString(c)).replace(' ','0'));
} else if (((int) c) < 32)
b.append("\\u"+String.format("%4s", Integer.toHexString(c)).replace(' ','0'));
else
b.append(c);
}
return b.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.quarkus.qute;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.util.Optional;

import org.junit.jupiter.api.Test;

import io.quarkus.qute.TemplateNode.Origin;

public class JsonEscaperTest {

@Test
public void testAppliesTo() {
JsonEscaper json = new JsonEscaper();
Origin jsonOrigin = new Origin() {

@Override
public Optional<Variant> getVariant() {
return Optional.of(Variant.forContentType(Variant.APPLICATION_JSON));
}

@Override
public String getTemplateId() {
return null;
}

@Override
public String getTemplateGeneratedId() {
return null;
}

@Override
public int getLineCharacterStart() {
return 0;
}

@Override
public int getLineCharacterEnd() {
return 0;
}

@Override
public int getLine() {
return 0;
}
};
assertFalse(json.appliesTo(jsonOrigin, new RawString("foo")));
assertTrue(json.appliesTo(jsonOrigin, "foo"));
}

@Test
public void testEscaping() throws IOException {
JsonEscaper json = new JsonEscaper();
assertEquals("Čolek", json.escapeJson("Čolek"));
assertEquals("\\rČolek\\n;", json.escapeJson("\rČolek\n"));
assertEquals("\\tČolek;", json.escapeJson("\tČolek"));
assertEquals("\\\"tČolek;", json.escapeJson("\"tČolek"));
assertEquals("\\\\tČolek;", json.escapeJson("\\tČolek"));
assertEquals("\\u000BČolek;", json.escapeJson("\u000BČolek"));
}

}
Loading