Skip to content

Commit

Permalink
Add simple escape method for special characters to template query
Browse files Browse the repository at this point in the history
The default mustache engine was using HTML escaping which breaks queries
if used with JSON etc. This commit adds escaping for:

```
\b  Backspace (ascii code 08)
\f  Form feed (ascii code 0C)
\n  New line
\r  Carriage return
\t  Tab
\v  Vertical tab
\"  Double quote
\\  Backslash
```

Closes elastic#5473
  • Loading branch information
s1monw committed Mar 20, 2014
1 parent 91847db commit fcd9dfb
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 13 deletions.
9 changes: 8 additions & 1 deletion rest-api-spec/test/search/30_template_query_execution.yaml
Expand Up @@ -12,7 +12,7 @@
index: test
type: testtype
id: 2
body: { "text": "value2" }
body: { "text": "value2 value3" }
- do:
indices.refresh: {}

Expand All @@ -39,3 +39,10 @@
body: { "query": { "template": { "query": "{\"match_{{template}}\": {}}", "params" : { "template" : "all" } } } }

- match: { hits.total: 2 }

- do:
search:
body: { "query": { "template": { "query": "{\"query_string\": { \"query\" : \"{{query}}\" }}", "params" : { "query" : "text:\"value2 value3\"" } } } }


- match: { hits.total: 1 }
@@ -0,0 +1,67 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.script.mustache;

import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.MustacheException;

import java.io.IOException;
import java.io.Writer;

/**
* A MustacheFactory that does simple JSON escaping.
*/
public final class JsonEscapingMustacheFactory extends DefaultMustacheFactory {

@Override
public void encode(String value, Writer writer) {
try {
escape(value, writer);
} catch (IOException e) {
throw new MustacheException("Failed to encode value: " + value);
}
}

public static Writer escape(String value, Writer writer) throws IOException {
for (int i = 0; i < value.length(); i++) {
final char character = value.charAt(i);
if (isEscapeChar(character)) {
writer.write('\\');
}
writer.write(character);
}
return writer;
}

public static boolean isEscapeChar(char c) {
switch(c) {
case '\b':
case '\f':
case '\n':
case '\r':
case '"':
case '\\':
case '\u000B': // vertical tab
case '\t':
return true;
}
return false;
}

}
Expand Up @@ -18,7 +18,6 @@
*/
package org.elasticsearch.script.mustache;

import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.component.AbstractComponent;
Expand Down Expand Up @@ -79,7 +78,7 @@ public MustacheScriptEngineService(Settings settings) {
* */
public Object compile(String template) {
/** Factory to generate Mustache objects from. */
return (new DefaultMustacheFactory()).compile(new FastStringReader(template), "query-template");
return (new JsonEscapingMustacheFactory()).compile(new FastStringReader(template), "query-template");
}

/**
Expand Down
Expand Up @@ -18,38 +18,98 @@
*/
package org.elasticsearch.script.mustache;

import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

import static org.hamcrest.Matchers.equalTo;

/**
* Mustache based templating test
* */
*/
public class MustacheScriptEngineTest extends ElasticsearchTestCase {
private MustacheScriptEngineService qe;

private static String TEMPLATE = "GET _search {\"query\": " + "{\"boosting\": {" + "\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}" + "}}, \"negative_boost\": {{boost_val}} } }}";

@Before
public void setup() {
qe = new MustacheScriptEngineService(ImmutableSettings.Builder.EMPTY_SETTINGS);
}

@Test
public void testSimpleParameterReplace() {
Map<String, Object> vars = new HashMap<String, Object>();
vars.put("boost_val", "0.3");
BytesReference o = (BytesReference) qe.execute(qe.compile(TEMPLATE), vars);
assertEquals("GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}}}, \"negative_boost\": 0.3 } }}",
new String(o.toBytes(), Charset.forName("UTF-8")));
{
String template = "GET _search {\"query\": " + "{\"boosting\": {" + "\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}" + "}}, \"negative_boost\": {{boost_val}} } }}";
Map<String, Object> vars = new HashMap<String, Object>();
vars.put("boost_val", "0.3");
BytesReference o = (BytesReference) qe.execute(qe.compile(template), vars);
assertEquals("GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}}}, \"negative_boost\": 0.3 } }}",
new String(o.toBytes(), Charset.forName("UTF-8")));
}
{
String template = "GET _search {\"query\": " + "{\"boosting\": {" + "\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"{{body_val}}\"}" + "}}, \"negative_boost\": {{boost_val}} } }}";
Map<String, Object> vars = new HashMap<String, Object>();
vars.put("boost_val", "0.3");
vars.put("body_val", "\"quick brown\"");
BytesReference o = (BytesReference) qe.execute(qe.compile(template), vars);
assertEquals("GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"\\\"quick brown\\\"\"}}}, \"negative_boost\": 0.3 } }}",
new String(o.toBytes(), Charset.forName("UTF-8")));
}
}

@Test
public void testEscapeJson() throws IOException {
{
StringWriter writer = new StringWriter();
JsonEscapingMustacheFactory.escape("hello \n world", writer);
assertThat(writer.toString(), equalTo("hello \\\n world"));
}
{
StringWriter writer = new StringWriter();
JsonEscapingMustacheFactory.escape("\n", writer);
assertThat(writer.toString(), equalTo("\\\n"));
}

Character[] specialChars = new Character[]{'\f', '\n', '\r', '"', '\\', (char) 11, '\t', '\b' };
int iters = scaledRandomIntBetween(100, 1000);
for (int i = 0; i < iters; i++) {
int rounds = scaledRandomIntBetween(1, 20);
StringWriter escaped = new StringWriter();
StringWriter writer = new StringWriter();
for (int j = 0; j < rounds; j++) {
String s = getChars();
writer.write(s);
escaped.write(s);
char c = RandomPicks.randomFrom(getRandom(), specialChars);
writer.append(c);
escaped.append('\\');
escaped.append(c);
}
StringWriter target = new StringWriter();
assertThat(escaped.toString(), equalTo(JsonEscapingMustacheFactory.escape(writer.toString(), target).toString()));
}
}

private String getChars() {
String string = randomRealisticUnicodeOfCodepointLengthBetween(0, 10);
for (int i = 0; i < string.length(); i++) {
if (JsonEscapingMustacheFactory.isEscapeChar(string.charAt(i))) {
return string.substring(0, i);
}
}
return string;
}

}

0 comments on commit fcd9dfb

Please sign in to comment.