From 97860d4f79d5d28a9c4135363aef1645266af567 Mon Sep 17 00:00:00 2001 From: Azareal Date: Thu, 12 Oct 2017 04:24:14 +0100 Subject: [PATCH] Began work on the new theme, Cosora. Added the ReplyStore and the ProfileReplyStore. Added more allowed file extensions for attachments. The tif, webp, and apng extensions are now recognised as images. Added the Delete method to the Reply struct. Added the Like method to the Reply struct. Refactored the topic list avatars to make things easier on Cosora. The attachment cap should now work properly on topics. You can now attach files to replies. The Markdown parser now ignores URLs rather than mangling them. Fixed a bug where themes weren't able to register custom resources. Added the ability to embed images. Added the ability to embed videos. Made the requirements for URLs looser. Misc improvements to the themes and templates. --- general_test.go | 3 + images/quick-topics.png | Bin 0 -> 55979 bytes main.go | 15 +- member_routes.go | 162 ++- misc_test.go | 52 + mod_routes.go | 21 +- pages.go | 112 +- plugin_markdown.go | 15 +- public/EQCSS.min.js | 37 + public/global.js | 4 +- reply.go | 90 +- routes_common.go | 24 +- template_forum.go | 57 +- template_forums.go | 7 +- template_list.go | 234 ++-- template_profile.go | 7 +- template_topic.go | 13 +- template_topic_alt.go | 13 +- template_topics.go | 63 +- templates/forum.html | 33 +- templates/menu.html | 4 +- templates/topic.html | 23 +- templates/topic_alt.html | 23 +- templates/topics.html | 35 +- themes/cosmo-classic/public/atombb-small.png | Bin 39837 -> 0 bytes themes/cosmo-classic/public/sample.css | 1 - themes/cosmo-classic/public/stars-mk1.png | Bin 138961 -> 0 bytes themes/cosmo-classic/theme.json | 16 - themes/cosmo-conflux/cosmo-conflux.png | Bin 397471 -> 0 bytes themes/cosmo-conflux/public/atombb-small.png | Bin 39837 -> 0 bytes themes/cosmo-conflux/public/main.css | 1031 ----------------- themes/cosmo-conflux/public/panel.css | 71 -- themes/cosmo-conflux/public/stars-mk1.png | Bin 138961 -> 0 bytes themes/cosmo-conflux/theme.json | 18 - themes/cosmo/cosmo.png | Bin 417798 -> 0 bytes themes/cosmo/public/atombb-small.png | Bin 39837 -> 0 bytes themes/cosmo/public/main.css | 1057 ------------------ themes/cosmo/public/panel.css | 71 -- themes/cosmo/public/stars-mk1.png | Bin 138961 -> 0 bytes themes/cosmo/theme.json | 17 - themes/cosora/public/main.css | 235 ++++ themes/cosora/theme.json | 14 +- themes/shadow/public/main.css | 42 +- themes/shadow/public/misc.js | 13 + themes/shadow/theme.json | 8 +- themes/tempra-conflux/public/main.css | 19 +- themes/tempra-conflux/public/misc.js | 13 + themes/tempra-conflux/theme.json | 6 + themes/tempra-cursive/public/main.css | 24 +- themes/tempra-simple/public/main.css | 66 +- themes/tempra-simple/public/misc.js | 13 + themes/tempra-simple/theme.json | 4 +- 52 files changed, 1151 insertions(+), 2635 deletions(-) create mode 100644 images/quick-topics.png create mode 100644 public/EQCSS.min.js delete mode 100644 themes/cosmo-classic/public/atombb-small.png delete mode 100644 themes/cosmo-classic/public/sample.css delete mode 100644 themes/cosmo-classic/public/stars-mk1.png delete mode 100644 themes/cosmo-classic/theme.json delete mode 100644 themes/cosmo-conflux/cosmo-conflux.png delete mode 100644 themes/cosmo-conflux/public/atombb-small.png delete mode 100644 themes/cosmo-conflux/public/main.css delete mode 100644 themes/cosmo-conflux/public/panel.css delete mode 100644 themes/cosmo-conflux/public/stars-mk1.png delete mode 100644 themes/cosmo-conflux/theme.json delete mode 100644 themes/cosmo/cosmo.png delete mode 100644 themes/cosmo/public/atombb-small.png delete mode 100644 themes/cosmo/public/main.css delete mode 100644 themes/cosmo/public/panel.css delete mode 100644 themes/cosmo/public/stars-mk1.png delete mode 100644 themes/cosmo/theme.json create mode 100644 themes/cosora/public/main.css create mode 100644 themes/shadow/public/misc.js create mode 100644 themes/tempra-conflux/public/misc.js create mode 100644 themes/tempra-simple/public/misc.js 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 0000000000000000000000000000000000000000..1f9e42e2e98c5bd4a73612e56056096a0b3064db GIT binary patch literal 55979 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 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(` -