From 3d8cf81ce5cadb853180aeb7396d07b73e562cd8 Mon Sep 17 00:00:00 2001 From: Jeff Bowen Date: Wed, 10 Aug 2022 15:02:43 -0400 Subject: [PATCH 1/2] Allow for well-formed JSON arrays in option values --- remote/remote.go | 54 +++++++++++++++++++++++++++++---- remote/remote_test.go | 70 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 107 insertions(+), 17 deletions(-) diff --git a/remote/remote.go b/remote/remote.go index b78e0b7..91526b0 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -433,7 +433,7 @@ func getCleanWpCliArgumentArray(wpCliCmdString string) ([]string, error) { // Remove quotes from the args for i := range cleanArgs { - if !isJSONObject(cleanArgs[i]) { //don't alter JSON arguments + if !isJSONObjectOrArray(cleanArgs[i]) { //don't alter JSON arguments cleanArgs[i] = strings.ReplaceAll(cleanArgs[i], "\"", "") } } @@ -1114,10 +1114,54 @@ func isJSON(str string) bool { return json.Valid([]byte(str)) } -func isJSONObject(str string) bool { - trimmedStr := strings.TrimSpace(str) - if !strings.HasPrefix(trimmedStr, "{") || !strings.HasSuffix(trimmedStr, "}") { +// See: https://stackoverflow.com/a/55017470 +func jsonType(str string) (string, error) { + if !isJSON(str) { + return "", errors.New("input is not valid JSON") + } + in := strings.NewReader(str) + dec := json.NewDecoder(in) + // Get just the first valid JSON token from input + t, err := dec.Token() + if err != nil { + return "token error!", err + } + if d, ok := t.(json.Delim); ok { + // The first token is a delimiter, so this is an array or an object + switch d { + case '[': + return "array", nil + case '{': + return "object", nil + default: + return "", errors.New("unexpected delimiter") + } + } + return "", errors.New("input does not represent a JSON object or array") +} + +func contains(haystack []string, needle string) bool { + for _, s := range haystack { + if s == needle { + return true + } + } + return false +} + +func isJSONObjectOrArray(str string) bool { + var _str = str + + // The type checking is intolerant of single quotes, strip them before checking if present + strlen := len(str) + if strlen >= 2 && str[0:1] == "'" && str[strlen-1:] == "'" { + _str = str[1 : strlen-1] + } + t, err := jsonType(_str) + if err != nil { + //fmt.Printf("%v doesn't parse\n", str) return false } - return isJSON(str) + + return contains([]string{"object", "array"}, t) } diff --git a/remote/remote_test.go b/remote/remote_test.go index eaa4154..3b9d18d 100644 --- a/remote/remote_test.go +++ b/remote/remote_test.go @@ -5,32 +5,78 @@ import ( "testing" ) -func TestCheckIsJSONObject(t *testing.T) { +func TestCheckIsJSONObjectOrArray(t *testing.T) { tests := map[string]struct { input string want bool }{ - "normal text with no quotes": {want: false, input: "normal text"}, - "array object": {want: false, input: "[1,2,3]"}, - "valid json string that should be excluded": {want: false, input: `"normal text inside quotes"`}, - "missing quotes json": {want: false, input: `{"broken":json"}`}, - "json inside extra quotes": {want: false, input: `"{"broken":"json"}"`}, - "missing closing parenthesis": {want: false, input: `{"broken":"json object"`}, - "wrong numerical key json": {want: false, input: ` { 1 : " wrong" } `}, - "standard json": {want: true, input: `{"object":"json"}`}, - "json with extra spacing": {want: true, input: ` { "object space" : "with spacing" } `}, + "empty string": {want: false, input: ""}, + "just single quotes": {want: false, input: "''"}, + "just double quotes": {want: false, input: `""`}, + "normal text with no quotes": {want: false, input: "normal text"}, + "number with no quotes": {want: false, input: "123456"}, + "normal text inside double quotes": {want: false, input: `"normal text inside quotes"`}, + "normal text inside single quotes": {want: false, input: "'normal text inside quotes'"}, + "normal text inside double quotes w/ padding": {want: false, input: ` "normal text inside quotes" `}, + "normal text inside single quotes w/ padding": {want: false, input: " 'normal text inside quotes' "}, + "missing quotes json": {want: false, input: `{"broken":json"}`}, + "json inside extra quotes": {want: false, input: `"{"broken":"json"}"`}, + "missing closing parenthesis": {want: false, input: `{"broken":"json object"`}, + "wrong numerical key json": {want: false, input: ` { 1 : " wrong" } `}, + "array object": {want: true, input: "[1,2,3]"}, + "array object with strings": {want: true, input: "[\"a\",\"b\",\"c\"]"}, + "array object with strings & padding": {want: true, input: " [\"a\",\"b\",\"c\"] "}, + "standard json": {want: true, input: `{"object":"json"}`}, + "json with extra spacing": {want: true, input: ` { "object space" : "with spacing" } `}, + "empty array should be true": {want: true, input: "[]"}, + "empty object should be true": {want: true, input: "{}"}, + "object surrounded with single quotes": {want: true, input: `'{"object":"json"}'`}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { - got := isJSONObject(tc.input) + got := isJSONObjectOrArray(tc.input) if !reflect.DeepEqual(tc.want, got) { - t.Fatalf("testing '%v' isJSONObject(\"%v\") expected: %v, got: %v", name, tc.input, tc.want, got) + t.Fatalf("testing '%v' isJSONObjectOrArray(\"%v\") expected: %v, got: %v", name, tc.input, tc.want, got) } }) } } +func TestGetCleanWpCliArgumentArray(t *testing.T) { + tests := map[string]struct { + errString string + input string + want []string + }{ + "vip whatever should not be changed": {errString: "", want: []string{"vip", "whatever"}, input: "vip whatever"}, + "wp option update with array should not be changed": {errString: "", want: []string{"wp", "option", "update", "someoption", `["stuff","things"]`, "--format=json"}, input: "wp option update someoption [\"stuff\",\"things\"] --format=json"}, + "wp option update with array wrapped in single quotes should not be changed": {errString: "", want: []string{"wp", "option", "update", "someoption", `'["stuff","things"]'`, "--format=json"}, input: "wp option update someoption '[\"stuff\",\"things\"]' --format=json"}, + "wp option update with object should not be changed": {errString: "", want: []string{"wp", "option", "update", "someoption", `{"val1":"stuff","val2":"things"}`, "--format=json"}, input: "wp option update someoption {\"val1\":\"stuff\",\"val2\":\"things\"} --format=json"}, + "wp option update with object wrapped in single quotes should not be changed": {errString: "", want: []string{"wp", "option", "update", "someoption", `'{"val1":"stuff","val2":"things"}'`, "--format=json"}, input: "wp option update someoption '{\"val1\":\"stuff\",\"val2\":\"things\"}' --format=json"}, + "wp option update with quotes in value should be changed": {errString: "", want: []string{"wp", "option", "update", "someoption", `stuff,things`}, input: "wp option update someoption \"stuff\",\"things\""}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got, err := getCleanWpCliArgumentArray(tc.input) + + if err != nil && tc.errString != err.Error() { + t.Fatalf("testing '%v' getCleanWpCliArgumentArray(\"%v\") expected error: %v, got: %v", name, tc.input, tc.errString, err.Error()) + } + + if err == nil && tc.errString != "" { + t.Fatalf("testing '%v' getCleanWpCliArgumentArray(\"%v\") expected error string: %v, got: nil", name, tc.input, tc.errString) + } + + if !reflect.DeepEqual(tc.want, got) { + t.Fatalf("testing '%v' getCleanWpCliArgumentArray(\"%v\") expected:\n\t%v\ngot:\n\t%v", name, tc.input, tc.want, got) + } + }) + } + +} + func TestValidateCommand(t *testing.T) { tests := map[string]struct { errString string From d91513918ef937482fea604f47067eebd5a1f92d Mon Sep 17 00:00:00 2001 From: Jeff Bowen Date: Wed, 10 Aug 2022 15:06:43 -0400 Subject: [PATCH 2/2] rm a commented out debug statement --- remote/remote.go | 1 - 1 file changed, 1 deletion(-) diff --git a/remote/remote.go b/remote/remote.go index 91526b0..8683215 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -1159,7 +1159,6 @@ func isJSONObjectOrArray(str string) bool { } t, err := jsonType(_str) if err != nil { - //fmt.Printf("%v doesn't parse\n", str) return false }