From a3511b1bfc3f5d1dacf00ac826707c558d4d6cec Mon Sep 17 00:00:00 2001 From: Tom Wiesing Date: Thu, 15 Sep 2022 23:21:14 +0200 Subject: [PATCH] instances: Add methods to evaluate PHP --- internal/component/instances/wisski_exec.go | 6 +- internal/component/instances/wisski_php.go | 122 ++++++++++++++++++ internal/component/instances/wisski_status.go | 31 ++++- 3 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 internal/component/instances/wisski_php.go diff --git a/internal/component/instances/wisski_exec.go b/internal/component/instances/wisski_exec.go index 952cefe..d9da2cc 100644 --- a/internal/component/instances/wisski_exec.go +++ b/internal/component/instances/wisski_exec.go @@ -1,8 +1,10 @@ package instances -import "github.com/tkw1536/goprogram/stream" +import ( + "github.com/tkw1536/goprogram/stream" +) // Shell executes a shell command inside the instance. -func (wisski WissKI) Shell(io stream.IOStream, argv ...string) (int, error) { +func (wisski *WissKI) Shell(io stream.IOStream, argv ...string) (int, error) { return wisski.Barrel().Exec(io, "barrel", "/bin/sh", append([]string{"/user_shell.sh"}, argv...)...) } diff --git a/internal/component/instances/wisski_php.go b/internal/component/instances/wisski_php.go new file mode 100644 index 0000000..cc585aa --- /dev/null +++ b/internal/component/instances/wisski_php.go @@ -0,0 +1,122 @@ +package instances + +import ( + "bytes" + "encoding/json" + "errors" + "strings" + + "github.com/tkw1536/goprogram/stream" +) + +var ErrExecInvalidCode = errors.New("invalid code to execute") +var ErrExecNonZero = errors.New("script returned non-zero code") + +// ExecPHPScript executes the PHP code as a script within the wisski instance. +// The script should define a function "main", and may define additional functions. +// +// Code must start with "" + code) + if err != nil { + return nil, err + } + + argsEscape, err := marshalPHP(args) + if err != nil { + return nil, err + } + + // assemble the script + script := ` + ob_start(null, 0, PHP_OUTPUT_HANDLER_CLEANABLE); + eval(` + codeEscape + `); + ob_end_clean(); + + call_user_func(function(){ + ob_start(null, 0, PHP_OUTPUT_HANDLER_CLEANABLE); + $result = call_user_func_array("main", ` + argsEscape + `); + ob_end_clean(); + echo json_encode($result); + }); +` + + // run the script + var output bytes.Buffer + res, err := wisski.Shell(io.Streams(&output, nil, strings.NewReader(script), 0), "-c", "drush php:script -") + if res != 0 { + return nil, ErrExecNonZero + } + if err != nil { + return nil, err + } + + // decode the output + var result any + err = json.NewDecoder(&output).Decode(&result) + return result, err +} + +// EvalPHP is similar to ExecPHPScript, except that it evaluates a single line of php. +// A single parameter may be passed, which can be accessed using the name $arg inside the expression. +func (wisski *WissKI) EvalPHP(expr string, arg any) (any, error) { + return wisski.ExecPHPScript(stream.FromEnv(), "function main($arg){return "+expr+";}", arg) +} + +const marshalRune = 'F' // press to pay respect + +// marshalPHP marshals some data which can be marshaled using [json.Encode] into a PHP Expression. +// the string can be safely used directly within php. +func marshalPHP(data any) (string, error) { + // this function uses json as a data format to transport the data into php. + // then we build a heredoc to encode it safely, and decode it in php + + // Step 1: Encode the data as json + jbytes, err := json.Marshal(data) + if err != nil { + return "", err + } + jstring := string(jbytes) + + // Step 2: Find a delimiter for the heredoc. + // Step 2a: Find the longest sequence of [marshalRune]s inside the encoded string. + var current, longest int + for _, r := range jstring { + + if r == marshalRune { + current++ + } else { + current = 0 + } + + if current > longest { + longest = current + } + } + // Step 2b: Build a string of marshalRune that is one longer! + delim := strings.Repeat(string(marshalRune), longest+1) + + // Step 3: Assemble the encoded string! + result := "call_user_func(function(){$x=<<<'" + delim + "'\n" + jstring + "\n" + delim + ";return json_decode(trim($x));})" // press to doubt + return result, nil +} diff --git a/internal/component/instances/wisski_status.go b/internal/component/instances/wisski_status.go index b8ddff4..df52b16 100644 --- a/internal/component/instances/wisski_status.go +++ b/internal/component/instances/wisski_status.go @@ -1,22 +1,41 @@ package instances -import "github.com/tkw1536/goprogram/stream" +import ( + "github.com/tkw1536/goprogram/stream" + "golang.org/x/sync/errgroup" +) // Info represents some info about this WissKI type Info struct { Slug string // The slug of the instance Running bool // is the instance running? + + DrupalVersion interface{} // version of drupal being used } -// Info returns info about this instance +// Info returns information about this WissKI instance. func (wisski *WissKI) Info() (info Info, err error) { + // static properties info.Slug = wisski.Slug - ps, err := wisski.Barrel().Ps(stream.FromNil()) - if err != nil { + // dynamic properties, TODO: Add more properties here! + var group errgroup.Group + + group.Go(func() (err error) { + info.Running, err = wisski.Alive() return - } - info.Running = len(ps) > 0 + }) + + err = group.Wait() return } + +// Alive checks if this WissKI is currently running. +func (wisski *WissKI) Alive() (bool, error) { + ps, err := wisski.Barrel().Ps(stream.FromNil()) + if err != nil { + return false, err + } + return len(ps) > 0, nil +}