diff --git a/packages/kbn-handlebars/index.test.ts b/packages/kbn-handlebars/index.test.ts index ed607db1e0bf6..573834c953ffa 100644 --- a/packages/kbn-handlebars/index.test.ts +++ b/packages/kbn-handlebars/index.test.ts @@ -20,18 +20,226 @@ it('Handlebars.create', () => { describe('Handlebars.compileAST', () => { describe('compiler options', () => { - it('noEscape', () => { - expectTemplate('{{value}}').withInput({ value: '' }).toCompileTo('<foo>'); - - expectTemplate('{{value}}') - .withCompileOptions({ noEscape: false }) - .withInput({ value: '' }) - .toCompileTo('<foo>'); - - expectTemplate('{{value}}') - .withCompileOptions({ noEscape: true }) - .withInput({ value: '' }) - .toCompileTo(''); + describe('noEscape', () => { + describe('basic', () => { + it('should escape a non-nested value with default value set for `noEscape`', () => { + expectTemplate('{{value}}').withInput({ value: '' }).toCompileTo('<foo>'); + }); + + it('should escape a nested value with default value set for `noEscape`', () => { + expectTemplate('{{nested.value}}') + .withInput({ nested: { value: '' } }) + .toCompileTo('<foo>'); + }); + + it('should escape a non-nested value with `noEscape` set to false', () => { + expectTemplate('{{value}}') + .withCompileOptions({ noEscape: false }) + .withInput({ value: '' }) + .toCompileTo('<foo>'); + }); + + it('should escape a nested value with `noEscape` set to false', () => { + expectTemplate('{{nested.value}}') + .withCompileOptions({ noEscape: false }) + .withInput({ nested: { value: '' } }) + .toCompileTo('<foo>'); + }); + + it('should not escape a non-nested value with `noEscape` set to true', () => { + expectTemplate('{{value}}') + .withCompileOptions({ noEscape: true }) + .withInput({ value: '' }) + .toCompileTo(''); + }); + + it('should not escape a nested value with `noEscape` set to true', () => { + expectTemplate('{{nested.value}}') + .withCompileOptions({ noEscape: true }) + .withInput({ nested: { value: '' } }) + .toCompileTo(''); + }); + }); + + describe('known helper', () => { + it('should escape a non-nested value with a known helper and default value set for `noEscape`', () => { + expectTemplate('{{#with foo}}{{value}}{{/with}}') + .withInput({ foo: { value: '' } }) + .toCompileTo('<bar>'); + }); + + it('should escape a nested value with a known helper and default value set for `noEscape`', () => { + expectTemplate('{{#with foo}}{{nested.value}}{{/with}}') + .withInput({ foo: { nested: { value: '' } } }) + .toCompileTo('<bar>'); + }); + + it('should escape a non-nested value with a known helper and false value set for `noEscape`', () => { + expectTemplate('{{#with foo}}{{value}}{{/with}}') + .withCompileOptions({ noEscape: false }) + .withInput({ foo: { value: '' } }) + .toCompileTo('<bar>'); + }); + + it('should escape a nested value with a known helper and false value set for `noEscape`', () => { + expectTemplate('{{#with foo}}{{nested.value}}{{/with}}') + .withCompileOptions({ noEscape: false }) + .withInput({ foo: { nested: { value: '' } } }) + .toCompileTo('<bar>'); + }); + + it('should not escape a non-nested value with a known helper and true value set for `noEscape`', () => { + expectTemplate('{{#with foo}}{{value}}{{/with}}') + .withCompileOptions({ noEscape: true }) + .withInput({ foo: { value: '' } }) + .toCompileTo(''); + }); + + it('should not escape a nested value with a known helper and true value set for `noEscape`', () => { + expectTemplate('{{#with foo}}{{nested.value}}{{/with}}') + .withCompileOptions({ noEscape: true }) + .withInput({ foo: { nested: { value: '' } } }) + .toCompileTo(''); + }); + }); + + describe('unknown helper', () => { + it('should escape a non-nested value with an unknown helper and no value set for `noEscape`', () => { + expectTemplate('{{foo value}}') + .withHelper('foo', (value: string) => { + return value + 'baz'; + }) + .withInput({ value: '' }) + .toCompileTo('<bar>baz'); + }); + + it('should escape a nested value with an unknown helper and no value set for `noEscape`', () => { + expectTemplate('{{foo nested.value}}') + .withHelper('foo', (value: string) => { + return value + 'baz'; + }) + .withInput({ nested: { value: '' } }) + .toCompileTo('<bar>baz'); + }); + + it('should escape a non-nested value with an unknown helper and false value set for `noEscape`', () => { + expectTemplate('{{foo value}}') + .withHelper('foo', (value: string) => { + return value + 'baz'; + }) + .withCompileOptions({ noEscape: false }) + .withInput({ value: '' }) + .toCompileTo('<bar>baz'); + }); + + it('should escape a nested value with an unknown helper and false value set for `noEscape`', () => { + expectTemplate('{{foo nested.value}}') + .withHelper('foo', (value: string) => { + return value + 'baz'; + }) + .withCompileOptions({ noEscape: false }) + .withInput({ nested: { value: '' } }) + .toCompileTo('<bar>baz'); + }); + + it('should not escape a non-nested value with an unknown helper and true value set for `noEscape`', () => { + expectTemplate('{{foo value}}') + .withHelper('foo', (value: string) => { + return value + 'baz'; + }) + .withCompileOptions({ noEscape: true }) + .withInput({ value: '' }) + .toCompileTo('baz'); + }); + + it('should not escape a nested value with an unknown helper and true value set for `noEscape`', () => { + expectTemplate('{{foo nested.value}}') + .withHelper('foo', (value: string) => { + return value + 'baz'; + }) + .withCompileOptions({ noEscape: true }) + .withInput({ nested: { value: '' } }) + .toCompileTo('baz'); + }); + }); + + describe('blocks', () => { + it('should escape a non-nested value with a block input and default value for `noEscape`', () => { + expectTemplate('{{#with foo}}{{#../myFunction}}{{value}}{{/../myFunction}}{{/with}}') + .withInput({ + foo: { value: '' }, + myFunction() { + return this; + }, + }) + .toCompileTo('<bar>'); + }); + + it('should escape a non-nested value with an block input and false value set for `noEscape`', () => { + expectTemplate('{{#with foo}}{{#../myFunction}}{{value}}{{/../myFunction}}{{/with}}') + .withInput({ + foo: { value: '' }, + myFunction() { + return this; + }, + }) + .withCompileOptions({ noEscape: false }) + .toCompileTo('<bar>'); + }); + + it('should not escape a non-nested value with an block input and true value set for `noEscape`', () => { + expectTemplate('{{#with foo}}{{#../myFunction}}{{value}}{{/../myFunction}}{{/with}}') + .withInput({ + foo: { value: '' }, + myFunction() { + return this; + }, + }) + .withCompileOptions({ noEscape: true }) + .toCompileTo(''); + }); + + it('should escape a nested value with an block input and default value for `noEscape`', () => { + expectTemplate( + '{{#with foo}}{{#../myFunction}}{{nested.value}}{{/../myFunction}}{{/with}}' + ) + .withInput({ + foo: { nested: { value: '' } }, + myFunction() { + return this; + }, + }) + .toCompileTo('<bar>'); + }); + + it('should escape a nested value with an block input and false value for `noEscape`', () => { + expectTemplate( + '{{#with foo}}{{#../myFunction}}{{nested.value}}{{/../myFunction}}{{/with}}' + ) + .withInput({ + foo: { nested: { value: '' } }, + myFunction() { + return this; + }, + }) + .withCompileOptions({ noEscape: false }) + .toCompileTo('<bar>'); + }); + + it('should escape a nested value with an block input and true value for `noEscape`', () => { + expectTemplate( + '{{#with foo}}{{#../myFunction}}{{nested.value}}{{/../myFunction}}{{/with}}' + ) + .withInput({ + foo: { nested: { value: '' } }, + myFunction() { + return this; + }, + }) + .withCompileOptions({ noEscape: true }) + .toCompileTo(''); + }); + }); }); }); diff --git a/packages/kbn-handlebars/src/spec/.upstream_git_hash b/packages/kbn-handlebars/src/spec/.upstream_git_hash index 0d4d453a0c5c3..3751f591ea3c3 100644 --- a/packages/kbn-handlebars/src/spec/.upstream_git_hash +++ b/packages/kbn-handlebars/src/spec/.upstream_git_hash @@ -1 +1 @@ -eab1d141cb4a1d93375d7380ed070aa1f576a2c9 \ No newline at end of file +7de4b41c344a5d702edca93d1841b59642fa32bd \ No newline at end of file diff --git a/packages/kbn-handlebars/src/visitor.ts b/packages/kbn-handlebars/src/visitor.ts index 1842c8e5d6a2b..16dc151df8640 100644 --- a/packages/kbn-handlebars/src/visitor.ts +++ b/packages/kbn-handlebars/src/visitor.ts @@ -405,6 +405,18 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor { return this.resolvePath(data, path); } + private pushToOutputWithEscapeCheck(result: any, node: ProcessableNodeWithPathParts) { + if ( + !(node as hbs.AST.MustacheStatement).escaped || + this.compileOptions.noEscape === true || + typeof result !== 'string' + ) { + this.output.push(result); + } else { + this.output.push(Handlebars.escapeExpression(result)); + } + } + private processSimpleNode(node: ProcessableNodeWithPathParts) { const path = node.path; // @ts-expect-error strict is not a valid property on PathExpression, but we used in the same way it's also used in the original handlebars @@ -415,7 +427,7 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor { if (isBlock(node)) { this.blockValue(node, lambdaResult); } else { - this.output.push(lambdaResult); + this.pushToOutputWithEscapeCheck(lambdaResult, node); } } @@ -435,7 +447,6 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor { private processHelperNode(node: ProcessableNodeWithPathParts) { const path = node.path; const name = path.parts[0]; - if (this.compileOptions.knownHelpers && this.compileOptions.knownHelpers[name]) { this.invokeKnownHelper(node); } else if (this.compileOptions.knownHelpersOnly) { @@ -455,7 +466,8 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor { const helper = this.setupHelper(node, name); // TypeScript: `helper.fn` might be `undefined` at this point, but to match the upstream behavior we call it without any guards const result = helper.fn!.call(helper.context, ...helper.params, helper.options); - this.output.push(result); + + this.pushToOutputWithEscapeCheck(result, node); } // Pops off the helper's parameters, invokes the helper, @@ -482,7 +494,7 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor { // TypeScript: `helper.fn` might be `undefined` at this point, but to match the upstream behavior we call it without any guards const result = helper.fn!.call(helper.context, ...helper.params, helper.options); - this.output.push(result); + this.pushToOutputWithEscapeCheck(result, node); } private invokePartial(partial: hbs.AST.PartialStatement | hbs.AST.PartialBlockStatement) {