@adobe/htlengine
Version:
Javascript Based HTL (Sightly) parser
239 lines (207 loc) • 10.1 kB
JavaScript
/*
* Copyright 2018 Adobe. All rights reserved.
* This file is licensed 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 REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
/* eslint-disable max-len,no-unused-vars,max-classes-per-file */
const Plugin = require('../html/Plugin');
const VariableBinding = require('../commands/VariableBinding');
const Conditional = require('../commands/Conditional');
const OutputVariable = require('../commands/OutputVariable');
const AddAttribute = require('../commands/AddAttribute');
const Loop = require('../commands/Loop');
const MapLiteral = require('../htl/nodes/MapLiteral');
const PropertyAccess = require('../htl/nodes/PropertyAccess');
const BooleanConstant = require('../htl/nodes/BooleanConstant');
const BinaryOperation = require('../htl/nodes/BinaryOperation');
const BinaryOperator = require('../htl/nodes/BinaryOperator');
const UnaryOperation = require('../htl/nodes/UnaryOperation');
const UnaryOperator = require('../htl/nodes/UnaryOperator');
const StringConstant = require('../htl/nodes/StringConstant');
const RuntimeCall = require('../htl/nodes/RuntimeCall');
const Identifier = require('../htl/nodes/Identifier');
const Expression = require('../htl/nodes/Expression');
const ExpressionContext = require('../html/ExpressionContext');
const MarkupContext = require('../html/MarkupContext');
const REJECTLIST_ATTRIBUTE = /^(style|(on.*))$/;
function escapeNodeWithHint(ctx, node, markupContext, 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('xss', node, [new StringConstant(markupContext), hint]);
}
return ctx.adjustToContext(new Expression(node), markupContext, ExpressionContext.ATTRIBUTE).root;
}
function decodeAttributeName(signature) {
if (signature.args.length > 0) {
return signature.args.join('-');
}
return null;
}
class SingleAttributePlugin extends Plugin {
constructor(signature, ctx, expression, attributeName, location) {
super(signature, ctx, expression, location);
this.attributeName = attributeName;
this.attrValue = ctx.generateVariable(`attrValue_${attributeName}`);
this.node = expression.root;
}
beforeElement(stream, elemContext) {
elemContext.addCallbackAttribute(this.attributeName, () => {
stream.write(new VariableBinding.Start(this.attrValue, this.node));
stream.write(new Conditional.Start(new UnaryOperation(
UnaryOperator.NOT,
new UnaryOperation(
UnaryOperator.IS_EMPTY,
new Identifier(this.attrValue),
),
), false, this.location));
stream.write(new AddAttribute(this.attributeName, new OutputVariable(this.attrValue)));
stream.write(Conditional.END);
stream.write(VariableBinding.END);
});
}
}
class MultiAttributePlugin extends Plugin {
constructor(signature, ctx, expression, location) {
super(signature, ctx, expression, location);
this.attrMap = expression.root;
this.attrMapVar = ctx.generateVariable('attrMap');
this.beforeCall = true;
this.ignored = {};
}
// eslint-disable-next-line class-methods-use-this
beforeElement(stream, elemContext) {
}
beforeAttributes(stream) {
stream.write(new VariableBinding.Start(this.attrMapVar, this.attrMap)); // attrMapVar =
}
beforeAttribute(stream, attributeName) {
this.ignored[attributeName] = new BooleanConstant(true);
if (this.beforeCall) {
const attrNameVar = this.pluginContext.generateVariable(`attrName_${attributeName}`);
const attrValue = this.pluginContext.generateVariable(`mapContains_${attributeName}`);
stream.write(new VariableBinding.Start(attrNameVar, new StringConstant(attributeName))); // attrNameVar = ...
stream.write(new VariableBinding.Start(attrValue, this._attributeValueNode(new StringConstant(attributeName)))); // attrValue = ...
this._writeAttribute(stream, attrNameVar, attrValue);
stream.write(new Conditional.Start(new BinaryOperation(BinaryOperator.EQ, new Identifier(attrValue), Identifier.NULL))); // if (mapContains != null)
}
}
afterAttribute(stream, attributeName) {
if (this.beforeCall) {
stream.write(Conditional.END); // end: if (map contains != null)
stream.write(VariableBinding.END); // end: attrVale = ...
stream.write(VariableBinding.END); // end: attrNameVar = ...
}
}
onPluginCall(stream, signature, expression) {
if (signature.name === 'attribute') {
const attrName = decodeAttributeName(signature);
if (attrName == null) {
this.beforeCall = false;
} else if (!this.beforeCall) {
this.ignored[attrName] = new BooleanConstant(true);
}
}
}
afterAttributes(stream) {
const ctx = this.pluginContext;
const ignoredLiteral = new MapLiteral(this.ignored);
const ignoredVar = ctx.generateVariable('ignoredAttributes');
const attrNameVar = ctx.generateVariable('attrName');
const attrNameVarNode = new Identifier(attrNameVar);
const attrNameEscaped = ctx.generateVariable('attrNameEscaped');
const attrIndex = ctx.generateVariable('attrIndex');
const attrContent = ctx.generateVariable('attrContent');
stream.write(new VariableBinding.Start(ignoredVar, ignoredLiteral)); // ignoredVar = [];
stream.write(new Loop.Start(this.attrMapVar, attrNameVar, attrIndex)); // for (attrNameVar in attrMapVar) {
stream.write(new VariableBinding.Start(attrNameEscaped, this._escapeNode(attrNameVarNode, MarkupContext.ATTRIBUTE_NAME, null))); // attrNameEscaped = ...
stream.write(new Conditional.Start(new Identifier(attrNameEscaped))); // if (attrNameEscaped) {
stream.write(new Conditional.Start(new PropertyAccess(new Identifier(ignoredVar), attrNameVarNode), true)); // if (!ignored[attrName) {
stream.write(new VariableBinding.Start(attrContent, this._attributeValueNode(attrNameVarNode))); // attrContent =
this._writeAttribute(stream, attrNameEscaped, attrContent);
stream.write(VariableBinding.END); // end: attrContent =
stream.write(Conditional.END); // end: if (!ignored[attrName]
stream.write(Conditional.END); // end: if (attrNameEscaped)
stream.write(VariableBinding.END); // end: attrNameEscaped =
stream.write(Loop.END); // end: for
stream.write(VariableBinding.END); // end: ignoredVar
stream.write(VariableBinding.END); // end: attrMapVar (created in beforeAttributes)
}
_writeAttribute(stream, attrNameVar, attrContentVar) {
stream.write(new Conditional.Start(new UnaryOperation(
UnaryOperator.NOT,
new UnaryOperation(
UnaryOperator.IS_EMPTY,
new Identifier(attrContentVar),
),
), false, this.location));
stream.write(new AddAttribute(new OutputVariable(attrNameVar), new OutputVariable(attrContentVar)));
stream.write(Conditional.END);
}
_attributeValueNode(attributeNameNode) {
return new PropertyAccess(new Identifier(this.attrMapVar), attributeNameNode);
}
_escapeNode(node, markupContext, hint) {
return escapeNodeWithHint(this.pluginContext, node, markupContext, hint);
}
}
module.exports = class AttributePlugin extends Plugin {
constructor(signature, ctx, expression, location) {
super(signature, ctx, expression);
const attributeName = decodeAttributeName(signature);
this.writeAtEnd = true;
this.beforeCall = true;
this.attributeName = attributeName;
this.delegate = attributeName == null
? new MultiAttributePlugin(signature, ctx, expression, location)
: new SingleAttributePlugin(signature, ctx, expression, attributeName, location);
}
isValid() {
if (this.attributeName == null || !REJECTLIST_ATTRIBUTE.test(this.attributeName)) {
return true;
}
const warningMessage = ''
+ `Sensible attribute (${this.attributeName}) 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.';
this.pluginContext.stream.warn(warningMessage, this.expression.rawText);
return false;
}
beforeElement() {
// eslint-disable-next-line prefer-spread,prefer-rest-params
this.delegate.beforeElement.apply(this.delegate, arguments);
}
beforeAttributes() {
// eslint-disable-next-line prefer-spread,prefer-rest-params
this.delegate.beforeAttributes.apply(this.delegate, arguments);
}
beforeAttribute(stream, attributeName) {
// eslint-disable-next-line prefer-spread,prefer-rest-params
this.delegate.beforeAttribute.apply(this.delegate, arguments);
}
beforeAttributeValue(stream, attributeName, attributeValue) {
// eslint-disable-next-line prefer-spread,prefer-rest-params
this.delegate.beforeAttributeValue.apply(this.delegate, arguments);
}
afterAttributeValue(stream, attributeName) {
// eslint-disable-next-line prefer-spread,prefer-rest-params
this.delegate.afterAttributeValue.apply(this.delegate, arguments);
}
afterAttribute(stream, attributeName) {
// eslint-disable-next-line prefer-spread,prefer-rest-params
this.delegate.afterAttribute.apply(this.delegate, arguments);
}
afterAttributes(stream) {
// eslint-disable-next-line prefer-spread,prefer-rest-params
this.delegate.afterAttributes.apply(this.delegate, arguments);
}
onPluginCall(stream) {
// eslint-disable-next-line prefer-spread,prefer-rest-params
this.delegate.onPluginCall.apply(this.delegate, arguments);
}
};