From 07b6a49223b061904c62c01b1746948489feab41 Mon Sep 17 00:00:00 2001 From: Markin Igor <markin.io210@gmail.com> Date: Tue, 6 Nov 2018 10:17:49 +0300 Subject: [PATCH] Add iframe generation code. --- .gitignore | 1 + Gopkg.toml | 3 + main.go | 774 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 778 insertions(+) diff --git a/.gitignore b/.gitignore index b200425..b01ed7d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea *.iml bin/ +Gopkg.lock diff --git a/Gopkg.toml b/Gopkg.toml index d7072c2..06f9de1 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -24,6 +24,9 @@ # go-tests = true # unused-packages = true +[[constraint]] + branch = "feature-transpile-js-libs" + name = "code.vereign.com/code/restful-api" [prune] go-tests = true diff --git a/main.go b/main.go index 4473a77..5eb0421 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,17 @@ package jslibs import ( + "bytes" "fmt" "io/ioutil" "path/filepath" + "reflect" + "sort" "strings" + "unicode" + "unicode/utf8" + + "code.vereign.com/code/restful-api/types" ) func GetClientJsLibrary(iframeUrl string) string { @@ -22,3 +29,770 @@ func GetClientJsLibrary(iframeUrl string) string { return strings.Replace(string(dat), "{{urlArg}}", iframeUrl, 1) + "\n" } + +func GetIframeJsLibrary(host string, + endPoints map[string]*types.EndPoint) string { + result := "<script>\n\n" + + var keys []string + for k := range endPoints { + keys = append(keys, k) + } + + sort.Strings(keys) + + keysLen := len(keys) + + dat, err := ioutil.ReadFile("../vcl/javascript/dist/viamapi-iframe.js") + + if err != nil { + fmt.Println(err.Error()) + } + + methods := generatePenpalRemoteMethods(host, endPoints) + methods += getWopiAPIPenpalMethods() + + viamApi := strings.Replace(string(dat), "placeholderForExternalMethods:1", methods, 1) + + if err != nil { + fmt.Println(err.Error()) + } + + result += viamApi + "\n\n" + + result += getWopiAPI(host) + + result += "\n\nfunction ViamAPI() {\n" + + " this.config = {\n" + + " headers: {\n" + + " 'publicKey': '',\n" + + " 'uuid': '',\n" + + " 'token': ''\n" + + " }\n" + + " }\n" + + "}\n\n" + + result += "ViamAPI.prototype.setSessionData = function(uuid, token) {\n" + + " this.config.headers.uuid = \" \" + uuid;\n" + + " this.config.headers.token = \" \" + token;\n" + + "}\n\n" + + result += "ViamAPI.prototype.setIdentity = function(authenticationPublicKey) {\n" + + " this.config.headers.publicKey = \" \" + window.btoa(authenticationPublicKey);\n" + + "}\n\n" + + result += "ViamAPI.prototype.getConfig = function() {\n" + + " return this.config;\n" + + "}\n\n" + + for i := 0; i < keysLen; i++ { + url := keys[i] + if endPoints[url].Form != nil { + splits := strings.Split(url, "/") + + packageStr := splits[len(splits)-2] + + /*if !packageCreated[packageStr] { + result += "ViamAPI.prototype.\"" + packageStr + "\" = {}\n\n" + packageCreated[packageStr] = true + }*/ + + methodStr := splits[len(splits)-1] + + form := endPoints[url].Form + s := reflect.ValueOf(form) + + typeOfT := s.Type() + + fields := typeFields(typeOfT) + + lenFields := len(fields) + + args := "" + + for i := 0; i < lenFields; i++ { + if i != lenFields-1 { + args += fmt.Sprintf("%sArg,", fields[i].name) + } else { + args += fmt.Sprintf("%sArg", fields[i].name) + } + } + + result += "ViamAPI.prototype." + packageStr + strings.Title(methodStr) + " = function(" + args + ") {\n" + + " return axios.post('" + host + packageStr + "/" + methodStr + "', {\n" + + for i := 0; i < lenFields; i++ { + result += " " + fields[i].name + ": " + fields[i].name + "Arg" + if i != lenFields-1 { + result += "," + } + result += "\n" + } + + result += " }, this.config);\n" + + "}\n\n" + } else { + splits := strings.Split(url, "/") + + packageStr := splits[len(splits)-2] + + /*if !packageCreated[packageStr] { + result += "api[\"" + packageStr + "\"] = {}\n" + packageCreated[packageStr] = true + }*/ + + methodStr := splits[len(splits)-1] + + result += "ViamAPI.prototype." + packageStr + strings.Title(methodStr) + " = function() {\n" + + " return axios.post('" + host + "/" + packageStr + "/" + methodStr + "', {}, this.config);\n" + + "}\n\n" + } + } + + result += "\n\n</script>" + + return result +} + +const ( + caseMask = ^byte(0x20) // Mask to ignore case in ASCII. + kelvin = '\u212a' + smallLongEss = '\u017f' +) + +// equalFoldRight is a specialization of bytes.EqualFold when s is +// known to be all ASCII (including punctuation), but contains an 's', +// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t. +// See comments on foldFunc. +func equalFoldRight(s, t []byte) bool { + for _, sb := range s { + if len(t) == 0 { + return false + } + tb := t[0] + if tb < utf8.RuneSelf { + if sb != tb { + sbUpper := sb & caseMask + if 'A' <= sbUpper && sbUpper <= 'Z' { + if sbUpper != tb&caseMask { + return false + } + } else { + return false + } + } + t = t[1:] + continue + } + // sb is ASCII and t is not. t must be either kelvin + // sign or long s; sb must be s, S, k, or K. + tr, size := utf8.DecodeRune(t) + switch sb { + case 's', 'S': + if tr != smallLongEss { + return false + } + case 'k', 'K': + if tr != kelvin { + return false + } + default: + return false + } + t = t[size:] + + } + if len(t) > 0 { + return false + } + return true +} + +// asciiEqualFold is a specialization of bytes.EqualFold for use when +// s is all ASCII (but may contain non-letters) and contains no +// special-folding letters. +// See comments on foldFunc. +func asciiEqualFold(s, t []byte) bool { + if len(s) != len(t) { + return false + } + for i, sb := range s { + tb := t[i] + if sb == tb { + continue + } + if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') { + if sb&caseMask != tb&caseMask { + return false + } + } else { + return false + } + } + return true +} + +// simpleLetterEqualFold is a specialization of bytes.EqualFold for +// use when s is all ASCII letters (no underscores, etc) and also +// doesn't contain 'k', 'K', 's', or 'S'. +// See comments on foldFunc. +func simpleLetterEqualFold(s, t []byte) bool { + if len(s) != len(t) { + return false + } + for i, b := range s { + if b&caseMask != t[i]&caseMask { + return false + } + } + return true +} + +func foldFunc(s []byte) func(s, t []byte) bool { + nonLetter := false + special := false // special letter + for _, b := range s { + if b >= utf8.RuneSelf { + return bytes.EqualFold + } + upper := b & caseMask + if upper < 'A' || upper > 'Z' { + nonLetter = true + } else if upper == 'K' || upper == 'S' { + // See above for why these letters are special. + special = true + } + } + if special { + return equalFoldRight + } + if nonLetter { + return asciiEqualFold + } + return simpleLetterEqualFold +} + +// A field represents a single field found in a struct. +type field struct { + name string + nameBytes []byte // []byte(name) + equalFold func(s, t []byte) bool // bytes.EqualFold or equivalent + + tag bool + index []int + typ reflect.Type + omitEmpty bool + quoted bool +} + +func fillField(f field) field { + f.nameBytes = []byte(f.name) + f.equalFold = foldFunc(f.nameBytes) + return f +} + +// byIndex sorts field by index sequence. +type byIndex []field + +func (x byIndex) Len() int { return len(x) } + +func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x byIndex) Less(i, j int) bool { + for k, xik := range x[i].index { + if k >= len(x[j].index) { + return false + } + if xik != x[j].index[k] { + return xik < x[j].index[k] + } + } + return len(x[i].index) < len(x[j].index) +} + +func isValidTag(s string) bool { + if s == "" { + return false + } + for _, c := range s { + switch { + case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c): + // Backslash and quote chars are reserved, but + // otherwise any punctuation chars are allowed + // in a tag name. + default: + if !unicode.IsLetter(c) && !unicode.IsDigit(c) { + return false + } + } + } + return true +} + +// tagOptions is the string following a comma in a struct field's "json" +// tag, or the empty string. It does not include the leading comma. +type tagOptions string + +// Contains reports whether a comma-separated list of options +// contains a particular substr flag. substr must be surrounded by a +// string boundary or commas. +func (o tagOptions) Contains(optionName string) bool { + if len(o) == 0 { + return false + } + s := string(o) + for s != "" { + var next string + i := strings.Index(s, ",") + if i >= 0 { + s, next = s[:i], s[i+1:] + } + if s == optionName { + return true + } + s = next + } + return false +} + +// parseTag splits a struct field's json tag into its name and +// comma-separated options. +func parseTag(tag string) (string, tagOptions) { + if idx := strings.Index(tag, ","); idx != -1 { + return tag[:idx], tagOptions(tag[idx+1:]) + } + return tag, tagOptions("") +} + +// typeFields returns a list of fields that JSON should recognize for the given type. +// The algorithm is breadth-first search over the set of structs to include - the top struct +// and then any reachable anonymous structs. +func typeFields(t reflect.Type) []field { + // Anonymous fields to explore at the current level and the next. + current := []field{} + next := []field{{typ: t}} + + // Count of queued names for current level and the next. + count := map[reflect.Type]int{} + nextCount := map[reflect.Type]int{} + + // Types already visited at an earlier level. + visited := map[reflect.Type]bool{} + + // Fields found. + var fields []field + + for len(next) > 0 { + current, next = next, current[:0] + count, nextCount = nextCount, map[reflect.Type]int{} + + for _, f := range current { + if visited[f.typ] { + continue + } + visited[f.typ] = true + + // Scan f.typ for fields to include. + for i := 0; i < f.typ.NumField(); i++ { + sf := f.typ.Field(i) + isUnexported := sf.PkgPath != "" + if sf.Anonymous { + t := sf.Type + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if isUnexported && t.Kind() != reflect.Struct { + // Ignore embedded fields of unexported non-struct types. + continue + } + // Do not ignore embedded fields of unexported struct types + // since they may have exported fields. + } else if isUnexported { + // Ignore unexported non-embedded fields. + continue + } + tag := sf.Tag.Get("json") + if tag == "-" { + continue + } + name, opts := parseTag(tag) + if !isValidTag(name) { + name = "" + } + index := make([]int, len(f.index)+1) + copy(index, f.index) + index[len(f.index)] = i + + ft := sf.Type + if ft.Name() == "" && ft.Kind() == reflect.Ptr { + // Follow pointer. + ft = ft.Elem() + } + + // Only strings, floats, integers, and booleans can be quoted. + quoted := false + if opts.Contains("string") { + switch ft.Kind() { + case reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, + reflect.String: + quoted = true + } + } + + // Record found field and index sequence. + if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct { + tagged := name != "" + if name == "" { + name = sf.Name + } + fields = append(fields, fillField(field{ + name: name, + tag: tagged, + index: index, + typ: ft, + omitEmpty: opts.Contains("omitempty"), + quoted: quoted, + })) + if count[f.typ] > 1 { + // If there were multiple instances, add a second, + // so that the annihilation code will see a duplicate. + // It only cares about the distinction between 1 or 2, + // so don't bother generating any more copies. + fields = append(fields, fields[len(fields)-1]) + } + continue + } + + // Record new anonymous struct to explore in next round. + nextCount[ft]++ + if nextCount[ft] == 1 { + next = append(next, fillField(field{name: ft.Name(), index: index, typ: ft})) + } + } + } + } + + sort.Slice(fields, func(i, j int) bool { + x := fields + // sort field by name, breaking ties with depth, then + // breaking ties with "name came from json tag", then + // breaking ties with index sequence. + if x[i].name != x[j].name { + return x[i].name < x[j].name + } + if len(x[i].index) != len(x[j].index) { + return len(x[i].index) < len(x[j].index) + } + if x[i].tag != x[j].tag { + return x[i].tag + } + return byIndex(x).Less(i, j) + }) + + // Delete all fields that are hidden by the Go rules for embedded fields, + // except that fields with JSON tags are promoted. + + // The fields are sorted in primary order of name, secondary order + // of field index length. Loop over names; for each name, delete + // hidden fields by choosing the one dominant field that survives. + out := fields[:0] + for advance, i := 0, 0; i < len(fields); i += advance { + // One iteration per name. + // Find the sequence of fields with the name of this first field. + fi := fields[i] + name := fi.name + for advance = 1; i+advance < len(fields); advance++ { + fj := fields[i+advance] + if fj.name != name { + break + } + } + if advance == 1 { // Only one field with this name + out = append(out, fi) + continue + } + dominant, ok := dominantField(fields[i : i+advance]) + if ok { + out = append(out, dominant) + } + } + + fields = out + sort.Sort(byIndex(fields)) + + return fields +} + +// dominantField looks through the fields, all of which are known to +// have the same name, to find the single field that dominates the +// others using Go's embedding rules, modified by the presence of +// JSON tags. If there are multiple top-level fields, the boolean +// will be false: This condition is an error in Go and we skip all +// the fields. +func dominantField(fields []field) (field, bool) { + // The fields are sorted in increasing index-length order. The winner + // must therefore be one with the shortest index length. Drop all + // longer entries, which is easy: just truncate the slice. + length := len(fields[0].index) + tagged := -1 // Index of first tagged field. + for i, f := range fields { + if len(f.index) > length { + fields = fields[:i] + break + } + if f.tag { + if tagged >= 0 { + // Multiple tagged fields at the same level: conflict. + // Return no field. + return field{}, false + } + tagged = i + } + } + if tagged >= 0 { + return fields[tagged], true + } + // All remaining fields have the same length. If there's more than one, + // we have a conflict (two fields named "X" at the same level) and we + // return no field. + if len(fields) > 1 { + return field{}, false + } + return fields[0], true +} + +func generatePenpalRemoteMethods(host string, + endPoints map[string]*types.EndPoint) string { + var keys []string + for k := range endPoints { + keys = append(keys, k) + } + + sort.Strings(keys) + + keysLen := len(keys) + + methods := "" + + privateCheckSnippet := ` + authenticationPublicKey = localStorage.getItem("authenticatedIdentity") + + if (authenticationPublicKey === null) { + result({ + "data" : "", + "code" : "400", + "status" : "Identity not authenticated" + }); + } + + if (loadedIdentities[authenticationPublicKey] === null) { + result({ + "data" : "", + "code" : "400", + "status" : "Identity not authenticated" + }); + } + + success = extendPinCodeTtl(authenticationPublicKey) + + if(success == false) { + result({"data" : "", + "code" : "400", + "status" : "Identity not authenticated" + }) + } + + ` + + for i := 0; i < keysLen; i++ { + url := keys[i] + + if endPoints[url].ManuallyWritten == true { + continue + } + + if url == "/identity/getIdentityProfileData" { + privateCheckSnippet = ` + authenticationPublicKey = localStorage.getItem("authenticatedIdentity") + + if (authenticationPublicKey === null) { + result({ + "data" : "", + "code" : "400", + "status" : "Identity not authenticated" + }); + } + + if (loadedIdentities[authenticationPublicKey] === null) { + result({ + "data" : "", + "code" : "400", + "status" : "Identity not authenticated" + }); + } + + ` + } + + if endPoints[url].Form != nil { + splits := strings.Split(url, "/") + + packageStr := splits[len(splits)-2] + + /*if !packageCreated[packageStr] { + result += "ViamAPI.prototype.\"" + packageStr + "\" = {}\n\n" + packageCreated[packageStr] = true + }*/ + + methodStr := splits[len(splits)-1] + + form := endPoints[url].Form + s := reflect.ValueOf(form) + + typeOfT := s.Type() + + fields := typeFields(typeOfT) + + lenFields := len(fields) + + args := "" + + for i := 0; i < lenFields; i++ { + if i != lenFields-1 { + args += fmt.Sprintf("%sArg,", fields[i].name) + } else { + args += fmt.Sprintf("%sArg", fields[i].name) + } + } + + /*identity_login(modeArg,codeArg,actionIDArg) { + return new Penpal.Promise(result => { + viamApi.identity_login(modeArg,codeArg,actionIDArg).then((response) => { result(response.data);}); + }); + }*/ + + snippet := "" + + if endPoints[url].HandlerType == "private" { + snippet = privateCheckSnippet + } + + lastComma := "," + + if args == "" { + lastComma = "" + } + + method := packageStr + strings.Title(methodStr) + ": function(" + args + ") {\n" + + " return new Penpal.Promise(function(result) {\n" + snippet + + " executeRestfulFunction(\"" + endPoints[url].HandlerType + "\", viamApi, viamApi." + packageStr + strings.Title(methodStr) + lastComma + args + ").then(function(executeResult) {\n" + + " result(executeResult);\n" + + " });\n" + + " });\n" + + "}" + + methods += method + + if i != keysLen-1 { + methods += "," + } + + methods += "\n" + } else { + splits := strings.Split(url, "/") + + packageStr := splits[len(splits)-2] + + /*if !packageCreated[packageStr] { + result += "ViamAPI.prototype.\"" + packageStr + "\" = {}\n\n" + packageCreated[packageStr] = true + }*/ + + methodStr := splits[len(splits)-1] + + snippet := "" + + if endPoints[url].HandlerType == "private" { + snippet = privateCheckSnippet + } + + method := packageStr + strings.Title(methodStr) + ": function() {\n" + + " return new Penpal.Promise(function(result) {\n" + snippet + + " executeRestfulFunction(\"" + endPoints[url].HandlerType + "\", viamApi, viamApi." + packageStr + strings.Title(methodStr) + ").then(function(executeResult) {\n" + + " result(executedResult);\n" + + " });\n" + + " });\n" + + "}" + + methods += method + + if i != keysLen-1 { + methods += "," + } + + methods += "\n" + + } + } + + return methods +} + +func getWopiAPIPenpalMethods() string { + return `getPassports: function(fileID) { + return new Penpal.Promise(function(result) { + authenticationPublicKey = localStorage.getItem("authenticatedIdentity") + if (authenticationPublicKey === null) { + result({ + "data" : "", + "code" : "400", + "status" : "Identity not authenticated" + }); + } + + if (loadedIdentities[authenticationPublicKey] === null) { + result({ + "data" : "", + "code" : "400", + "status" : "Identity not authenticated" + }); + } + + success = extendPinCodeTtl(authenticationPublicKey) + + if(success == false) { + result({"data" : "", + "code" : "400", + "status" : "Identity not authenticated" + }) + } + + wopiAPI.getPassports( + authenticationPublicKey, + viamApi.getConfig().headers.uuid, + viamApi.getConfig().headers.token, + fileID + ).then(function(response) { + result(response.data); + }); + }); + }` +} + +func getWopiAPI(url string) string { + dat, err := ioutil.ReadFile("../vcl/javascript/dist/wopiapi-iframe.js") + if err != nil { + fmt.Println(err.Error()) + } + + urlParts := strings.Split(url, ":") + + wopiAPI := strings.Replace(string(dat), "{{host}}", urlParts[0]+":"+urlParts[1], -1) + wopiAPI = strings.Replace(wopiAPI, "{{port}}", "8787", -1) + wopiAPI += "\n\n" + + return wopiAPI +} -- GitLab