diff --git a/general_test.go b/general_test.go index f8fbda1b..46a4ad92 100644 --- a/general_test.go +++ b/general_test.go @@ -39,6 +39,9 @@ func gloinit() error { return err } + rstore = NewSQLReplyStore() + prstore = NewSQLProfileReplyStore() + dbProd = db //db_test, err = sql.Open("testdb","") //if err != nil { diff --git a/images/quick-topics.png b/images/quick-topics.png new file mode 100644 index 00000000..1f9e42e2 Binary files /dev/null and b/images/quick-topics.png differ diff --git a/main.go b/main.go index aee61d2a..5f0e059f 100644 --- a/main.go +++ b/main.go @@ -44,12 +44,16 @@ type StringList []string // ? - Should we allow users to upload .php or .go files? It could cause security issues. We could store them with a mangled extension to render them inert // TODO: Let admins manage this from the Control Panel var allowedFileExts = StringList{ - "png", "jpg", "jpeg", "svg", "bmp", "gif", - "txt", "xml", "json", "yaml", "js", "py", "rb", - "mp3", "mp4", "avi", "wmv", + "png", "jpg", "jpeg", "svg", "bmp", "gif", "tif", "webp", "apng", // images + + "txt", "xml", "json", "yaml", "toml", "ini", "md", "html", "rtf", "js", "py", "rb", "css", "scss", "less", "java", "ts", "cs", "c", "cc", "cpp", "cxx", "C", "c++", "h", "hh", "hpp", "hxx", "h++", "rs", "rlib", "htaccess", "gitignore", // text + + "mp3", "mp4", "avi", "wmv", "webm", // video + + "otf", "woff2", "woff", "ttf", "eot", // fonts } var imageFileExts = StringList{ - "png", "jpg", "jpeg", "svg", "bmp", "gif", + "png", "jpg", "jpeg", "svg", "bmp", "gif", "tif", "webp", "apng", } // TODO: Write a test for this @@ -100,6 +104,9 @@ func main() { log.Fatal(err) } + rstore = NewSQLReplyStore() + prstore = NewSQLProfileReplyStore() + initTemplates() err = initPhrases() diff --git a/member_routes.go b/member_routes.go index ada8cbea..ea848574 100644 --- a/member_routes.go +++ b/member_routes.go @@ -170,17 +170,18 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { } // Handle the file attachments + // TODO: Stop duplicating this code if user.Perms.UploadFiles { - var mpartFiles = r.MultipartForm.File - if len(mpartFiles) > 5 { - LocalError("You can't attach more than five files", w, r, user) - return - } + files, ok := r.MultipartForm.File["upload_files"] + if ok { + if len(files) > 5 { + LocalError("You can't attach more than five files", w, r, user) + return + } - for _, fheaders := range r.MultipartForm.File { - for _, hdr := range fheaders { - log.Print("hdr.Filename ", hdr.Filename) - extarr := strings.Split(hdr.Filename, ".") + for _, file := range files { + log.Print("file.Filename ", file.Filename) + extarr := strings.Split(file.Filename, ".") if len(extarr) < 2 { LocalError("Bad file", w, r, user) return @@ -195,11 +196,11 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { } ext = strings.ToLower(reg.ReplaceAllString(ext, "")) if !allowedFileExts.Contains(ext) { - LocalError("You're not allowed this upload files with this extension", w, r, user) + LocalError("You're not allowed to upload files with this extension", w, r, user) return } - infile, err := hdr.Open() + infile, err := file.Open() if err != nil { LocalError("Upload failed", w, r, user) return @@ -223,7 +224,7 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { } defer outfile.Close() - infile, err = hdr.Open() + infile, err = file.Open() if err != nil { LocalError("Upload failed", w, r, user) return @@ -249,11 +250,20 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { } func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { - err := r.ParseForm() - if err != nil { - PreError("Bad Form", w, r) + // TODO: Reduce this to 1MB for attachments for each file? + if r.ContentLength > int64(config.MaxRequestSize) { + size, unit := convertByteUnit(float64(config.MaxRequestSize)) + CustomError("Your attachments are too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, user) return } + r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxRequestSize)) + + err := r.ParseMultipartForm(int64(megabyte)) + if err != nil { + LocalError("Unable to parse the form", w, r, user) + return + } + tid, err := strconv.Atoi(r.PostFormValue("tid")) if err != nil { PreError("Failed to convert the Topic ID", w, r) @@ -279,6 +289,83 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { return } + // Handle the file attachments + // TODO: Stop duplicating this code + if user.Perms.UploadFiles { + files, ok := r.MultipartForm.File["upload_files"] + if ok { + if len(files) > 5 { + LocalError("You can't attach more than five files", w, r, user) + return + } + + for _, file := range files { + log.Print("file.Filename ", file.Filename) + extarr := strings.Split(file.Filename, ".") + if len(extarr) < 2 { + LocalError("Bad file", w, r, user) + return + } + ext := extarr[len(extarr)-1] + + // TODO: Can we do this without a regex? + reg, err := regexp.Compile("[^A-Za-z0-9]+") + if err != nil { + LocalError("Bad file extension", w, r, user) + return + } + ext = strings.ToLower(reg.ReplaceAllString(ext, "")) + if !allowedFileExts.Contains(ext) { + LocalError("You're not allowed to upload files with this extension", w, r, user) + return + } + + infile, err := file.Open() + if err != nil { + LocalError("Upload failed", w, r, user) + return + } + defer infile.Close() + + hasher := sha256.New() + _, err = io.Copy(hasher, infile) + if err != nil { + LocalError("Upload failed [Hashing Failed]", w, r, user) + return + } + infile.Close() + + checksum := hex.EncodeToString(hasher.Sum(nil)) + filename := checksum + "." + ext + outfile, err := os.Create("." + "/attachs/" + filename) + if err != nil { + LocalError("Upload failed [File Creation Failed]", w, r, user) + return + } + defer outfile.Close() + + infile, err = file.Open() + if err != nil { + LocalError("Upload failed", w, r, user) + return + } + defer infile.Close() + + _, err = io.Copy(outfile, infile) + if err != nil { + LocalError("Upload failed [Copy Failed]", w, r, user) + return + } + + _, err = addAttachmentStmt.Exec(topic.ParentID, "forums", tid, "replies", user.ID, filename) + if err != nil { + InternalError(err, w) + return + } + } + } + } + content := preparseMessage(html.EscapeString(r.PostFormValue("reply-content"))) ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { @@ -286,25 +373,12 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { return } - wcount := wordCount(content) - _, err = createReplyStmt.Exec(tid, content, parseMessage(content, topic.ParentID, "forums"), ipaddress, wcount, user.ID) + _, err = rstore.Create(tid, content, ipaddress, topic.ParentID, user.ID) if err != nil { InternalError(err, w) return } - _, err = addRepliesToTopicStmt.Exec(1, user.ID, tid) - if err != nil { - InternalError(err, w) - return - } - - // Flush the topic out of the cache - tcache, ok := topics.(TopicCache) - if ok { - tcache.CacheRemove(tid) - } - err = fstore.UpdateLastTopic(tid, user.ID, topic.ParentID) if err != nil && err != ErrNoRows { InternalError(err, w) @@ -334,6 +408,8 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { } http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) + + wcount := wordCount(content) err = user.increasePostStats(wcount, false) if err != nil { InternalError(err, w) @@ -341,6 +417,7 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { } } +// TODO: Refactor this func routeLikeTopic(w http.ResponseWriter, r *http.Request, user User) { err := r.ParseForm() if err != nil { @@ -450,7 +527,7 @@ func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user User) { return } - reply, err := getReply(rid) + reply, err := rstore.Get(rid) if err == ErrNoRows { PreError("You can't like something which doesn't exist!", w, r) return @@ -484,15 +561,6 @@ func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user User) { return } - err = hasLikedReplyStmt.QueryRow(user.ID, rid).Scan(&rid) - if err != nil && err != ErrNoRows { - InternalError(err, w) - return - } else if err != ErrNoRows { - LocalError("You already liked this!", w, r, user) - return - } - _, err = users.Get(reply.CreatedBy) if err != nil && err != ErrNoRows { LocalError("The target user doesn't exist", w, r, user) @@ -502,15 +570,11 @@ func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user User) { return } - score := 1 - _, err = createLikeStmt.Exec(score, rid, "replies", user.ID) - if err != nil { - InternalError(err, w) + err = reply.Like(user.ID) + if err == ErrAlreadyLiked { + LocalError("You've already liked this!", w, r, user) return - } - - _, err = addLikesToReplyStmt.Exec(1, rid) - if err != nil { + } else if err != nil { InternalError(err, w) return } @@ -612,7 +676,7 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI var fid = 1 var title, content string if itemType == "reply" { - reply, err := getReply(itemID) + reply, err := rstore.Get(itemID) if err == ErrNoRows { LocalError("We were unable to find the reported post", w, r, user) return @@ -633,7 +697,7 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI title = "Reply: " + topic.Title content = reply.Content + "\n\nOriginal Post: #rid-" + strconv.Itoa(itemID) } else if itemType == "user-reply" { - userReply, err := getUserReply(itemID) + userReply, err := prstore.Get(itemID) if err == ErrNoRows { LocalError("We weren't able to find the reported post", w, r, user) return diff --git a/misc_test.go b/misc_test.go index 018c2dde..e142fe1c 100644 --- a/misc_test.go +++ b/misc_test.go @@ -377,6 +377,58 @@ func TestGroupStore(t *testing.T) { } } +func TestReplyStore(t *testing.T) { + if !gloinited { + gloinit() + } + if !pluginsInited { + initPlugins() + } + + reply, err := rstore.Get(-1) + if err == nil { + t.Error("RID #-1 shouldn't exist") + } + + reply, err = rstore.Get(0) + if err == nil { + t.Error("RID #0 shouldn't exist") + } + + reply, err = rstore.Get(1) + if err != nil { + t.Fatal(err) + } + if reply.ID != 1 { + t.Error("RID #1 has the wrong ID. It should be 1 not " + strconv.Itoa(reply.ID)) + } + if reply.ParentID != 1 { + t.Error("The parent topic of RID #1 should be 1 not " + strconv.Itoa(reply.ParentID)) + } + if reply.CreatedBy != 1 { + t.Error("The creator of RID #1 should be 1 not " + strconv.Itoa(reply.CreatedBy)) + } +} + +func TestProfileReplyStore(t *testing.T) { + if !gloinited { + gloinit() + } + if !pluginsInited { + initPlugins() + } + + _, err := prstore.Get(-1) + if err == nil { + t.Error("RID #-1 shouldn't exist") + } + + _, err = prstore.Get(0) + if err == nil { + t.Error("RID #0 shouldn't exist") + } +} + func TestSlugs(t *testing.T) { var res string var msgList []MEPair diff --git a/mod_routes.go b/mod_routes.go index b77c2d6b..7bc9c461 100644 --- a/mod_routes.go +++ b/mod_routes.go @@ -387,6 +387,7 @@ func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) { } } +// TODO: Refactor this // TODO: Disable stat updates in posts handled by plugin_socialgroups func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user User) { err := r.ParseForm() @@ -402,7 +403,7 @@ func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user User) { return } - reply, err := getReply(rid) + reply, err := rstore.Get(rid) if err == ErrNoRows { PreErrorJSQ("The reply you tried to delete doesn't exist.", w, r, isJs) return @@ -431,11 +432,12 @@ func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user User) { return } - _, err = deleteReplyStmt.Exec(rid) + err = reply.Delete() if err != nil { InternalErrorJSQ(err, w, r, isJs) return } + //log.Print("Reply #" + strconv.Itoa(rid) + " was deleted by User #" + strconv.Itoa(user.ID)) if !isJs { //http.Redirect(w,r, "/topic/" + strconv.Itoa(tid), http.StatusSeeOther) @@ -455,24 +457,15 @@ func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user User) { InternalErrorJSQ(err, w, r, isJs) return } - _, err = removeRepliesFromTopicStmt.Exec(1, reply.ParentID) - if err != nil { - InternalErrorJSQ(err, w, r, isJs) - } ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - LocalError("Bad IP", w, r, user) + LocalErrorJSQ("Bad IP", w, r, user, isJs) return } err = addModLog("delete", reply.ParentID, "reply", ipaddress, user.ID) if err != nil { - InternalError(err, w) - return - } - tcache, ok := topics.(TopicCache) - if ok { - tcache.CacheRemove(reply.ParentID) + InternalErrorJSQ(err, w, r, isJs) } } @@ -570,7 +563,7 @@ func routeIps(w http.ResponseWriter, r *http.Request, user User) { return } - ip := r.FormValue("ip") + var ip = r.FormValue("ip") var uid int var reqUserList = make(map[int]bool) diff --git a/pages.go b/pages.go index acd84cee..525a3987 100644 --- a/pages.go +++ b/pages.go @@ -611,13 +611,56 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/) continue } + //log.Print("Normal URL") outbytes = append(outbytes, msgbytes[lastItem:i]...) urlLen := partialURLBytesLen(msgbytes[i:]) if msgbytes[i+urlLen] > 32 { // space and invisibles + //log.Print("INVALID URL") + //log.Print("msgbytes[i+urlLen]", msgbytes[i+urlLen]) + //log.Print("string(msgbytes[i+urlLen])", string(msgbytes[i+urlLen])) + //log.Print("msgbytes[i:i+urlLen]", msgbytes[i:i+urlLen]) + //log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen])) outbytes = append(outbytes, invalidURL...) i += urlLen continue } + + media, ok := parseMediaBytes(msgbytes[i : i+urlLen]) + if !ok { + outbytes = append(outbytes, invalidURL...) + i += urlLen + continue + } + + if media.Type == "attach" { + outbytes = append(outbytes, imageOpen...) + outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"§ionType="+sectionType)...) + outbytes = append(outbytes, imageOpen2...) + outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"§ionType="+sectionType)...) + outbytes = append(outbytes, imageClose...) + i += urlLen + lastItem = i + continue + } else if media.Type == "image" { + outbytes = append(outbytes, imageOpen...) + outbytes = append(outbytes, []byte(media.URL)...) + outbytes = append(outbytes, imageOpen2...) + outbytes = append(outbytes, []byte(media.URL)...) + outbytes = append(outbytes, imageClose...) + i += urlLen + lastItem = i + continue + } else if media.Type == "raw" { + outbytes = append(outbytes, []byte(media.Body)...) + i += urlLen + lastItem = i + continue + } else if media.Type != "" { + outbytes = append(outbytes, unknownMedia...) + i += urlLen + continue + } + outbytes = append(outbytes, urlOpen...) outbytes = append(outbytes, msgbytes[i:i+urlLen]...) outbytes = append(outbytes, urlOpen2...) @@ -649,7 +692,7 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/) continue } - if media.Type == "image" { + if media.Type == "attach" { outbytes = append(outbytes, imageOpen...) outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"§ionType="+sectionType)...) outbytes = append(outbytes, imageOpen2...) @@ -658,6 +701,20 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/) i += urlLen lastItem = i continue + } else if media.Type == "image" { + outbytes = append(outbytes, imageOpen...) + outbytes = append(outbytes, []byte(media.URL)...) + outbytes = append(outbytes, imageOpen2...) + outbytes = append(outbytes, []byte(media.URL)...) + outbytes = append(outbytes, imageClose...) + i += urlLen + lastItem = i + continue + } else if media.Type == "raw" { + outbytes = append(outbytes, []byte(media.Body)...) + i += urlLen + lastItem = i + continue } else if media.Type != "" { outbytes = append(outbytes, unknownMedia...) i += urlLen @@ -730,9 +787,9 @@ func validateURLBytes(data []byte) bool { i = 2 } - // ? - There should only be one : and that's only if the URL is on a non-standard port + // ? - There should only be one : and that's only if the URL is on a non-standard port. Same for ?s. for ; datalen > i; i++ { - if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { + if data[i] != '\\' && data[i] != '_' && data[i] != ':' && data[i] != '?' && data[i] != '&' && data[i] != '=' && data[i] != ';' && data[i] != '@' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { return false } } @@ -756,9 +813,9 @@ func validatedURLBytes(data []byte) (url []byte) { i = 2 } - // ? - There should only be one : and that's only if the URL is on a non-standard port + // ? - There should only be one : and that's only if the URL is on a non-standard port. Same for ?s. for ; datalen > i; i++ { - if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { + if data[i] != '\\' && data[i] != '_' && data[i] != ':' && data[i] != '?' && data[i] != '&' && data[i] != '=' && data[i] != ';' && data[i] != '@' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { return invalidURL } } @@ -785,9 +842,9 @@ func partialURLBytes(data []byte) (url []byte) { i = 2 } - // ? - There should only be one : and that's only if the URL is on a non-standard port + // ? - There should only be one : and that's only if the URL is on a non-standard port. Same for ?s. for ; end >= i; i++ { - if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { + if data[i] != '\\' && data[i] != '_' && data[i] != ':' && data[i] != '?' && data[i] != '&' && data[i] != '=' && data[i] != ';' && data[i] != '@' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { end = i } } @@ -814,9 +871,9 @@ func partialURLBytesLen(data []byte) int { i = 2 } - // ? - There should only be one : and that's only if the URL is on a non-standard port + // ? - There should only be one : and that's only if the URL is on a non-standard port. Same for ?s. for ; datalen > i; i++ { - if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { + if data[i] != '\\' && data[i] != '_' && data[i] != ':' && data[i] != '?' && data[i] != '&' && data[i] != '=' && data[i] != ';' && data[i] != '@' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { //log.Print("Bad Character: ", data[i]) return i } @@ -828,6 +885,7 @@ func partialURLBytesLen(data []byte) int { type MediaEmbed struct { Type string //image URL string + Body string } // TODO: Write a test for this @@ -846,6 +904,8 @@ func parseMediaBytes(data []byte) (media MediaEmbed, ok bool) { port := url.Port() //log.Print("hostname ", hostname) //log.Print("scheme ", scheme) + query := url.Query() + //log.Printf("query %+v\n", query) var samesite = hostname == "localhost" || hostname == site.URL if samesite { @@ -870,15 +930,47 @@ func parseMediaBytes(data []byte) (media MediaEmbed, ok bool) { if len(pathFrags) >= 2 { if samesite && pathFrags[1] == "attachs" && (scheme == "http" || scheme == "https") { //log.Print("Attachment") - media.Type = "image" + media.Type = "attach" var sport string // ? - Assumes the sysadmin hasn't mixed up the two standard ports if port != "443" && port != "80" { sport = ":" + port } media.URL = scheme + "://" + hostname + sport + path + return media, true } } + + // ? - I don't think this hostname will hit every YT domain + // TODO: Make this a more customisable handler rather than hard-coding it in here + if hostname == "www.youtube.com" && path == "/watch" { + video, ok := query["v"] + if ok && len(video) >= 1 && video[0] != "" { + media.Type = "raw" + // TODO: Filter the URL to make sure no nasties end up in there + media.Body = "" + return media, true + } + } + + lastFrag := pathFrags[len(pathFrags)-1] + if lastFrag != "" { + // TODO: Write a function for getting the file extension of a string + extarr := strings.Split(lastFrag, ".") + if len(extarr) >= 2 { + ext := extarr[len(extarr)-1] + if imageFileExts.Contains(ext) { + media.Type = "image" + var sport string + if port != "443" && port != "80" { + sport = ":" + port + } + media.URL = scheme + "://" + hostname + sport + path + return media, true + } + } + } + return media, true } diff --git a/plugin_markdown.go b/plugin_markdown.go index 87028eb6..95603b09 100644 --- a/plugin_markdown.go +++ b/plugin_markdown.go @@ -1,8 +1,10 @@ package main //import "fmt" -import "regexp" -import "strings" +import ( + "regexp" + "strings" +) var markdownMaxDepth = 25 // How deep the parser will go when parsing Markdown strings var markdownUnclosedElement []byte @@ -87,6 +89,15 @@ func _markdownParse(msg string, n int) string { //log.Print(" ")*/ switch msg[index] { + // TODO: Do something slightly less hacky for skipping URLs + case '/': + if len(msg) > (index+2) && msg[index+1] == '/' { + for ; index < len(msg) && msg[index] != ' '; index++ { + + } + index-- + continue + } case '_': var startIndex = index if (index + 1) >= len(msg) { diff --git a/public/EQCSS.min.js b/public/EQCSS.min.js new file mode 100644 index 00000000..9082aa1c --- /dev/null +++ b/public/EQCSS.min.js @@ -0,0 +1,37 @@ +// EQCSS / Tommy Hodgins, Maxime EuziΓ¨re / MIT license +// version 1.7.0 +(function(root,factory){if(typeof define==="function"&&define.amd)define([],factory);else if(typeof module==="object"&&module.exports)module.exports=factory();else root.EQCSS=factory()})(this,function(){var EQCSS={data:[]};EQCSS.load=function(){var styles=document.getElementsByTagName("style");for(var i=0;i=final_value)){test=false;break test_conditions}}if(EQCSS.data[i].conditions[k].unit==="%"){element_width=parseInt(computed_style.getPropertyValue("width")); +parent_width=parseInt(parent_computed_style.getPropertyValue("width"));if(!(parent_width/element_width<=100/final_value)){test=false;break test_conditions}}break;case "max-width":if(recomputed===true||EQCSS.data[i].conditions[k].unit==="px"){element_width=parseInt(computed_style.getPropertyValue("width"));if(!(element_width<=final_value)){test=false;break test_conditions}}if(EQCSS.data[i].conditions[k].unit==="%"){element_width=parseInt(computed_style.getPropertyValue("width"));parent_width=parseInt(parent_computed_style.getPropertyValue("width")); +if(!(parent_width/element_width>=100/final_value)){test=false;break test_conditions}}break;case "min-height":if(recomputed===true||EQCSS.data[i].conditions[k].unit==="px"){element_height=parseInt(computed_style.getPropertyValue("height"));if(!(element_height>=final_value)){test=false;break test_conditions}}if(EQCSS.data[i].conditions[k].unit==="%"){element_height=parseInt(computed_style.getPropertyValue("height"));parent_height=parseInt(parent_computed_style.getPropertyValue("height"));if(!(parent_height/ +element_height<=100/final_value)){test=false;break test_conditions}}break;case "max-height":if(recomputed===true||EQCSS.data[i].conditions[k].unit==="px"){element_height=parseInt(computed_style.getPropertyValue("height"));if(!(element_height<=final_value)){test=false;break test_conditions}}if(EQCSS.data[i].conditions[k].unit==="%"){element_height=parseInt(computed_style.getPropertyValue("height"));parent_height=parseInt(parent_computed_style.getPropertyValue("height"));if(!(parent_height/element_height>= +100/final_value)){test=false;break test_conditions}}break;case "min-scroll-x":var element=elements[j];var element_scroll=element.scrollLeft;if(!element.hasScrollListener)if(element===document.documentElement||element===document.body)window.addEventListener("scroll",function(){EQCSS.throttle();element.hasScrollListener=true});else element.addEventListener("scroll",function(){EQCSS.throttle();element.hasScrollListener=true});if(recomputed===true||EQCSS.data[i].conditions[k].unit==="px"){if(!(element_scroll>= +final_value)){test=false;break test_conditions}}else if(EQCSS.data[i].conditions[k].unit==="%"){var element_scroll_size=elements[j].scrollWidth;var element_size;if(elements[j]===document.documentElement||elements[j]===document.body)element_size=window.innerWidth;else element_size=parseInt(computed_style.getPropertyValue("width"));if(!(element_scroll/(element_scroll_size-element_size)*100>=final_value)){test=false;break test_conditions}}break;case "min-scroll-y":var element=elements[j];element_scroll= +elements[j].scrollTop;if(!element.hasScrollListener)if(element===document.documentElement||element===document.body)window.addEventListener("scroll",function(){EQCSS.throttle();element.hasScrollListener=true});else element.addEventListener("scroll",function(){EQCSS.throttle();element.hasScrollListener=true});if(recomputed===true||EQCSS.data[i].conditions[k].unit==="px"){if(!(element_scroll>=final_value)){test=false;break test_conditions}}else if(EQCSS.data[i].conditions[k].unit==="%"){var element_scroll_size= +elements[j].scrollHeight;var element_size;if(elements[j]===document.documentElement||elements[j]===document.body)element_size=window.innerHeight;else element_size=parseInt(computed_style.getPropertyValue("height"));if(!(element_scroll/(element_scroll_size-element_size)*100>=final_value)){test=false;break test_conditions}}break;case "max-scroll-x":var element=elements[j];element_scroll=elements[j].scrollLeft;if(!element.hasScrollListener)if(element===document.documentElement||element===document.body)window.addEventListener("scroll", +function(){EQCSS.throttle();element.hasScrollListener=true});else element.addEventListener("scroll",function(){EQCSS.throttle();element.hasScrollListener=true});if(recomputed===true||EQCSS.data[i].conditions[k].unit==="px"){if(!(element_scroll<=final_value)){test=false;break test_conditions}}else if(EQCSS.data[i].conditions[k].unit==="%"){var element_scroll_size=elements[j].scrollWidth;var element_size;if(elements[j]===document.documentElement||elements[j]===document.body)element_size=window.innerWidth; +else element_size=parseInt(computed_style.getPropertyValue("width"));if(!(element_scroll/(element_scroll_size-element_size)*100<=final_value)){test=false;break test_conditions}}break;case "max-scroll-y":var element=elements[j];element_scroll=elements[j].scrollTop;if(!element.hasScrollListener)if(element===document.documentElement||element===document.body)window.addEventListener("scroll",function(){EQCSS.throttle();element.hasScrollListener=true});else element.addEventListener("scroll",function(){EQCSS.throttle(); +element.hasScrollListener=true});if(recomputed===true||EQCSS.data[i].conditions[k].unit==="px"){if(!(element_scroll<=final_value)){test=false;break test_conditions}}else if(EQCSS.data[i].conditions[k].unit==="%"){var element_scroll_size=elements[j].scrollHeight;var element_size;if(elements[j]===document.documentElement||elements[j]===document.body)element_size=window.innerHeight;else element_size=parseInt(computed_style.getPropertyValue("height"));if(!(element_scroll/(element_scroll_size-element_size)* +100<=final_value)){test=false;break test_conditions}}break;case "min-characters":if(elements[j].value){if(!(elements[j].value.length>=final_value)){test=false;break test_conditions}}else if(!(elements[j].textContent.length>=final_value)){test=false;break test_conditions}break;case "max-characters":if(elements[j].value){if(!(elements[j].value.length<=final_value)){test=false;break test_conditions}}else if(!(elements[j].textContent.length<=final_value)){test=false;break test_conditions}break;case "min-children":if(!(elements[j].children.length>= +final_value)){test=false;break test_conditions}break;case "max-children":if(!(elements[j].children.length<=final_value)){test=false;break test_conditions}break;case "min-lines":element_height=parseInt(computed_style.getPropertyValue("height"))-parseInt(computed_style.getPropertyValue("border-top-width"))-parseInt(computed_style.getPropertyValue("border-bottom-width"))-parseInt(computed_style.getPropertyValue("padding-top"))-parseInt(computed_style.getPropertyValue("padding-bottom"));element_line_height= +computed_style.getPropertyValue("line-height");if(element_line_height==="normal"){var element_font_size=parseInt(computed_style.getPropertyValue("font-size"));element_line_height=element_font_size*1.125}else element_line_height=parseInt(element_line_height);if(!(element_height/element_line_height>=final_value)){test=false;break test_conditions}break;case "max-lines":element_height=parseInt(computed_style.getPropertyValue("height"))-parseInt(computed_style.getPropertyValue("border-top-width"))-parseInt(computed_style.getPropertyValue("border-bottom-width"))- +parseInt(computed_style.getPropertyValue("padding-top"))-parseInt(computed_style.getPropertyValue("padding-bottom"));element_line_height=computed_style.getPropertyValue("line-height");if(element_line_height==="normal"){var element_font_size=parseInt(computed_style.getPropertyValue("font-size"));element_line_height=element_font_size*1.125}else element_line_height=parseInt(element_line_height);if(!(element_height/element_line_height+1<=final_value)){test=false;break test_conditions}break;case "orientation":if(EQCSS.data[i].conditions[k].value=== +"square")if(!(elements[j].offsetWidth===elements[j].offsetHeight)){test=false;break test_conditions}if(EQCSS.data[i].conditions[k].value==="portrait")if(!(elements[j].offsetWidth ('00' + b.toString(16)).slice(-2)).join('') }).then(function(hash) { console.log("hash",hash); - let content = document.getElementById("topic_content") + let content = document.getElementById("input_content") console.log("content.value",content.value); if(content.value == "") content.value = content.value + "//" + siteURL + "/attachs/" + hash + "." + ext; @@ -462,7 +462,7 @@ $(document).ready(function(){ } } - var uploadFiles = document.getElementById("quick_topic_upload_files"); + var uploadFiles = document.getElementById("upload_files"); if(uploadFiles != null) { uploadFiles.addEventListener("change", uploadFileHandler, false); } diff --git a/reply.go b/reply.go index 78f42293..d02543ad 100644 --- a/reply.go +++ b/reply.go @@ -6,7 +6,11 @@ */ package main +import "errors" + // ? - Should we add a reply store to centralise all the reply logic? Would this cover profile replies too or would that be seperate? +var rstore ReplyStore +var prstore ProfileReplyStore type ReplyUser struct { ID int @@ -50,18 +54,100 @@ type Reply struct { LikeCount int } +var ErrAlreadyLiked = errors.New("You already liked this!") + +// TODO: Write tests for this +// TODO: Wrap these queries in a transaction to make sure the state is consistent +func (reply *Reply) Like(uid int) (err error) { + var rid int // unused, just here to avoid mutating reply.ID + err = hasLikedReplyStmt.QueryRow(uid, reply.ID).Scan(&rid) + if err != nil && err != ErrNoRows { + return err + } else if err != ErrNoRows { + return ErrAlreadyLiked + } + + score := 1 + _, err = createLikeStmt.Exec(score, reply.ID, "replies", uid) + if err != nil { + return err + } + _, err = addLikesToReplyStmt.Exec(1, reply.ID) + return err +} + +// TODO: Write tests for this +func (reply *Reply) Delete() error { + _, err := deleteReplyStmt.Exec(reply.ID) + if err != nil { + return err + } + _, err = removeRepliesFromTopicStmt.Exec(1, reply.ParentID) + tcache, ok := topics.(TopicCache) + if ok { + tcache.CacheRemove(reply.ParentID) + } + return err +} + // Copy gives you a non-pointer concurrency safe copy of the reply func (reply *Reply) Copy() Reply { return *reply } -func getReply(id int) (*Reply, error) { +type ReplyStore interface { + Get(id int) (*Reply, error) + Create(tid int, content string, ipaddress string, fid int, uid int) (id int, err error) +} + +type SQLReplyStore struct { +} + +func NewSQLReplyStore() *SQLReplyStore { + return &SQLReplyStore{} +} + +func (store *SQLReplyStore) Get(id int) (*Reply, error) { reply := Reply{ID: id} err := getReplyStmt.QueryRow(id).Scan(&reply.ParentID, &reply.Content, &reply.CreatedBy, &reply.CreatedAt, &reply.LastEdit, &reply.LastEditBy, &reply.IPAddress, &reply.LikeCount) return &reply, err } -func getUserReply(id int) (*Reply, error) { +// TODO: Write a test for this +func (store *SQLReplyStore) Create(tid int, content string, ipaddress string, fid int, uid int) (id int, err error) { + wcount := wordCount(content) + res, err := createReplyStmt.Exec(tid, content, parseMessage(content, fid, "forums"), ipaddress, wcount, uid) + if err != nil { + return 0, err + } + lastID, err := res.LastInsertId() + if err != nil { + return 0, err + } + + _, err = addRepliesToTopicStmt.Exec(1, uid, tid) + if err != nil { + return int(lastID), err + } + tcache, ok := topics.(TopicCache) + if ok { + tcache.CacheRemove(tid) + } + return int(lastID), err +} + +type ProfileReplyStore interface { + Get(id int) (*Reply, error) +} + +type SQLProfileReplyStore struct { +} + +func NewSQLProfileReplyStore() *SQLProfileReplyStore { + return &SQLProfileReplyStore{} +} + +func (store *SQLProfileReplyStore) Get(id int) (*Reply, error) { reply := Reply{ID: id} err := getUserReplyStmt.QueryRow(id).Scan(&reply.ParentID, &reply.Content, &reply.CreatedBy, &reply.CreatedAt, &reply.LastEdit, &reply.LastEditBy, &reply.IPAddress) return &reply, err diff --git a/routes_common.go b/routes_common.go index 210d541d..a1194337 100644 --- a/routes_common.go +++ b/routes_common.go @@ -169,17 +169,15 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerV } headerVars.Stylesheets = append(headerVars.Stylesheets, headerVars.ThemeName+"/panel.css") - if len(themes[headerVars.ThemeName].Resources) != 0 { + if len(themes[headerVars.ThemeName].Resources) > 0 { rlist := themes[headerVars.ThemeName].Resources for _, resource := range rlist { if resource.Location == "global" || resource.Location == "panel" { - halves := strings.Split(resource.Name, ".") - if len(halves) != 2 { - continue - } - if halves[1] == "css" { + extarr := strings.Split(resource.Name, ".") + ext := extarr[len(extarr)-1] + if ext == "css" { headerVars.Stylesheets = append(headerVars.Stylesheets, resource.Name) - } else if halves[1] == "js" { + } else if ext == "js" { headerVars.Scripts = append(headerVars.Scripts, resource.Name) } } @@ -268,17 +266,15 @@ func userCheck(w http.ResponseWriter, r *http.Request, user *User) (headerVars * headerVars.NoticeList = append(headerVars.NoticeList, "Your account has been suspended. Some of your permissions may have been revoked.") } - if len(themes[headerVars.ThemeName].Resources) != 0 { + if len(themes[headerVars.ThemeName].Resources) > 0 { rlist := themes[headerVars.ThemeName].Resources for _, resource := range rlist { if resource.Location == "global" || resource.Location == "frontend" { - halves := strings.Split(resource.Name, ".") - if len(halves) != 2 { - continue - } - if halves[1] == "css" { + extarr := strings.Split(resource.Name, ".") + ext := extarr[len(extarr)-1] + if ext == "css" { headerVars.Stylesheets = append(headerVars.Stylesheets, resource.Name) - } else if halves[1] == "js" { + } else if ext == "js" { headerVars.Scripts = append(headerVars.Scripts, resource.Name) } } diff --git a/template_forum.go b/template_forum.go index 2b945bf5..beb36495 100644 --- a/template_forum.go +++ b/template_forum.go @@ -56,12 +56,13 @@ if tmpl_forum_vars.CurrentUser.Loggedin { w.Write(menu_3) w.Write([]byte(tmpl_forum_vars.CurrentUser.Link)) w.Write(menu_4) -w.Write([]byte(tmpl_forum_vars.CurrentUser.Session)) w.Write(menu_5) -} else { +w.Write([]byte(tmpl_forum_vars.CurrentUser.Session)) w.Write(menu_6) -} +} else { w.Write(menu_7) +} +w.Write(menu_8) w.Write(header_14) if tmpl_forum_vars.Header.Widgets.RightSidebar != "" { w.Write(header_15) @@ -137,48 +138,54 @@ w.Write([]byte(item.Creator.Avatar)) w.Write(forum_27) } w.Write(forum_28) -w.Write([]byte(strconv.Itoa(item.PostCount))) -w.Write(forum_29) -w.Write([]byte(item.LastReplyAt)) -w.Write(forum_30) w.Write([]byte(item.Link)) -w.Write(forum_31) +w.Write(forum_29) w.Write([]byte(item.Title)) -w.Write(forum_32) +w.Write(forum_30) w.Write([]byte(item.Creator.Link)) -w.Write(forum_33) +w.Write(forum_31) w.Write([]byte(item.Creator.Name)) -w.Write(forum_34) +w.Write(forum_32) if item.IsClosed { -w.Write(forum_35) +w.Write(forum_33) } if item.Sticky { +w.Write(forum_34) +} +w.Write(forum_35) +w.Write([]byte(strconv.Itoa(item.PostCount))) w.Write(forum_36) -} +if item.Sticky { w.Write(forum_37) -if item.LastUser.Avatar != "" { +} else { +if item.IsClosed { w.Write(forum_38) -w.Write([]byte(item.LastUser.Avatar)) -w.Write(forum_39) } +} +w.Write(forum_39) +if item.LastUser.Avatar != "" { w.Write(forum_40) -w.Write([]byte(item.LastUser.Link)) +w.Write([]byte(item.LastUser.Avatar)) w.Write(forum_41) -w.Write([]byte(item.LastUser.Name)) +} w.Write(forum_42) -w.Write([]byte(item.LastReplyAt)) +w.Write([]byte(item.LastUser.Link)) w.Write(forum_43) +w.Write([]byte(item.LastUser.Name)) +w.Write(forum_44) +w.Write([]byte(item.LastReplyAt)) +w.Write(forum_45) } } else { -w.Write(forum_44) -if tmpl_forum_vars.CurrentUser.Perms.CreateTopic { -w.Write(forum_45) -w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) w.Write(forum_46) -} +if tmpl_forum_vars.CurrentUser.Perms.CreateTopic { w.Write(forum_47) -} +w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) w.Write(forum_48) +} +w.Write(forum_49) +} +w.Write(forum_50) w.Write(footer_0) if len(tmpl_forum_vars.Header.Themes) != 0 { for _, item := range tmpl_forum_vars.Header.Themes { diff --git a/template_forums.go b/template_forums.go index 62bb9835..323a2f24 100644 --- a/template_forums.go +++ b/template_forums.go @@ -55,12 +55,13 @@ if tmpl_forums_vars.CurrentUser.Loggedin { w.Write(menu_3) w.Write([]byte(tmpl_forums_vars.CurrentUser.Link)) w.Write(menu_4) -w.Write([]byte(tmpl_forums_vars.CurrentUser.Session)) w.Write(menu_5) -} else { +w.Write([]byte(tmpl_forums_vars.CurrentUser.Session)) w.Write(menu_6) -} +} else { w.Write(menu_7) +} +w.Write(menu_8) w.Write(header_14) if tmpl_forums_vars.Header.Widgets.RightSidebar != "" { w.Write(header_15) diff --git a/template_list.go b/template_list.go index b72b08b9..851d260b 100644 --- a/template_list.go +++ b/template_list.go @@ -56,16 +56,17 @@ var menu_2 = []byte(` var menu_3 = []byte(` - +var menu_4 = []byte(`">Profile`) +var menu_5 = []byte(` + +var menu_6 = []byte(`">Logout `) -var menu_6 = []byte(` +var menu_7 = []byte(` `) -var menu_7 = []byte(` +var menu_8 = []byte(` @@ -233,20 +234,29 @@ var topic_93 = []byte(` `) var topic_94 = []byte(` -
-
- -
-
+
+
+
-
-
+
+
+
+ + `) +var topic_96 = []byte(` + + +
`) +var topic_97 = []byte(`
- +
`) -var topic_96 = []byte(` +var topic_98 = []byte(` @@ -428,20 +438,29 @@ var topic_alt_86 = []byte(` var topic_alt_87 = []byte(`
`) var topic_alt_88 = []byte(` -
-
- -
-
+
+
+
-
-
+
+
+
+ + `) +var topic_alt_90 = []byte(` + + +
`) +var topic_alt_91 = []byte(`
- +
`) -var topic_alt_90 = []byte(` +var topic_alt_92 = []byte(` @@ -675,7 +694,7 @@ var topics_6 = []byte(`
`) var topics_7 = []byte(` -