mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-16 12:50:38 -07:00
fix: revert ownPropertyOnly iteration hardening
Iteration is documented as an ownPropertyOnly exception; restore isIterable/toEnumerable and document inherited Symbol.iterator behavior. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -57,7 +57,7 @@ The `memoryLimit` option was removed in v11; enforce memory limits at the host o
|
||||
**Exceptions** — `ownPropertyOnly` does not restrict:
|
||||
|
||||
- [`Drop`][drop] values: prototype chain and [`liquidMethodMissing`][liquidMethodMissing] still apply; audit custom drops like privileged code.
|
||||
- Iteration (`{% for %}`, `{% tablerow %}`, `{% render for %}`): class instances and drops keep their iterators; plain objects only iterate via an own `Symbol.iterator`.
|
||||
- Iteration (`{% for %}`, `{% tablerow %}`, `{% render for %}`): uses `Symbol.iterator` when present, including inherited iterators on plain objects; class instances and drops keep their iterators too.
|
||||
- Liquid pseudo-properties `.size`, `.first`, and `.last`: arrays and strings use length/index rules; `Map`/`Set` use their native size; plain objects with an own `size` property use that value (inherited `size` on plain objects is ignored when `ownPropertyOnly` is `true`).
|
||||
- Filters and custom tags: operate on resolved values with their own semantics.
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ export function * reject_exp<T extends object> (this: FilterImpl, arr: T[], item
|
||||
|
||||
export function * group_by<T extends object> (this: FilterImpl, arr: T[], property: string): IterableIterator<unknown> {
|
||||
const map = new Map()
|
||||
arr = toEnumerable(arr, this.context.ownPropertyOnly)
|
||||
arr = toEnumerable(arr)
|
||||
const token = new Tokenizer(stringify(property)).readScopeValue()
|
||||
for (const item of arr) {
|
||||
const key = yield evalToken(token, this.context.spawn(item))
|
||||
@@ -174,7 +174,7 @@ export function * group_by<T extends object> (this: FilterImpl, arr: T[], proper
|
||||
export function * group_by_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
|
||||
const map = new Map()
|
||||
const keyTemplate = new Value(stringify(exp), this.liquid)
|
||||
arr = toEnumerable(arr, this.context.ownPropertyOnly)
|
||||
arr = toEnumerable(arr)
|
||||
for (const item of arr) {
|
||||
this.context.push(createScope({ [itemName]: item }))
|
||||
const key = yield keyTemplate.value(this.context)
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ export default class extends Tag {
|
||||
? Object.keys(hash).filter(x => MODIFIERS.includes(x))
|
||||
: MODIFIERS.filter(x => hash[x] !== undefined)
|
||||
|
||||
let collection = toEnumerable(yield evalToken(this.collection, ctx), ctx.ownPropertyOnly)
|
||||
let collection = toEnumerable(yield evalToken(this.collection, ctx))
|
||||
collection = modifiers.reduce((collection, modifier: valueOf<typeof MODIFIERS>) => {
|
||||
if (modifier === 'offset') return offset(collection, hash['offset'])
|
||||
if (modifier === 'limit') return limit(collection, hash['limit'])
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ export default class extends Tag {
|
||||
|
||||
if (this.forBinding) {
|
||||
const { value, alias } = this.forBinding
|
||||
const collection = toEnumerable(yield evalToken(value, ctx), ctx.ownPropertyOnly)
|
||||
const collection = toEnumerable(yield evalToken(value, ctx))
|
||||
scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias as string)
|
||||
for (const item of collection) {
|
||||
scope[alias as string] = item
|
||||
|
||||
@@ -39,7 +39,7 @@ export default class extends Tag {
|
||||
}
|
||||
|
||||
* render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
|
||||
let collection = toEnumerable(yield evalToken(this.collection, ctx), ctx.ownPropertyOnly)
|
||||
let collection = toEnumerable(yield evalToken(this.collection, ctx))
|
||||
const args = (yield this.args.render(ctx)) as Record<string, any>
|
||||
const offset = args.offset || 0
|
||||
const limit = (args.limit === undefined) ? collection.length : args.limit
|
||||
|
||||
+4
-13
@@ -47,11 +47,11 @@ export function readArrayElement (arr: any[], index: number, ownPropertyOnly: bo
|
||||
return arr[index]
|
||||
}
|
||||
|
||||
export function toEnumerable<T = unknown> (val: any, ownPropertyOnly = false): T[] {
|
||||
export function toEnumerable<T = unknown> (val: any): T[] {
|
||||
val = toValue(val)
|
||||
if (isArray(val)) return val
|
||||
if (isString(val) && val.length > 0) return [val] as unknown as T[]
|
||||
if (isIterable(val, ownPropertyOnly)) return Array.from(val)
|
||||
if (isIterable(val)) return Array.from(val)
|
||||
if (isObject(val)) return Object.keys(val).map((key) => [key, val[key]]) as unknown as T[]
|
||||
return []
|
||||
}
|
||||
@@ -96,17 +96,8 @@ export function isArrayLike (value: any): value is any[] {
|
||||
return value && isNumber(value.length)
|
||||
}
|
||||
|
||||
export function isIterable (value: any, ownPropertyOnly = false): value is Iterable<any> {
|
||||
value = toValue(value)
|
||||
if (!isObject(value)) return false
|
||||
if (isArray(value)) return true
|
||||
if (value instanceof Drop) return Symbol.iterator in value
|
||||
if (ownPropertyOnly) {
|
||||
const proto = Object.getPrototypeOf(value)
|
||||
const isPlain = proto === null || proto === Object.prototype
|
||||
if (isPlain) return hasOwnProperty.call(value, Symbol.iterator)
|
||||
}
|
||||
return Symbol.iterator in value
|
||||
export function isIterable (value: any): value is Iterable<any> {
|
||||
return isObject(value) && Symbol.iterator in value
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -88,14 +88,14 @@ describe('scope security', function () {
|
||||
expect((Object.prototype as any).polluted).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not iterate plain objects via inherited Symbol.iterator', async function () {
|
||||
it('should iterate plain objects via inherited Symbol.iterator (ownPropertyOnly exception)', async function () {
|
||||
// eslint-disable-next-line no-extend-native
|
||||
(Object.prototype as any)[Symbol.iterator] = function * () { yield 'inherited' }
|
||||
try {
|
||||
await expect(liquid.parseAndRender(
|
||||
'{% for x in obj %}{{ x }}{% endfor %}',
|
||||
{ obj: {} }
|
||||
)).resolves.toBe('')
|
||||
)).resolves.toBe('inherited')
|
||||
} finally {
|
||||
delete (Object.prototype as any)[Symbol.iterator]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user