Skip to content

Commit

Permalink
Support escape-by-default and raw HTML arguments in Internationalized…
Browse files Browse the repository at this point in the history
…StringExpressions
  • Loading branch information
mcculls committed Apr 17, 2012
1 parent 058aa0d commit fff608f
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@
import org.apache.commons.jelly.expression.ExpressionFactory;
import org.apache.commons.jelly.expression.Expression;
import org.apache.commons.jelly.expression.ExpressionSupport;
import org.apache.commons.jelly.impl.ExpressionScript;
import org.apache.commons.jelly.impl.ScriptBlock;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyException;
import org.apache.commons.jelly.TagLibrary;
import org.kohsuke.stapler.MetaClassLoader;

import java.lang.reflect.Field;
import java.net.URL;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -109,6 +112,18 @@ public TagLibrary getTagLibrary(String namespaceURI) {

private static class CustomXMLParser extends XMLParser implements ExpressionFactory {
private ResourceBundle resourceBundle;

private static Field escapeByDefaultField;
static
{
try {
escapeByDefaultField = XMLParser.class.getDeclaredField("escapeByDefault");
escapeByDefaultField.setAccessible(true);
} catch (Exception e) {
// inconsistent base class - ignore
}
}

@Override
protected ExpressionFactory createExpressionFactory() {
return this;
Expand Down Expand Up @@ -152,14 +167,18 @@ private InternationalizedStringExpression createI18nExp(String text) throws Jell
return new InternationalizedStringExpression(getResourceBundle(),text);
}

// @Override
// protected Expression createEscapingExpression(Expression exp) {
// if ( exp instanceof InternationalizedStringExpression) {
// InternationalizedStringExpression i18nexp = (InternationalizedStringExpression) exp;
// return i18nexp.makeEscapingExpression();
// }
// return super.createEscapingExpression(exp);
// }
@Override
protected void addExpressionScript(ScriptBlock script, Expression exp) {
try {
if (exp instanceof InternationalizedStringExpression && escapeByDefaultField.getBoolean(this)) {
script.addScript(new ExpressionScript(((InternationalizedStringExpression) exp).escape()));
return; // stick with our escaped+internationalized script
}
} catch (Exception e) {
// fall back to original behaviour...
}
super.addExpressionScript(script, exp);
}

private String unquote(String s) {
return s.substring(1,s.length()-1);
Expand Down Expand Up @@ -199,4 +218,4 @@ public Object evaluate(JellyContext context) {

// "%...." string literal that starts with '%'
private static final Pattern RESOURCE_LITERAL_STRING = Pattern.compile("(\"%[^\"]+\")|('%[^']+')");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,93 +133,90 @@ public String getExpressionText() {
return expressionText;
}

public Object evaluate(JellyContext context) {
return format(evaluateArguments(context));
public Object evaluate(JellyContext jellyContext) {
return format(evaluateArguments(jellyContext));
}

private Object format(Object[] args) {
// notify the listener if set
InternationalizedStringExpressionListener listener = (InternationalizedStringExpressionListener) Stapler.getCurrentRequest().getAttribute(LISTENER_NAME);
if(listener!=null)
listener.onUsed(this, args);

return resourceBundle.format(LocaleProvider.getLocale(), key, args);
}

private Object[] evaluateArguments(JellyContext jellyContext) {
Object[] evaluateArguments(JellyContext jellyContext) {
Object[] args = new Object[arguments.length];
for (int i = 0; i < args.length; i++)
args[i] = arguments[i].evaluate(jellyContext);
return args;
}

String format(Object[] args) {
// notify the listener if set
InternationalizedStringExpressionListener listener = (InternationalizedStringExpressionListener)Stapler.getCurrentRequest().getAttribute(LISTENER_NAME);
if(listener!=null)
listener.onUsed(this,args);

return resourceBundle.format(LocaleProvider.getLocale(),key,args);
}

/**
* Creates a new {@link Expression} that performs proper HTML escaping.
* Wraps value to indicate it contains raw HTML that should not be escaped.
*/
public Expression makeEscapingExpression() {
public static Object rawHtml(Object value) {
return value != null ? new RawHtml(value) : null;
}

static final class RawHtml {
final Object value;

RawHtml(Object value) {
this.value = value;
}

@Override
public String toString() {
return String.valueOf(value);
}
}

Expression escape() {
return new ExpressionSupport() {
public String getExpressionText() {
return expressionText;
}

public Object evaluate(JellyContext context) {
Object[] args = evaluateArguments(context);
for (int i=0; i<args.length; i++) {
if (args[i] instanceof RawHtmlArgument)
args[i] = ((RawHtmlArgument)args[i]).value;
else
if (args[i] instanceof Number || args[i] instanceof Calendar || args[i] instanceof Date)
; // formatting numbers and date often requires that they be kept intact
else
args[i] = args[i]==null ? null : escape(args[i].toString());
for (int i = 0; i < args.length; i++) {
args[i] = escapeArgument(args[i]);
}
return format(args);
}

private String escape(String text) {
int len = text.length();
StringBuilder buf = new StringBuilder(len);
boolean escaped = false;

for (int i=0; i< len; i++) {
char ch = text.charAt(i);
switch (ch) {
case '<':
buf.append("&lt;");
escaped = true;
continue;
case '&':
buf.append("&amp;");
escaped = true;
continue;
default:
buf.append(ch);
}
}

if (!escaped) return text; // nothing to escape. no need to create a new string

return buf.toString();

}
};
}

/**
* Argument to {@link InternationalizedStringExpression} that indicates this value is raw HTML
* and therefore should not be further escaped.
*/
public static final class RawHtmlArgument {
private final Object value;

public RawHtmlArgument(Object value) {
this.value = value;
static Object escapeArgument(Object arg) {
if (arg instanceof RawHtml) {
return ((RawHtml)arg).value; // no escaping wanted
}

@Override
public String toString() {
return value==null?"null":value.toString();
if ( arg == null || arg instanceof Number || arg instanceof Date || arg instanceof Calendar ) {
return arg; // no escaping required
}
final String text = arg.toString();
StringBuilder buf = null; // create on-demand
for (int i = 0, len = text.length(); i < len; i++) {
final char c = text.charAt(i);
String replacement = null;
if (c == '&') {
replacement = "&amp;";
} else if (c == '<') {
replacement = "&lt;";
} else if (buf != null) {
buf.append(c); // maintain buffer
}
if (replacement != null) {
if (buf == null) {
// only create translation buffer when we actually need it
buf = new StringBuilder(len+8).append(text.substring(0, i));
}
buf.append(replacement);
}
}
return buf != null ? buf : text;
}

private static final Expression[] EMPTY_ARGUMENTS = new Expression[0];
Expand Down

0 comments on commit fff608f

Please sign in to comment.