diff --git a/cli_composer.go b/cli_composer.go
index ef6e19b6..d46e70be 100644
--- a/cli_composer.go
+++ b/cli_composer.go
@@ -332,9 +332,8 @@ func composeFormReport(ctx context.Context, args []string) {
msg.SetBody(formMsg.Body)
- if xml := formMsg.AttachmentXML; xml != "" {
- attachmentFile := fbb.NewFile(formMsg.AttachmentName, []byte(xml))
- msg.AddFile(attachmentFile)
+ for _, f := range formMsg.Attachments() {
+ msg.AddFile(f)
}
postMessage(msg)
diff --git a/http.go b/http.go
index 2bcd0800..834eee61 100644
--- a/http.go
+++ b/http.go
@@ -32,6 +32,7 @@ import (
"github.com/la5nta/wl2k-go/transport/ardop"
"github.com/la5nta/pat/internal/buildinfo"
+ "github.com/la5nta/pat/internal/debug"
"github.com/la5nta/pat/internal/directories"
"github.com/la5nta/pat/internal/gpsd"
@@ -277,12 +278,20 @@ func postOutboundMessageHandler(w http.ResponseWriter, r *http.Request) {
}
}
- cookie, err := r.Cookie("forminstance")
- if err == nil {
- formData := formsMgr.GetPostedFormData(cookie.Value)
- if xml := formData.MsgXML; xml != "" {
- name := formsMgr.GetXMLAttachmentNameForForm(formData.TargetForm, formData.IsReply)
- msg.AddFile(fbb.NewFile(name, []byte(formData.MsgXML)))
+ if cookie, err := r.Cookie("forminstance"); err == nil {
+ // We must add the attachment files here because it is impossible
+ // for the frontend to dynamically add form files due to legacy
+ // security vulnerabilities in older HTML specs.
+ // The rest of the form data (to, subject, body etc) is added by
+ // the frontend.
+ formData, ok := formsMgr.GetPostedFormData(cookie.Value)
+ if !ok {
+ debug.Printf("form instance key (%q) not valid", cookie.Value)
+ http.Error(w, "form instance key not valid", http.StatusBadRequest)
+ return
+ }
+ for _, f := range formData.Attachments() {
+ msg.AddFile(f)
}
}
diff --git a/internal/forms/forms.go b/internal/forms/forms.go
index e15ff2b0..6cbe317e 100644
--- a/internal/forms/forms.go
+++ b/internal/forms/forms.go
@@ -36,6 +36,7 @@ import (
"github.com/la5nta/pat/internal/debug"
"github.com/la5nta/pat/internal/directories"
"github.com/la5nta/pat/internal/gpsd"
+ "github.com/la5nta/wl2k-go/fbb"
"github.com/pd0mz/go-maidenhead"
)
@@ -47,15 +48,21 @@ const (
)
// Manager manages the forms subsystem
-// When the web frontend POSTs the form template data, this map holds the POST'ed data.
-// Each form composer instance renders into another browser tab, and has a unique instance cookie.
-// This instance cookie is the key into the map, so that we can keep the values
-// from different form authoring sessions separate from each other.
type Manager struct {
- config Config
+ config Config
+
+ // postedFormData serves as an kv-store holding intermediate data for
+ // communicating form values submitted by the served HTML form files to
+ // the rest of the app.
+ //
+ // When the web frontend POSTs the form template data, this map holds
+ // the POST'ed data. Each form composer instance renders into another
+ // browser tab, and has a unique instance cookie. This instance cookie
+ // is the key into the map, so that we can keep the values from
+ // different form authoring sessions separate from each other.
postedFormData struct {
- sync.RWMutex
- internalFormDataMap map[string]FormData
+ mu sync.RWMutex
+ m map[string]MessageForm
}
}
@@ -91,27 +98,27 @@ type FormFolder struct {
Folders []FormFolder `json:"folders"`
}
-// FormData holds the instance data that define a filled-in form
-type FormData struct {
- TargetForm Form `json:"target_form"`
- Fields map[string]string `json:"fields"`
- MsgTo string `json:"msg_to"`
- MsgCc string `json:"msg_cc"`
- MsgSubject string `json:"msg_subject"`
- MsgBody string `json:"msg_body"`
- MsgXML string `json:"msg_xml"`
- IsReply bool `json:"is_reply"`
- Submitted time.Time `json:"submitted"`
-}
-
// MessageForm represents a concrete form-based message
type MessageForm struct {
- To string
- Cc string
- Subject string
- Body string
- AttachmentXML string
- AttachmentName string
+ To string `json:"msg_to"`
+ Cc string `json:"msg_cc"`
+ Subject string `json:"msg_subject"`
+ Body string `json:"msg_body"`
+
+ attachmentXML string
+ targetForm Form
+ isReply bool
+ submitted time.Time
+}
+
+// Attachments returns the attachments generated by the filled-in form
+func (m MessageForm) Attachments() []*fbb.File {
+ var files []*fbb.File
+ if xml := m.attachmentXML; xml != "" {
+ name := getXMLAttachmentNameForForm(m.targetForm, m.isReply)
+ files = append(files, fbb.NewFile(name, []byte(xml)))
+ }
+ return files
}
// UpdateResponse is the API response format for the upgrade forms endpoint
@@ -128,7 +135,7 @@ func NewManager(conf Config) *Manager {
retval := &Manager{
config: conf,
}
- retval.postedFormData.internalFormDataMap = make(map[string]FormData)
+ retval.postedFormData.m = make(map[string]MessageForm)
return retval
}
@@ -180,18 +187,14 @@ func (m *Manager) PostFormDataHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("missing cookie %s %s", formPath, r.URL)
return
}
- formData := FormData{
- IsReply: composeReply,
- TargetForm: form,
- Fields: make(map[string]string),
- }
+ fields := make(map[string]string, len(r.PostForm))
for key, values := range r.PostForm {
- formData.Fields[strings.TrimSpace(strings.ToLower(key))] = values[0]
+ fields[strings.TrimSpace(strings.ToLower(key))] = values[0]
}
formMsg, err := formMessageBuilder{
Template: form,
- FormValues: formData.Fields,
+ FormValues: fields,
Interactive: false,
IsReply: composeReply,
FormsMgr: m,
@@ -200,16 +203,10 @@ func (m *Manager) PostFormDataHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("%s %s: %s", r.Method, r.URL.Path, err)
}
- formData.MsgTo = formMsg.To
- formData.MsgCc = formMsg.Cc
- formData.MsgSubject = formMsg.Subject
- formData.MsgBody = formMsg.Body
- formData.MsgXML = formMsg.AttachmentXML
- formData.Submitted = time.Now()
- m.postedFormData.Lock()
- m.postedFormData.internalFormDataMap[formInstanceKey.Value] = formData
- m.postedFormData.Unlock()
+ m.postedFormData.mu.Lock()
+ m.postedFormData.m[formInstanceKey.Value] = formMsg
+ m.postedFormData.mu.Unlock()
m.cleanupOldFormData()
_, _ = io.WriteString(w, "")
@@ -223,14 +220,20 @@ func (m *Manager) GetFormDataHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("missing cookie %s %s", formInstanceKey, r.URL)
return
}
- _ = json.NewEncoder(w).Encode(m.GetPostedFormData(formInstanceKey.Value))
+ v, ok := m.GetPostedFormData(formInstanceKey.Value)
+ if !ok {
+ http.NotFound(w, r)
+ return
+ }
+ _ = json.NewEncoder(w).Encode(v)
}
// GetPostedFormData is similar to GetFormDataHandler, but used when posting the form-based message to the outbox
-func (m *Manager) GetPostedFormData(key string) FormData {
- m.postedFormData.RLock()
- defer m.postedFormData.RUnlock()
- return m.postedFormData.internalFormDataMap[key]
+func (m *Manager) GetPostedFormData(key string) (MessageForm, bool) {
+ m.postedFormData.mu.RLock()
+ defer m.postedFormData.mu.RUnlock()
+ v, ok := m.postedFormData.m[key]
+ return v, ok
}
// GetFormTemplateHandler handles the request for viewing a form filled-in with instance values
@@ -392,8 +395,8 @@ func unzip(srcArchivePath, dstRoot string) error {
return nil
}
-// GetXMLAttachmentNameForForm returns the user-visible filename for the message attachment that holds the form instance values
-func (m *Manager) GetXMLAttachmentNameForForm(f Form, isReply bool) string {
+// getXMLAttachmentNameForForm returns the user-visible filename for the message attachment that holds the form instance values
+func getXMLAttachmentNameForForm(f Form, isReply bool) string {
attachmentName := filepath.Base(f.ViewerURI)
if isReply {
attachmentName = filepath.Base(f.ReplyViewerURI)
@@ -917,15 +920,19 @@ func (b formMessageBuilder) build() (MessageForm, error) {
b.initFormValues()
- formVarsAsXML := ""
- for varKey, varVal := range b.FormValues {
- formVarsAsXML += fmt.Sprintf(" <%s>%s%s>\n", xmlEscape(varKey), xmlEscape(varVal), xmlEscape(varKey))
- }
-
msgForm, err := b.scanTmplBuildMessage(tmplPath)
if err != nil {
return MessageForm{}, err
}
+ //TODO: Should any of these be set in scanTmplBuildMessage?
+ msgForm.targetForm = b.Template
+ msgForm.isReply = b.IsReply
+ msgForm.submitted = time.Now()
+
+ formVarsAsXML := ""
+ for varKey, varVal := range b.FormValues {
+ formVarsAsXML += fmt.Sprintf(" <%s>%s%s>\n", xmlEscape(varKey), xmlEscape(varVal), xmlEscape(varKey))
+ }
// Add XML if a viewer is defined for this form
if b.Template.ViewerURI != "" {
@@ -933,7 +940,7 @@ func (b formMessageBuilder) build() (MessageForm, error) {
if b.IsReply && b.Template.ReplyViewerURI != "" {
viewer = b.Template.ReplyViewerURI
}
- msgForm.AttachmentXML = fmt.Sprintf(`%s
+ msgForm.attachmentXML = fmt.Sprintf(`%s
%s
%s
@@ -957,7 +964,6 @@ func (b formMessageBuilder) build() (MessageForm, error) {
filepath.Base(viewer),
filepath.Base(b.Template.ReplyTxtFileURI),
formVarsAsXML)
- msgForm.AttachmentName = b.FormsMgr.GetXMLAttachmentNameForForm(b.Template, false)
}
msgForm.To = strings.TrimSpace(msgForm.To)
@@ -1075,13 +1081,13 @@ func fillPlaceholders(s string, re *regexp.Regexp, values map[string]string) str
}
func (m *Manager) cleanupOldFormData() {
- m.postedFormData.Lock()
- defer m.postedFormData.Unlock()
- for key, form := range m.postedFormData.internalFormDataMap {
- elapsed := time.Since(form.Submitted).Hours()
+ m.postedFormData.mu.Lock()
+ defer m.postedFormData.mu.Unlock()
+ for key, form := range m.postedFormData.m {
+ elapsed := time.Since(form.submitted).Hours()
if elapsed > 24 {
log.Println("deleting old FormData after", elapsed, "hrs")
- delete(m.postedFormData.internalFormDataMap, key)
+ delete(m.postedFormData.m, key)
}
}
}
diff --git a/web/src/js/app.js b/web/src/js/app.js
index be66c84e..4af245a6 100644
--- a/web/src/js/app.js
+++ b/web/src/js/app.js
@@ -217,37 +217,38 @@ function forgetFormData() {
let pollTimer;
function pollFormData() {
- $.get(
- 'api/form',
- {},
- function (data) {
+ $.ajax({
+ method: 'GET',
+ url: '/api/form',
+ dataType: 'json',
+ success: function (data) {
+ // TODO: Should verify forminstance key in case of multi-user scenario
+ console.log('done polling');
console.log(data);
- if (!$('#composer').hasClass('hidden') && (!data.target_form || !data.target_form.name)) {
+ if (!$('#composer').hasClass('hidden')) {
+ writeFormDataToComposer(data);
+ }
+ },
+ error: function () {
+ if (!$('#composer').hasClass('hidden')) {
+ // TODO: Consider replacing this polling mechanism with a WS message (push)
pollTimer = window.setTimeout(pollFormData, 1000);
- } else {
- console.log('done polling');
- if (!$('#composer').hasClass('hidden') && data.target_form && data.target_form.name) {
- writeFormDataToComposer(data);
- }
}
},
- 'json'
- );
+ });
}
function writeFormDataToComposer(data) {
- if (data.target_form) {
- $('#msg_body').val(data.msg_body);
- if (data.msg_to) {
- $('#msg_to').tokenfield('setTokens', data.msg_to.split(/[ ;,]/).filter(Boolean));
- }
- if (data.msg_cc) {
- $('#msg_cc').tokenfield('setTokens', data.msg_cc.split(/[ ;,]/).filter(Boolean));
- }
- if (data.msg_subject) {
- // in case of composing a form-based reply we keep the 'Re: ...' subject line
- $('#msg_subject').val(data.msg_subject);
- }
+ $('#msg_body').val(data.msg_body);
+ if (data.msg_to) {
+ $('#msg_to').tokenfield('setTokens', data.msg_to.split(/[ ;,]/).filter(Boolean));
+ }
+ if (data.msg_cc) {
+ $('#msg_cc').tokenfield('setTokens', data.msg_cc.split(/[ ;,]/).filter(Boolean));
+ }
+ if (data.msg_subject) {
+ // in case of composing a form-based reply we keep the 'Re: ...' subject line
+ $('#msg_subject').val(data.msg_subject);
}
}