diff --git a/README.md b/README.md index 7b671ad9..9d17c2a8 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ Discord Server: https://discord.gg/eyYvtTf # Features -Basic Forum Functionality +Basic Forum Functionality. All of the little things you would expect of any forum software. E.g. Moderation, Custom Themes, Avatars, and so on. -Custom Pages. Under development. +Custom Pages. Under development. Mainly the Control Panel portion to come, but you can create them by hand today. -Emojis +Emojis. Allow your users to express themselves without resorting to serving tons upon tons of image files. -In-memory static file, forum and group caches. +In-memory static file, forum and group caches. We're pondering over extending this solution over to topics, users, etc. to some extent. A profile system including profile comments and moderation tools for the profile owner. @@ -22,7 +22,7 @@ A template engine which compiles templates down into machine code. Over ten time A plugin system. Under development. -A responsive design. Looks good on mobile phones, tablets, laptops, desktops and more! +A responsive design. Looks great on mobile phones, tablets, laptops, desktops and more! # Dependencies @@ -38,19 +38,15 @@ Instructions on how to do so on Linux: https://downloads.mariadb.org/mariadb/rep **Run the following commands:** -go get github.com/go-sql-driver/mysql +go get -u github.com/go-sql-driver/mysql -go install github.com/go-sql-driver/mysql - -go get golang.org/x/crypto/bcrypt - -go install golang.org/x/crypto/bcrypt +go get -u golang.org/x/crypto/bcrypt Tweak the config.go file and put your database details in there. Import data.sql into the same database. Comment out the first line (put /* and */ around it), if you've already made a database, and don't want the script to generate it for you. Set the password column of your user account in the database to what you want your password to be. The system will encrypt your password when you login for the first time. -Add -u after go get to update those libraries, if you've already got them installed. +You can run these commands again at any time to update these dependencies to their latest versions. # Run the program @@ -67,7 +63,7 @@ go build Open up cmd.exe -cd to the directory / folder the code is in. E.g. cd /Users/Blah/Documents/gosora +cd to the directory / folder the code is in. E.g. `cd /Users/Blah/Documents/gosora` go build @@ -101,9 +97,11 @@ We're looking for ways to clean-up the plugin system so that all of them (except Oh my, you caught me right at the start of this project. There's nothing to see here yet, asides from the absolute basics. You might want to look again later! -More moderation features. +The various little features which somehow got stuck in the net. Don't worry, I'll get to them! -Add a simple anti-spam measure. +More moderation features. E.g. Move, Approval Queue (Posts made by users in certain usergroups will need to be approved by a moderator before they're publically visible), etc. + +Add a simple anti-spam measure. I have quite a few ideas in mind, but it'll take a while to implement the more advanced ones, so I'd like to put off some of those to a later date and focus on the basics. E.g. CAPTCHAs, hidden fields, etc. Add an alert system. @@ -111,12 +109,18 @@ Add per-forum permissions to finish up the foundations of the permissions system Add a *better* plugin system. E.g. Allow for plugins written in Javascript and ones written in Go. Also, we need to add many, many, many more plugin hooks. -Implement a faster router. +I will need to ponder over implementing an even faster router. We don't need one immediately, although it would be nice if we could get one in the near future. It really depends. Ideally, it would be one which can easily integrate with the current structure without much work, although I'm not beyond making some alterations to faciliate it, assuming that we don't get too tightly bound to that specific router. + +Allow themes to define their own templates. Add a friend system. Add more administration features. -Add more features for improving user engagement. +Add more features for improving user engagement. I have quite a few of these in mind, but I'm mostly occupied with implementing the essentials right now. Add a widget system. + +Add support for multi-factor authentication. + +Add support for secondary emails for users. diff --git a/config.go b/config.go index d82fbf06..40c8760f 100644 --- a/config.go +++ b/config.go @@ -12,16 +12,23 @@ var max_request_size = 5 * megabyte // Misc var default_route = route_topics -var default_group = 3 -var activation_group = 5 +var default_group = 3 // Should be a setting +var activation_group = 5 // Should be a setting var staff_css = " background-color: #ffeaff;" var uncategorised_forum_visible = true var enable_emails = false -var site_email = "" +var site_name = "Test Install" // Should be a setting +var site_email = "" // Should be a setting var smtp_server = "" -var siteurl = "localhost:8080" -var noavatar = "https://api.adorable.io/avatars/285/{id}@" + siteurl + ".png" -var items_per_page = 50 +//var noavatar = "https://api.adorable.io/avatars/{width}/{id}@{site_url}.png" +var noavatar = "https://api.adorable.io/avatars/285/{id}@" + site_url + ".png" +var items_per_page = 40 // Should be a setting + +var site_url = "localhost:8080" +var server_port = "8080" +var enable_ssl = false +var ssl_privkey = "" +var ssl_fullchain = "" // Developer flag -var debug = false \ No newline at end of file +var debug = false diff --git a/data.sql b/data.sql index a5b8a076..1439f4f4 100644 --- a/data.sql +++ b/data.sql @@ -32,6 +32,13 @@ CREATE TABLE `users_groups`( primary key(`gid`) ) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; +CREATE TABLE `emails`( + `email` varchar(200) not null, + `uid` int not null, + `validated` tinyint DEFAULT 0 not null, + `token` varchar(200) DEFAULT '' not null +); + CREATE TABLE `forums`( `fid` int not null AUTO_INCREMENT, `name` varchar(100) not null, @@ -111,6 +118,7 @@ INSERT INTO themes(`uname`,`default`) VALUES ('tempra-simple',1); INSERT INTO users(`name`,`email`,`group`,`is_super_admin`,`createdAt`,`lastActiveAt`,`message`) VALUES ('Admin','admin@localhost',1,1,NOW(),NOW(),''); +INSERT INTO emails(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1); /* The Permissions: diff --git a/errors.go b/errors.go index 281351d2..354d29af 100644 --- a/errors.go +++ b/errors.go @@ -140,15 +140,24 @@ func NotFound(w http.ResponseWriter, r *http.Request, user User) { fmt.Fprintln(w,errpage) } +func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User) { + pi := Page{errtitle,"error",user,nList,tList,errmsg} + var b bytes.Buffer + templates.ExecuteTemplate(&b,"error.html", pi) + errpage := b.String() + w.WriteHeader(errcode) + fmt.Fprintln(w,errpage) +} + func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User, is_js string) { if is_js == "0" { pi := Page{errtitle,"error",user,nList,tList,errmsg} var b bytes.Buffer templates.ExecuteTemplate(&b,"error.html", pi) errpage := b.String() - w.WriteHeader(500) + w.WriteHeader(errcode) fmt.Fprintln(w,errpage) } else { - http.Error(w,"{'errmsg': '" + errmsg + "'}",500) + http.Error(w,"{'errmsg': '" + errmsg + "'}",errcode) } } diff --git a/experimental/config.json b/experimental/config.json new file mode 100644 index 00000000..4a09ee4d --- /dev/null +++ b/experimental/config.json @@ -0,0 +1,23 @@ +{ + "dbhost": "127.0.0.1", + "dbuser": "root", + "dbpassword": "password", + "dbname": "gosora", + "dbport": "3306", + + "default_group": 3, + "activation_group": 5, + "staff_css": " background-color: #ffeaff;", + "uncategorised_forum_visible": true, + "enable_emails": false, + "smtp_server": "", + "items_per_page": 40, + + "site_url": "localhost:8080", + "server_port": "8080", + "enable_ssl": false, + "ssl_privkey": "", + "ssl_fullchain": "", + + "debug": false +} \ No newline at end of file diff --git a/general_test.go b/general_test.go index d0485e3a..3b4d1598 100644 --- a/general_test.go +++ b/general_test.go @@ -1,5 +1,4 @@ package main -//import "fmt" import "log" import "bytes" import "math/rand" @@ -8,6 +7,7 @@ import "net/http" import "net/http/httptest" import "io/ioutil" import "html/template" +//import "github.com/husobee/vestigo" func BenchmarkTopicTemplate(b *testing.B) { b.ReportAllocs() @@ -107,7 +107,7 @@ func BenchmarkRoute(b *testing.B) { b.ReportAllocs() admin_uid_cookie := http.Cookie{Name: "uid",Value: "1",Path: "/",MaxAge: year} - // TO-DO: Stop hard-coding this value + // TO-DO: Stop hard-coding this value. Seriously. admin_session_cookie := http.Cookie{Name: "session",Value: "TKBh5Z-qEQhWDBnV6_XVmOhKAowMYPhHeRlrQjjbNc0QRrRiglvWOYFDc1AaMXQIywvEsyA2AOBRYUrZ5kvnGhThY1GhOW6FSJADnRWm_bI=",Path: "/",MaxAge: year} topic_w := httptest.NewRecorder() @@ -231,7 +231,7 @@ func addEmptyRoutesToMux(routes []string, serveMux *http.ServeMux) { } } -func BenchmarkRouter(b *testing.B) { +func BenchmarkDefaultGoRouter(b *testing.B) { w := httptest.NewRecorder() req := httptest.NewRequest("get","/topics/",bytes.NewReader(nil)) routes := make([]string, 0) @@ -388,5 +388,332 @@ func BenchmarkRouter(b *testing.B) { }) } +/*func addEmptyRoutesToVestigo(routes []string, router *vestigo.Router) { + for _, route := range routes { + router.HandleFunc(route, func(_ http.ResponseWriter,_ *http.Request){}) + } +} + +func BenchmarkVestigoRouter(b *testing.B) { + w := httptest.NewRecorder() + req := httptest.NewRequest("get","/topics/",bytes.NewReader(nil)) + routes := make([]string, 0) + + routes = append(routes,"/test/") + router := vestigo.NewRouter() + router.HandleFunc("/test/", func(_ http.ResponseWriter,_ *http.Request){}) + b.Run("one-route", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + routes = append(routes,"/topic/") + routes = append(routes,"/forums/") + routes = append(routes,"/forum/") + routes = append(routes,"/panel/") + router = vestigo.NewRouter() + addEmptyRoutesToVestigo(routes, router) + b.Run("five-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = vestigo.NewRouter() + routes = append(routes,"/panel/plugins/") + routes = append(routes,"/panel/groups/") + routes = append(routes,"/panel/settings/") + routes = append(routes,"/panel/users/") + routes = append(routes,"/panel/forums/") + addEmptyRoutesToVestigo(routes, router) + b.Run("ten-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = vestigo.NewRouter() + routes = append(routes,"/panel/forums/create/submit/") + routes = append(routes,"/panel/forums/delete/") + routes = append(routes,"/users/ban/") + routes = append(routes,"/panel/users/edit/") + routes = append(routes,"/panel/forums/create/") + routes = append(routes,"/users/unban/") + routes = append(routes,"/pages/") + routes = append(routes,"/users/activate/") + routes = append(routes,"/panel/forums/edit/submit/") + routes = append(routes,"/panel/plugins/activate/") + addEmptyRoutesToVestigo(routes, router) + b.Run("twenty-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = vestigo.NewRouter() + routes = append(routes,"/panel/plugins/deactivate/") + routes = append(routes,"/panel/plugins/install/") + routes = append(routes,"/panel/plugins/uninstall/") + routes = append(routes,"/panel/templates/") + routes = append(routes,"/panel/templates/edit/") + routes = append(routes,"/panel/templates/create/") + routes = append(routes,"/panel/templates/delete/") + routes = append(routes,"/panel/templates/edit/submit/") + routes = append(routes,"/panel/themes/") + routes = append(routes,"/panel/themes/edit/") + addEmptyRoutesToVestigo(routes, router) + b.Run("thirty-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = vestigo.NewRouter() + routes = append(routes,"/panel/themes/create/") + routes = append(routes,"/panel/themes/delete/") + routes = append(routes,"/panel/themes/delete/submit/") + routes = append(routes,"/panel/templates/create/submit/") + routes = append(routes,"/panel/templates/delete/submit/") + routes = append(routes,"/panel/widgets/") + routes = append(routes,"/panel/widgets/edit/") + routes = append(routes,"/panel/widgets/activate/") + routes = append(routes,"/panel/widgets/deactivate/") + routes = append(routes,"/panel/magical/wombat/path") + addEmptyRoutesToVestigo(routes, router) + b.Run("forty-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = vestigo.NewRouter() + routes = append(routes,"/report/") + routes = append(routes,"/report/submit/") + routes = append(routes,"/topic/create/submit/") + routes = append(routes,"/topics/create/") + routes = append(routes,"/overview/") + routes = append(routes,"/uploads/") + routes = append(routes,"/static/") + routes = append(routes,"/reply/edit/submit/") + routes = append(routes,"/reply/delete/submit/") + routes = append(routes,"/topic/edit/submit/") + addEmptyRoutesToVestigo(routes, router) + b.Run("fifty-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = vestigo.NewRouter() + routes = append(routes,"/topic/delete/submit/") + routes = append(routes,"/topic/stick/submit/") + routes = append(routes,"/topic/unstick/submit/") + routes = append(routes,"/accounts/login/") + routes = append(routes,"/accounts/create/") + routes = append(routes,"/accounts/logout/") + routes = append(routes,"/accounts/login/submit/") + routes = append(routes,"/accounts/create/submit/") + routes = append(routes,"/user/edit/critical/") + routes = append(routes,"/user/edit/critical/submit/") + addEmptyRoutesToVestigo(routes, router) + b.Run("sixty-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = vestigo.NewRouter() + routes = append(routes,"/user/edit/avatar/") + routes = append(routes,"/user/edit/avatar/submit/") + routes = append(routes,"/user/edit/username/") + routes = append(routes,"/user/edit/username/submit/") + routes = append(routes,"/profile/reply/create/") + routes = append(routes,"/profile/reply/edit/submit/") + routes = append(routes,"/profile/reply/delete/submit/") + routes = append(routes,"/arcane/tower/") + routes = append(routes,"/magical/kingdom/") + routes = append(routes,"/insert/name/here/") + addEmptyRoutesToVestigo(routes, router) + b.Run("seventy-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) +}*/ + +func addEmptyRoutesToCustom(routes []string, router *Router) { + for _, route := range routes { + router.HandleFunc(route, func(_ http.ResponseWriter,_ *http.Request){}) + } +} + +func BenchmarkCustomRouter(b *testing.B) { + w := httptest.NewRecorder() + req := httptest.NewRequest("get","/topics/",bytes.NewReader(nil)) + routes := make([]string, 0) + + routes = append(routes,"/test/") + router := NewRouter() + router.HandleFunc("/test/", func(_ http.ResponseWriter,_ *http.Request){}) + b.Run("one-route", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + routes = append(routes,"/topic/") + routes = append(routes,"/forums/") + routes = append(routes,"/forum/") + routes = append(routes,"/panel/") + router = NewRouter() + addEmptyRoutesToCustom(routes, router) + b.Run("five-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = NewRouter() + routes = append(routes,"/panel/plugins/") + routes = append(routes,"/panel/groups/") + routes = append(routes,"/panel/settings/") + routes = append(routes,"/panel/users/") + routes = append(routes,"/panel/forums/") + addEmptyRoutesToCustom(routes, router) + b.Run("ten-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = NewRouter() + routes = append(routes,"/panel/forums/create/submit/") + routes = append(routes,"/panel/forums/delete/") + routes = append(routes,"/users/ban/") + routes = append(routes,"/panel/users/edit/") + routes = append(routes,"/panel/forums/create/") + routes = append(routes,"/users/unban/") + routes = append(routes,"/pages/") + routes = append(routes,"/users/activate/") + routes = append(routes,"/panel/forums/edit/submit/") + routes = append(routes,"/panel/plugins/activate/") + addEmptyRoutesToCustom(routes, router) + b.Run("twenty-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = NewRouter() + routes = append(routes,"/panel/plugins/deactivate/") + routes = append(routes,"/panel/plugins/install/") + routes = append(routes,"/panel/plugins/uninstall/") + routes = append(routes,"/panel/templates/") + routes = append(routes,"/panel/templates/edit/") + routes = append(routes,"/panel/templates/create/") + routes = append(routes,"/panel/templates/delete/") + routes = append(routes,"/panel/templates/edit/submit/") + routes = append(routes,"/panel/themes/") + routes = append(routes,"/panel/themes/edit/") + addEmptyRoutesToCustom(routes, router) + b.Run("thirty-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = NewRouter() + routes = append(routes,"/panel/themes/create/") + routes = append(routes,"/panel/themes/delete/") + routes = append(routes,"/panel/themes/delete/submit/") + routes = append(routes,"/panel/templates/create/submit/") + routes = append(routes,"/panel/templates/delete/submit/") + routes = append(routes,"/panel/widgets/") + routes = append(routes,"/panel/widgets/edit/") + routes = append(routes,"/panel/widgets/activate/") + routes = append(routes,"/panel/widgets/deactivate/") + routes = append(routes,"/panel/magical/wombat/path") + addEmptyRoutesToCustom(routes, router) + b.Run("forty-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = NewRouter() + routes = append(routes,"/report/") + routes = append(routes,"/report/submit/") + routes = append(routes,"/topic/create/submit/") + routes = append(routes,"/topics/create/") + routes = append(routes,"/overview/") + routes = append(routes,"/uploads/") + routes = append(routes,"/static/") + routes = append(routes,"/reply/edit/submit/") + routes = append(routes,"/reply/delete/submit/") + routes = append(routes,"/topic/edit/submit/") + addEmptyRoutesToCustom(routes, router) + b.Run("fifty-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = NewRouter() + routes = append(routes,"/topic/delete/submit/") + routes = append(routes,"/topic/stick/submit/") + routes = append(routes,"/topic/unstick/submit/") + routes = append(routes,"/accounts/login/") + routes = append(routes,"/accounts/create/") + routes = append(routes,"/accounts/logout/") + routes = append(routes,"/accounts/login/submit/") + routes = append(routes,"/accounts/create/submit/") + routes = append(routes,"/user/edit/critical/") + routes = append(routes,"/user/edit/critical/submit/") + addEmptyRoutesToCustom(routes, router) + b.Run("sixty-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) + + router = NewRouter() + routes = append(routes,"/user/edit/avatar/") + routes = append(routes,"/user/edit/avatar/submit/") + routes = append(routes,"/user/edit/username/") + routes = append(routes,"/user/edit/username/submit/") + routes = append(routes,"/profile/reply/create/") + routes = append(routes,"/profile/reply/edit/submit/") + routes = append(routes,"/profile/reply/delete/submit/") + routes = append(routes,"/arcane/tower/") + routes = append(routes,"/magical/kingdom/") + routes = append(routes,"/insert/name/here/") + addEmptyRoutesToCustom(routes, router) + b.Run("seventy-routes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil)) + router.ServeHTTP(w,req) + } + }) +} + /*func TestRoute(t *testing.T) { + }*/ \ No newline at end of file diff --git a/gosora.exe b/gosora.exe index 1860abde..94aadcae 100644 Binary files a/gosora.exe and b/gosora.exe differ diff --git a/images/account-email-manager.PNG b/images/account-email-manager.PNG new file mode 100644 index 00000000..5d009c96 Binary files /dev/null and b/images/account-email-manager.PNG differ diff --git a/images/bench_vestigo_router.PNG b/images/bench_vestigo_router.PNG new file mode 100644 index 00000000..875d0537 Binary files /dev/null and b/images/bench_vestigo_router.PNG differ diff --git a/main.go b/main.go index 1dc6cc73..7825017d 100644 --- a/main.go +++ b/main.go @@ -138,7 +138,7 @@ func main(){ init_plugins() // In a directory to stop it clashing with the other paths - http.HandleFunc("/static/", route_static) + /*http.HandleFunc("/static/", route_static) fs_u := http.FileServer(http.Dir("./uploads")) http.Handle("/uploads/", http.StripPrefix("/uploads/",fs_u)) @@ -180,6 +180,7 @@ func main(){ http.HandleFunc("/user/edit/avatar/submit/", route_account_own_edit_avatar_submit) http.HandleFunc("/user/edit/username/", route_account_own_edit_username) http.HandleFunc("/user/edit/username/submit/", route_account_own_edit_username_submit) + http.HandleFunc("/user/edit/email/token/", route_account_own_edit_email_token_submit) http.HandleFunc("/user/", route_profile) http.HandleFunc("/profile/reply/create/", route_profile_reply_create) http.HandleFunc("/profile/reply/edit/submit/", route_profile_reply_edit_submit) @@ -210,8 +211,96 @@ func main(){ http.HandleFunc("/panel/users/edit/submit/", route_panel_users_edit_submit) http.HandleFunc("/panel/groups/", route_panel_groups) - http.HandleFunc("/", default_route) + http.HandleFunc("/", default_route)*/ + + router := NewRouter() + router.HandleFunc("/static/", route_static) + + fs_u := http.FileServer(http.Dir("./uploads")) + router.Handle("/uploads/", http.StripPrefix("/uploads/",fs_u)) + + router.HandleFunc("/overview/", route_overview) + router.HandleFunc("/topics/create/", route_topic_create) + router.HandleFunc("/topics/", route_topics) + router.HandleFunc("/forums/", route_forums) + router.HandleFunc("/forum/", route_forum) + router.HandleFunc("/topic/create/submit/", route_create_topic) + router.HandleFunc("/topic/", route_topic_id) + router.HandleFunc("/reply/create/", route_create_reply) + //router.HandleFunc("/reply/edit/", route_reply_edit) + //router.HandleFunc("/reply/delete/", route_reply_delete) + router.HandleFunc("/reply/edit/submit/", route_reply_edit_submit) + router.HandleFunc("/reply/delete/submit/", route_reply_delete_submit) + router.HandleFunc("/report/submit/", route_report_submit) + router.HandleFunc("/topic/edit/submit/", route_edit_topic) + router.HandleFunc("/topic/delete/submit/", route_delete_topic) + router.HandleFunc("/topic/stick/submit/", route_stick_topic) + router.HandleFunc("/topic/unstick/submit/", route_unstick_topic) + + // Custom Pages + router.HandleFunc("/pages/", route_custom_page) + + // Accounts + router.HandleFunc("/accounts/login/", route_login) + router.HandleFunc("/accounts/create/", route_register) + router.HandleFunc("/accounts/logout/", route_logout) + router.HandleFunc("/accounts/login/submit/", route_login_submit) + router.HandleFunc("/accounts/create/submit/", route_register_submit) + + //router.HandleFunc("/accounts/list/", route_login) // Redirect /accounts/ and /user/ to here.. // Get a list of all of the accounts on the forum + //router.HandleFunc("/accounts/create/full/", route_logout) // Advanced account creator for admins? + //router.HandleFunc("/user/edit/", route_logout) + router.HandleFunc("/user/edit/critical/", route_account_own_edit_critical) // Password & Email + router.HandleFunc("/user/edit/critical/submit/", route_account_own_edit_critical_submit) + router.HandleFunc("/user/edit/avatar/", route_account_own_edit_avatar) + router.HandleFunc("/user/edit/avatar/submit/", route_account_own_edit_avatar_submit) + router.HandleFunc("/user/edit/username/", route_account_own_edit_username) + router.HandleFunc("/user/edit/username/submit/", route_account_own_edit_username_submit) + router.HandleFunc("/user/edit/email/", route_account_own_edit_email) + router.HandleFunc("/user/edit/email/token/", route_account_own_edit_email_token_submit) + router.HandleFunc("/user/", route_profile) + router.HandleFunc("/profile/reply/create/", route_profile_reply_create) + router.HandleFunc("/profile/reply/edit/submit/", route_profile_reply_edit_submit) + router.HandleFunc("/profile/reply/delete/submit/", route_profile_reply_delete_submit) + //router.HandleFunc("/user/edit/submit/", route_logout) + router.HandleFunc("/users/ban/", route_ban) + router.HandleFunc("/users/ban/submit/", route_ban_submit) + router.HandleFunc("/users/unban/", route_unban) + router.HandleFunc("/users/activate/", route_activate) + + // Admin + router.HandleFunc("/panel/", route_panel) + router.HandleFunc("/panel/forums/", route_panel_forums) + router.HandleFunc("/panel/forums/create/", route_panel_forums_create_submit) + router.HandleFunc("/panel/forums/delete/", route_panel_forums_delete) + router.HandleFunc("/panel/forums/delete/submit/", route_panel_forums_delete_submit) + router.HandleFunc("/panel/forums/edit/submit/", route_panel_forums_edit_submit) + router.HandleFunc("/panel/settings/", route_panel_settings) + router.HandleFunc("/panel/settings/edit/", route_panel_setting) + router.HandleFunc("/panel/settings/edit/submit/", route_panel_setting_edit) + router.HandleFunc("/panel/themes/", route_panel_themes) + router.HandleFunc("/panel/themes/default/", route_panel_themes_default) + router.HandleFunc("/panel/plugins/", route_panel_plugins) + router.HandleFunc("/panel/plugins/activate/", route_panel_plugins_activate) + router.HandleFunc("/panel/plugins/deactivate/", route_panel_plugins_deactivate) + router.HandleFunc("/panel/users/", route_panel_users) + router.HandleFunc("/panel/users/edit/", route_panel_users_edit) + router.HandleFunc("/panel/users/edit/submit/", route_panel_users_edit_submit) + router.HandleFunc("/panel/groups/", route_panel_groups) + + router.HandleFunc("/", default_route) defer db.Close() - http.ListenAndServe(":8080", nil) + if !enable_ssl { + if server_port == "" { + server_port = "80" + } + //http.ListenAndServe(":" + server_port, nil) + http.ListenAndServe(":" + server_port, router) + } else { + if server_port == "" { + server_port = "443" + } + http.ListenAndServeTLS(":" + server_port, ssl_fullchain, ssl_privkey, router) + } } \ No newline at end of file diff --git a/mod_routes.go b/mod_routes.go index 34ed7ddf..d52b011d 100644 --- a/mod_routes.go +++ b/mod_routes.go @@ -488,7 +488,6 @@ func route_activate(w http.ResponseWriter, r *http.Request) { NoPermissions(w,r,user) return } - if r.FormValue("session") != user.Session { SecurityError(w,r,user) return @@ -515,7 +514,6 @@ func route_activate(w http.ResponseWriter, r *http.Request) { LocalError("The account you're trying to activate has already been activated.",w,r,user) return } - _, err = activate_user_stmt.Exec(uid) if err != nil { InternalError(err,w,r,user) diff --git a/mysql.go b/mysql.go index dede2a45..e9337b69 100644 --- a/mysql.go +++ b/mysql.go @@ -27,6 +27,9 @@ var set_password_stmt *sql.Stmt var get_password_stmt *sql.Stmt var set_avatar_stmt *sql.Stmt var set_username_stmt *sql.Stmt +var add_email_stmt *sql.Stmt +var update_email_stmt *sql.Stmt +var verify_email_stmt *sql.Stmt var register_stmt *sql.Stmt var username_exists_stmt *sql.Stmt var change_group_stmt *sql.Stmt @@ -61,7 +64,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`, `avatar`, `message`, `url_prefix`, `url_name` FROM `users` WHERE `uid` = ? AND `session` = ? AND `session` <> ''") + get_session_stmt, err = db.Prepare("select `uid`, `name`, `group`, `is_super_admin`, `session`, `email`, `avatar`, `message`, `url_prefix`, `url_name` FROM `users` WHERE `uid` = ? AND `session` = ? AND `session` <> ''") if err != nil { log.Fatal(err) } @@ -195,6 +198,24 @@ func init_database(err error) { log.Fatal(err) } + log.Print("Preparing add_email statement.") + add_email_stmt, err = db.Prepare("INSERT INTO emails(`email`,`uid`,`validated`,`token`) VALUES(?,?,?,?)") + if err != nil { + log.Fatal(err) + } + + log.Print("Preparing update_email statement.") + update_email_stmt, err = db.Prepare("UPDATE emails SET email = ?, uid = ?, validated = ?, token = ? WHERE email = ?") + if err != nil { + log.Fatal(err) + } + + log.Print("Preparing verify_email statement.") + verify_email_stmt, err = db.Prepare("UPDATE emails SET validated = 1, token = '' WHERE email = ?") + if err != nil { + log.Fatal(err) + } + log.Print("Preparing activate_user statement.") activate_user_stmt, err = db.Prepare("UPDATE users SET active = 1 WHERE uid = ?") if err != nil { @@ -291,7 +312,6 @@ func init_database(err error) { if err != nil { log.Fatal(err) } - if !nogrouplog { fmt.Println(group.Name + ": ") fmt.Printf("%+v\n", group.Perms) @@ -328,7 +348,6 @@ func init_database(err error) { forum.LastTopic = "None" forum.LastTopicTime = "" } - forums[forum.ID] = forum } err = rows.Err() @@ -387,7 +406,6 @@ func init_database(err error) { if !ok { continue } - plugin.Active = active plugins[uname] = plugin } diff --git a/router.go b/router.go index 36d956bb..d7834c60 100644 --- a/router.go +++ b/router.go @@ -1,31 +1,66 @@ package main -/*import "sync" +//import "fmt" +import "strings" +import "sync" import "net/http" type Router struct { mu sync.RWMutex - routes map[string]http.Handler + routes map[string]func(http.ResponseWriter, *http.Request) } -func (route *Router) ServeHTTP() { - route.mu.RLock() - defer route.mu.RUnlock() +func NewRouter() *Router { + return &Router{ + routes: make(map[string]func(http.ResponseWriter, *http.Request)), + } +} + +func (router *Router) Handle(pattern string, handle http.Handler) { + router.routes[pattern] = handle.ServeHTTP +} + +func (router *Router) HandleFunc(pattern string, handle func(http.ResponseWriter, *http.Request)) { + router.routes[pattern] = handle +} + +func (router *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + router.mu.RLock() + defer router.mu.RUnlock() - if path[0] != "/" { - return route.routes["/"] + if req.URL.Path[0] != '/' { + w.WriteHeader(405) + w.Write([]byte("")) + return } // Do something on the path to turn slashes facing the wrong way "\" into "/" slashes. If it's bytes, then alter the bytes in place for the maximum speed - handle := route.routes[path] - if !ok { - if path[-1] != "/" { - handle = route.routes[path + "/"] - if !ok { - return route.routes["/"] - } - return handle - } + handle, ok := router.routes[req.URL.Path] + if ok { + handle(w,req) + return } - return handle -}*/ \ No newline at end of file + + if req.URL.Path[len(req.URL.Path) - 1] == '/' { + w.WriteHeader(404) + w.Write([]byte("")) + return + } + + handle, ok = router.routes[req.URL.Path[:strings.LastIndexByte(req.URL.Path,'/') + 1]] + if ok { + handle(w,req) + return + } + //fmt.Println(req.URL.Path[:strings.LastIndexByte(req.URL.Path,'/')]) + + handle, ok = router.routes[req.URL.Path + "/"] + if ok { + handle(w,req) + return + } + + w.WriteHeader(404) + w.Write([]byte("")) + return +} \ No newline at end of file diff --git a/routes.go b/routes.go index 68fa0a25..7f92697a 100644 --- a/routes.go +++ b/routes.go @@ -877,19 +877,10 @@ func route_account_own_edit_critical(w http.ResponseWriter, r *http.Request) { if !ok { return } - if !user.Loggedin { - errmsg := "You need to login to edit your own account." - pi := Page{"Error","error",user,nList,tList,errmsg} - - var b bytes.Buffer - templates.ExecuteTemplate(&b,"error.html", pi) - errpage := b.String() - w.WriteHeader(500) - fmt.Fprintln(w,errpage) + LocalError("You need to login to edit your account.",w,r,user) return } - pi := Page{"Edit Password","account-own-edit",user,noticeList,tList,0} templates.ExecuteTemplate(w,"account-own-edit.html", pi) } @@ -899,16 +890,8 @@ func route_account_own_edit_critical_submit(w http.ResponseWriter, r *http.Reque if !ok { return } - if !user.Loggedin { - errmsg := "You need to login to edit your own account." - pi := Page{"Error","error",user,nList,tList,errmsg} - - var b bytes.Buffer - templates.ExecuteTemplate(&b,"error.html", pi) - errpage := b.String() - w.WriteHeader(500) - fmt.Fprintln(w,errpage) + LocalError("You need to login to edit your account.",w,r,user) return } @@ -973,8 +956,9 @@ func route_account_own_edit_critical_submit(w http.ResponseWriter, r *http.Reque return } - pi := Page{"Edit Password","account-own-edit-success",user,noticeList,tList,0} - templates.ExecuteTemplate(w,"account-own-edit-success.html", pi) + noticeList[len(noticeList)] = "Your password was successfully updated" + pi := Page{"Edit Password","account-own-edit",user,noticeList,tList,0} + templates.ExecuteTemplate(w,"account-own-edit.html", pi) } func route_account_own_edit_avatar(w http.ResponseWriter, r *http.Request) { @@ -982,19 +966,10 @@ func route_account_own_edit_avatar(w http.ResponseWriter, r *http.Request) { if !ok { return } - if !user.Loggedin { - errmsg := "You need to login to edit your own account." - pi := Page{"Error","error",user,nList,tList,errmsg} - - var b bytes.Buffer - templates.ExecuteTemplate(&b,"error.html", pi) - errpage := b.String() - w.WriteHeader(500) - fmt.Fprintln(w,errpage) + LocalError("You need to login to edit your account.",w,r,user) return } - pi := Page{"Edit Avatar","account-own-edit-avatar",user,noticeList,tList,0} templates.ExecuteTemplate(w,"account-own-edit-avatar.html", pi) } @@ -1011,14 +986,7 @@ func route_account_own_edit_avatar_submit(w http.ResponseWriter, r *http.Request return } if !user.Loggedin { - errmsg := "You need to login to edit your own account." - pi := Page{"Error","error",user,nList,tList,errmsg} - - var b bytes.Buffer - templates.ExecuteTemplate(&b,"error.html", pi) - errpage := b.String() - w.WriteHeader(500) - fmt.Fprintln(w,errpage) + LocalError("You need to login to edit your account.",w,r,user) return } @@ -1028,7 +996,7 @@ func route_account_own_edit_avatar_submit(w http.ResponseWriter, r *http.Request return } - var filename string = "" + var filename string var ext string for _, fheaders := range r.MultipartForm.File { for _, hdr := range fheaders { @@ -1087,7 +1055,6 @@ func route_account_own_edit_avatar_submit(w http.ResponseWriter, r *http.Request InternalError(err,w,r,user) return } - user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext noticeList[len(noticeList)] = "Your avatar was successfully updated" @@ -1100,16 +1067,8 @@ func route_account_own_edit_username(w http.ResponseWriter, r *http.Request) { if !ok { return } - if !user.Loggedin { - errmsg := "You need to login to edit your own account." - pi := Page{"Error","error",user,nList,tList,errmsg} - - var b bytes.Buffer - templates.ExecuteTemplate(&b,"error.html", pi) - errpage := b.String() - w.WriteHeader(500) - fmt.Fprintln(w,errpage) + LocalError("You need to login to edit your account.",w,r,user) return } @@ -1122,19 +1081,10 @@ func route_account_own_edit_username_submit(w http.ResponseWriter, r *http.Reque if !ok { return } - if !user.Loggedin { - errmsg := "You need to login to edit your own account." - pi := Page{"Error","error",user,nList,tList,errmsg} - - var b bytes.Buffer - templates.ExecuteTemplate(&b,"error.html", pi) - errpage := b.String() - w.WriteHeader(500) - fmt.Fprintln(w,errpage) + LocalError("You need to login to edit your account.",w,r,user) return } - err := r.ParseForm() if err != nil { LocalError("Bad Form", w, r, user) @@ -1149,10 +1099,131 @@ func route_account_own_edit_username_submit(w http.ResponseWriter, r *http.Reque } user.Name = new_username - pi := Page{"Edit Username","account-own-edit-username",user,noticeList,tList,user.Name} + noticeList[len(noticeList)] = "Your username was successfully updated" + pi := Page{"Edit Username","account-own-edit-username",user,noticeList,tList,0} templates.ExecuteTemplate(w,"account-own-edit-username.html", pi) } +func route_account_own_edit_email(w http.ResponseWriter, r *http.Request) { + user, noticeList, ok := SessionCheck(w,r) + if !ok { + return + } + if !user.Loggedin { + LocalError("You need to login to edit your account.",w,r,user) + return + } + + email := Email{UserID: user.ID} + var emailList []interface{} + rows, err := db.Query("SELECT email, validated FROM emails WHERE uid = ?", user.ID) + if err != nil { + log.Fatal(err) + } + defer rows.Close() + + for rows.Next() { + err := rows.Scan(&email.Email, &email.Validated) + if err != nil { + log.Fatal(err) + } + + if email.Email == user.Email { + email.Primary = true + } + emailList = append(emailList, email) + } + err = rows.Err() + if err != nil { + log.Fatal(err) + } + + // Was this site migrated from another forum software? Most of them don't have multiple emails for a single user. This also applies when the admin switches enable_emails on after having it off for a while + if len(emailList) == 0 { + email.Email = user.Email + email.Validated = false + email.Primary = true + emailList = append(emailList, email) + } + + if !enable_emails { + noticeList[len(noticeList)] = "The email system has been turned off. All features involving sending emails have been disabled." + } + pi := Page{"Email Manager","account-own-edit-email",user,noticeList,emailList,0} + templates.ExecuteTemplate(w,"account-own-edit-email.html", pi) +} + +func route_account_own_edit_email_token_submit(w http.ResponseWriter, r *http.Request) { + user, noticeList, ok := SessionCheck(w,r) + if !ok { + return + } + if !user.Loggedin { + LocalError("You need to login to edit your account.",w,r,user) + return + } + token := r.URL.Path[len("/user/edit/email/token/"):] + + email := Email{UserID: user.ID} + targetEmail := Email{UserID: user.ID} + var emailList []interface{} + rows, err := db.Query("SELECT email, validated, token FROM emails WHERE uid = ?", user.ID) + if err != nil { + log.Fatal(err) + } + defer rows.Close() + + for rows.Next() { + err := rows.Scan(&email.Email, &email.Validated, &email.Token) + if err != nil { + log.Fatal(err) + } + + if email.Email == user.Email { + email.Primary = true + } + if email.Token == token { + targetEmail = email + } + emailList = append(emailList, email) + } + err = rows.Err() + if err != nil { + log.Fatal(err) + } + + if len(emailList) == 0 { + LocalError("A verification email was never sent for you!",w,r,user) + return + } + if targetEmail.Token == "" { + LocalError("That's not a valid token!",w,r,user) + return + } + + _, err = verify_email_stmt.Exec(user.Email) + if err != nil { + InternalError(err,w,r,user) + return + } + + // If Email Activation is on, then activate the account while we're here + if settings["activation_type"] == 2 { + _, err = activate_user_stmt.Exec(user.ID) + if err != nil { + InternalError(err,w,r,user) + return + } + } + + if !enable_emails { + noticeList[len(noticeList)] = "The email system has been turned off. All features involving sending emails have been disabled." + } + noticeList[len(noticeList)] = "Your email was successfully verified" + pi := Page{"Email Manager","account-own-edit-email",user,noticeList,emailList,0} + templates.ExecuteTemplate(w,"account-own-edit-email.html", pi) +} + func route_logout(w http.ResponseWriter, r *http.Request) { user, ok := SimpleSessionCheck(w,r) if !ok { @@ -1312,7 +1383,6 @@ func route_register(w http.ResponseWriter, r *http.Request) { if !ok { return } - if user.Loggedin { errmsg := "You're already logged in." pi := Page{"Error","error",user,nList,tList,errmsg} @@ -1346,12 +1416,12 @@ func route_register_submit(w http.ResponseWriter, r *http.Request) { LocalError("You didn't put in a username.", w, r, user) return } - email := html.EscapeString(r.PostFormValue("email")) if email == "" { LocalError("You didn't put in an email.", w, r, user) return } + password := r.PostFormValue("password") if password == "" { LocalError("You didn't put in a password.", w, r, user) @@ -1416,12 +1486,12 @@ func route_register_submit(w http.ResponseWriter, r *http.Request) { var active int var group int - if settings["activation_type"] == 1 { - active = 1 - group = default_group - } else { - active = 0 - group = activation_group + switch settings["activation_type"] { + case 1: // Activate All + active = 1 + group = default_group + default: // Anything else. E.g. Admin Activation or Email Activation. + group = activation_group } res, err := register_stmt.Exec(username,email,string(hashed_password),salt,group,session,active) @@ -1436,6 +1506,25 @@ func route_register_submit(w http.ResponseWriter, r *http.Request) { return } + // Check if this user actually owns this email, if email activation is on, automatically flip their account to active when the email is validated. Validation is also useful for determining whether this user should receive any alerts, etc. via email + if enable_emails { + token, err := GenerateSafeString(80) + if err != nil { + InternalError(err,w,r,user) + return + } + _, err = add_email_stmt.Exec(email, lastId, 0, token) + if err != nil { + InternalError(err,w,r,user) + return + } + + if !SendValidationEmail(username, email, token) { + LocalError("We were unable to send the email for you to confirm that this email address belongs to you. You may not have access to some functionality until you do so. Please ask an administrator for assistance.",w,r,user) + return + } + } + cookie := http.Cookie{Name: "uid",Value: strconv.FormatInt(lastId, 10),Path: "/",MaxAge: year} http.SetCookie(w,&cookie) cookie = http.Cookie{Name: "session",Value: session,Path: "/",MaxAge: year} diff --git a/template_forum.go b/template_forum.go index 58b2b2a7..59770a81 100644 --- a/template_forum.go +++ b/template_forum.go @@ -1,7 +1,7 @@ /* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */ package main -import "io" import "strconv" +import "io" func init() { template_forum_handle = template_forum diff --git a/template_topic_alt.go b/template_topic_alt.go index 20b7149b..f1963175 100644 --- a/template_topic_alt.go +++ b/template_topic_alt.go @@ -1,8 +1,8 @@ /* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */ package main -import "html/template" import "io" import "strconv" +import "html/template" func init() { template_topic_alt_handle = template_topic_alt diff --git a/templates/account-menu.html b/templates/account-menu.html new file mode 100644 index 00000000..81d8cf7a --- /dev/null +++ b/templates/account-menu.html @@ -0,0 +1,10 @@ +