Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/context/context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,17 @@ describe('Context', function () {
expect(ctx.getSync(['foo'])).toEqual('zoo')
})
})
describe('scope storage', function () {
it('should not treat Object.prototype properties as implicit keys on bottom scope', function () {
const bottom = new Context().bottom() as Record<string, unknown>
expect('toString' in bottom).toBe(false)
expect('hasOwnProperty' in bottom).toBe(false)
})
it('should merge into a scope result without implicit Object.prototype keys', function () {
const all = new Context({ a: 1 }).getAll() as Record<string, unknown>
expect(all.a).toBe(1)
expect('toString' in all).toBe(false)
expect('hasOwnProperty' in all).toBe(false)
})
})
})
8 changes: 4 additions & 4 deletions src/context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getPerformance } from '../util/performance'
import { Drop } from '../drop/drop'
import { __assign } from 'tslib'
import { NormalizedFullOptions, defaultOptions, RenderOptions } from '../liquid-options'
import { Scope } from './scope'
import { Scope, createScope } from './scope'
import { hasOwnProperty, isArray, isNil, isUndefined, isString, isFunction, toLiquid, InternalUndefinedVariableError, toValueSync, isObject, Limiter, toValue } from '../util'

type PropertyKey = string | number;
Expand All @@ -12,7 +12,7 @@ export class Context {
* insert a Context-level empty scope,
* for tags like `{% capture %}` `{% assign %}` to operate
*/
private scopes: Scope[] = [{}]
private scopes: Scope[] = [createScope()]
private registers = {}
/**
* user passed in scope
Expand Down Expand Up @@ -62,7 +62,7 @@ export class Context {
}
public getAll () {
return [this.globals, this.environments, ...this.scopes]
.reduce((ctx, val) => __assign(ctx, val), {})
.reduce((ctx, val) => __assign(ctx, val), createScope())
}
/**
* @deprecated use `_get()` or `getSync()` instead
Expand Down Expand Up @@ -102,7 +102,7 @@ export class Context {
public bottom () {
return this.scopes[0]
}
public spawn (scope = {}) {
public spawn (scope: object = createScope()) {
return new Context(scope, this.opts, {
sync: this.sync,
globals: this.globals,
Expand Down
10 changes: 10 additions & 0 deletions src/context/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@ interface ScopeObject extends Record<string | number | symbol, any> {
}

export type Scope = ScopeObject | Drop

/**
* Plain scope bag with a null prototype so lookups like `__proto__` are not the
* Object.prototype accessor unless explicitly assigned as an own property.
*/
export function createScope (props?: Record<PropertyKey, any>): ScopeObject {
return props == null
? Object.create(null)
: Object.assign(Object.create(null), props)
}
8 changes: 4 additions & 4 deletions src/filters/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare,
import { arrayIncludes, equals, evalToken, isTruthy } from '../render'
import { Value, FilterImpl } from '../template'
import { Tokenizer } from '../parser'
import type { Scope } from '../context'
import { createScope, type Scope } from '../context'
import { EmptyDrop } from '../drop'

export const join = argumentsToValue(function (this: FilterImpl, v: any[], arg: string) {
Expand Down Expand Up @@ -139,7 +139,7 @@ function * filter_exp<T extends object> (this: FilterImpl, include: boolean, arr
const array = toArray(arr)
this.context.memoryLimit.use(array.length)
for (const item of array) {
this.context.push({ [itemName]: item })
this.context.push(createScope({ [itemName]: item }))
const value = yield keyTemplate.value(this.context)
this.context.pop()
if (value === include) filtered.push(item)
Expand Down Expand Up @@ -182,7 +182,7 @@ export function * group_by_exp<T extends object> (this: FilterImpl, arr: T[], it
arr = toEnumerable(arr)
this.context.memoryLimit.use(arr.length)
for (const item of arr) {
this.context.push({ [itemName]: item })
this.context.push(createScope({ [itemName]: item }))
const key = yield keyTemplate.value(this.context)
this.context.pop()
if (!map.has(key)) map.set(key, [])
Expand All @@ -205,7 +205,7 @@ function * search_exp<T extends object> (this: FilterImpl, arr: T[], itemName: s
const predicate = new Value(stringify(exp), this.liquid)
const array = toArray(arr)
for (let index = 0; index < array.length; index++) {
this.context.push({ [itemName]: array[index] })
this.context.push(createScope({ [itemName]: array[index] }))
const value = yield predicate.value(this.context)
this.context.pop()
if (value) return [index, array[index]]
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export { Drop } from './drop'
export type { Comparable } from './drop'
export { Emitter } from './emitters'
export { defaultOperators, Operators, evalToken, evalQuotedToken, Expression, isFalsy, isTruthy } from './render'
export { Context, Scope } from './context'
export { Context, Scope, createScope } from './context'
export { Value, Hash, Template, FilterImplOptions, Tag, Filter, Output, Variable, VariableLocation, VariableSegments, Variables, StaticAnalysis, StaticAnalysisOptions, analyze, analyzeSync, Arguments, PartialScope } from './template'
export type { TagRenderReturn } from './template'
export { Token, TopLevelToken, TagToken, ValueToken } from './tokens'
Expand Down
4 changes: 2 additions & 2 deletions src/liquid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export class Liquid {
* @deprecated will be removed. In tags use `this.parser` instead
*/
public readonly parser: Parser
public readonly filters: Record<string, FilterImplOptions> = {}
public readonly tags: Record<string, TagClass> = {}
public readonly filters: Record<string, FilterImplOptions> = Object.create(null)
public readonly tags: Record<string, TagClass> = Object.create(null)

public constructor (opts: LiquidOptions = {}) {
this.options = normalize(opts)
Expand Down
4 changes: 2 additions & 2 deletions src/tags/block.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BlockMode } from '../context'
import { BlockMode, createScope } from '../context'
import { isTagToken } from '../util'
import { BlockDrop } from '../drop'
import { Liquid, TagToken, TopLevelToken, Template, Context, Emitter, Tag } from '..'
Expand Down Expand Up @@ -38,7 +38,7 @@ export default class extends Tag {
if (stack.includes(self)) throw new Error('block tag cannot be nested')

stack.push(self)
ctx.push({ block: superBlock })
ctx.push(createScope({ block: superBlock }))
yield liquid.renderer.renderTemplates(templates, ctx, emitter)
ctx.pop()
stack.pop()
Expand Down
6 changes: 3 additions & 3 deletions src/tags/for.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Hash, ValueToken, Liquid, Tag, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, ParseStream } from '..'
import { Hash, ValueToken, Liquid, Tag, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, ParseStream, createScope } from '..'
import { assertEmpty, isValueToken, toEnumerable } from '../util'
import { ForloopDrop } from '../drop/forloop-drop'
import { Parser } from '../parser'
Expand Down Expand Up @@ -50,7 +50,7 @@ export default class extends Tag {
}

const continueKey = 'continue-' + this.variable + '-' + this.collection.getText()
ctx.push({ continue: ctx.getRegister(continueKey, {}) })
ctx.push(createScope({ continue: ctx.getRegister(continueKey, 0) }))
const hash = yield this.hash.render(ctx)
ctx.pop()

Expand All @@ -65,7 +65,7 @@ export default class extends Tag {
}, collection)

ctx.setRegister(continueKey, (hash['offset'] || 0) + collection.length)
const scope = { forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) }
const scope = createScope({ forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) })
ctx.push(scope)
for (const item of collection) {
scope[this.variable] = item
Expand Down
4 changes: 2 additions & 2 deletions src/tags/include.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Template, ValueToken, TopLevelToken, Liquid, Tag, assert, evalToken, Hash, Emitter, TagToken, Context } from '..'
import { Template, ValueToken, TopLevelToken, Liquid, Tag, assert, evalToken, Hash, Emitter, TagToken, Context, createScope } from '..'
import { BlockMode, Scope } from '../context'
import { Parser } from '../parser'
import { Argument, Arguments, PartialScope } from '../template'
Expand Down Expand Up @@ -37,7 +37,7 @@ export default class extends Tag {
const scope = (yield hash.render(ctx)) as Scope
if (withVar) scope[filepath] = yield evalToken(withVar, ctx)
const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile'])) as Template[]
ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope)
ctx.push(ctx.opts.jekyllInclude ? createScope({ include: scope }) : Object.assign(createScope(), scope))
yield renderer.renderTemplates(templates, ctx, emitter)
ctx.pop()
ctx.restoreRegister(saved)
Expand Down
4 changes: 2 additions & 2 deletions src/tags/layout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Scope, Template, Liquid, Tag, assert, Emitter, Hash, TagToken, TopLevelToken, Context } from '..'
import { Scope, Template, Liquid, Tag, assert, Emitter, Hash, TagToken, TopLevelToken, Context, createScope } from '..'
import { BlockMode } from '../context'
import { parseFilePath, renderFilePath, ParsedFileName } from './render'
import { BlankDrop } from '../drop'
Expand Down Expand Up @@ -39,7 +39,7 @@ export default class extends Tag {
ctx.setRegister('blockMode', BlockMode.OUTPUT)

// render the layout file use stored blocks
ctx.push((yield args.render(ctx)) as Scope)
ctx.push(Object.assign(createScope(), (yield args.render(ctx)) as Scope))
yield renderer.renderTemplates(templates, ctx, emitter)
ctx.pop()
}
Expand Down
4 changes: 2 additions & 2 deletions src/tags/tablerow.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isValueToken, toEnumerable } from '../util'
import { ValueToken, Liquid, Tag, evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, ParseStream } from '..'
import { ValueToken, Liquid, Tag, evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, ParseStream, createScope } from '..'
import { TablerowloopDrop } from '../drop/tablerowloop-drop'
import { Parser } from '../parser'
import { Arguments } from '../template'
Expand Down Expand Up @@ -48,7 +48,7 @@ export default class extends Tag {

const r = this.liquid.renderer
const tablerowloop = new TablerowloopDrop(collection.length, cols, this.collection.getText(), this.variable)
const scope = { tablerowloop }
const scope = createScope({ tablerowloop })
ctx.push(scope)

for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) {
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/issues.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ describe('Issues', function () {
)
expect(html).toBe('BAR')
})
it('filter/tag maps are null-prototype (node + UMD)', async () => {
const nodeEngine = new Liquid()
const umdEngine = new LiquidUMD()
expect(Object.getPrototypeOf(nodeEngine.filters)).toBeNull()
expect(Object.getPrototypeOf(nodeEngine.tags)).toBeNull()
expect(Object.getPrototypeOf(umdEngine.filters)).toBeNull()
expect(Object.getPrototypeOf(umdEngine.tags)).toBeNull()
})
it('lenientIf not working as expected in umd #313', async () => {
const engine = new LiquidUMD({
strictVariables: true,
Expand Down
16 changes: 16 additions & 0 deletions test/integration/liquid/register-filters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,20 @@ describe('liquid#registerFilter()', function () {
return expect(html).toBe(dst)
})
})

describe('filter registry storage', () => {
it('should use a null-prototype map for filters', () => {
expect(Object.getPrototypeOf(liquid.filters)).toBeNull()
})
it('should treat Object.prototype keys as unregistered unless explicitly registered', async () => {
const registered = new Set(Object.keys(liquid.filters))
const strict = new Liquid({ strictFilters: true })
for (const name of Object.getOwnPropertyNames(Object.prototype)) {
if (registered.has(name)) continue
const out = await liquid.parseAndRender(`{{ x | ${name} }}`, { x: 42 })
expect(out).toBe('42')
await expect(strict.parseAndRender(`{{ 1 | ${name} }}`)).rejects.toThrow('undefined filter')
}
})
})
})
14 changes: 14 additions & 0 deletions test/integration/liquid/register-tags.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,18 @@ describe('liquid#registerTag()', function () {
})
return expect(html).toBe('ABC')
})

describe('tag registry storage', () => {
it('should use a null-prototype map for tags', () => {
expect(Object.getPrototypeOf(new Liquid().tags)).toBeNull()
})
it('should not resolve names that exist only on Object.prototype', () => {
const l = new Liquid()
const registered = new Set(Object.keys(l.tags))
for (const name of Object.getOwnPropertyNames(Object.prototype)) {
if (registered.has(name)) continue
expect(() => l.parse(`{% ${name} %}`)).toThrow(`tag "${name}" not found`)
}
})
})
})
Loading