templates.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. package tpl
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "regexp"
  6. "strings"
  7. "text/template"
  8. "github.com/OliveTin/OliveTin/internal/entities"
  9. "github.com/OliveTin/OliveTin/internal/env"
  10. "github.com/OliveTin/OliveTin/internal/installationinfo"
  11. log "github.com/sirupsen/logrus"
  12. )
  13. func jsonFunc(v any) (string, error) {
  14. if v == nil {
  15. return "null", nil
  16. }
  17. data, err := json.Marshal(v)
  18. if err != nil {
  19. return "", err
  20. }
  21. return string(data), nil
  22. }
  23. var tpl = template.New("tpl").
  24. Option("missingkey=error").
  25. Funcs(template.FuncMap{"Json": jsonFunc})
  26. type olivetinInfo struct {
  27. Build *installationinfo.BuildInfo
  28. Runtime *installationinfo.RuntimeInfo
  29. }
  30. var legacyArgumentRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\s*}}`)
  31. var legacyEntityPropertiesRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\.([a-zA-Z0-9_\.]+)\s*}}`)
  32. type generalTemplateContext struct {
  33. OliveTin olivetinInfo
  34. Env map[string]string
  35. }
  36. // FileUpload is exposed in action templates as .Arguments.<name> for type file_upload.
  37. // TmpName is the absolute path of the staged file on the server (similar to PHP's tmp_name).
  38. // Name is normalized to shell-safe ASCII (see fileupload.SanitizeUploadFilename) before template rendering.
  39. type FileUpload struct {
  40. TmpName string
  41. Name string
  42. MimeType string
  43. Size int64
  44. }
  45. type actionTemplateContext struct {
  46. CurrentEntity interface{}
  47. Arguments map[string]any
  48. // These are deliberately repeated because embedding structs
  49. // won't work in text/template.
  50. OliveTin olivetinInfo
  51. Env map[string]string
  52. }
  53. func newActionTemplateContext(entdata any, args map[string]any) *actionTemplateContext {
  54. return &actionTemplateContext{
  55. OliveTin: cachedOliveTinInfo,
  56. Env: cachedEnvMap,
  57. Arguments: args,
  58. CurrentEntity: entdata,
  59. }
  60. }
  61. var (
  62. cachedOliveTinInfo olivetinInfo
  63. cachedEnvMap map[string]string
  64. )
  65. func init() {
  66. cachedOliveTinInfo = olivetinInfo{
  67. Build: installationinfo.Build,
  68. Runtime: installationinfo.Runtime,
  69. }
  70. cachedEnvMap = env.BuildEnvMap()
  71. }
  72. func GetNewGeneralTemplateContext() *generalTemplateContext {
  73. return &generalTemplateContext{
  74. OliveTin: cachedOliveTinInfo,
  75. Env: cachedEnvMap,
  76. }
  77. }
  78. func migrateLegacyEntityProperties(rawShellCommand string) string {
  79. foundArgumentNames := legacyEntityPropertiesRegex.FindAllStringSubmatch(rawShellCommand, -1)
  80. for _, match := range foundArgumentNames {
  81. entityName := match[1]
  82. argName := match[2]
  83. fullMatch := match[0] // The entire matched string like "{{ server.hostname }}"
  84. if strings.Contains(argName, ".") {
  85. replacement := "{{ .CurrentEntity." + argName + " }}"
  86. rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)
  87. log.WithFields(log.Fields{
  88. "old": entityName,
  89. "new": ".CurrentEntity",
  90. }).Debugf("Legacy entity variable name found, changing to CurrentEntity")
  91. continue
  92. }
  93. if !strings.HasPrefix(argName, ".Arguments.") {
  94. replacement := "{{ .CurrentEntity." + argName + " }}"
  95. rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)
  96. log.WithFields(log.Fields{
  97. "old": argName,
  98. "new": ".CurrentEntity." + argName,
  99. }).Debugf("Legacy variable name found, changing to CurrentEntity")
  100. }
  101. }
  102. return rawShellCommand
  103. }
  104. func migrateLegacyArgumentNames(rawShellCommand string) string {
  105. matches := legacyArgumentRegex.FindAllStringSubmatchIndex(rawShellCommand, -1)
  106. for i := len(matches) - 1; i >= 0; i-- {
  107. match := matches[i]
  108. fullMatchStart := match[0]
  109. fullMatchEnd := match[1]
  110. argNameStart := match[2]
  111. argNameEnd := match[3]
  112. argName := rawShellCommand[argNameStart:argNameEnd]
  113. log.WithFields(log.Fields{
  114. "old": argName,
  115. "new": ".Arguments." + argName,
  116. }).Debugf("Legacy variable name found, changing to Argument")
  117. replacement := "{{ .Arguments." + argName + " }}"
  118. rawShellCommand = rawShellCommand[:fullMatchStart] + replacement + rawShellCommand[fullMatchEnd:]
  119. }
  120. return rawShellCommand
  121. }
  122. func ParseTemplateWithActionContext(source string, ent *entities.Entity, args map[string]any) (string, error) {
  123. source = migrateLegacyArgumentNames(source)
  124. source = migrateLegacyEntityProperties(source)
  125. return parseTemplateWithEntityAndArgs(source, ent, args)
  126. }
  127. func parseTemplateWithEntityAndArgs(source string, ent *entities.Entity, args map[string]any) (string, error) {
  128. result, err := execActionTemplateParse(source, ent, args)
  129. return finishActionTemplateParse(result, err)
  130. }
  131. func execActionTemplateParse(source string, ent *entities.Entity, args map[string]any) (string, error) {
  132. var entdata any
  133. if ent != nil {
  134. entdata = ent.Data
  135. }
  136. if args == nil {
  137. args = map[string]any{}
  138. }
  139. templateVariables := newActionTemplateContext(entdata, args)
  140. return parseTemplate(source, templateVariables)
  141. }
  142. func finishActionTemplateParse(result string, err error) (string, error) {
  143. if isMissingArgumentError, argName := checkMissingArgumentError(err); isMissingArgumentError {
  144. return "", fmt.Errorf("required arg not provided: %s", argName)
  145. }
  146. if err != nil {
  147. return "", err
  148. }
  149. return result, nil
  150. }
  151. func checkMissingArgumentError(err error) (bool, string) {
  152. if err == nil {
  153. return false, ""
  154. }
  155. if strings.Contains(err.Error(), "map has no entry for key") {
  156. re := regexp.MustCompile(`\.Arguments\.(\w+)`)
  157. match := re.FindStringSubmatch(err.Error())
  158. if len(match) > 1 {
  159. return true, match[1]
  160. }
  161. }
  162. return false, ""
  163. }
  164. func parseTemplate(source string, data any) (string, error) {
  165. t, err := tpl.Parse(source)
  166. if err != nil {
  167. return "", err
  168. }
  169. var sb strings.Builder
  170. err = t.Execute(&sb, data)
  171. if err != nil {
  172. log.WithFields(log.Fields{
  173. "source": source,
  174. "err": err,
  175. }).Errorf("Error executing template")
  176. return "", err
  177. } else {
  178. return sb.String(), nil
  179. }
  180. }
  181. func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string {
  182. result, err := ParseTemplateWithActionContext(source, ent, nil)
  183. if err != nil {
  184. log.WithFields(log.Fields{
  185. "source": source,
  186. "err": err,
  187. }).Errorf("Error parsing template of action before exec")
  188. return ""
  189. }
  190. return result
  191. }
  192. /*
  193. func ParseTemplateBoolWith(source string, ent *entities.Entity) bool {
  194. source = strings.TrimSpace(source)
  195. tplBool := ParseTemplateOfActionBeforeExec(source, ent)
  196. return tplBool == "true"
  197. }
  198. */