diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 6aef63bfe..18bebad67 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -109,7 +109,7 @@ export function template(templateSpec, env) { // Just add water let container = { strict: function (obj, name, loc) { - if (!obj || !(name in obj)) { + if (obj == null || !(name in Object(obj))) { throw new Exception('"' + name + '" not defined in ' + obj, { loc: loc, }); @@ -117,20 +117,10 @@ export function template(templateSpec, env) { return container.lookupProperty(obj, name); }, strictLookup: function (depths, name, loc) { - const len = depths.length; - let depth; - for (let i = 0; i < len; i++) { - const d = depths[i]; - if ( - d && - (typeof d === 'object' || typeof d === 'function') && - name in d - ) { - depth = d; - break; - } - } - return container.strict(depth, name, loc); + const result = container.lookup(depths, name); + return result !== undefined + ? result + : container.strict(undefined, name, loc); }, lookupProperty: function (parent, propertyName) { if (Utils.isMap(parent)) { @@ -153,9 +143,17 @@ export function template(templateSpec, env) { lookup: function (depths, name) { const len = depths.length; for (let i = 0; i < len; i++) { - let result = depths[i] && container.lookupProperty(depths[i], name); - if (result != null) { - return depths[i][name]; + const d = depths[i]; + if (d == null) continue; + if (typeof d === 'object' || typeof d === 'function') { + if (Utils.isMap(d) ? d.has(name) : name in d) { + return container.lookupProperty(d, name); + } + } else { + const result = container.lookupProperty(d, name); + if (result != null) { + return result; + } } } }, diff --git a/spec/basic.js b/spec/basic.js index 1668b7930..f3d37e250 100644 --- a/spec/basic.js +++ b/spec/basic.js @@ -388,6 +388,18 @@ describe('basic context', function () { .toCompileTo('Goodbye beautiful world!'); }); + it('compat mode resolves Map values through depthed lookup', function () { + expectTemplate('{{#with nested}}{{value}}/{{constructor}}{{/with}}') + .withInput({ + nested: new Map([ + ['value', 'map-value'], + ['constructor', 'map-constructor'], + ]), + }) + .withCompileOptions({ compat: true }) + .toCompileTo('map-value/map-constructor'); + }); + it('nested paths with empty string value', function () { expectTemplate('Goodbye {{alan/expression}} world!') .withInput({ alan: { expression: '' } }) diff --git a/spec/blocks.js b/spec/blocks.js index 0e1746289..2883d567c 100644 --- a/spec/blocks.js +++ b/spec/blocks.js @@ -259,6 +259,13 @@ describe('blocks', function () { }); describe('compat mode', function () { + it('should directly return an explicitly null property', function () { + expectTemplate('{{#each items}}{{name}}{{/each}}') + .withCompileOptions({ compat: true }) + .withInput({ name: 'root', items: [{ name: null }] }) + .toCompileTo(''); + }); + it('block with deep recursive lookup lookup', function () { expectTemplate( '{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}' diff --git a/spec/security.js b/spec/security.js index 8187fc611..02b60a9b3 100644 --- a/spec/security.js +++ b/spec/security.js @@ -302,6 +302,16 @@ describe('security issues', function () { }) .toCompileTo('abc'); }); + + it('should not cause recursive lookup if allowed through options(in "compat+strict" mode)', function () { + expectTemplate('{{#aString}}{{trim}}{{/aString}}') + .withInput({ aString: ' abc ', trim: 'trim' }) + .withCompileOptions({ compat: true, strict: true }) + .withRuntimeOptions({ + allowedProtoMethods: { trim: true }, + }) + .toCompileTo('abc'); + }); }); describe('control access to prototype non-methods via "allowedProtoProperties" and "allowProtoPropertiesByDefault', function () { diff --git a/spec/strict.js b/spec/strict.js index bfcb0a9ad..210b7975d 100644 --- a/spec/strict.js +++ b/spec/strict.js @@ -27,6 +27,25 @@ describe('strict', function () { .toCompileTo(''); }); + it('should handle array length', function () { + expectTemplate('{{hello.length}}') + .withCompileOptions({ strict: true }) + .withInput({ hello: [1, 2, 3] }) + .toCompileTo('3'); + }); + + it('should handle string length', function () { + expectTemplate('{{hello.length}}') + .withCompileOptions({ strict: true }) + .withInput({ hello: 'world' }) + .toCompileTo('5'); + + expectTemplate('{{hello.length}}') + .withCompileOptions({ strict: true }) + .withInput({ hello: '' }) + .toCompileTo('0'); + }); + it('should error on missing property lookup in known helpers mode', function () { expectTemplate('{{hello}}') .withCompileOptions({ @@ -166,7 +185,7 @@ describe('strict', function () { it('should still perform recursive lookup with a multi-part path not in context', function () { expectTemplate('{{#with child}}{{name.first}}{{/with}}') .withCompileOptions({ strict: true, compat: true }) - .withInput({ name: { first: 'root' }, child: { name: null } }) + .withInput({ name: { first: 'root' }, child: { x: 'y' } }) .toCompileTo('root'); });