| /******************************************************************************* |
| * 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.List; |
| import java.util.Map; |
| import java.util.Stack; |
| |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.sling.scripting.sightly.compiler.RuntimeFunction; |
| 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.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<>(); |
| |
| public MarkupHandler(PushStream stream, Map<String, Plugin> pluginRegistry, List<Filter> filters) { |
| this.stream = stream; |
| this.pluginRegistry = pluginRegistry; |
| this.expressionWrapper = new ExpressionWrapper(filters); |
| 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())) { |
| throw new SightlyCompilerException(e.getMessage(), |
| name + (StringUtils.isNotEmpty(value) ? "=" + 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("\"", """); |
| } |
| |
| 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"); |
| String shouldDisplayAttr = symbolGenerator.next("shouldDisplayAttr"); |
| stream.write(new VariableBinding.Start(attrContent, expression.getRoot())); |
| stream.write( |
| new VariableBinding.Start( |
| shouldDisplayAttr, |
| new BinaryOperation( |
| BinaryOperator.OR, |
| new Identifier(attrContent), |
| new BinaryOperation(BinaryOperator.EQ, new StringConstant("false"), new Identifier(attrContent)) |
| ) |
| ) |
| ); |
| stream.write(new Conditional.Start(shouldDisplayAttr, true)); |
| emitAttributeStart(name); |
| invoke.beforeAttributeValue(stream, name, expression.getRoot()); |
| emitAttributeValueStart(quoteChar); |
| stream.write(new OutputVariable(attrContent)); |
| emitAttributeEnd(quoteChar); |
| invoke.afterAttributeValue(stream, name); |
| stream.write(Conditional.END); |
| stream.write(VariableBinding.END); |
| 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 (RuntimeFunction.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, |
| ExpressionContext.ATTRIBUTE).getRoot())); |
| stream.write( |
| new VariableBinding.Start( |
| shouldDisplayAttr, |
| new BinaryOperation( |
| BinaryOperator.OR, |
| new Identifier(attrContent), |
| new BinaryOperation(BinaryOperator.EQ, new StringConstant("false"), new Identifier(attrValue)) |
| ) |
| ) |
| ); |
| |
| } else { |
| stream.write( |
| new VariableBinding.Start( |
| shouldDisplayAttr, |
| new BinaryOperation( |
| BinaryOperator.OR, |
| new Identifier(attrValue), |
| new BinaryOperation(BinaryOperator.EQ, new StringConstant("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, new Identifier(attrValue), BooleanConstant.TRUE))); |
| 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 ("src".equalsIgnoreCase(attributeName) || "href".equalsIgnoreCase(attributeName)) { |
| 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, |
| ExpressionContext expressionContext) { |
| ExpressionNode root = expression.getRoot(); |
| if (root instanceof RuntimeCall) { |
| RuntimeCall runtimeCall = (RuntimeCall) root; |
| if (runtimeCall.getFunctionName().equals(RuntimeFunction.XSS)) { |
| return expression; |
| } |
| } |
| return compilerContext.adjustToContext(expression, markupContext, expressionContext); |
| } |
| } |