diff --git a/README.md b/README.md index 9ed8982c..6db13ad3 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Set the password column of your user account in the database to what you want yo # Run the program -go run errors.go main.go pages.go post.go routes.go topic.go user.go utils.go config.go +go run errors.go main.go pages.go reply.go routes.go topic.go user.go utils.go config.go Alternatively, you could run the run.bat batch file on Windows. @@ -47,6 +47,10 @@ More moderation features. Fix the bug where errors are sent off in raw HTML rather than formatted HTML. +Fix the custom pages. + +Add emails as a requirement for registration and add a simple anti-spam measure. + Add an alert system. Add a report feature. diff --git a/ren.PNG b/ren.PNG new file mode 100644 index 00000000..46a8602b Binary files /dev/null and b/ren.PNG differ diff --git a/src/config.go b/src/config.go index f26a4cde..73962c8c 100644 --- a/src/config.go +++ b/src/config.go @@ -6,3 +6,6 @@ var dbuser = "root" var dbpassword = "password" var dbname = "grosolo" var dbport = "3306" // You probably won't need to change this + +// Limiters +var max_request_size = 5 * megabyte \ No newline at end of file diff --git a/src/data.sql b/src/data.sql index 357325d1..f31c9673 100644 --- a/src/data.sql +++ b/src/data.sql @@ -10,6 +10,7 @@ CREATE TABLE `users`( `createdAt` datetime not null, `lastActiveAt` datetime not null, `session` varchar(200) DEFAULT '', + `avatar` varchar(20) DEFAULT '', primary key(`uid`) ); diff --git a/src/main.go b/src/main.go index a90c2950..1c201b09 100644 --- a/src/main.go +++ b/src/main.go @@ -13,6 +13,8 @@ const hour int = 60 * 60 const day int = hour * 24 const month int = day * 30 const year int = day * 365 +const kilobyte int = 1024 +const megabyte int = 1024 * 1024 const saltLength int = 32 const sessionLength int = 80 var db *sql.DB @@ -27,6 +29,7 @@ var update_session_stmt *sql.Stmt var logout_stmt *sql.Stmt var set_password_stmt *sql.Stmt var get_password_stmt *sql.Stmt +var set_avatar_stmt *sql.Stmt var register_stmt *sql.Stmt var username_exists_stmt *sql.Stmt var custom_pages map[string]string = make(map[string]string) @@ -48,7 +51,7 @@ func init_database(err error) { } log.Print("Preparing get_session statement.") - get_session_stmt, err = db.Prepare("SELECT `uid`, `name`, `group`, `is_super_admin`, `session` FROM `users` WHERE `uid` = ? AND `session` = ? AND `session` <> ''") + get_session_stmt, err = db.Prepare("SELECT `uid`, `name`, `group`, `is_super_admin`, `session`, `avatar` FROM `users` WHERE `uid` = ? AND `session` = ? AND `session` <> ''") if err != nil { log.Fatal(err) } @@ -113,6 +116,12 @@ func init_database(err error) { log.Fatal(err) } + log.Print("Preparing set_avatar statement.") + set_avatar_stmt, err = db.Prepare("UPDATE users SET avatar = ? WHERE uid = ?") + if err != nil { + log.Fatal(err) + } + // Add an admin version of register_stmt with more flexibility // create_account_stmt, err = db.Prepare("INSERT INTO @@ -122,7 +131,7 @@ func init_database(err error) { log.Fatal(err) } - log.Print("Preparing get_session statement.") + log.Print("Preparing username_exists statement.") username_exists_stmt, err = db.Prepare("SELECT `name` FROM `users` WHERE `name` = ?") if err != nil { log.Fatal(err) @@ -140,8 +149,10 @@ func main(){ } // In a directory to stop it clashing with the other paths - fs := http.FileServer(http.Dir("./public")) - http.Handle("/static/", http.StripPrefix("/static/",fs)) + fs_p := http.FileServer(http.Dir("./public")) + http.Handle("/static/", http.StripPrefix("/static/",fs_p)) + fs_u := http.FileServer(http.Dir("./uploads")) + http.Handle("/uploads/", http.StripPrefix("/uploads/",fs_u)) http.HandleFunc("/overview/", route_overview) http.HandleFunc("/topics/create/", route_topic_create) @@ -170,6 +181,8 @@ func main(){ //http.HandleFunc("/user/edit/", route_logout) http.HandleFunc("/user/edit/critical/", route_account_own_edit_critical) // Password & Email http.HandleFunc("/user/edit/critical/submit/", route_account_own_edit_critical_submit) + http.HandleFunc("/user/edit/avatar/", route_account_own_edit_avatar) // Password & Email + http.HandleFunc("/user/edit/avatar/submit/", route_account_own_edit_avatar_submit) //http.HandleFunc("/user/:id/edit/", route_logout) //http.HandleFunc("/user/:id/ban/", route_logout) http.HandleFunc("/", route_topics) diff --git a/src/public/main.css b/src/public/main.css index 63c8abd7..0f0d5605 100644 --- a/src/public/main.css +++ b/src/public/main.css @@ -59,6 +59,31 @@ li:not(:last-child) display: none; } +.colblock_left +{ + border: 1px solid #ccc; + padding: 0px; + padding-top: 0px; + width: 30%; + float: left; +} +.colblock_right +{ + border: 1px solid #ccc; + padding: 0px; + padding-top: 0px; + width: 65%; + overflow: hidden; +} +.colblock_left:empty +{ + display: none; +} +.colblock_right:empty +{ + display: none; +} + .rowitem { width: 99%; @@ -69,29 +94,59 @@ li:not(:last-child) font-weight: bold; text-transform: uppercase; } - .rowitem.passive { font-weight: normal; text-transform: none; } - .rowitem:not(:last-child)/*:not(:only-child)*/ { border-bottom: 1px dotted #ccc; } - .rowitem a { text-decoration: none; color: black; } - .rowitem a:hover { color: silver; } +.col_left +{ + width: 30%; + float: left; +} +.col_right +{ + width: 69%; + overflow: hidden; +} +.colitem +{ + padding-left: 8px; + padding-right: 8px; + padding-top: 17px; + padding-bottom: 12px; + font-weight: bold; + text-transform: uppercase; +} +.colitem.passive +{ + font-weight: normal; + text-transform: none; +} +.colitem a +{ + text-decoration: none; + color: black; +} +.colitem a:hover +{ + color: silver; +} + .formrow { /*height: 40px;*/ diff --git a/src/post.go b/src/reply.go similarity index 83% rename from src/post.go rename to src/reply.go index a3969508..b6fac089 100644 --- a/src/post.go +++ b/src/reply.go @@ -10,5 +10,6 @@ type Reply struct CreatedAt string LastEdit int LastEditBy int + Avatar string + HasAvatar bool } - diff --git a/src/routes.go b/src/routes.go index 9b2d2b89..93b46896 100644 --- a/src/routes.go +++ b/src/routes.go @@ -4,7 +4,11 @@ import "log" import "fmt" import "strconv" import "bytes" +import "regexp" +import "strings" import "time" +import "io" +import "os" import "net/http" import "html" import "database/sql" @@ -110,6 +114,8 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){ replyCreatedAt string replyLastEdit int replyLastEditBy int + replyAvatar string + replyHasAvatar bool is_closed bool sticky bool @@ -160,7 +166,7 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){ // Get the replies.. //rows, err := db.Query("select rid, content, createdBy, createdAt from replies where tid = ?", tid) - rows, err := db.Query("select replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.name from replies left join users ON replies.createdBy = users.uid where tid = ?", tid) + rows, err := db.Query("select replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name from replies left join users ON replies.createdBy = users.uid where tid = ?", tid) if err != nil { InternalError(err,w,r,user) return @@ -168,12 +174,22 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){ defer rows.Close() for rows.Next() { - err := rows.Scan(&rid, &replyContent, &replyCreatedBy, &replyCreatedAt, &replyLastEdit, &replyLastEditBy, &replyCreatedByName) + err := rows.Scan(&rid, &replyContent, &replyCreatedBy, &replyCreatedAt, &replyLastEdit, &replyLastEditBy, &replyAvatar, &replyCreatedByName) if err != nil { InternalError(err,w,r,user) return } - replyList[currentID] = Reply{rid,tid,replyContent,replyCreatedBy,replyCreatedByName,replyCreatedAt,replyLastEdit,replyLastEditBy} + + if replyAvatar != "" { + replyHasAvatar = true + if replyAvatar[0] == '.' { + replyAvatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + replyAvatar + } + } else { + replyHasAvatar = false + } + + replyList[currentID] = Reply{rid,tid,replyContent,replyCreatedBy,replyCreatedByName,replyCreatedAt,replyLastEdit,replyLastEditBy,replyAvatar,replyHasAvatar} currentID++ } err = rows.Err() @@ -515,7 +531,115 @@ func route_account_own_edit_critical_submit(w http.ResponseWriter, r *http.Reque pi := Page{"Edit Password","account-own-edit-success",user,tList,0} templates.ExecuteTemplate(w,"account-own-edit-success.html", pi) } + +func route_account_own_edit_avatar(w http.ResponseWriter, r *http.Request) { + user := SessionCheck(w,r) + if !user.Loggedin { + errmsg := "You need to login to edit your own account." + pi := Page{"Error","error",user,tList,errmsg} + + var b bytes.Buffer + templates.ExecuteTemplate(&b,"error.html", pi) + errpage := b.String() + http.Error(w,errpage,500) + return + } + pi := Page{"Edit Avatar","account-own-edit-avatar",user,tList,0} + templates.ExecuteTemplate(w,"account-own-edit-avatar.html", pi) +} + +func route_account_own_edit_avatar_submit(w http.ResponseWriter, r *http.Request) { + if r.ContentLength > int64(max_request_size) { + http.Error(w, "request too large", http.StatusExpectationFailed) + return + } + r.Body = http.MaxBytesReader(w, r.Body, int64(max_request_size)) + + user := SessionCheck(w,r) + if !user.Loggedin { + errmsg := "You need to login to edit your own account." + pi := Page{"Error","error",user,tList,errmsg} + + var b bytes.Buffer + templates.ExecuteTemplate(&b,"error.html", pi) + errpage := b.String() + http.Error(w,errpage,500) + return + } + + err := r.ParseMultipartForm(int64(max_request_size)) + if err != nil { + LocalError("Upload failed", w, r, user) + return + } + + var filename string = "" + var ext string + for _, fheaders := range r.MultipartForm.File { + for _, hdr := range fheaders { + infile, err := hdr.Open(); + if err != nil { + LocalError("Upload failed", w, r, user) + return + } + defer infile.Close() + + // We don't want multiple files + if filename != "" { + if filename != hdr.Filename { + os.Remove("./uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext) + LocalError("You may only upload one avatar", w, r, user) + return + } + } else { + filename = hdr.Filename + } + + if ext == "" { + extarr := strings.Split(hdr.Filename,".") + if len(extarr) < 2 { + LocalError("Bad file", w, r, user) + return + } + ext = extarr[len(extarr) - 1] + + reg, err := regexp.Compile("[^A-Za-z0-9]+") + if err != nil { + LocalError("Bad file extension", w, r, user) + return + } + ext = reg.ReplaceAllString(ext,"") + ext = strings.ToLower(ext) + } + + outfile, err := os.Create("./uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext); + if err != nil { + LocalError("Upload failed [File Creation Failed]", w, r, user) + return + } + defer outfile.Close() + + _, err = io.Copy(outfile, infile); + if err != nil { + LocalError("Upload failed [Copy Failed]", w, r, user) + return + } + } + } + + _, err = set_avatar_stmt.Exec("." + ext, strconv.Itoa(user.ID)) + if err != nil { + InternalError(err,w,r,user) + return + } + + user.HasAvatar = true + user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext + + pi := Page{"Edit Avatar","account-own-edit-avatar-success",user,tList,0} + templates.ExecuteTemplate(w,"account-own-edit-avatar-success.html", pi) +} func route_logout(w http.ResponseWriter, r *http.Request) { user := SessionCheck(w,r) if !user.Loggedin { diff --git a/src/run.bat b/src/run.bat index 2ad8939a..675bbf78 100644 --- a/src/run.bat +++ b/src/run.bat @@ -1,2 +1,2 @@ -go run errors.go main.go pages.go post.go routes.go topic.go user.go utils.go config.go +go run errors.go main.go pages.go reply.go routes.go topic.go user.go utils.go config.go pause \ No newline at end of file diff --git a/src/templates/account-own-edit-avatar-success.html b/src/templates/account-own-edit-avatar-success.html new file mode 100644 index 00000000..23ed7217 --- /dev/null +++ b/src/templates/account-own-edit-avatar-success.html @@ -0,0 +1,30 @@ +{{template "header.html" . }} +