diff --git a/internal/wisski/ingredient/php/marshal.go b/internal/wisski/ingredient/php/marshal.go new file mode 100644 index 0000000..5c7eb1e --- /dev/null +++ b/internal/wisski/ingredient/php/marshal.go @@ -0,0 +1,33 @@ +package php + +import ( + "encoding/json" + "strings" +) + +// Marshal marshals data as a PHP expression, meaning it can safely be used inside code. +// +// Typically data is marshaled using [json.Marshal] and decoded in PHP using 'json_decode'. +// Special cases may exist for specific datatypes. +func Marshal(data any) (string, error) { + switch d := data.(type) { + case string: + return MarshalString(d), nil + } + + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + + return "json_decode(" + MarshalString(string(bytes)) + ")", nil +} + +var replacer = strings.NewReplacer("'", "\\'", "\\", "\\\\") + +// MarshalString marshals s as a php string that can be used safely as a PHP expression. +// +// See [https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.single]. +func MarshalString(s string) string { + return "'" + replacer.Replace(s) + "'" +} diff --git a/internal/wisski/ingredient/php/server.go b/internal/wisski/ingredient/php/server.go index f25b296..d7050f2 100644 --- a/internal/wisski/ingredient/php/server.go +++ b/internal/wisski/ingredient/php/server.go @@ -80,7 +80,6 @@ func (php *PHP) NewServer() (*Server, error) { cancel() }() - // start the server io := stream.NewIOStream(ow, nil, ir, 0) php.Barrel.Shell(io, "-c", shellescape.QuoteCommand([]string{"drush", "php:eval", serverPHP})) @@ -122,14 +121,9 @@ func (server *Server) MarshalEval(value any, code string) error { return errPHPClosed } - // marshal the code, and send it to the server - bytes, err := json.Marshal(code) - if err != nil { - return ServerError{Message: errPHPMarshal, Err: err} - } - - // send it to the server - io.WriteString(server.in, string(bytes)+"\n") + // find a delimiter for the code, and then send + delim := findDelimiter(code) + io.WriteString(server.in, delim+"\n"+code+"\n"+delim+"\n") // read the next line (as a response) data, err := nobufio.ReadLine(server.out) @@ -169,22 +163,30 @@ func (server *Server) Eval(code string) (value any, err error) { // // Return values are received as in [MarshalEval]. func (server *Server) MarshalCall(value any, function string, args ...any) error { - // marshal a code for the call - userFunction, err := marshalPHP(function) - if err != nil { - return ServerError{Message: errPHPMarshal, Err: err} - } - userFunctionArgs := "[]" - if len(args) > 0 { - userFunctionArgs, err = marshalPHP(args) + // name of function to call + name := MarshalString(function) + + // generate code to call + var code string + switch len(args) { + case 0: + code = "return call_user_func(" + name + ");" + case 1: + param, err := Marshal(args[0]) if err != nil { - return ServerError{Message: errPHPMarshal, Err: err} + return err } + code = "return call_user_func(" + name + "," + param + ");" + default: + params, err := Marshal(args) + if err != nil { + return err + } + code = "return call_user_func_array(" + name + "," + params + ");" } - code := "return call_user_func_array(" + userFunction + "," + userFunctionArgs + ");" - // and return the evaluated code! + // and evaluate the code return server.MarshalEval(value, code) } @@ -194,27 +196,14 @@ func (server *Server) Call(function string, args ...any) (value any, err error) return } -const marshalRune = 'F' // press to pay respect +const delimiterRune = '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. +// findDelimiter finds a delimiter that does not occur in the input string +func findDelimiter(input string) string { + // find the longest sequence of delimiter rune var current, longest int - for _, r := range jstring { - - if r == marshalRune { + for _, r := range input { + if r == delimiterRune { current++ } else { current = 0 @@ -224,12 +213,8 @@ func marshalPHP(data any) (string, error) { 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 + // and then return it multipled longer than that + return strings.Repeat(string(delimiterRune), longest+1) } // Close closes this server and prevents any further code from being run. diff --git a/internal/wisski/ingredient/php/server.php b/internal/wisski/ingredient/php/server.php index 59f37d8..af8f2f9 100644 --- a/internal/wisski/ingredient/php/server.php +++ b/internal/wisski/ingredient/php/server.php @@ -5,21 +5,33 @@ // // As such it is preprocessed and shortened. // It should only contain comments at the beginning of each line, and only starting with '//'. -// See wisski_php_server.go. +// See server.go. -// don't buffer stdin! +// prevent STDIN from being buffered stream_set_read_buffer(STDIN,0); -// stop all other output +// stop outputting errors when executing ob_start(null,0,PHP_OUTPUT_HANDLER_CLEANABLE); -while($line = fgets(STDIN)){ - // decode the command to run - $code=@json_decode($line); + +while(1){ + // read the next line to get an end-of-line marker + $marker = fgets(STDIN); + if (!$marker) break; + + // accumulate the buffer until the marker is reached + // bail out if there is an unexpected end of input + $buffer = ""; + while(1) { + $line = fgets(STDIN); + if (!$line) break 2; + if ($line === $marker) break; + $buffer .= $line . "\n"; + } // execute it try{ - $json = json_encode([eval($code),""]); + $json = json_encode([eval($buffer),""]); }catch(Throwable $t){ $json = json_encode([null,(string)$t]); }