blob: a2767d41e31e3101d3a209013c58656460fb85ed [file] [log] [blame]
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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.apache.sling.scripting.sightly.impl.html.dom;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.scripting.sightly.compiler.SightlyCompilerException;
import org.apache.sling.scripting.sightly.compiler.commands.Conditional;
import org.apache.sling.scripting.sightly.compiler.commands.OutText;
import org.apache.sling.scripting.sightly.compiler.commands.OutputVariable;
import org.apache.sling.scripting.sightly.compiler.commands.VariableBinding;
import org.apache.sling.scripting.sightly.compiler.expression.Expression;
import org.apache.sling.scripting.sightly.compiler.expression.ExpressionNode;
import org.apache.sling.scripting.sightly.compiler.expression.MarkupContext;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.BinaryOperation;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.BinaryOperator;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.BooleanConstant;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.Identifier;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.NullLiteral;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.RuntimeCall;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.StringConstant;
import org.apache.sling.scripting.sightly.impl.compiler.Patterns;
import org.apache.sling.scripting.sightly.impl.compiler.PushStream;
import org.apache.sling.scripting.sightly.impl.compiler.Syntax;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.CompilerContext;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.ElementContext;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.ExpressionParser;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.ExpressionWrapper;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.Fragment;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.Interpolation;
import org.apache.sling.scripting.sightly.impl.compiler.util.SymbolGenerator;
import org.apache.sling.scripting.sightly.impl.filter.ExpressionContext;
import org.apache.sling.scripting.sightly.impl.filter.Filter;
import org.apache.sling.scripting.sightly.impl.html.MarkupUtils;
import org.apache.sling.scripting.sightly.impl.plugin.Plugin;
import org.apache.sling.scripting.sightly.impl.plugin.PluginCallInfo;
import org.apache.sling.scripting.sightly.impl.plugin.PluginInvoke;
/**
* Implementation for the markup handler
*/
public class MarkupHandler {
private final PushStream stream;
private final SymbolGenerator symbolGenerator = new SymbolGenerator();
private final ExpressionParser expressionParser = new ExpressionParser();
private final Map<String, Plugin> pluginRegistry;
private final CompilerContext compilerContext;
private final ExpressionWrapper expressionWrapper;
private final Stack<ElementContext> elementStack = new Stack<>();
private static final Set<String> URI_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("action", "cite",
"data", "formaction", "href", "manifest", "poster", "src")));
public MarkupHandler(PushStream stream, Map<String, Plugin> pluginRegistry, List<Filter> filters, Set<String> knownExpressionOptions) {
this.stream = stream;
this.pluginRegistry = pluginRegistry;
this.expressionWrapper = new ExpressionWrapper(stream, filters, knownExpressionOptions);
this.compilerContext = new CompilerContext(symbolGenerator, expressionWrapper, stream);
}
public void onOpenTagStart(String markup, String tagName) {
ElementContext context = new ElementContext(tagName, markup);
elementStack.push(context);
}
public void onAttribute(String name, String value, char quoteChar) {
ElementContext context = elementStack.peek();
if (Syntax.isPluginAttribute(name)) {
try {
handlePlugin(name, StringUtils.defaultString(value, ""), context);
} catch (SightlyCompilerException e) {
if (StringUtils.isEmpty(e.getOffendingInput()) && StringUtils.isNotEmpty(value)) {
throw new SightlyCompilerException(e.getMessage(), name + "=" + quoteChar + value + quoteChar );
}
throw e;
}
} else {
context.addAttribute(name, value, quoteChar);
}
}
public void onOpenTagEnd(String markup) {
ElementContext context = elementStack.peek();
PluginInvoke invoke = context.pluginInvoke();
invoke.beforeElement(stream, context.getTagName());
boolean slyTag = "sly".equalsIgnoreCase(context.getTagName());
if (slyTag) {
Patterns.beginStreamIgnore(stream);
}
invoke.beforeTagOpen(stream);
out(context.getOpenTagStartMarkup());
invoke.beforeAttributes(stream);
traverseAttributes(context, invoke);
invoke.afterAttributes(stream);
out(markup);
invoke.afterTagOpen(stream);
if (slyTag) {
Patterns.endStreamIgnore(stream);
}
invoke.beforeChildren(stream);
}
private void traverseAttributes(ElementContext context, PluginInvoke invoke) {
for (ElementContext.Attribute attribute : context.getAttributes()) {
String attrName = attribute.getName();
Object contentObj = attribute.getValue();
if (contentObj == null || contentObj instanceof String) {
String content = (String) contentObj;
emitAttribute(attrName, content, attribute.getQuoteChar(), invoke);
} else if (contentObj instanceof Map.Entry) {
Map.Entry entry = (Map.Entry) contentObj;
PluginCallInfo info = (PluginCallInfo) entry.getKey();
Expression expression = (Expression) entry.getValue();
invoke.onPluginCall(stream, info, expression);
}
}
}
private void emitAttribute(String name, String content, char quoteChar, PluginInvoke invoke) {
invoke.beforeAttribute(stream, name);
if (content == null) {
emitSimpleTextAttribute(name, null, quoteChar, invoke);
} else {
Interpolation interpolation = expressionParser.parseInterpolation(content);
String text = tryAsSimpleText(interpolation);
if (text != null) {
emitSimpleTextAttribute(name, text, quoteChar, invoke);
} else {
emitExpressionAttribute(name, interpolation, quoteChar, invoke);
}
}
invoke.afterAttribute(stream, name);
}
private void emitSimpleTextAttribute(String name, String textValue, char quoteChar, PluginInvoke invoke) {
emitAttributeStart(name);
invoke.beforeAttributeValue(stream, name, new StringConstant(textValue));
if (textValue != null) {
emitAttributeValueStart(quoteChar);
textValue = escapeQuotes(textValue);
out(textValue);
emitAttributeEnd(quoteChar);
}
invoke.afterAttributeValue(stream, name);
}
private String escapeQuotes(String textValue) {
return textValue.replace("\"", "&quot;");
}
private void emitExpressionAttribute(String name, Interpolation interpolation, char quoteChar, PluginInvoke invoke) {
interpolation = attributeChecked(name, interpolation);
if (interpolation.size() == 1) {
emitSingleFragment(name, interpolation, quoteChar, invoke);
} else {
emitMultipleFragment(name, interpolation, quoteChar, invoke);
}
}
private void emitMultipleFragment(String name, Interpolation interpolation, char quoteChar, PluginInvoke invoke) {
// Simplified algorithm for attribute output, which works when the interpolation is not of size 1. In this
// case we are certain that the attribute value cannot be the boolean value true, so we can skip this test
// altogether
Expression expression = expressionWrapper.transform(interpolation, getAttributeMarkupContext(name), ExpressionContext.ATTRIBUTE);
String attrContent = symbolGenerator.next("attrContent");
stream.write(new VariableBinding.Start(attrContent, expression.getRoot()));
emitAttributeStart(name);
invoke.beforeAttributeValue(stream, name, expression.getRoot());
emitAttributeValueStart(quoteChar);
stream.write(new OutputVariable(attrContent));
emitAttributeEnd(quoteChar);
invoke.afterAttributeValue(stream, name);
stream.write(VariableBinding.END);
}
private void emitSingleFragment(String name, Interpolation interpolation, char quoteChar, PluginInvoke invoke) {
Expression valueExpression = expressionWrapper.transform(interpolation, null, ExpressionContext.ATTRIBUTE); //raw expression
String attrValue = symbolGenerator.next("attrValue"); //holds the raw attribute value
String attrContent = symbolGenerator.next("attrContent"); //holds the escaped attribute value
String isTrueVar = symbolGenerator.next("isTrueAttr"); // holds the comparison (attrValue == true)
String shouldDisplayAttr = symbolGenerator.next("shouldDisplayAttr");
MarkupContext markupContext = getAttributeMarkupContext(name);
boolean alreadyEscaped = false;
if (valueExpression.getRoot() instanceof RuntimeCall) {
RuntimeCall rc = (RuntimeCall) valueExpression.getRoot();
if (RuntimeCall.XSS.equals(rc.getFunctionName())) {
alreadyEscaped = true;
}
}
ExpressionNode node = valueExpression.getRoot();
stream.write(new VariableBinding.Start(attrValue, node)); //attrContent = <expr>
if (!alreadyEscaped) {
Expression contentExpression = valueExpression.withNode(new Identifier(attrValue));
stream.write(new VariableBinding.Start(attrContent, adjustContext(compilerContext, contentExpression, markupContext).getRoot()));
stream.write(
new VariableBinding.Start(
shouldDisplayAttr,
new BinaryOperation(
BinaryOperator.AND,
new BinaryOperation(
BinaryOperator.AND,
new BinaryOperation(BinaryOperator.NEQ, NullLiteral.INSTANCE, new Identifier(attrContent)),
new BinaryOperation(BinaryOperator.NEQ, StringConstant.EMPTY, new Identifier(attrContent))
),
new BinaryOperation(BinaryOperator.AND,
new BinaryOperation(BinaryOperator.NEQ, StringConstant.EMPTY, new Identifier(attrValue)),
new BinaryOperation(BinaryOperator.NEQ, BooleanConstant.FALSE, new Identifier(attrValue))
)
)
)
);
} else {
stream.write(
new VariableBinding.Start(
shouldDisplayAttr,
new BinaryOperation(
BinaryOperator.AND,
new BinaryOperation(BinaryOperator.NEQ, NullLiteral.INSTANCE, new Identifier(attrValue)),
new BinaryOperation(
BinaryOperator.AND,
new BinaryOperation(BinaryOperator.NEQ, StringConstant.EMPTY, new Identifier(attrValue)),
new BinaryOperation(BinaryOperator.NEQ, BooleanConstant.FALSE, new Identifier(attrValue))
)
)
)
);
}
stream.write(new Conditional.Start(shouldDisplayAttr, true)); // if (attrContent)
emitAttributeStart(name); //write("attrName");
invoke.beforeAttributeValue(stream, name, node);
stream.write(new VariableBinding.Start(isTrueVar, //isTrueAttr = (attrValue == true)
new BinaryOperation(BinaryOperator.EQ, BooleanConstant.TRUE, new Identifier(attrValue))));
stream.write(new Conditional.Start(isTrueVar, false)); //if (!isTrueAttr)
emitAttributeValueStart(quoteChar); // write("='");
if (!alreadyEscaped) {
stream.write(new OutputVariable(attrContent)); //write(attrContent)
} else {
stream.write(new OutputVariable(attrValue)); // write(attrValue)
}
emitAttributeEnd(quoteChar); //write("'");
stream.write(Conditional.END); //end if isTrueAttr
stream.write(VariableBinding.END); //end scope for isTrueAttr
invoke.afterAttributeValue(stream, name);
stream.write(Conditional.END); //end if attrContent
stream.write(VariableBinding.END); //end scope for attrContent
if (!alreadyEscaped) {
stream.write(VariableBinding.END);
}
stream.write(VariableBinding.END); //end scope for attrValue
}
private void emitAttributeStart(String name) {
out(" " + name);
}
private void emitAttributeValueStart(char quoteChar) {
char quote = '"';
if (quoteChar != 0) {
quote = quoteChar;
}
out("=");
out(String.valueOf(quote));
}
private void emitAttributeEnd(char quoteChar) {
char quote = '"';
if (quoteChar != 0) {
quote = quoteChar;
}
out(String.valueOf(quote));
}
public void onCloseTag(String markup) {
ElementContext context = elementStack.pop();
PluginInvoke invoke = context.pluginInvoke();
invoke.afterChildren(stream);
boolean selfClosingTag = StringUtils.isEmpty(markup);
boolean slyTag = "sly".equalsIgnoreCase(context.getTagName());
if (slyTag) {
Patterns.beginStreamIgnore(stream);
}
invoke.beforeTagClose(stream, selfClosingTag);
out(markup);
invoke.afterTagClose(stream, selfClosingTag);
if (slyTag) {
Patterns.endStreamIgnore(stream);
}
invoke.afterElement(stream);
}
public void onText(String text) {
String tag = currentElementTag();
boolean explicitContextRequired = isExplicitContextRequired(tag);
MarkupContext markupContext = (explicitContextRequired) ? null : MarkupContext.TEXT;
outText(text, markupContext);
}
public void onComment(String markup) {
if (!Syntax.isSightlyComment(markup)) {
outText(markup, MarkupContext.COMMENT);
}
}
public void onDataNode(String markup) {
out(markup);
}
public void onDocType(String markup) {
out(markup);
}
public void onDocumentFinished() {
this.stream.close();
}
private void outText(String content, MarkupContext context) {
Interpolation interpolation = expressionParser.parseInterpolation(content);
if (context == null) {
interpolation = requireContext(interpolation);
}
String text = tryAsSimpleText(interpolation);
if (text != null) {
out(text);
} else {
outExprNode(expressionWrapper.transform(interpolation, context, ExpressionContext.TEXT).getRoot());
}
}
private Interpolation requireContext(Interpolation interpolation) {
Interpolation result = new Interpolation();
for (Fragment fragment : interpolation.getFragments()) {
Fragment addedFragment;
if (fragment.isString()) {
addedFragment = fragment;
} else {
if (fragment.getExpression().containsOption(Syntax.CONTEXT_OPTION)) {
addedFragment = fragment;
} else {
String currentTag = currentElementTag();
String warningMessage = String.format("Element %s requires that all expressions have an explicit context specified. " +
"The expression will be replaced with an empty string.", currentTag);
stream.warn(new PushStream.StreamMessage(warningMessage, fragment.getExpression().getRawText()));
addedFragment = new Fragment.Expr(new Expression(StringConstant.EMPTY));
}
}
result.addFragment(addedFragment);
}
return result;
}
private Interpolation attributeChecked(String attributeName, Interpolation interpolation) {
if (!MarkupUtils.isSensitiveAttribute(attributeName)) {
return interpolation;
}
Interpolation newInterpolation = new Interpolation();
for (Fragment fragment : interpolation.getFragments()) {
Fragment addedFragment = fragment;
if (fragment.isExpression()) {
Expression expression = fragment.getExpression();
if (!expression.containsOption(Syntax.CONTEXT_OPTION)) {
String warningMessage = String.format("Expressions within the value of attribute %s need to have an explicit context " +
"option. The expression will be replaced with an empty string.", attributeName);
stream.warn(new PushStream.StreamMessage(warningMessage, expression.getRawText()));
addedFragment = new Fragment.Text("");
}
}
newInterpolation.addFragment(addedFragment);
}
return newInterpolation;
}
private void outExprNode(ExpressionNode node) {
String variable = symbolGenerator.next();
stream.write(new VariableBinding.Start(variable, node));
stream.write(new OutputVariable(variable));
stream.write(VariableBinding.END);
}
private String tryAsSimpleText(Interpolation interpolation) {
if (interpolation.size() == 1) {
Fragment fragment = interpolation.getFragment(0);
if (fragment.isString()) {
return fragment.getText();
}
} else if (interpolation.size() == 0) {
return "";
}
return null;
}
private void out(String text) {
stream.write(new OutText(text));
}
private void handlePlugin(String name, String value, ElementContext context) {
PluginCallInfo callInfo = Syntax.parsePluginAttribute(name);
if (callInfo != null) {
Plugin plugin = obtainPlugin(callInfo.getName());
ExpressionContext expressionContext = ExpressionContext.getContextForPlugin(plugin.name());
Expression expr = expressionWrapper.transform(expressionParser.parseInterpolation(value), null, expressionContext);
PluginInvoke invoke = plugin.invoke(expr, callInfo, compilerContext);
context.addPlugin(invoke, plugin.priority());
context.addPluginCall(name, callInfo, expr);
}
}
private Plugin obtainPlugin(String name) {
Plugin plugin = pluginRegistry.get(name);
if (plugin == null) {
throw new SightlyCompilerException(String.format("None of the registered plugins can handle the data-sly-%s block element.",
name), "data-sly-" + name);
}
return plugin;
}
private MarkupContext getAttributeMarkupContext(String attributeName) {
if (URI_ATTRIBUTES.contains(attributeName.toLowerCase())) {
return MarkupContext.URI;
}
return MarkupContext.ATTRIBUTE;
}
private String currentElementTag() {
if (elementStack.isEmpty()) {
return null;
}
ElementContext current = elementStack.peek();
return current.getTagName();
}
private boolean isExplicitContextRequired(String parentElementName) {
return parentElementName != null &&
("script".equals(parentElementName) || "style".equals(parentElementName));
}
private Expression adjustContext(CompilerContext compilerContext, Expression expression, MarkupContext markupContext) {
ExpressionNode root = expression.getRoot();
if (root instanceof RuntimeCall) {
RuntimeCall runtimeCall = (RuntimeCall) root;
if (runtimeCall.getFunctionName().equals(RuntimeCall.XSS)) {
return expression;
}
}
return compilerContext.adjustToContext(expression, markupContext, ExpressionContext.ATTRIBUTE);
}
}