diff --git a/js/common/frozen_object.go b/js/common/frozen_object.go new file mode 100644 index 00000000000..9f0c872a6a5 --- /dev/null +++ b/js/common/frozen_object.go @@ -0,0 +1,69 @@ +package common + +import ( + "fmt" + + "github.com/dop251/goja" +) + +// FreezeObject replicates the JavaScript Object.freeze function. +func FreezeObject(rt *goja.Runtime, obj goja.Value) error { + global := rt.GlobalObject().Get("Object").ToObject(rt) + freeze, ok := goja.AssertFunction(global.Get("freeze")) + if !ok { + panic("failed to get the Object.freeze function from the runtime") + } + isFrozen, ok := goja.AssertFunction(global.Get("isFrozen")) + if !ok { + panic("failed to get the Object.isFrozen function from the runtime") + } + fobj := &freezing{ + global: global, + rt: rt, + freeze: freeze, + isFrozen: isFrozen, + } + return fobj.deepFreeze(obj) +} + +type freezing struct { + rt *goja.Runtime + global goja.Value + freeze goja.Callable + isFrozen goja.Callable +} + +func (f *freezing) deepFreeze(val goja.Value) error { + if val != nil && goja.IsNull(val) { + return nil + } + + _, err := f.freeze(goja.Undefined(), val) + if err != nil { + return fmt.Errorf("object freeze failed: %w", err) + } + + o := val.ToObject(f.rt) + if o == nil { + return nil + } + + for _, key := range o.Keys() { + prop := o.Get(key) + if prop == nil { + continue + } + frozen, err := f.isFrozen(goja.Undefined(), prop) + if err != nil { + return err + } + if frozen.ToBoolean() { // prevent cycles + continue + } + if err = f.deepFreeze(prop); err != nil { + return fmt.Errorf("deep freezing the property %s failed: %w", key, err) + } + } + + return nil +} diff --git a/js/common/frozen_object_test.go b/js/common/frozen_object_test.go new file mode 100644 index 00000000000..9269b7d711a --- /dev/null +++ b/js/common/frozen_object_test.go @@ -0,0 +1,66 @@ +package common + +import ( + "testing" + + "github.com/dop251/goja" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFrozenObject(t *testing.T) { + t.Parallel() + + rt := goja.New() + obj := rt.NewObject() + require.NoError(t, obj.Set("foo", "bar")) + require.NoError(t, rt.Set("obj", obj)) + + v, err := rt.RunString(`obj.foo`) + require.NoError(t, err) + require.Equal(t, "bar", v.String()) + + // Set a nested object + _, err = rt.RunString(`obj.nested = {propkey: "value1"}`) + require.NoError(t, err) + + // Not yet frozen + v, err = rt.RunString(`Object.isFrozen(obj)`) + require.NoError(t, err) + require.False(t, v.ToBoolean()) + + require.NoError(t, FreezeObject(rt, obj)) + + // It has been frozen + v, err = rt.RunString(`Object.isFrozen(obj)`) + require.NoError(t, err) + require.True(t, v.ToBoolean()) + + // It has deeply frozen the properties + vfoo, err := rt.RunString(`Object.isFrozen(obj.foo)`) + require.NoError(t, err) + require.True(t, vfoo.ToBoolean()) + + // And deeply frozen the nested objects + vnested, err := rt.RunString(`Object.isFrozen(obj.nested)`) + require.NoError(t, err) + require.True(t, vnested.ToBoolean()) + + nestedProp, err := rt.RunString(`Object.isFrozen(obj.nested.propkey)`) + require.NoError(t, err) + require.True(t, nestedProp.ToBoolean()) + + // The assign is silently ignored + _, err = rt.RunString(`obj.foo = "bad change"`) + require.NoError(t, err) + + v, err = rt.RunString(`obj.foo`) + require.NoError(t, err) + assert.Equal(t, "bar", v.String()) + + // If the strict mode is enabled then it fails + v, err = rt.RunString(`'use strict'; obj.foo = "bad change"`) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "Cannot assign to read only property 'foo'") + assert.Nil(t, v) +} diff --git a/js/common/util.go b/js/common/util.go index 82bdd5e9e63..8c994ec2423 100644 --- a/js/common/util.go +++ b/js/common/util.go @@ -18,6 +18,7 @@ * */ +// Package common contains helpers for interacting with the JavaScript runtime. package common import (