mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-17 05:10:40 -07:00
feat: harden scope writes, iteration, and readSize (#898)
Block writes to dangerous keys in assign/capture/increment/decrement, use own-property Symbol.iterator for plain objects when ownPropertyOnly is true, fix inherited size reads, and sanitize filter iteration scopes. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -139,7 +139,7 @@ export class Context {
|
||||
const value = readJSProperty(obj, key, this.ownPropertyOnly)
|
||||
if (value === undefined && obj instanceof Drop) return obj.liquidMethodMissing(key, this)
|
||||
if (isFunction(value)) return value.call(obj)
|
||||
if (key === 'size') return readSize(obj)
|
||||
if (key === 'size') return readSize(obj, this.ownPropertyOnly)
|
||||
else if (key === 'first') return readFirst(obj, this.ownPropertyOnly)
|
||||
else if (key === 'last') return readLast(obj, this.ownPropertyOnly)
|
||||
return value
|
||||
@@ -162,8 +162,10 @@ function readLast (obj: Scope, ownPropertyOnly: boolean) {
|
||||
return readJSProperty(obj, 'last', ownPropertyOnly)
|
||||
}
|
||||
|
||||
function readSize (obj: Scope) {
|
||||
if (hasOwnProperty.call(obj, 'size') || obj['size'] !== undefined) return obj['size']
|
||||
function readSize (obj: Scope, ownPropertyOnly: boolean) {
|
||||
if (hasOwnProperty.call(obj, 'size')) return obj['size']
|
||||
if (!ownPropertyOnly && obj['size'] !== undefined) return obj['size']
|
||||
if (isArray(obj) || isString(obj)) return obj.length
|
||||
if (obj instanceof Map || obj instanceof Set) return obj.size
|
||||
if (typeof obj === 'object') return Object.keys(obj).length
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { arrayIncludes, equals, evalToken, isTruthy } from '../render'
|
||||
import { Value, FilterImpl } from '../template'
|
||||
import { Tokenizer } from '../parser'
|
||||
import type { Scope } from '../context'
|
||||
import { createScope } from '../context/scope'
|
||||
import { EmptyDrop } from '../drop'
|
||||
|
||||
export const join = argumentsToValue(function (this: FilterImpl, v: any[], arg: string) {
|
||||
@@ -134,7 +135,7 @@ function * filter_exp<T extends object> (this: FilterImpl, include: boolean, arr
|
||||
const keyTemplate = new Value(stringify(exp), this.liquid)
|
||||
const array = toArray(arr)
|
||||
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)
|
||||
@@ -160,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)
|
||||
arr = toEnumerable(arr, this.context.ownPropertyOnly)
|
||||
const token = new Tokenizer(stringify(property)).readScopeValue()
|
||||
for (const item of arr) {
|
||||
const key = yield evalToken(token, this.context.spawn(item))
|
||||
@@ -173,9 +174,9 @@ 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)
|
||||
arr = toEnumerable(arr, this.context.ownPropertyOnly)
|
||||
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, [])
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Value, Liquid, TopLevelToken, TagToken, Context, Tag } from '..'
|
||||
import { isBlockedScopeKey } from '../context/scope'
|
||||
import { Arguments } from '../template'
|
||||
import { IdentifierToken } from '../tokens'
|
||||
|
||||
@@ -20,6 +21,7 @@ export default class extends Tag {
|
||||
this.value = new Value(this.tokenizer.readFilteredValue(), this.liquid)
|
||||
}
|
||||
* render (ctx: Context): Generator<unknown, void, unknown> {
|
||||
if (isBlockedScopeKey(this.key)) return
|
||||
ctx.bottom()[this.key] = yield this.value.value(ctx, this.liquid.options.lenientIf)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Liquid, Tag, Template, Context, TagToken, TopLevelToken } from '..'
|
||||
import { isBlockedScopeKey } from '../context/scope'
|
||||
import { Parser } from '../parser'
|
||||
import { IdentifierToken, QuotedToken } from '../tokens'
|
||||
import { isTagToken } from '../util'
|
||||
@@ -31,6 +32,7 @@ export default class extends Tag {
|
||||
* render (ctx: Context): Generator<unknown, void, string> {
|
||||
const r = this.liquid.renderer
|
||||
const html = yield r.renderTemplates(this.templates, ctx)
|
||||
if (isBlockedScopeKey(this.variable)) return
|
||||
ctx.bottom()[this.variable] = html
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Tag, Liquid, TopLevelToken, Emitter, TagToken, Context } from '..'
|
||||
import { isBlockedScopeKey } from '../context/scope'
|
||||
import { IdentifierToken } from '../tokens'
|
||||
import { isNumber, stringify } from '../util'
|
||||
|
||||
@@ -11,6 +12,7 @@ export default class extends Tag {
|
||||
this.variable = this.identifier.content
|
||||
}
|
||||
render (context: Context, emitter: Emitter) {
|
||||
if (isBlockedScopeKey(this.variable)) return
|
||||
const scope = context.environments
|
||||
if (!isNumber(scope[this.variable])) {
|
||||
scope[this.variable] = 0
|
||||
|
||||
+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))
|
||||
let collection = toEnumerable(yield evalToken(this.collection, ctx), ctx.ownPropertyOnly)
|
||||
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,4 +1,5 @@
|
||||
import { isNumber, stringify } from '../util'
|
||||
import { isBlockedScopeKey } from '../context/scope'
|
||||
import { Tag, Liquid, TopLevelToken, Emitter, TagToken, Context } from '..'
|
||||
import { IdentifierToken } from '../tokens'
|
||||
|
||||
@@ -11,6 +12,7 @@ export default class extends Tag {
|
||||
this.variable = this.identifier.content
|
||||
}
|
||||
render (context: Context, emitter: Emitter) {
|
||||
if (isBlockedScopeKey(this.variable)) return
|
||||
const scope = context.environments
|
||||
if (!isNumber(scope[this.variable])) {
|
||||
scope[this.variable] = 0
|
||||
|
||||
+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))
|
||||
const collection = toEnumerable(yield evalToken(value, ctx), ctx.ownPropertyOnly)
|
||||
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))
|
||||
let collection = toEnumerable(yield evalToken(this.collection, ctx), ctx.ownPropertyOnly)
|
||||
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
|
||||
|
||||
+13
-4
@@ -47,11 +47,11 @@ export function readArrayElement (arr: any[], index: number, ownPropertyOnly: bo
|
||||
return arr[index]
|
||||
}
|
||||
|
||||
export function toEnumerable<T = unknown> (val: any): T[] {
|
||||
export function toEnumerable<T = unknown> (val: any, ownPropertyOnly = false): T[] {
|
||||
val = toValue(val)
|
||||
if (isArray(val)) return val
|
||||
if (isString(val) && val.length > 0) return [val] as unknown as T[]
|
||||
if (isIterable(val)) return Array.from(val)
|
||||
if (isIterable(val, ownPropertyOnly)) return Array.from(val)
|
||||
if (isObject(val)) return Object.keys(val).map((key) => [key, val[key]]) as unknown as T[]
|
||||
return []
|
||||
}
|
||||
@@ -96,8 +96,17 @@ export function isArrayLike (value: any): value is any[] {
|
||||
return value && isNumber(value.length)
|
||||
}
|
||||
|
||||
export function isIterable (value: any): value is Iterable<any> {
|
||||
return isObject(value) && Symbol.iterator in value
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user