Posts should now be properly rendered after an inline edit.

Moved uploadAttachment and deleteAttachment from route/topic.go to route/attachments.go

Fixes #47
This commit is contained in:
Azareal 2018-12-28 12:08:35 +10:00
parent bf2af0ae96
commit 4813403fbb
4 changed files with 156 additions and 102 deletions

View File

@ -380,7 +380,7 @@ function mainInit(){
$(".topic_name").html(topicNameInput); $(".topic_name").html(topicNameInput);
$(".topic_name").attr(topicNameInput); $(".topic_name").attr(topicNameInput);
let topicContentInput = $('.topic_content_input').val(); let topicContentInput = $('.topic_content_input').val();
$(".topic_content").html(topicContentInput.replace("\n","<br><br>")); $(".topic_content").html(quickParse(topicContentInput));
let topicStatusInput = $('.topic_status_input').val(); let topicStatusInput = $('.topic_status_input').val();
$(".topic_status_e:not(.open_edit)").html(topicStatusInput); $(".topic_status_e:not(.open_edit)").html(topicStatusInput);
@ -408,6 +408,20 @@ function mainInit(){
$(this).closest('.deletable_block').remove(); $(this).closest('.deletable_block').remove();
}); });
// Miniature implementation of the parser to avoid sending as much data back and forth
function quickParse(msg) {
msg = msg.replace(":)", "😀")
msg = msg.replace(":(", "😞")
msg = msg.replace(":D", "😃")
msg = msg.replace(":P", "😛")
msg = msg.replace(":O", "😲")
msg = msg.replace(":p", "😛")
msg = msg.replace(":o", "😲")
msg = msg.replace(";)", "😉")
msg = msg.replace("\n","<br>")
return msg
}
$(".edit_item").click(function(event){ $(".edit_item").click(function(event){
event.preventDefault(); event.preventDefault();
let blockParent = this.closest('.editable_parent'); let blockParent = this.closest('.editable_parent');
@ -418,18 +432,26 @@ function mainInit(){
if(srcNode!=null) source = srcNode.innerText; if(srcNode!=null) source = srcNode.innerText;
else source = block.innerHTML; else source = block.innerHTML;
// TODO: Add a client template for this // TODO: Add a client template for this
block.innerHTML = "<textarea style='width: 99%;' name='edit_item'>" + source + "</textarea><br /><a href='" + this.closest('a').getAttribute("href") + "'><button class='submit_edit' type='submit'>Update</button></a>"; block.innerHTML = "<textarea style='width: 99%;' name='edit_item'>" + source + "</textarea><br><a href='" + this.closest('a').getAttribute("href") + "'><button class='submit_edit' type='submit'>Update</button></a>";
$(".submit_edit").click(function(event){ $(".submit_edit").click(function(event){
event.preventDefault(); event.preventDefault();
block.classList.remove("in_edit"); block.classList.remove("in_edit");
let newContent = block.querySelector('textarea').value; let newContent = block.querySelector('textarea').value;
block.innerHTML = newContent.replace("\n","<br><br>"); block.innerHTML = quickParse(newContent);
if(srcNode!=null) srcNode.innerText = newContent; if(srcNode!=null) srcNode.innerText = newContent;
let formAction = this.closest('a').getAttribute("href"); let formAction = this.closest('a').getAttribute("href");
// TODO: Bounce the parsed post back and set innerHTML to it? // TODO: Bounce the parsed post back and set innerHTML to it?
$.ajax({ url: formAction, type: "POST", error: ajaxError, dataType: "json", data: { isJs: "1", edit_item: newContent } $.ajax({
url: formAction,
type: "POST",
dataType: "json",
data: { js: "1", edit_item: newContent },
error: ajaxError,
success: (data,status,xhr) => {
if("Content" in data) block.innerHTML = data["Content"];
}
}); });
}); });
}); });

View File

@ -3,6 +3,7 @@ package routes
import ( import (
"database/sql" "database/sql"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -84,3 +85,75 @@ func ShowAttachment(w http.ResponseWriter, r *http.Request, user common.User, fi
http.ServeFile(w, r, "./attachs/"+filename) http.ServeFile(w, r, "./attachs/"+filename)
return nil return nil
} }
// TODO: Add a table for the files and lock the file row when performing tasks related to the file
func deleteAttachment(w http.ResponseWriter, r *http.Request, user common.User, aid int, js bool) common.RouteError {
attach, err := common.Attachments.Get(aid)
if err == sql.ErrNoRows {
return common.NotFoundJSQ(w, r, nil, js)
} else if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
err = common.Attachments.Delete(aid)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
count := common.Attachments.CountInPath(attach.Path)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
if count == 0 {
err := os.Remove("./attachs/" + attach.Path)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
}
return nil
}
// TODO: Stop duplicating this code
// TODO: Use a transaction here
// TODO: Move this function to neutral ground
func uploadAttachment(w http.ResponseWriter, r *http.Request, user common.User, sid int, sectionTable string, oid int, originTable string) (pathMap map[string]string, rerr common.RouteError) {
pathMap = make(map[string]string)
files, rerr := uploadFilesWithHash(w, r, user, "./attachs/")
if rerr != nil {
return nil, rerr
}
for _, filename := range files {
aid, err := common.Attachments.Add(sid, sectionTable, oid, originTable, user.ID, filename)
if err != nil {
return nil, common.InternalError(err, w, r)
}
_, ok := pathMap[filename]
if ok {
pathMap[filename] += "," + strconv.Itoa(aid)
} else {
pathMap[filename] = strconv.Itoa(aid)
}
switch originTable {
case "topics":
_, err = topicStmts.updateAttachs.Exec(common.Attachments.CountIn(originTable, oid), oid)
if err != nil {
return nil, common.InternalError(err, w, r)
}
err = common.Topics.Reload(oid)
if err != nil {
return nil, common.InternalError(err, w, r)
}
case "replies":
_, err = replyStmts.updateAttachs.Exec(common.Attachments.CountIn(originTable, oid), oid)
if err != nil {
return nil, common.InternalError(err, w, r)
}
}
}
return pathMap, nil
}

View File

@ -2,6 +2,7 @@ package routes
import ( import (
"database/sql" "database/sql"
"encoding/json"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@ -28,17 +29,24 @@ func init() {
}) })
} }
type JsonReply struct {
Content string
}
func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
// TODO: Use this
js := r.FormValue("js") == "1"
tid, err := strconv.Atoi(r.PostFormValue("tid")) tid, err := strconv.Atoi(r.PostFormValue("tid"))
if err != nil { if err != nil {
return common.PreError("Failed to convert the Topic ID", w, r) return common.PreErrorJSQ("Failed to convert the Topic ID", w, r, js)
} }
topic, err := common.Topics.Get(tid) topic, err := common.Topics.Get(tid)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return common.PreError("Couldn't find the parent topic", w, r) return common.PreErrorJSQ("Couldn't find the parent topic", w, r, js)
} else if err != nil { } else if err != nil {
return common.InternalError(err, w, r) return common.InternalErrorJSQ(err, w, r, js)
} }
// TODO: Add hooks to make use of headerLite // TODO: Add hooks to make use of headerLite
@ -47,22 +55,22 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
return ferr return ferr
} }
if !user.Perms.ViewTopic || !user.Perms.CreateReply { if !user.Perms.ViewTopic || !user.Perms.CreateReply {
return common.NoPermissions(w, r, user) return common.NoPermissionsJSQ(w, r, user, js)
} }
if topic.IsClosed && !user.Perms.CloseTopic { if topic.IsClosed && !user.Perms.CloseTopic {
return common.NoPermissions(w, r, user) return common.NoPermissionsJSQ(w, r, user, js)
} }
content := common.PreparseMessage(r.PostFormValue("reply-content")) content := common.PreparseMessage(r.PostFormValue("reply-content"))
// TODO: Fully parse the post and put that in the parsed column // TODO: Fully parse the post and put that in the parsed column
rid, err := common.Rstore.Create(topic, content, user.LastIP, user.ID) rid, err := common.Rstore.Create(topic, content, user.LastIP, user.ID)
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalErrorJSQ(err, w, r, js)
} }
reply, err := common.Rstore.Get(rid) reply, err := common.Rstore.Get(rid)
if err != nil { if err != nil {
return common.LocalError("Unable to load the reply", w, r, user) return common.LocalErrorJSQ("Unable to load the reply", w, r, user, js)
} }
// Handle the file attachments // Handle the file attachments
@ -84,13 +92,13 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
if strings.HasPrefix(key, "pollinputitem[") { if strings.HasPrefix(key, "pollinputitem[") {
halves := strings.Split(key, "[") halves := strings.Split(key, "[")
if len(halves) != 2 { if len(halves) != 2 {
return common.LocalError("Malformed pollinputitem", w, r, user) return common.LocalErrorJSQ("Malformed pollinputitem", w, r, user, js)
} }
halves[1] = strings.TrimSuffix(halves[1], "]") halves[1] = strings.TrimSuffix(halves[1], "]")
index, err := strconv.Atoi(halves[1]) index, err := strconv.Atoi(halves[1])
if err != nil { if err != nil {
return common.LocalError("Malformed pollinputitem", w, r, user) return common.LocalErrorJSQ("Malformed pollinputitem", w, r, user, js)
} }
// If there are duplicates, then something has gone horribly wrong, so let's ignore them, this'll likely happen during an attack // If there are duplicates, then something has gone horribly wrong, so let's ignore them, this'll likely happen during an attack
@ -115,25 +123,35 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
pollType := 0 // Basic single choice pollType := 0 // Basic single choice
_, err := common.Polls.Create(reply, pollType, seqPollInputItems) _, err := common.Polls.Create(reply, pollType, seqPollInputItems)
if err != nil { if err != nil {
return common.LocalError("Failed to add poll to reply", w, r, user) // TODO: Might need to be an internal error as it could leave phantom polls? return common.LocalErrorJSQ("Failed to add poll to reply", w, r, user, js) // TODO: Might need to be an internal error as it could leave phantom polls?
} }
} }
err = common.Forums.UpdateLastTopic(tid, user.ID, topic.ParentID) err = common.Forums.UpdateLastTopic(tid, user.ID, topic.ParentID)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r) return common.InternalErrorJSQ(err, w, r, js)
} }
common.AddActivityAndNotifyAll(user.ID, topic.CreatedBy, "reply", "topic", tid) common.AddActivityAndNotifyAll(user.ID, topic.CreatedBy, "reply", "topic", tid)
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalErrorJSQ(err, w, r, js)
} }
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
wcount := common.WordCount(content) wcount := common.WordCount(content)
err = user.IncreasePostStats(wcount, false) err = user.IncreasePostStats(wcount, false)
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalErrorJSQ(err, w, r, js)
}
if js {
outBytes, err := json.Marshal(JsonReply{common.ParseMessage(reply.Content, topic.ParentID, "forums")})
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
w.Write(outBytes)
} else {
// TODO: Send the user to the specific post on the page
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
} }
counters.PostCounter.Bump() counters.PostCounter.Bump()
@ -143,25 +161,25 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
// TODO: Disable stat updates in posts handled by plugin_guilds // TODO: Disable stat updates in posts handled by plugin_guilds
// TODO: Update the stats after edits so that we don't under or over decrement stats during deletes // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes
func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, srid string) common.RouteError { func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, srid string) common.RouteError {
isJs := (r.PostFormValue("js") == "1") js := (r.PostFormValue("js") == "1")
rid, err := strconv.Atoi(srid) rid, err := strconv.Atoi(srid)
if err != nil { if err != nil {
return common.PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, isJs) return common.PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, js)
} }
reply, err := common.Rstore.Get(rid) reply, err := common.Rstore.Get(rid)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return common.PreErrorJSQ("The target reply doesn't exist.", w, r, isJs) return common.PreErrorJSQ("The target reply doesn't exist.", w, r, js)
} else if err != nil { } else if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs) return common.InternalErrorJSQ(err, w, r, js)
} }
topic, err := reply.Topic() topic, err := reply.Topic()
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return common.PreErrorJSQ("The parent topic doesn't exist.", w, r, isJs) return common.PreErrorJSQ("The parent topic doesn't exist.", w, r, js)
} else if err != nil { } else if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs) return common.InternalErrorJSQ(err, w, r, js)
} }
// TODO: Add hooks to make use of headerLite // TODO: Add hooks to make use of headerLite
@ -170,24 +188,37 @@ func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, s
return ferr return ferr
} }
if !user.Perms.ViewTopic || !user.Perms.EditReply { if !user.Perms.ViewTopic || !user.Perms.EditReply {
return common.NoPermissionsJSQ(w, r, user, isJs) return common.NoPermissionsJSQ(w, r, user, js)
} }
if topic.IsClosed && !user.Perms.CloseTopic { if topic.IsClosed && !user.Perms.CloseTopic {
return common.NoPermissionsJSQ(w, r, user, isJs) return common.NoPermissionsJSQ(w, r, user, js)
} }
err = reply.SetPost(r.PostFormValue("edit_item")) err = reply.SetPost(r.PostFormValue("edit_item"))
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return common.PreErrorJSQ("The parent topic doesn't exist.", w, r, isJs) return common.PreErrorJSQ("The parent topic doesn't exist.", w, r, js)
} else if err != nil { } else if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs) return common.InternalErrorJSQ(err, w, r, js)
} }
if !isJs { // TODO: Avoid the load to get this faster?
reply, err = common.Rstore.Get(rid)
if err == sql.ErrNoRows {
return common.PreErrorJSQ("The updated reply doesn't exist.", w, r, js)
} else if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
if !js {
http.Redirect(w, r, "/topic/"+strconv.Itoa(topic.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther) http.Redirect(w, r, "/topic/"+strconv.Itoa(topic.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther)
} else { } else {
w.Write(successJSONBytes) outBytes, err := json.Marshal(JsonReply{common.ParseMessage(reply.Content, topic.ParentID, "forums")})
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
} }
w.Write(outBytes)
}
return nil return nil
} }

