Browse Source

More input validation on message compose

This also adds a more user-friendly way of reporting errors in the web
gui composer, and some sane defaults for empty subject and message body.

In addition, the cli reader and web gui had some wrongful assumptions
that every message has at least one 'To' address. These are now fixed.

Issue #89
Martin Hebnes Pedersen 2 years ago
parent
commit
4eb10ff201
5 changed files with 81 additions and 47 deletions
  1. 6 4
      http.go
  2. 14 2
      main.go
  3. 4 1
      read.go
  4. 55 39
      res/js/index.js
  5. 2 1
      res/tmpl/index.html

+ 6 - 4
http.go

@@ -259,11 +259,13 @@ func postOutboundMessageHandler(w http.ResponseWriter, r *http.Request) {
 	if err := mbox.AddOut(msg); err != nil {
 		log.Println(err)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
-	} else {
-		var buf bytes.Buffer
-		msg.Write(&buf)
-		fmt.Fprintf(w, "Message posted (%.2f kB)", float64(buf.Len()/1024))
+		return
 	}
+
+	w.WriteHeader(http.StatusCreated)
+	var buf bytes.Buffer
+	msg.Write(&buf)
+	fmt.Fprintf(w, "Message posted (%.2f kB)", float64(buf.Len()/1024))
 }
 
 func wsHandler(w http.ResponseWriter, r *http.Request) {

+ 14 - 2
main.go

@@ -554,12 +554,16 @@ func composeMessage(replyMsg *fbb.Message) {
 		}
 	}
 
-	if len(msg.Receivers()) == 1 {
+	switch len(msg.Receivers()) {
+	case 1:
 		fmt.Print("P2P only [y/N]: ")
 		ans := readLine()
 		if strings.EqualFold("y", ans) {
 			msg.Header.Set("X-P2POnly", "true")
 		}
+	case 0:
+		fmt.Println("Message must have at least one recipient")
+		os.Exit(1)
 	}
 
 	fmt.Print(`Subject: `)
@@ -571,9 +575,12 @@ func composeMessage(replyMsg *fbb.Message) {
 	} else {
 		msg.SetSubject(readLine())
 	}
+	// A message without subject is not valid, so let's use a sane default
+	if msg.Subject() == "" {
+		msg.SetSubject("<No subject>")
+	}
 
 	// Read body
-
 	fmt.Printf(`Press ENTER to start composing the message body. `)
 	readLine()
 
@@ -616,6 +623,11 @@ func composeMessage(replyMsg *fbb.Message) {
 	f.Close()
 	os.Remove(f.Name())
 
+	// An empty message body is illegal. Let's set a sane default.
+	if msg.BodySize() == 0 {
+		msg.SetBody("<No message body>\n")
+	}
+
 	// END Read body
 
 	fmt.Print("\n")

+ 4 - 1
read.go

@@ -120,7 +120,10 @@ func printMailboxes(w io.Writer) {
 func printMessages(w io.Writer, msgs []*fbb.Message) {
 	rows := make([][]string, len(msgs))
 	for i, msg := range msgs {
-		to := msg.To()[0].Addr
+		var to string
+		if len(msg.To()) > 0 {
+			to = msg.To()[0].Addr
+		}
 		if len(msg.To()) > 1 {
 			to = to + ", ..."
 		}

+ 55 - 39
res/js/index.js

@@ -117,7 +117,7 @@ function updateProgress(p) {
 		op = p.receiving ? "Receiving" : "Sending";
 		text = op + " " + p.mid + " (" + p.bytes_total + " bytes)"
 		if( p.subject ){
-			text += " - " + p.subject
+			text += " - " + htmlEscape(p.subject)
 		}
 		$('#navbar_progress .progress-text').text(text);
 		$('#navbar_progress .progress-bar').css("width", percent + "%").text(percent + "%");
@@ -164,7 +164,41 @@ function initComposeModal() {
 	$('#msg_to').tokenfield(tokenfieldConfig);
 	$('#msg_cc').tokenfield(tokenfieldConfig);
 	$('#composer').on('change', '.btn-file :file', previewAttachmentFiles);
-	$('#post_btn').click(postMessage);
+	$('#composer_error').hide();
+
+
+	$('#composer_form').submit(function(e) {
+		var form = $('#composer_form');
+		var d = new Date().toJSON();
+		$("#msg_form_date").remove();
+		form.append('<input id="msg_form_date" type="hidden" name="date" value="' + d + '" />');
+
+		// Set some defaults that makes the message pass validation (as Winlink Express does)
+		if( $('#msg_body').val().length == 0 ){
+			$('#msg_body').val('<No message body>');
+		}
+		if( $('#msg_subject').val().length == 0 ){
+			$('#msg_subject').val('<No subject>');
+		}
+
+		$.ajax({
+			url: "/api/mailbox/out",
+			method: "POST",
+			data: new FormData(form[0]),
+			processData: false,
+			contentType: false,
+			success: function(result) {
+				$('#composer').modal('hide');
+				closeComposer(true);
+				alert(result);
+			},
+			error: function(error) {
+				$('#composer_error').html(error.responseText);
+				$('#composer_error').show();
+			},
+		});
+		e.preventDefault();
+	});
 }
 
 function initConnectModal() {
@@ -323,37 +357,6 @@ function postPosition() {
 	});
 }
 
-function postMessage() {
-	var iframe = $('<iframe name="postiframe" id="postiframe" style="display: none"></iframe>');
-
-	$("body").append(iframe);
-
-	var form = $('#composer_form');
-	form.attr("action", "/api/mailbox/out");
-	form.attr("method", "POST");
-
-	form.attr("encoding", "multipart/form-data");
-	form.attr("enctype", "multipart/form-data");
-
-	form.attr("target", "postiframe");
-
-	// Use client's date
-	var d = new Date().toJSON();
-	$("#msg_form_date").remove();
-	form.append('<input id="msg_form_date" type="hidden" name="date" value="' + d + '" />');
-
-	form.submit();
-
-	$("#postiframe").load(function () {
-		iframeContents = this.contentWindow.document.body.innerHTML;
-		$('#composer').modal('hide');
-		closeComposer(true);
-		alert(iframeContents);
-	});
-
-	return false;
-}
-
 function previewAttachmentFiles() {
 	var files = $(this).get(0).files;
 	attachments = $('#composer_attachments');
@@ -418,6 +421,7 @@ function updateStatus(data)
 function closeComposer(clear)
 {
 	if(clear){
+		$('#composer_error').val('').hide();
 		$('#msg_body').val('')
 		$('#msg_subject').val('')
 		$('#msg_to').tokenfield('setTokens', '')
@@ -567,11 +571,19 @@ function displayFolder(dir) {
 			if(msg.Files.length > 0){
 				html += '<span class="glyphicon glyphicon-paperclip" />';
 			}
-
-			html += '</td><td>' + msg.Subject + "</td>"
-			     + '<td>' + (is_from ? msg.From.Addr : msg.To[0].Addr) + (!is_from && msg.To.length > 1 ? "..." : "") + '</td>'
-				+ (is_from ? '' : '<td>' + (msg.P2POnly ? '<span class="glyphicon glyphicon-ok" />' : '') + '</td>')
-			    + '<td>' + msg.Date + '</td><td>' + msg.MID + '</td></tr>';
+			html += '</td><td>' + htmlEscape(msg.Subject) + "</td><td>";
+			if( !is_from && !msg.To ){
+				html += '';
+			} else if( is_from ) {
+				html += msg.From.Addr;
+			} else if( msg.To.length == 1 ){
+				html += msg.To[0].Addr;
+			} else if( msg.To.length > 1 ){
+				html += msg.To[0].Addr + "...";
+			}
+			html += '</td>'
+			html += (is_from ? '' : '<td>' + (msg.P2POnly ? '<span class="glyphicon glyphicon-ok" />' : '') + '</td>')
+			html += '<td>' + msg.Date + '</td><td>' + msg.MID + '</td></tr>';
 
 			var elem = $(html)
 			tbody.append(elem);
@@ -595,7 +607,7 @@ function displayMessage(elem) {
 		view.find('#headers').append('Date: ' + data.Date + '<br />');
 		view.find('#headers').append('From: ' + data.From.Addr + '<br />');
 		view.find('#headers').append('To: ');
-		for(var i = 0; i < data.To.length; i++){
+		for(var i = 0; data.To && i < data.To.length; i++){
 			view.find('#headers').append('<el>' + data.To[i].Addr + '</el>' + (data.To.length-1 > i ? ', ' : ''));
 		}
 		if(data.P2POnly){
@@ -712,6 +724,10 @@ function quoteMsg(data) {
 	return output
 }
 
+function htmlEscape(str) {
+	return $('<div/>').text(str).html();
+}
+
 function archiveMessage(box, mid) {
 	$.ajax("/api/mailbox/archive", {
 		headers: {

+ 2 - 1
res/tmpl/index.html

@@ -218,7 +218,7 @@
 
       <!-- Begin composer -->
       <div class="modal" id="composer" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
-      <form id="composer_form">
+      <form id="composer_form" enctype="multipart/form-data">
         <div class="modal-dialog">
           <div class="modal-content">
             <div class="modal-header primary">
@@ -236,6 +236,7 @@
                 <span class="input-group-addon">Subject</span>
                 <input type="text" id="msg_subject" name="subject" class="form-control" placeholder="Look ma!">
               </div>
+              <span id="composer_error" class="label label-danger pull-right">This is an error</span>
                 <div class="input-group input-group-sm compose-options">
                   <label><input type="checkbox" id="msg_p2p_only" name="p2ponly"> P2P Only</label>
                 </div>