blob: b5b92a177174e5863b2219ad164b6794e4b52502 [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.plugin;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.scripting.sightly.compiler.commands.Conditional;
import org.apache.sling.scripting.sightly.compiler.commands.Loop;
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.MapLiteral;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.NullLiteral;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.PropertyAccess;
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.filter.ExpressionContext;
import org.apache.sling.scripting.sightly.impl.html.MarkupUtils;
/**
* Implementation for the attribute plugin.
*/
public class AttributePlugin extends AbstractPlugin {
public AttributePlugin() {
name = "attribute";
priority = 150;
}
@Override
public PluginInvoke invoke(Expression expression, PluginCallInfo callInfo, CompilerContext compilerContext) {
String attributeName = decodeAttributeName(callInfo);
if (attributeName != null && MarkupUtils.isSensitiveAttribute(attributeName)) {
String warningMessage = String.format("Sensible attribute (%s) detected: event attributes (on*) and the style attribute " +
"cannot be generated with the data-sly-attribute block element; if you need to output a dynamic value for " +
"this attribute then use an expression with an appropriate context.", attributeName);
compilerContext.getPushStream().warn(new PushStream.StreamMessage(warningMessage, expression.getRawText()));
return new DefaultPluginInvoke(); //no-op invocation
}
return (attributeName != null)
? new SingleAttributeInvoke(attributeName, expression, compilerContext)
: new MultiAttributeInvoke(expression.getRoot(), compilerContext);
}
private String decodeAttributeName(PluginCallInfo info) {
String[] arguments = info.getArguments();
if (arguments.length == 0) {
return null;
}
return StringUtils.join(arguments, '-');
}
private final class SingleAttributeInvoke extends DefaultPluginInvoke {
private final String attributeName;
private final String isTrueValue;
private final String escapedAttrValue;
private final String shouldDisplayAttribute;
private boolean writeAtEnd = true;
private boolean beforeCall = true;
private final String attrValue;
private final ExpressionNode node;
private final ExpressionNode contentNode;
private SingleAttributeInvoke(String attributeName, Expression expression, CompilerContext compilerContext) {
this.attributeName = attributeName;
this.attrValue = compilerContext.generateVariable("attrValue_" + attributeName);
this.escapedAttrValue = compilerContext.generateVariable("attrValueEscaped_" + attributeName);
this.isTrueValue = compilerContext.generateVariable("isTrueValue_" + attributeName);
this.shouldDisplayAttribute = compilerContext.generateVariable("shouldDisplayAttr_" + attributeName);
this.node = expression.getRoot();
if (!expression.containsOption(Syntax.CONTEXT_OPTION)) {
this.contentNode = escapeNodeWithHint(compilerContext, new Identifier(attrValue), MarkupContext.ATTRIBUTE,
new StringConstant(attributeName));
} else {
this.contentNode = new Identifier(attrValue);
}
}
@Override
public void beforeAttribute(PushStream stream, String attributeName) {
if (attributeName.equals(this.attributeName)) {
if (beforeCall) {
emitStart(stream);
}
writeAtEnd = false;
}
}
@Override
public void beforeAttributeValue(PushStream stream, String attributeName, ExpressionNode attributeValue) {
if (attributeName.equals(this.attributeName) && beforeCall) {
emitWrite(stream);
Patterns.beginStreamIgnore(stream);
}
}
@Override
public void afterAttributeValue(PushStream stream, String attributeName) {
if (attributeName.equals(this.attributeName) && beforeCall) {
Patterns.endStreamIgnore(stream);
}
}
@Override
public void afterAttribute(PushStream stream, String attributeName) {
if (attributeName.equals(this.attributeName) && beforeCall) {
emitEnd(stream);
}
}
@Override
public void afterAttributes(PushStream stream) {
if (writeAtEnd) {
emitStart(stream);
stream.write(new OutText(" " + this.attributeName));
emitWrite(stream);
emitEnd(stream);
}
}
@Override
public void onPluginCall(PushStream stream, PluginCallInfo callInfo, Expression expression) {
if ("attribute".equals(callInfo.getName())) {
String attributeName = decodeAttributeName(callInfo);
if (this.attributeName.equals(attributeName)) {
beforeCall = false;
}
}
}
private void emitStart(PushStream stream) {
stream.write(new VariableBinding.Start(attrValue, node));
stream.write(new VariableBinding.Start(escapedAttrValue, contentNode));
stream.write(
new VariableBinding.Start(
shouldDisplayAttribute,
new BinaryOperation(
BinaryOperator.AND,
new BinaryOperation(
BinaryOperator.AND,
new BinaryOperation(BinaryOperator.NEQ, NullLiteral.INSTANCE, new Identifier(escapedAttrValue)),
new BinaryOperation(BinaryOperator.NEQ, StringConstant.EMPTY, new Identifier(escapedAttrValue))
),
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(shouldDisplayAttribute, true));
}
private void emitWrite(PushStream stream) {
stream.write(new VariableBinding.Start(isTrueValue,
new BinaryOperation(BinaryOperator.EQ,
BooleanConstant.TRUE,
new Identifier(attrValue))));
stream.write(new Conditional.Start(isTrueValue, false));
stream.write(new OutText("=\""));
stream.write(new OutputVariable(escapedAttrValue));
stream.write(new OutText("\""));
stream.write(Conditional.END);
stream.write(VariableBinding.END);
}
private void emitEnd(PushStream stream) {
stream.write(Conditional.END);
stream.write(VariableBinding.END);
stream.write(VariableBinding.END);
stream.write(VariableBinding.END);
}
}
private final class MultiAttributeInvoke extends DefaultPluginInvoke {
private final ExpressionNode attrMap;
private final String attrMapVar;
private final CompilerContext compilerContext;
private boolean beforeCall = true;
private final Set<String> ignored = new HashSet<>();
private MultiAttributeInvoke(ExpressionNode attrMap, CompilerContext context) {
this.attrMap = attrMap;
this.compilerContext = context;
this.attrMapVar = context.generateVariable("attrMap");
}
@Override
public void beforeAttributes(PushStream stream) {
stream.write(new VariableBinding.Start(attrMapVar, attrMap));
}
@Override
public void beforeAttribute(PushStream stream, String attributeName) {
ignored.add(attributeName);
if (beforeCall) {
String attrNameVar = compilerContext.generateVariable("attrName_" + attributeName);
String attrValue = compilerContext.generateVariable("mapContains_" + attributeName);
stream.write(new VariableBinding.Start(attrNameVar, new StringConstant(attributeName)));
stream.write(new VariableBinding.Start(attrValue, attributeValueNode(new StringConstant(attributeName))));
writeAttribute(stream, attrNameVar, attrValue);
stream.write(new Conditional.Start(attrValue, false));
}
}
@Override
public void afterAttribute(PushStream stream, String attributeName) {
if (beforeCall) {
stream.write(Conditional.END);
stream.write(VariableBinding.END);
stream.write(VariableBinding.END);
}
}
@Override
public void onPluginCall(PushStream stream, PluginCallInfo callInfo, Expression expression) {
if ("attribute".equals(callInfo.getName())) {
String attrName = decodeAttributeName(callInfo);
if (attrName == null) {
beforeCall = false;
} else {
if (!beforeCall) {
ignored.add(attrName);
}
}
}
}
@Override
public void afterAttributes(PushStream stream) {
HashMap<String, ExpressionNode> ignoredLiteralMap = new HashMap<>();
for (String attr : ignored) {
ignoredLiteralMap.put(attr, new BooleanConstant(true));
}
MapLiteral ignoredLiteral = new MapLiteral(ignoredLiteralMap);
String ignoredVar = compilerContext.generateVariable("ignoredAttributes");
stream.write(new VariableBinding.Start(ignoredVar, ignoredLiteral));
String attrNameVar = compilerContext.generateVariable("attrName");
String attrNameEscaped = compilerContext.generateVariable("attrNameEscaped");
String attrIndex = compilerContext.generateVariable("attrIndex");
stream.write(new Loop.Start(attrMapVar, attrNameVar, attrIndex));
stream.write(new VariableBinding.Start(attrNameEscaped,
escapeNode(new Identifier(attrNameVar), MarkupContext.ATTRIBUTE_NAME, null)));
stream.write(new Conditional.Start(attrNameEscaped, true));
String isIgnoredAttr = compilerContext.generateVariable("isIgnoredAttr");
stream.write(
new VariableBinding.Start(isIgnoredAttr, new PropertyAccess(new Identifier(ignoredVar), new Identifier(attrNameVar))));
stream.write(new Conditional.Start(isIgnoredAttr, false));
String attrContent = compilerContext.generateVariable("attrContent");
stream.write(new VariableBinding.Start(attrContent, attributeValueNode(new Identifier(attrNameVar))));
writeAttribute(stream, attrNameEscaped, attrContent);
stream.write(VariableBinding.END); //end of attrContent
stream.write(Conditional.END);
stream.write(VariableBinding.END);
stream.write(Conditional.END);
stream.write(VariableBinding.END);
stream.write(Loop.END);
stream.write(VariableBinding.END);
stream.write(VariableBinding.END);
}
private void writeAttribute(PushStream stream, String attrNameVar, String attrContentVar) {
String escapedContent = compilerContext.generateVariable("attrContentEscaped");
String shouldDisplayAttribute = compilerContext.generateVariable("shouldDisplayAttr");
stream.write(new VariableBinding.Start(escapedContent,
escapedExpression(new Identifier(attrContentVar), new Identifier(attrNameVar))));
stream.write(
new VariableBinding.Start(
shouldDisplayAttribute,
new BinaryOperation(
BinaryOperator.AND,
new BinaryOperation(
BinaryOperator.AND,
new BinaryOperation(BinaryOperator.NEQ, NullLiteral.INSTANCE, new Identifier(escapedContent)),
new BinaryOperation(BinaryOperator.NEQ, StringConstant.EMPTY, new Identifier(escapedContent))
),
new BinaryOperation(BinaryOperator.AND,
new BinaryOperation(BinaryOperator.NEQ, StringConstant.EMPTY, new Identifier(attrContentVar)),
new BinaryOperation(BinaryOperator.NEQ, BooleanConstant.FALSE, new Identifier(attrContentVar))
)
)
)
);
stream.write(new Conditional.Start(shouldDisplayAttribute, true));
stream.write(new OutText(" ")); //write("attrName");
writeAttributeName(stream, attrNameVar);
writeAttributeValue(stream, escapedContent, attrContentVar);
stream.write(Conditional.END);
stream.write(VariableBinding.END);
stream.write(VariableBinding.END);
}
private void writeAttributeName(PushStream stream, String attrNameVar) {
stream.write(new OutputVariable(attrNameVar));
}
private void writeAttributeValue(PushStream stream, String escapedContent, String attrContentVar) {
String isTrueVar = compilerContext.generateVariable("isTrueAttr"); // holds the comparison (attrValue == true)
stream.write(new VariableBinding.Start(isTrueVar, //isTrueAttr = (attrContent == true)
new BinaryOperation(BinaryOperator.EQ, BooleanConstant.TRUE, new Identifier(attrContentVar))));
stream.write(new Conditional.Start(isTrueVar, false)); //if (!isTrueAttr)
stream.write(new OutText("=\""));
stream.write(new OutputVariable(escapedContent)); //write(escapedContent)
stream.write(new OutText("\""));
stream.write(Conditional.END); //end if isTrueAttr
stream.write(VariableBinding.END); //end scope for isTrueAttr
}
private ExpressionNode attributeValueNode(ExpressionNode attributeNameNode) {
return new PropertyAccess(new Identifier(attrMapVar), attributeNameNode);
}
private ExpressionNode escapedExpression(ExpressionNode original, ExpressionNode hint) {
return escapeNode(original, MarkupContext.ATTRIBUTE, hint);
}
private ExpressionNode escapeNode(ExpressionNode node, MarkupContext markupContext, ExpressionNode hint) {
return escapeNodeWithHint(compilerContext, node, markupContext, hint);
}
}
private static ExpressionNode escapeNodeWithHint(CompilerContext compilerContext, ExpressionNode node, MarkupContext markupContext,
ExpressionNode hint) {
if (hint != null) {
//todo: this is not the indicated way to escape via XSS. Correct after modifying the compiler context API
return new RuntimeCall(RuntimeCall.XSS, node, new StringConstant(markupContext.getName()), hint);
}
return compilerContext.adjustToContext(new Expression(node), markupContext, ExpressionContext.ATTRIBUTE).getRoot();
}
}