View File

@ -569,78 +569,6 @@ func uploadFilesWithHash(w http.ResponseWriter, r *http.Request, user common.Use
return filenames, nil return filenames, nil
} }
// TODO: Add a table for the files and lock the file row when performing tasks related to the file
func deleteAttachment(w http.ResponseWriter, r *http.Request, user common.User, aid int, js bool) common.RouteError {
attach, err := common.Attachments.Get(aid)
if err == sql.ErrNoRows {
return common.NotFoundJSQ(w, r, nil, js)
} else if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
err = common.Attachments.Delete(aid)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
count := common.Attachments.CountInPath(attach.Path)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
if count == 0 {
err := os.Remove("./attachs/" + attach.Path)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
}
return nil
}
// TODO: Stop duplicating this code
// TODO: Use a transaction here
// TODO: Move this function to neutral ground
func uploadAttachment(w http.ResponseWriter, r *http.Request, user common.User, sid int, sectionTable string, oid int, originTable string) (pathMap map[string]string, rerr common.RouteError) {
pathMap = make(map[string]string)
files, rerr := uploadFilesWithHash(w, r, user, "./attachs/")
if rerr != nil {
return nil, rerr
}
for _, filename := range files {
aid, err := common.Attachments.Add(sid, sectionTable, oid, originTable, user.ID, filename)
if err != nil {
return nil, common.InternalError(err, w, r)
}
_, ok := pathMap[filename]
if ok {
pathMap[filename] += "," + strconv.Itoa(aid)
} else {
pathMap[filename] = strconv.Itoa(aid)
}
switch originTable {
case "topics":
_, err = topicStmts.updateAttachs.Exec(common.Attachments.CountIn(originTable,oid), oid)
if err != nil {
return nil, common.InternalError(err, w, r)
}
err = common.Topics.Reload(oid)
if err != nil {
return nil, common.InternalError(err, w, r)
}
case "replies":
_, err = replyStmts.updateAttachs.Exec(common.Attachments.CountIn(originTable,oid), oid)
if err != nil {
return nil, common.InternalError(err, w, r)
}
}
}
return pathMap, nil
}
// TODO: Update the stats after edits so that we don't under or over decrement stats during deletes // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes
// TODO: Disable stat updates in posts handled by plugin_guilds // TODO: Disable stat updates in posts handled by plugin_guilds
func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError {