From e30707bc171b7d8693c4941739a6391064d6bc3a Mon Sep 17 00:00:00 2001 From: Azareal Date: Wed, 12 Jul 2017 12:05:18 +0100 Subject: [PATCH] Refactored the installer's code. We're moving towards supporting multiple database engines. More progress has been made with PostgreSQL, but it's still incomplete and experimental. The items on the Social Group menu bar are no longer decorative and now let you jump from page to page. Social Group permissions are now fully functional. More features to come like privacy levels, secondary board permissions, etc. for Social Groups. One of our dependencies added a dependecy, so we've added that dependency as a dependency. Tweaked the ForumStore to better conform to the tests. Fixed an issue with the generated file comment stopping the build tags from being read by the compiler for a few files. Fixed the test system. Renamed the route_create_topic handler to route_topic_create_submit Moved the user table into the query generator. Fixed a bug in MySQL where it doesn't allow unique keys longer than 180 characters. Fixed a race condition in the forum deletion handler. Fixed a crash bug where Gosora crashes when [rand]0[/rand] is passed to the BBCode parser. Fixed a bug with superadmins not being able to see all the forums they should be able to. Fixed a bug in simple_forum_session_check where it assumes every request has an error. Tests: Added 8 ForumStore tests. Added 4 Auth tests. Added 14 bbcode_full_parse tests. Added 4 bbcode_regex_parse tests. Fixed a bug in one of the bbcode_full_parse tests. Apparently, routes.go wasn't commited in the previous commit o_o --- README.md | 2 + auth.go | 1 + forum_store.go | 9 +- gen_mysql.go | 7 +- gen_pgsql.go | 275 +++++++++++++++++++ general_test.go | 25 +- install-gosora-linux | 5 +- install.bat | 25 +- install/install.go | 145 +++++----- install/mysql.go | 151 +++++++++++ install/pgsql.go | 37 +++ install/utils.go | 37 +++ main.go | 2 +- misc_test.go | 107 +++++++- mysql.go | 2 +- mysql.sql | 37 +-- panel_routes.go | 10 +- pgsql.go | 5 +- plugin_bbcode.go | 8 +- plugin_socialgroups.go | 48 +++- plugin_test.go | 108 +++++++- query_gen/lib/install.go | 61 +++++ query_gen/lib/mysql.go | 67 +++-- query_gen/lib/pgsql.go | 338 ++++++++++++++++++++++++ query_gen/lib/querygen.go | 6 + query_gen/main.go | 53 +++- routes.go | 333 ++++++++++++++++------- schema/mysql/inserts.sql | 0 schema/mysql/query_users.sql | 26 ++ schema/pgsql/inserts.sql | 0 schema/pgsql/query_users.sql | 26 ++ templates/socialgroups_member_list.html | 4 +- templates/socialgroups_view_group.html | 5 +- themes/cosmo-conflux/public/main.css | 1 - themes/cosmo/public/main.css | 1 - update-deps.bat | 9 +- user.go | 30 ++- 37 files changed, 1704 insertions(+), 302 deletions(-) create mode 100644 gen_pgsql.go create mode 100644 install/mysql.go create mode 100644 install/pgsql.go create mode 100644 install/utils.go create mode 100644 query_gen/lib/install.go create mode 100644 query_gen/lib/pgsql.go create mode 100644 schema/mysql/inserts.sql create mode 100644 schema/mysql/query_users.sql create mode 100644 schema/pgsql/inserts.sql create mode 100644 schema/pgsql/query_users.sql diff --git a/README.md b/README.md index d32a7d4e..78051c14 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,8 @@ We're looking for ways to clean-up the plugin system so that all of them (except * github.com/shirou/gopsutil For pulling information on CPU and memory usage. * github.com/StackExchange/wmi Dependency for gopsutil on Windows. + + * golang.org/x/sys/windows Also a dependency for gopsutil on Windows. * github.com/gorilla/websocket Needed for Gosora's Optional WebSockets Module. diff --git a/auth.go b/auth.go index 1f720cf3..9785e858 100644 --- a/auth.go +++ b/auth.go @@ -12,6 +12,7 @@ import "golang.org/x/crypto/bcrypt" var auth Auth var ErrMismatchedHashAndPassword = bcrypt.ErrMismatchedHashAndPassword +var ErrPasswordTooLong = errors.New("The password you selected is too long") // Silly, but we don't want bcrypt to bork on us type Auth interface { diff --git a/forum_store.go b/forum_store.go index d55d0169..20cc019b 100644 --- a/forum_store.go +++ b/forum_store.go @@ -2,7 +2,6 @@ package main import "log" -import "errors" import "sync" //import "sync/atomic" import "database/sql" @@ -13,8 +12,6 @@ var forum_create_mutex sync.Mutex var forum_perms map[int]map[int]ForumPerms // [gid][fid]Perms var fstore ForumStore -var err_noforum = errors.New("This forum doesn't exist") - type ForumStore interface { LoadForums() error @@ -122,21 +119,21 @@ func (sfs *StaticForumStore) DirtyGet(id int) *Forum { func (sfs *StaticForumStore) Get(id int) (*Forum, error) { if !((id <= sfs.forumCapCount) && (id >= 0) && sfs.forums[id].Name!="") { - return nil, err_noforum + return nil, ErrNoRows } return sfs.forums[id], nil } func (sfs *StaticForumStore) CascadeGet(id int) (*Forum, error) { if !((id <= sfs.forumCapCount) && (id >= 0) && sfs.forums[id].Name != "") { - return nil, err_noforum + return nil, ErrNoRows } return sfs.forums[id], nil } func (sfs *StaticForumStore) CascadeGetCopy(id int) (forum Forum, err error) { if !((id <= sfs.forumCapCount) && (id >= 0) && sfs.forums[id].Name != "") { - return forum, err_noforum + return forum, ErrNoRows } return *sfs.forums[id], nil } diff --git a/gen_mysql.go b/gen_mysql.go index e2d062d7..8a5860a2 100644 --- a/gen_mysql.go +++ b/gen_mysql.go @@ -1,6 +1,7 @@ -// Code generated by Gosora. More below: -/* This file was generated by Gosora's Query Generator. Please try to avoid modifying this file, as it might change at any time. */ // +build !pgsql !sqlite !mssql + +/* This file was generated by Gosora's Query Generator. Please try to avoid modifying this file, as it might change at any time. */ + package main import "log" @@ -112,7 +113,7 @@ var add_forum_perms_to_forum_staff_stmt *sql.Stmt var add_forum_perms_to_forum_members_stmt *sql.Stmt var notify_watchers_stmt *sql.Stmt -func gen_mysql() (err error) { +func _gen_mysql() (err error) { if debug_mode { log.Print("Building the generated statements") } diff --git a/gen_pgsql.go b/gen_pgsql.go new file mode 100644 index 00000000..0feef6bc --- /dev/null +++ b/gen_pgsql.go @@ -0,0 +1,275 @@ +// +build pgsql + +// This file was generated by Gosora's Query Generator. Please try to avoid modifying this file, as it might change at any time. +package main + +import "log" +import "database/sql" + +var add_replies_to_topic_stmt *sql.Stmt +var remove_replies_from_topic_stmt *sql.Stmt +var add_topics_to_forum_stmt *sql.Stmt +var remove_topics_from_forum_stmt *sql.Stmt +var update_forum_cache_stmt *sql.Stmt +var add_likes_to_topic_stmt *sql.Stmt +var add_likes_to_reply_stmt *sql.Stmt +var edit_topic_stmt *sql.Stmt +var edit_reply_stmt *sql.Stmt +var stick_topic_stmt *sql.Stmt +var unstick_topic_stmt *sql.Stmt +var update_last_ip_stmt *sql.Stmt +var update_session_stmt *sql.Stmt +var set_password_stmt *sql.Stmt +var set_avatar_stmt *sql.Stmt +var set_username_stmt *sql.Stmt +var change_group_stmt *sql.Stmt +var activate_user_stmt *sql.Stmt +var update_user_level_stmt *sql.Stmt +var increment_user_score_stmt *sql.Stmt +var increment_user_posts_stmt *sql.Stmt +var increment_user_bigposts_stmt *sql.Stmt +var increment_user_megaposts_stmt *sql.Stmt +var increment_user_topics_stmt *sql.Stmt +var edit_profile_reply_stmt *sql.Stmt +var delete_forum_stmt *sql.Stmt +var update_forum_stmt *sql.Stmt +var update_setting_stmt *sql.Stmt +var update_plugin_stmt *sql.Stmt +var update_plugin_install_stmt *sql.Stmt +var update_theme_stmt *sql.Stmt +var update_user_stmt *sql.Stmt +var update_group_perms_stmt *sql.Stmt +var update_group_rank_stmt *sql.Stmt +var update_group_stmt *sql.Stmt +var update_email_stmt *sql.Stmt +var verify_email_stmt *sql.Stmt + +func _gen_pgsql() (err error) { + if debug_mode { + log.Print("Building the generated statements") + } + + log.Print("Preparing add_replies_to_topic statement.") + add_replies_to_topic_stmt, err = db.Prepare("UPDATE `topics` SET `postCount` = `postCount` + ?,`lastReplyAt` = NOW() WHERE `tid` = ?") + if err != nil { + return err + } + + log.Print("Preparing remove_replies_from_topic statement.") + remove_replies_from_topic_stmt, err = db.Prepare("UPDATE `topics` SET `postCount` = `postCount` - ? WHERE `tid` = ?") + if err != nil { + return err + } + + log.Print("Preparing add_topics_to_forum statement.") + add_topics_to_forum_stmt, err = db.Prepare("UPDATE `forums` SET `topicCount` = `topicCount` + ? WHERE `fid` = ?") + if err != nil { + return err + } + + log.Print("Preparing remove_topics_from_forum statement.") + remove_topics_from_forum_stmt, err = db.Prepare("UPDATE `forums` SET `topicCount` = `topicCount` - ? WHERE `fid` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_forum_cache statement.") + update_forum_cache_stmt, err = db.Prepare("UPDATE `forums` SET `lastTopic` = ?,`lastTopicID` = ?,`lastReplyer` = ?,`lastReplyerID` = ?,`lastTopicTime` = NOW() WHERE `fid` = ?") + if err != nil { + return err + } + + log.Print("Preparing add_likes_to_topic statement.") + add_likes_to_topic_stmt, err = db.Prepare("UPDATE `topics` SET `likeCount` = `likeCount` + ? WHERE `tid` = ?") + if err != nil { + return err + } + + log.Print("Preparing add_likes_to_reply statement.") + add_likes_to_reply_stmt, err = db.Prepare("UPDATE `replies` SET `likeCount` = `likeCount` + ? WHERE `rid` = ?") + if err != nil { + return err + } + + log.Print("Preparing edit_topic statement.") + edit_topic_stmt, err = db.Prepare("UPDATE `topics` SET `title` = ?,`content` = ?,`parsed_content` = ?,`is_closed` = ? WHERE `tid` = ?") + if err != nil { + return err + } + + log.Print("Preparing edit_reply statement.") + edit_reply_stmt, err = db.Prepare("UPDATE `replies` SET `content` = ?,`parsed_content` = ? WHERE `rid` = ?") + if err != nil { + return err + } + + log.Print("Preparing stick_topic statement.") + stick_topic_stmt, err = db.Prepare("UPDATE `topics` SET `sticky` = 1 WHERE `tid` = ?") + if err != nil { + return err + } + + log.Print("Preparing unstick_topic statement.") + unstick_topic_stmt, err = db.Prepare("UPDATE `topics` SET `sticky` = 0 WHERE `tid` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_last_ip statement.") + update_last_ip_stmt, err = db.Prepare("UPDATE `users` SET `last_ip` = ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_session statement.") + update_session_stmt, err = db.Prepare("UPDATE `users` SET `session` = ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing set_password statement.") + set_password_stmt, err = db.Prepare("UPDATE `users` SET `password` = ?,`salt` = ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing set_avatar statement.") + set_avatar_stmt, err = db.Prepare("UPDATE `users` SET `avatar` = ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing set_username statement.") + set_username_stmt, err = db.Prepare("UPDATE `users` SET `name` = ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing change_group statement.") + change_group_stmt, err = db.Prepare("UPDATE `users` SET `group` = ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing activate_user statement.") + activate_user_stmt, err = db.Prepare("UPDATE `users` SET `active` = 1 WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_user_level statement.") + update_user_level_stmt, err = db.Prepare("UPDATE `users` SET `level` = ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing increment_user_score statement.") + increment_user_score_stmt, err = db.Prepare("UPDATE `users` SET `score` = `score` + ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing increment_user_posts statement.") + increment_user_posts_stmt, err = db.Prepare("UPDATE `users` SET `posts` = `posts` + ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing increment_user_bigposts statement.") + increment_user_bigposts_stmt, err = db.Prepare("UPDATE `users` SET `posts` = `posts` + ?,`bigposts` = `bigposts` + ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing increment_user_megaposts statement.") + increment_user_megaposts_stmt, err = db.Prepare("UPDATE `users` SET `posts` = `posts` + ?,`bigposts` = `bigposts` + ?,`megaposts` = `megaposts` + ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing increment_user_topics statement.") + increment_user_topics_stmt, err = db.Prepare("UPDATE `users` SET `topics` = `topics` + ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing edit_profile_reply statement.") + edit_profile_reply_stmt, err = db.Prepare("UPDATE `users_replies` SET `content` = ?,`parsed_content` = ? WHERE `rid` = ?") + if err != nil { + return err + } + + log.Print("Preparing delete_forum statement.") + delete_forum_stmt, err = db.Prepare("UPDATE `forums` SET `name` = '',`active` = 0 WHERE `fid` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_forum statement.") + update_forum_stmt, err = db.Prepare("UPDATE `forums` SET `name` = ?,`desc` = ?,`active` = ?,`preset` = ? WHERE `fid` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_setting statement.") + update_setting_stmt, err = db.Prepare("UPDATE `settings` SET `content` = ? WHERE `name` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_plugin statement.") + update_plugin_stmt, err = db.Prepare("UPDATE `plugins` SET `active` = ? WHERE `uname` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_plugin_install statement.") + update_plugin_install_stmt, err = db.Prepare("UPDATE `plugins` SET `installed` = ? WHERE `uname` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_theme statement.") + update_theme_stmt, err = db.Prepare("UPDATE `themes` SET `default` = ? WHERE `uname` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_user statement.") + update_user_stmt, err = db.Prepare("UPDATE `users` SET `name` = ?,`email` = ?,`group` = ? WHERE `uid` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_group_perms statement.") + update_group_perms_stmt, err = db.Prepare("UPDATE `users_groups` SET `permissions` = ? WHERE `gid` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_group_rank statement.") + update_group_rank_stmt, err = db.Prepare("UPDATE `users_groups` SET `is_admin` = ?,`is_mod` = ?,`is_banned` = ? WHERE `gid` = ?") + if err != nil { + return err + } + + log.Print("Preparing update_group statement.") + update_group_stmt, err = db.Prepare("UPDATE `users_groups` SET `name` = ?,`tag` = ? WHERE `gid` = ?") + if err != nil { + return 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 { + return err + } + + log.Print("Preparing verify_email statement.") + verify_email_stmt, err = db.Prepare("UPDATE `emails` SET `validated` = 1,`token` = '' WHERE `email` = ?") + if err != nil { + return err + } + + return nil +} diff --git a/general_test.go b/general_test.go index 1a38b5cf..c628c00d 100644 --- a/general_test.go +++ b/general_test.go @@ -1,7 +1,7 @@ package main import ( - "os" + //"os" "log" "bytes" "strings" @@ -14,7 +14,7 @@ import ( "html/template" "io/ioutil" "database/sql" - "runtime/pprof" + //"runtime/pprof" //_ "github.com/go-sql-driver/mysql" //"github.com/erikstmartin/go-testdb" @@ -66,6 +66,8 @@ func gloinit() { init_static_files() external_sites["YT"] = "https://www.youtube.com/" //log.SetOutput(os.Stdout) + + router = NewGenRouter(http.FileServer(http.Dir("./uploads"))) gloinited = true } @@ -76,8 +78,8 @@ func init() { func BenchmarkTopicTemplateSerial(b *testing.B) { b.ReportAllocs() - user := User{0,"bob","Bob","bob@localhost",0,false,false,false,false,false,false,GuestPerms,"",false,"","","","","",0,0,"127.0.0.1"} - admin := User{1,"admin-alice","Admin Alice","admin@localhost",0,true,true,true,true,true,false,AllPerms,"",false,"","","","","",-1,58,"127.0.0.1"} + user := User{0,"bob","Bob","bob@localhost",0,false,false,false,false,false,false,GuestPerms,make(map[string]bool),"",false,"","","","","",0,0,"127.0.0.1"} + admin := User{1,"admin-alice","Admin Alice","admin@localhost",0,true,true,true,true,true,false,AllPerms,make(map[string]bool),"",false,"","","","","",-1,58,"127.0.0.1"} topic := TopicUser{Title: "Lol",Content: "Hey everyone!",CreatedBy: 1,CreatedAt: "0000-00-00 00:00:00",ParentID: 1,CreatedByName:"Admin",Css: no_css_tmpl,Tag: "Admin", Level: 58, IpAddress: "127.0.0.1"} @@ -164,8 +166,8 @@ func BenchmarkTopicTemplateSerial(b *testing.B) { func BenchmarkTopicsTemplateSerial(b *testing.B) { b.ReportAllocs() - user := User{0,"bob","Bob","bob@localhost",0,false,false,false,false,false,false,GuestPerms,"",false,"","","","","",0,0,"127.0.0.1"} - admin := User{1,"admin-alice","Admin Alice","admin@localhost",0,true,true,true,true,true,false,AllPerms,"",false,"","","","","",-1,58,"127.0.0.1"} + user := User{0,"bob","Bob","bob@localhost",0,false,false,false,false,false,false,GuestPerms,make(map[string]bool),"",false,"","","","","",0,0,"127.0.0.1"} + admin := User{1,"admin-alice","Admin Alice","admin@localhost",0,true,true,true,true,true,false,AllPerms,make(map[string]bool),"",false,"","","","","",-1,58,"127.0.0.1"} var topicList []TopicsRow topicList = append(topicList, TopicsRow{Title: "Hey everyone!",Content: "Hey everyone!",CreatedBy: 1,CreatedAt: "0000-00-00 00:00:00",ParentID: 1,UserSlug:"admin-alice",CreatedByName:"Admin Alice",Css: no_css_tmpl,Tag: "Admin", Level: 58, IpAddress: "127.0.0.1"}) @@ -239,6 +241,8 @@ func BenchmarkStaticRouteParallel(b *testing.B) { }) } +// TO-DO: Make these routes compatible with the changes to the router +/* func BenchmarkTopicAdminRouteParallel(b *testing.B) { b.ReportAllocs() if !gloinited { @@ -287,7 +291,6 @@ func BenchmarkTopicGuestRouteParallel(b *testing.B) { }) } - func BenchmarkForumsAdminRouteParallel(b *testing.B) { b.ReportAllocs() if !gloinited { @@ -371,7 +374,7 @@ func BenchmarkForumsGuestRouteParallel(b *testing.B) { } }) } - +*/ /*func BenchmarkRoutesSerial(b *testing.B) { b.ReportAllocs() @@ -1225,6 +1228,8 @@ func TestLevels(t *testing.T) { } } +// TO-DO: Make this compatible with the changes to the router +/* func TestStaticRoute(t *testing.T) { if !gloinited { gloinit() @@ -1242,6 +1247,7 @@ func TestStaticRoute(t *testing.T) { t.Fatal(static_w.Body) } } +*/ /*func TestTopicAdminRoute(t *testing.T) { if !gloinited { @@ -1297,6 +1303,8 @@ func TestStaticRoute(t *testing.T) { fmt.Println("No problems found in the topic-guest route!") }*/ +// TO-DO: Make these routes compatible with the changes to the router +/* func TestForumsAdminRoute(t *testing.T) { if !gloinited { gloinit() @@ -1345,6 +1353,7 @@ func TestForumsGuestRoute(t *testing.T) { t.Fatal(forums_w.Body) } } +*/ /*func TestForumAdminRoute(t *testing.T) { if !gloinited { diff --git a/install-gosora-linux b/install-gosora-linux index bd1482db..ead6a1d4 100644 --- a/install-gosora-linux +++ b/install-gosora-linux @@ -9,10 +9,9 @@ go get -u github.com/shirou/gopsutil echo "Installing Gorilla WebSockets" go get -u github.com/gorilla/websocket -echo "Preparing the installer" -go generate -go build -o Gosora +echo "Building the installer" cd ./install +go generate go build -o Install mv ./Install .. cd .. diff --git a/install.bat b/install.bat index d1218411..9cc62203 100644 --- a/install.bat +++ b/install.bat @@ -1,50 +1,51 @@ @echo off -echo Installing dependencies +echo Installing the dependencies +echo Installing the MySQL Driver go get -u github.com/go-sql-driver/mysql if %errorlevel% neq 0 ( pause exit /b %errorlevel% ) +echo Installing the PostgreSQL Driver go get -u github.com/lib/pq if %errorlevel% neq 0 ( pause exit /b %errorlevel% ) +echo Installing the bcrypt library go get -u golang.org/x/crypto/bcrypt if %errorlevel% neq 0 ( pause exit /b %errorlevel% ) +go get -u golang.org/x/sys/windows +if %errorlevel% neq 0 ( + pause + exit /b %errorlevel% +) go get -u github.com/StackExchange/wmi if %errorlevel% neq 0 ( pause exit /b %errorlevel% ) +echo Installing the gopsutil library go get -u github.com/shirou/gopsutil if %errorlevel% neq 0 ( pause exit /b %errorlevel% ) +echo Installing the WebSockets library go get -u github.com/gorilla/websocket if %errorlevel% neq 0 ( pause exit /b %errorlevel% ) -echo Preparing the installer +echo Building the installer go generate -if %errorlevel% neq 0 ( - pause - exit /b %errorlevel% -) -go build -o gosora.exe -if %errorlevel% neq 0 ( - pause - exit /b %errorlevel% -) go build ./install if %errorlevel% neq 0 ( pause exit /b %errorlevel% ) -install.exe \ No newline at end of file +install.exe diff --git a/install/install.go b/install/install.go index 51538454..5fff8df8 100644 --- a/install/install.go +++ b/install/install.go @@ -4,20 +4,27 @@ package main import ( "fmt" "os" - "bytes" "bufio" "strconv" - "io/ioutil" "database/sql" - _ "github.com/go-sql-driver/mysql" + "runtime/debug" + + "../query_gen/lib" ) +const saltLength int = 32 +var db *sql.DB var scanner *bufio.Scanner -var db_host, db_username, db_password, db_name string -//var db_collation string = "utf8mb4_general_ci" -var db_port string = "3306" + +var db_adapter string = "mysql" +var db_host string +var db_username string +var db_password string +var db_name string +var db_port string var site_name, site_url, server_port string +var default_adapter string = "mysql" var default_host string = "localhost" var default_username string = "root" var default_dbname string = "gosora" @@ -25,7 +32,22 @@ var default_site_name string = "Site Name" var default_site_url string = "localhost" var default_server_port string = "80" // 8080's a good one, if you're testing and don't want it to clash with port 80 +var init_database func()error = _init_mysql +var table_defs func()error = _table_defs_mysql +var initial_data func()error = _initial_data_mysql + func main() { + // Capture panics rather than immediately closing the window on Windows + defer func() { + r := recover() + if r != nil { + fmt.Println(r) + debug.PrintStack() + press_any_key() + return + } + }() + scanner = bufio.NewScanner(os.Stdin) fmt.Println("Welcome to Gosora's Installer") fmt.Println("We're going to take you through a few steps to help you get started :)") @@ -52,21 +74,8 @@ func main() { press_any_key() return } - - _db_password := db_password - if(_db_password != ""){ - _db_password = ":" + _db_password - } - db, err := sql.Open("mysql",db_username + _db_password + "@tcp(" + db_host + ":" + db_port + ")/") - if err != nil { - fmt.Println(err) - fmt.Println("Aborting installation...") - press_any_key() - return - } - - // Make sure that the connection is alive.. - err = db.Ping() + + err := init_database() if err != nil { fmt.Println(err) fmt.Println("Aborting installation...") @@ -74,56 +83,50 @@ func main() { return } - fmt.Println("Successfully connected to the database") - fmt.Println("Opening the database seed file") - sqlContents, err := ioutil.ReadFile("./mysql.sql") + err = table_defs() if err != nil { fmt.Println(err) fmt.Println("Aborting installation...") press_any_key() return } - - var waste string - err = db.QueryRow("SHOW DATABASES LIKE '" + db_name + "'").Scan(&waste) - if err != nil && err != sql.ErrNoRows { - fmt.Println(err) - fmt.Println("Aborting installation...") - press_any_key() - return - } - - if err == sql.ErrNoRows { - fmt.Println("Unable to find the database. Attempting to create it") - _,err = db.Exec("CREATE DATABASE IF NOT EXISTS " + db_name + "") - if err != nil { - fmt.Println(err) - fmt.Println("Aborting installation...") - press_any_key() - return - } - fmt.Println("The database was successfully created") - } - - fmt.Println("Switching to database " + db_name) - _, err = db.Exec("USE " + db_name) + + hashed_password, salt, err := BcryptGeneratePassword("password") if err != nil { fmt.Println(err) fmt.Println("Aborting installation...") press_any_key() return } - - fmt.Println("Preparing installation queries") - sqlContents = bytes.TrimSpace(sqlContents) - statements := bytes.Split(sqlContents, []byte(";")) - for key, statement := range statements { - if len(statement) == 0 { - continue - } - - fmt.Println("Executing query #" + strconv.Itoa(key) + " " + string(statement)) - _, err = db.Exec(string(statement)) + + // Build the admin user query + admin_user_stmt, err := qgen.Builder.SimpleInsert("users","name, password, salt, email, group, is_super_admin, active, createdAt, lastActiveAt, message, last_ip","'Admin',?,?,'admin@localhost',1,1,1,NOW(),NOW(),'','127.0.0.1'") + if err != nil { + fmt.Println(err) + fmt.Println("Aborting installation...") + press_any_key() + return + } + + // Run the admin user query + _, err = admin_user_stmt.Exec(hashed_password,salt) + if err != nil { + fmt.Println(err) + fmt.Println("Aborting installation...") + press_any_key() + return + } + + err = initial_data() + if err != nil { + fmt.Println(err) + fmt.Println("Aborting installation...") + press_any_key() + return + } + + if db_adapter == "mysql" { + err = _mysql_seed_database() if err != nil { fmt.Println(err) fmt.Println("Aborting installation...") @@ -131,8 +134,7 @@ func main() { return } } - fmt.Println("Finished inserting the database data") - + configContents := []byte(`package main // Site Info @@ -213,6 +215,17 @@ var profiling = false } func get_database_details() bool { + fmt.Println("Which database driver do you wish to use? mysql, mysql, or mysql? Default: mysql") + if !scanner.Scan() { + return false + } + db_adapter = scanner.Text() + if db_adapter == "" { + db_adapter = default_adapter + } + db_adapter = set_db_adapter(db_adapter) + fmt.Println("Set database adapter to " + db_adapter) + fmt.Println("Database Host? Default: " + default_host) if !scanner.Scan() { return false @@ -295,6 +308,16 @@ func get_site_details() bool { return true } +func set_db_adapter(name string) string { + switch(name) { + //case "wip-pgsql": + // set_pgsql_adapter() + // return "wip-pgsql" + } + _set_mysql_adapter() + return "mysql" +} + func obfuscate_password(password string) (out string) { for i := 0; i < len(password); i++ { out += "*" diff --git a/install/mysql.go b/install/mysql.go new file mode 100644 index 00000000..9b96f356 --- /dev/null +++ b/install/mysql.go @@ -0,0 +1,151 @@ +/* Copyright Azareal 2017 - 2018 */ +package main + +import ( + "fmt" + "bytes" + "strings" + "strconv" + "io/ioutil" + "path/filepath" + "database/sql" + + "../query_gen/lib" + _ "github.com/go-sql-driver/mysql" +) + +//var db_collation string = "utf8mb4_general_ci" + +func _set_mysql_adapter() { + db_port = "3306" + init_database = _init_mysql + table_defs = _table_defs_mysql + initial_data = _initial_data_mysql +} + +func _init_mysql() (err error) { + _db_password := db_password + if _db_password != "" { + _db_password = ":" + _db_password + } + db, err = sql.Open("mysql",db_username + _db_password + "@tcp(" + db_host + ":" + db_port + ")/") + if err != nil { + return err + } + + // Make sure that the connection is alive.. + err = db.Ping() + if err != nil { + return err + } + fmt.Println("Successfully connected to the database") + + var waste string + err = db.QueryRow("SHOW DATABASES LIKE '" + db_name + "'").Scan(&waste) + if err != nil && err != sql.ErrNoRows { + return err + } + + if err == sql.ErrNoRows { + fmt.Println("Unable to find the database. Attempting to create it") + _,err = db.Exec("CREATE DATABASE IF NOT EXISTS " + db_name + "") + if err != nil { + return err + } + fmt.Println("The database was successfully created") + } + + fmt.Println("Switching to database " + db_name) + _, err = db.Exec("USE " + db_name) + if err != nil { + return err + } + + // Ready the query builder + qgen.Builder.SetConn(db) + err = qgen.Builder.SetAdapter("mysql") + if err != nil { + return err + } + + return nil +} + +func _table_defs_mysql() error { + //fmt.Println("Creating the tables") + files, _ := ioutil.ReadDir("./schema/mysql/") + for _, f := range files { + if !strings.HasPrefix(f.Name(),"query_") { + continue + } + + var table string + var ext string + table = strings.TrimPrefix(f.Name(),"query_") + ext = filepath.Ext(table) + if ext != ".sql" { + continue + } + table = strings.TrimSuffix(table,ext) + + fmt.Println("Creating table '" + table + "'") + data, err := ioutil.ReadFile("./schema/mysql/" + f.Name()) + if err != nil { + return err + } + data = bytes.TrimSpace(data) + + _, err = db.Exec(string(data)) + if err != nil { + fmt.Println("Failed query:",string(data)) + return err + } + } + //fmt.Println("Finished creating the tables") + return nil +} + +func _initial_data_mysql() error { + return nil // Coming Soon + + fmt.Println("Seeding the tables") + data, err := ioutil.ReadFile("./schema/mysql/inserts.sql") + if err != nil { + return err + } + data = bytes.TrimSpace(data) + + fmt.Println("Executing query",string(data)) + _, err = db.Exec(string(data)) + if err != nil { + return err + } + + //fmt.Println("Finished inserting the database data") + return nil +} + +func _mysql_seed_database() error { + fmt.Println("Opening the database seed file") + sqlContents, err := ioutil.ReadFile("./mysql.sql") + if err != nil { + return err + } + + fmt.Println("Preparing installation queries") + sqlContents = bytes.TrimSpace(sqlContents) + statements := bytes.Split(sqlContents, []byte(";")) + for key, statement := range statements { + if len(statement) == 0 { + continue + } + + fmt.Println("Executing query #" + strconv.Itoa(key) + " " + string(statement)) + _, err = db.Exec(string(statement)) + if err != nil { + return err + } + } + fmt.Println("Finished inserting the database data") + return nil +} diff --git a/install/pgsql.go b/install/pgsql.go new file mode 100644 index 00000000..b4db22a9 --- /dev/null +++ b/install/pgsql.go @@ -0,0 +1,37 @@ +/* Under heavy development */ +/* Copyright Azareal 2017 - 2018 */ +package main + +import "fmt" +import "strings" +import "database/sql" +import _ "github.com/go-sql-driver/mysql" + +// We don't need SSL to run an installer... Do we? +var db_sslmode = "disable" + +func _set_pgsql_adapter() { + db_port = "3306" + init_database = _init_pgsql +} + +func _init_pgsql() (err error) { + _db_password := db_password + if _db_password != "" { + _db_password = " password=" + _pg_escape_bit(_db_password) + } + db, err = sql.Open("postgres", "host='" + _pg_escape_bit(db_host) + "' port='" + _pg_escape_bit(db_port) + "' user='" + _pg_escape_bit(db_username) + "' dbname='" + _pg_escape_bit(db_name) + "'" + _db_password + " sslmode='" + db_sslmode + "'") + if err != nil { + return err + } + fmt.Println("Successfully connected to the database") + + // TO-DO: Create the database, if it doesn't exist + + return nil +} + +func _pg_escape_bit(bit string) string { + // TO-DO: Write a custom parser, so that backslashes work properly in the sql.Open string. Do something similar for the database driver, if possible? + return strings.Replace(bit,"'","\\'",-1) +} \ No newline at end of file diff --git a/install/utils.go b/install/utils.go new file mode 100644 index 00000000..d07988a6 --- /dev/null +++ b/install/utils.go @@ -0,0 +1,37 @@ +package main + +import "encoding/base64" +import "crypto/rand" +import "golang.org/x/crypto/bcrypt" + +// Generate a cryptographically secure set of random bytes.. +func GenerateSafeString(length int) (string, error) { + rb := make([]byte,length) + _, err := rand.Read(rb) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(rb), nil +} + +func BcryptGeneratePassword(password string) (hashed_password string, salt string, err error) { + salt, err = GenerateSafeString(saltLength) + if err != nil { + return "", "", err + } + + password = password + salt + hashed_password, err = BcryptGeneratePasswordNoSalt(password) + if err != nil { + return "", "", err + } + return hashed_password, salt, nil +} + +func BcryptGeneratePasswordNoSalt(password string) (hash string, err error) { + hashed_password, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashed_password), nil +} diff --git a/main.go b/main.go index a0e95a3e..30f56a8f 100644 --- a/main.go +++ b/main.go @@ -196,7 +196,7 @@ func main(){ ///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/create/submit/", route_topic_create_submit) router.HandleFunc("/topic/", route_topic_id) router.HandleFunc("/reply/create/", route_create_reply) //router.HandleFunc("/reply/edit/", route_reply_edit) diff --git a/misc_test.go b/misc_test.go index a0e19034..6abbbf4d 100644 --- a/misc_test.go +++ b/misc_test.go @@ -1,5 +1,6 @@ package main +import "strconv" import "testing" // TO-DO: Generate a test database to work with rather than a live one @@ -36,7 +37,61 @@ func TestUserStore(t *testing.T) { } if user.ID != 1 { - t.Error("user.ID doesn't not match the requested UID") + t.Error("user.ID doesn't not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.") + } +} + +func TestForumStore(t *testing.T) { + if !gloinited { + gloinit() + } + if !plugins_inited { + init_plugins() + } + + var forum *Forum + var err error + + forum, err = fstore.CascadeGet(-1) + if err == nil { + t.Error("FID #-1 shouldn't exist") + } else if err != ErrNoRows { + t.Fatal(err) + } + + forum, err = fstore.CascadeGet(0) + if err == ErrNoRows { + t.Error("Couldn't find FID #0") + } else if err != nil { + t.Fatal(err) + } + + if forum.ID != 0 { + t.Error("forum.ID doesn't not match the requested UID. Got '" + strconv.Itoa(forum.ID) + "' instead.") + } + if forum.Name != "Uncategorised" { + t.Error("FID #0 is named '" + forum.Name + "' and not 'Uncategorised'") + } + + forum, err = fstore.CascadeGet(1) + if err == ErrNoRows { + t.Error("Couldn't find FID #1") + } else if err != nil { + t.Fatal(err) + } + + if forum.ID != 1 { + t.Error("forum.ID doesn't not match the requested UID. Got '" + strconv.Itoa(forum.ID) + "' instead.'") + } + if forum.Name != "Reports" { + t.Error("FID #0 is named '" + forum.Name + "' and not 'Reports'") + } + + forum, err = fstore.CascadeGet(2) + if err == ErrNoRows { + t.Error("Couldn't find FID #2") + } else if err != nil { + t.Fatal(err) } } @@ -72,3 +127,53 @@ func TestSlugs(t *testing.T) { } } } + +func TestAuth(t *testing.T) { + // bcrypt likes doing stupid things, so this test will probably fail + var real_password string + var hashed_password string + var password string + var salt string + var err error + + /* No extra salt tests, we might not need this extra salt, as bcrypt has it's own? */ + real_password = "Madame Cassandra's Mystic Orb" + t.Log("Set real_password to '" + real_password + "'") + t.Log("Hashing the real password") + hashed_password, err = BcryptGeneratePasswordNoSalt(real_password) + if err != nil { + t.Error(err) + } + + password = real_password + t.Log("Testing password '" + password + "'") + t.Log("Testing salt '" + salt + "'") + err = CheckPassword(hashed_password,password,salt) + if err == ErrMismatchedHashAndPassword { + t.Error("The two don't match") + } else if err == ErrPasswordTooLong { + t.Error("CheckPassword thinks the password is too long") + } else if err != nil { + t.Error(err) + } + + password = "hahaha" + t.Log("Testing password '" + password + "'") + t.Log("Testing salt '" + salt + "'") + err = CheckPassword(hashed_password,password,salt) + if err == ErrPasswordTooLong { + t.Error("CheckPassword thinks the password is too long") + } else if err == nil { + t.Error("The two shouldn't match!") + } + + password = "Madame Cassandra's Mystic" + t.Log("Testing password '" + password + "'") + t.Log("Testing salt '" + salt + "'") + err = CheckPassword(hashed_password,password,salt) + if err == ErrPasswordTooLong { + t.Error("CheckPassword thinks the password is too long") + } else if err == nil { + t.Error("The two shouldn't match!") + } +} diff --git a/mysql.go b/mysql.go index 59349591..5b4a9824 100644 --- a/mysql.go +++ b/mysql.go @@ -41,7 +41,7 @@ func _init_database() (err error) { db.SetMaxOpenConns(64) // Build the generated prepared statements, we are going to slowly move the queries over to the query generator rather than writing them all by hand, this'll make it easier for us to implement database adapters for other databases like PostgreSQL, MSSQL, SQlite, etc. - err = gen_mysql() + err = _gen_mysql() if err != nil { return err } diff --git a/mysql.sql b/mysql.sql index c2860b02..623a0685 100644 --- a/mysql.sql +++ b/mysql.sql @@ -1,30 +1,3 @@ -CREATE TABLE `users`( - `uid` int not null AUTO_INCREMENT, - `name` varchar(100) not null, - `password` varchar(100) not null, - `salt` varchar(80) default '' not null, - `group` int not null, - `active` tinyint default 0 not null, - `is_super_admin` tinyint(1) not null, - `createdAt` datetime not null, - `lastActiveAt` datetime not null, - `session` varchar(200) default '' not null, - `last_ip` varchar(200) default '0.0.0.0.0' not null, - `email` varchar(200) default '' not null, - `avatar` varchar(20) default '' not null, - `message` text not null, - `url_prefix` varchar(20) default '' not null, - `url_name` varchar(100) default '' not null, - `level` tinyint default 0 not null, - `score` int default 0 not null, - `posts` int default 0 not null, - `bigposts` int default 0 not null, - `megaposts` int default 0 not null, - `topics` int default 0 not null, - primary key(`uid`), - unique(`name`) -) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; - CREATE TABLE `users_groups`( `gid` int not null AUTO_INCREMENT, `name` varchar(100) not null, @@ -157,8 +130,9 @@ CREATE TABLE `activity_subscriptions`( `level` tinyint DEFAULT 0 not null /* 0: Mentions (aka the global default for any post), 1: Replies To You, 2: All Replies*/ ); +/* Due to MySQL's design, we have to drop the unique keys for table settings, plugins, and themes down from 200 to 180 or it will error */ CREATE TABLE `settings`( - `name` varchar(200) not null, + `name` varchar(180) not null, `content` varchar(250) not null, `type` varchar(50) not null, `constraints` varchar(200) DEFAULT '' not null, @@ -166,14 +140,14 @@ CREATE TABLE `settings`( ); CREATE TABLE `plugins`( - `uname` varchar(200) not null, + `uname` varchar(180) not null, `active` tinyint DEFAULT 0 not null, `installed` tinyint DEFAULT 0 not null, unique(`uname`) ); CREATE TABLE `themes`( - `uname` varchar(200) not null, + `uname` varchar(180) not null, `default` tinyint DEFAULT 0 not null, unique(`uname`) ); @@ -215,9 +189,6 @@ INSERT INTO settings(`name`,`content`,`type`,`constraints`) VALUES ('activation_ INSERT INTO settings(`name`,`content`,`type`) VALUES ('bigpost_min_words','250','int'); INSERT INTO settings(`name`,`content`,`type`) VALUES ('megapost_min_words','1000','int'); INSERT INTO themes(`uname`,`default`) VALUES ('tempra-simple',1); - -INSERT INTO users(`name`,`password`,`email`,`group`,`is_super_admin`,`createdAt`,`lastActiveAt`,`message`,`last_ip`) -VALUES ('Admin','password','admin@localhost',1,1,NOW(),NOW(),'','127.0.0.1'); INSERT INTO emails(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1); /* diff --git a/panel_routes.go b/panel_routes.go index 360deecd..b8a859ae 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -325,16 +325,16 @@ func route_panel_forums_delete_submit(w http.ResponseWriter, r *http.Request, us LocalError("The provided Forum ID is not a valid number.",w,r,user) return } - if !fstore.Exists(fid) { - LocalError("The forum you're trying to delete doesn't exist.",w,r,user) - return - } err = fstore.CascadeDelete(fid) - if err != nil { + if err == ErrNoRows { + LocalError("The forum you're trying to delete doesn't exist.",w,r,user) + return + } else if err != nil { InternalError(err,w,r) return } + http.Redirect(w,r,"/panel/forums/",http.StatusSeeOther) } diff --git a/pgsql.go b/pgsql.go index 9f139699..ad6e9e74 100644 --- a/pgsql.go +++ b/pgsql.go @@ -30,12 +30,13 @@ func _init_database() (err error) { return err } - // TO-DO: Get the version number + // Fetch the database version + db.QueryRow("SELECT VERSION()").Scan(&db_version) // Set the number of max open connections. How many do we need? Might need to do some tests. db.SetMaxOpenConns(64) - err = gen_pgsql() + err = _gen_pgsql() if err != nil { return err } diff --git a/plugin_bbcode.go b/plugin_bbcode.go index c3d472e1..1961ccc4 100644 --- a/plugin_bbcode.go +++ b/plugin_bbcode.go @@ -374,7 +374,13 @@ func bbcode_full_parse(msg string) string { goto MainLoop } - dat := []byte(strconv.FormatInt((random.Int63n(number)),10)) + var dat []byte + if number == 0 { + dat = []byte("0") + } else { + dat = []byte(strconv.FormatInt((random.Int63n(number)),10)) + } + outbytes = append(outbytes, dat...) //log.Print("Outputted the random number") i += 7 diff --git a/plugin_socialgroups.go b/plugin_socialgroups.go index 8df9600e..52c82c0c 100644 --- a/plugin_socialgroups.go +++ b/plugin_socialgroups.go @@ -40,6 +40,7 @@ type SocialGroup struct CreatedAt string LastUpdateTime string + MainForumID int MainForum *Forum Forums []*Forum ExtData ExtData @@ -316,6 +317,12 @@ func socialgroups_group_list(w http.ResponseWriter, r *http.Request, user User) } } +func socialgroups_get_group(sgid int) (sgItem SocialGroup, err error) { + sgItem = SocialGroup{ID:sgid} + err = socialgroups_get_group_stmt.QueryRow(sgid).Scan(&sgItem.Name, &sgItem.Desc, &sgItem.Active, &sgItem.Privacy, &sgItem.Owner, &sgItem.MemberCount, &sgItem.MainForumID, &sgItem.Backdrop, &sgItem.CreatedAt, &sgItem.LastUpdateTime) + return sgItem, err +} + func socialgroups_view_group(w http.ResponseWriter, r *http.Request, user User) { // SEO URLs... halves := strings.Split(r.URL.Path[len("/group/"):],".") @@ -328,9 +335,7 @@ func socialgroups_view_group(w http.ResponseWriter, r *http.Request, user User) return } - var sgItem SocialGroup = SocialGroup{ID:sgid} - var mainForum int - err = socialgroups_get_group_stmt.QueryRow(sgid).Scan(&sgItem.Name, &sgItem.Desc, &sgItem.Active, &sgItem.Privacy, &sgItem.Owner, &sgItem.MemberCount, &mainForum, &sgItem.Backdrop, &sgItem.CreatedAt, &sgItem.LastUpdateTime) + sgItem, err := socialgroups_get_group(sgid) if err != nil { LocalError("Bad group",w,r,user) return @@ -341,7 +346,7 @@ func socialgroups_view_group(w http.ResponseWriter, r *http.Request, user User) // Re-route the request to route_forums var ctx context.Context = context.WithValue(r.Context(),"socialgroups_current_group",sgItem) - route_forum(w,r.WithContext(ctx),user,strconv.Itoa(mainForum)) + route_forum(w,r.WithContext(ctx),user,strconv.Itoa(sgItem.MainForumID)) } func socialgroups_create_group(w http.ResponseWriter, r *http.Request, user User) { @@ -557,21 +562,35 @@ func socialgroups_topic_create_pre_loop(args ...interface{}) interface{} { return nil } -// TO-DO: Permissions Override. It doesn't quite work yet. +// TO-DO: Add privacy options +// TO-DO: Add support for multiple boards and add per-board simplified permissions +// TO-DO: Take is_js into account for routes which expect JSON responses func socialgroups_forum_check(args ...interface{}) (skip interface{}) { var r = args[1].(*http.Request) var fid *int = args[3].(*int) - if fstore.DirtyGet(*fid).ParentType == "socialgroup" { + var forum *Forum = fstore.DirtyGet(*fid) + + if forum.ParentType == "socialgroup" { + var err error + var w = args[0].(http.ResponseWriter) + var success *bool = args[4].(*bool) sgItem, ok := r.Context().Value("socialgroups_current_group").(SocialGroup) if !ok { - LogError(errors.New("Unable to find a parent group in the context data")) - return false + sgItem, err = socialgroups_get_group(forum.ParentID) + if err != nil { + InternalError(errors.New("Unable to find the parent group for a forum"),w,r) + *success = false + return false + } + if !sgItem.Active { + NotFound(w,r) + *success = false + return false + } + r = r.WithContext(context.WithValue(r.Context(),"socialgroups_current_group",sgItem)) } - //run_vhook("simple_forum_check_pre_perms", w, r, user, &fid, &success).(bool) - var w = args[0].(http.ResponseWriter) var user *User = args[2].(*User) - var success *bool = args[4].(*bool) var rank int var posts int var joinedAt string @@ -583,18 +602,18 @@ func socialgroups_forum_check(args ...interface{}) (skip interface{}) { override_forum_perms(&user.Perms, false) user.Perms.ViewTopic = true - err := socialgroups_get_member_stmt.QueryRow(sgItem.ID,user.ID).Scan(&rank,&posts,&joinedAt) + err = socialgroups_get_member_stmt.QueryRow(sgItem.ID,user.ID).Scan(&rank,&posts,&joinedAt) if err != nil && err != ErrNoRows { *success = false InternalError(err,w,r) return false } else if err != nil { - return false + return true } // TO-DO: Implement bans properly by adding the Local Ban API in the next commit if rank < 0 { - return false + return true } // Basic permissions for members, more complicated permissions coming in the next commit! @@ -607,6 +626,7 @@ func socialgroups_forum_check(args ...interface{}) (skip interface{}) { } else { override_forum_perms(&user.Perms,true) } + return true } return false diff --git a/plugin_test.go b/plugin_test.go index bbcb2d3b..aa92896a 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -30,9 +30,13 @@ func TestBBCodeRender(t *testing.T) { msgList = addMEPair(msgList,"[b]hi[/i]","[b]hi[/i]") msgList = addMEPair(msgList,"[/b]hi[b]","[/b]hi[b]") msgList = addMEPair(msgList,"[/b]hi[/b]","[/b]hi[/b]") + msgList = addMEPair(msgList,"[b][b]hi[/b]","hi") + msgList = addMEPair(msgList,"[b][b]hi","[b][b]hi") + msgList = addMEPair(msgList,"[b][b][b]hi","[b][b][b]hi") msgList = addMEPair(msgList,"[/b]hi","[/b]hi") msgList = addMEPair(msgList,"[code]hi[/code]","hi") msgList = addMEPair(msgList,"[code][b]hi[/b][/code]","[b]hi[/b]") + msgList = addMEPair(msgList,"[code][b]hi[/code][/b]","[b]hi[/b]") msgList = addMEPair(msgList,"[quote]hi[/quote]","hi") msgList = addMEPair(msgList,"[quote][b]hi[/b][/quote]","hi") msgList = addMEPair(msgList,"[quote][b]h[/b][/quote]","h") @@ -41,7 +45,7 @@ func TestBBCodeRender(t *testing.T) { t.Log("Testing bbcode_full_parse") for _, item := range msgList { t.Log("Testing string '"+item.Msg+"'") - res = bbcode_full_parse(item.Msg).(string) + res = bbcode_full_parse(item.Msg) if res != item.Expects { t.Error("Bad output:","'"+res+"'") t.Error("Expected:",item.Expects) @@ -54,7 +58,7 @@ func TestBBCodeRender(t *testing.T) { msg = "[rand][/rand]" expects = "[Invalid Number][rand][/rand]" t.Log("Testing string '"+msg+"'") - res = bbcode_full_parse(msg).(string) + res = bbcode_full_parse(msg) if res != expects { t.Error("Bad output:","'"+res+"'") t.Error("Expected:",expects) @@ -63,7 +67,52 @@ func TestBBCodeRender(t *testing.T) { msg = "[rand]-1[/rand]" expects = "[No Negative Numbers][rand]-1[/rand]" t.Log("Testing string '"+msg+"'") - res = bbcode_full_parse(msg).(string) + res = bbcode_full_parse(msg) + if res != expects { + t.Error("Bad output:","'"+res+"'") + t.Error("Expected:",expects) + } + + msg = "[rand]-01[/rand]" + expects = "[No Negative Numbers][rand]-01[/rand]" + t.Log("Testing string '"+msg+"'") + res = bbcode_full_parse(msg) + if res != expects { + t.Error("Bad output:","'"+res+"'") + t.Error("Expected:",expects) + } + + msg = "[rand]NaN[/rand]" + expects = "[Invalid Number][rand]NaN[/rand]" + t.Log("Testing string '"+msg+"'") + res = bbcode_full_parse(msg) + if res != expects { + t.Error("Bad output:","'"+res+"'") + t.Error("Expected:",expects) + } + + msg = "[rand]Inf[/rand]" + expects = "[Invalid Number][rand]Inf[/rand]" + t.Log("Testing string '"+msg+"'") + res = bbcode_full_parse(msg) + if res != expects { + t.Error("Bad output:","'"+res+"'") + t.Error("Expected:",expects) + } + + msg = "[rand]+[/rand]" + expects = "[Invalid Number][rand]+[/rand]" + t.Log("Testing string '"+msg+"'") + res = bbcode_full_parse(msg) + if res != expects { + t.Error("Bad output:","'"+res+"'") + t.Error("Expected:",expects) + } + + msg = "[rand]1+1[/rand]" + expects = "[Invalid Number][rand]1+1[/rand]" + t.Log("Testing string '"+msg+"'") + res = bbcode_full_parse(msg) if res != expects { t.Error("Bad output:","'"+res+"'") t.Error("Expected:",expects) @@ -72,17 +121,62 @@ func TestBBCodeRender(t *testing.T) { var conv int msg = "[rand]1[/rand]" t.Log("Testing string '"+msg+"'") - res = bbcode_full_parse(msg).(string) + res = bbcode_full_parse(msg) conv, err = strconv.Atoi(res) - if err != nil && (conv > 1 || conv < 0) { + if err != nil || (conv > 1 || conv < 0) { t.Error("Bad output:","'"+res+"'") t.Error("Expected a number in the range 0-1") } + msg = "[rand]0[/rand]" + t.Log("Testing string '"+msg+"'") + res = bbcode_full_parse(msg) + conv, err = strconv.Atoi(res) + if err != nil || conv != 0 { + t.Error("Bad output:","'"+res+"'") + t.Error("Expected the number 0") + } + + msg = "[rand]2147483647[/rand]" // Signed 32-bit MAX + t.Log("Testing string '"+msg+"'") + res = bbcode_full_parse(msg) + conv, err = strconv.Atoi(res) + if err != nil || (conv > 2147483647 || conv < 0) { + t.Error("Bad output:","'"+res+"'") + t.Error("Expected a number between 0 and 2147483647") + } + + msg = "[rand]9223372036854775807[/rand]" // Signed 64-bit MAX + t.Log("Testing string '"+msg+"'") + res = bbcode_full_parse(msg) + conv, err = strconv.Atoi(res) + if err != nil || (conv > 9223372036854775807 || conv < 0) { + t.Error("Bad output:","'"+res+"'") + t.Error("Expected a number between 0 and 9223372036854775807") + } + + // Note: conv is commented out in these two, as these numbers overflow int + msg = "[rand]18446744073709551615[/rand]" // Unsigned 64-bit MAX + t.Log("Testing string '"+msg+"'") + res = bbcode_full_parse(msg) + conv, err = strconv.Atoi(res) + if err != nil || (/*conv > 18446744073709551615 || */conv < 0) { + t.Error("Bad output:","'"+res+"'") + t.Error("Expected a number between 0 and 18446744073709551615") + } + msg = "[rand]170141183460469231731687303715884105727[/rand]" // Signed 128-bit MAX + t.Log("Testing string '"+msg+"'") + res = bbcode_full_parse(msg) + conv, err = strconv.Atoi(res) + if err != nil || (/*conv > 170141183460469231731687303715884105727 || */conv < 0) { + t.Error("Bad output:","'"+res+"'") + t.Error("Expected a number between 0 and 170141183460469231731687303715884105727") + } + t.Log("Testing bbcode_regex_parse") for _, item := range msgList { t.Log("Testing string '"+item.Msg+"'") - res = bbcode_regex_parse(item.Msg).(string) + res = bbcode_regex_parse(item.Msg) if res != item.Expects { t.Error("Bad output:","'"+res+"'") t.Error("Expected:",item.Expects) @@ -123,7 +217,7 @@ func TestMarkdownRender(t *testing.T) { for _, item := range msgList { t.Log("Testing string '"+item.Msg+"'") - res = markdown_parse(item.Msg).(string) + res = markdown_parse(item.Msg) if res != item.Expects { t.Error("Bad output:","'"+res+"'") t.Error("Expected:",item.Expects) diff --git a/query_gen/lib/install.go b/query_gen/lib/install.go new file mode 100644 index 00000000..6f34be17 --- /dev/null +++ b/query_gen/lib/install.go @@ -0,0 +1,61 @@ +/* WIP Under Construction */ +package qgen + +var Install *installer + +func init() { + Install = &installer{instructions:[]DB_Install_Instruction{}} +} + +type DB_Install_Instruction struct +{ + Table string + Contents string + Type string +} + +// A set of wrappers around the generator methods, so we can use this in the installer +// TO-DO: Re-implement the query generation, query builder and installer adapters as layers on-top of a query text adapter +type installer struct +{ + adapter DB_Adapter + instructions []DB_Install_Instruction +} + +func (install *installer) SetAdapter(name string) error { + adap, err := GetAdapter(name) + if err != nil { + return err + } + install.adapter = adap + return nil +} + +func (install *installer) SetAdapterInstance(adapter DB_Adapter) { + install.adapter = adapter +} + +func (install *installer) CreateTable(table string, charset string, collation string, columns []DB_Table_Column, keys []DB_Table_Key) error { + res, err := install.adapter.CreateTable("_installer", table, charset, collation, columns, keys) + if err != nil { + return err + } + install.instructions = append(install.instructions,DB_Install_Instruction{table,res,"create-table"}) + return nil +} + +func (install *installer) Write() error { + var inserts string + // We can't escape backticks, so we have to dump it out a file at a time + for _, instr := range install.instructions { + if instr.Type == "create-table" { + err := write_file("./schema/" + install.adapter.GetName() + "/query_" + instr.Table + ".sql", instr.Contents) + if err != nil { + return err + } + } else { + inserts += instr.Contents + "\n" + } + } + return write_file("./schema/" + install.adapter.GetName() + "/inserts.sql", inserts) +} diff --git a/query_gen/lib/mysql.go b/query_gen/lib/mysql.go index 8dbe142e..79536ff1 100644 --- a/query_gen/lib/mysql.go +++ b/query_gen/lib/mysql.go @@ -8,14 +8,14 @@ import "errors" func init() { DB_Registry = append(DB_Registry, - &Mysql_Adapter{Name:"mysql",Buffer:make(map[string]string)}, + &Mysql_Adapter{Name:"mysql",Buffer:make(map[string]DB_Stmt)}, ) } type Mysql_Adapter struct { Name string - Buffer map[string]string + Buffer map[string]DB_Stmt BufferOrder []string // Map iteration order is random, so we need this to track the order, so we don't get huge diffs every commit } @@ -23,11 +23,11 @@ func (adapter *Mysql_Adapter) GetName() string { return adapter.Name } -func (adapter *Mysql_Adapter) GetStmt(name string) string { +func (adapter *Mysql_Adapter) GetStmt(name string) DB_Stmt { return adapter.Buffer[name] } -func (adapter *Mysql_Adapter) GetStmts() map[string]string { +func (adapter *Mysql_Adapter) GetStmts() map[string]DB_Stmt { return adapter.Buffer } @@ -55,9 +55,10 @@ func (adapter *Mysql_Adapter) CreateTable(name string, table string, charset str } var end string - if column.Default != "" { + // TO-DO: Exclude the other variants of text like mediumtext and longtext too + if column.Default != "" && column.Type != "text" { end = " DEFAULT " - if adapter.stringy_type(column.Type) { + if adapter.stringy_type(column.Type) && column.Default != "''" { end += "'" + column.Default + "'" } else { end += column.Default @@ -79,7 +80,11 @@ func (adapter *Mysql_Adapter) CreateTable(name string, table string, charset str if len(keys) > 0 { for _, key := range keys { - querystr += "\n\t" + key.Type + " key(" + querystr += "\n\t" + key.Type + if key.Type != "unique" { + querystr += " key" + } + querystr += "(" for _, column := range strings.Split(key.Columns,",") { querystr += "`" + column + "`," } @@ -95,7 +100,7 @@ func (adapter *Mysql_Adapter) CreateTable(name string, table string, charset str querystr += " COLLATE " + collation } - adapter.push_statement(name,querystr + ";") + adapter.push_statement(name,"create-table",querystr + ";") return querystr + ";", nil } @@ -133,7 +138,7 @@ func (adapter *Mysql_Adapter) SimpleInsert(name string, table string, columns st } querystr = querystr[0:len(querystr) - 1] - adapter.push_statement(name,querystr + ")") + adapter.push_statement(name,"insert",querystr + ")") return querystr + ")", nil } @@ -170,7 +175,7 @@ func (adapter *Mysql_Adapter) SimpleReplace(name string, table string, columns s } querystr = querystr[0:len(querystr) - 1] - adapter.push_statement(name,querystr + ")") + adapter.push_statement(name,"replace",querystr + ")") return querystr + ")", nil } @@ -225,7 +230,7 @@ func (adapter *Mysql_Adapter) SimpleUpdate(name string, table string, set string querystr = querystr[0:len(querystr) - 4] } - adapter.push_statement(name,querystr) + adapter.push_statement(name,"update",querystr) return querystr, nil } @@ -260,7 +265,7 @@ func (adapter *Mysql_Adapter) SimpleDelete(name string, table string, where stri } querystr = strings.TrimSpace(querystr[0:len(querystr) - 4]) - adapter.push_statement(name,querystr) + adapter.push_statement(name,"delete",querystr) return querystr, nil } @@ -272,7 +277,7 @@ func (adapter *Mysql_Adapter) Purge(name string, table string) (string, error) { if table == "" { return "", errors.New("You need a name for this table") } - adapter.push_statement(name,"DELETE FROM `" + table + "`") + adapter.push_statement(name,"purge","DELETE FROM `" + table + "`") return "DELETE FROM `" + table + "`", nil } @@ -335,7 +340,7 @@ func (adapter *Mysql_Adapter) SimpleSelect(name string, table string, columns st } querystr = strings.TrimSpace(querystr) - adapter.push_statement(name,querystr) + adapter.push_statement(name,"select",querystr) return querystr, nil } @@ -425,7 +430,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2 } querystr = strings.TrimSpace(querystr) - adapter.push_statement(name,querystr) + adapter.push_statement(name,"select",querystr) return querystr, nil } @@ -515,7 +520,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2 } querystr = strings.TrimSpace(querystr) - adapter.push_statement(name,querystr) + adapter.push_statement(name,"select",querystr) return querystr, nil } @@ -589,7 +594,7 @@ func (adapter *Mysql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel } querystr = strings.TrimSpace(querystr) - adapter.push_statement(name,querystr) + adapter.push_statement(name,"insert",querystr) return querystr, nil } @@ -674,7 +679,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s } querystr = strings.TrimSpace(querystr) - adapter.push_statement(name,querystr) + adapter.push_statement(name,"insert",querystr) return querystr, nil } @@ -759,7 +764,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert, } querystr = strings.TrimSpace(querystr) - adapter.push_statement(name,querystr) + adapter.push_statement(name,"insert",querystr) return querystr, nil } @@ -802,34 +807,38 @@ func (adapter *Mysql_Adapter) SimpleCount(name string, table string, where strin } querystr = strings.TrimSpace(querystr) - adapter.push_statement(name,querystr) + adapter.push_statement(name,"select",querystr) return querystr, nil } func (adapter *Mysql_Adapter) Write() error { var stmts, body string - for _, name := range adapter.BufferOrder { - stmts += "var " + name + "_stmt *sql.Stmt\n" - body += ` + stmt := adapter.Buffer[name] + // TO-DO: Add support for create-table? Table creation might be a little complex for Go to do outside a SQL file :( + if stmt.Type != "create-table" { + stmts += "var " + name + "_stmt *sql.Stmt\n" + body += ` log.Print("Preparing ` + name + ` statement.") - ` + name + `_stmt, err = db.Prepare("` + adapter.Buffer[name] + `") + ` + name + `_stmt, err = db.Prepare("` + stmt.Contents + `") if err != nil { return err } ` + } } - out := `// Code generated by Gosora. More below: + out := `// +build !pgsql !sqlite !mssql + /* This file was generated by Gosora's Query Generator. Please try to avoid modifying this file, as it might change at any time. */ -// +build !pgsql !sqlite !mssql + package main import "log" import "database/sql" ` + stmts + ` -func gen_mysql() (err error) { +func _gen_mysql() (err error) { if debug_mode { log.Print("Building the generated statements") } @@ -841,8 +850,8 @@ func gen_mysql() (err error) { } // Internal methods, not exposed in the interface -func (adapter *Mysql_Adapter) push_statement(name string, querystr string) { - adapter.Buffer[name] = querystr +func (adapter *Mysql_Adapter) push_statement(name string, stype string, querystr string) { + adapter.Buffer[name] = DB_Stmt{querystr,stype} adapter.BufferOrder = append(adapter.BufferOrder,name) } diff --git a/query_gen/lib/pgsql.go b/query_gen/lib/pgsql.go new file mode 100644 index 00000000..2010098a --- /dev/null +++ b/query_gen/lib/pgsql.go @@ -0,0 +1,338 @@ +/* WIP Under *Heavy* Construction */ +package qgen + +import "strings" +import "strconv" +import "errors" + +func init() { + DB_Registry = append(DB_Registry, + &Pgsql_Adapter{Name:"pgsql",Buffer:make(map[string]DB_Stmt)}, + ) +} + +type Pgsql_Adapter struct +{ + Name string + Buffer map[string]DB_Stmt + BufferOrder []string // Map iteration order is random, so we need this to track the order, so we don't get huge diffs every commit +} + +func (adapter *Pgsql_Adapter) GetName() string { + return adapter.Name +} + +func (adapter *Pgsql_Adapter) GetStmt(name string) DB_Stmt { + return adapter.Buffer[name] +} + +func (adapter *Pgsql_Adapter) GetStmts() map[string]DB_Stmt { + return adapter.Buffer +} + +// TO-DO: Implement this +// We may need to change the CreateTable API to better suit PGSQL and the other database drivers which are coming up +func (adapter *Pgsql_Adapter) CreateTable(name string, table string, charset string, collation string, columns []DB_Table_Column, keys []DB_Table_Key) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if len(columns) == 0 { + return "", errors.New("You can't have a table with no columns") + } + + var querystr string = "CREATE TABLE `" + table + "` (" + for _, column := range columns { + if column.Auto_Increment { + column.Type = "serial" + } else if column.Type == "createdAt" { + column.Type = "timestamp" + } else if column.Type == "datetime" { + column.Type = "timestamp" + } + + var size string + if column.Size > 0 { + size = " (" + strconv.Itoa(column.Size) + ")" + } + + var end string + if column.Default != "" { + end = " DEFAULT " + if adapter.stringy_type(column.Type) && column.Default != "''" { + end += "'" + column.Default + "'" + } else { + end += column.Default + } + } + + if !column.Null { + end += " not null" + } + + querystr += "\n\t`"+column.Name+"` " + column.Type + size + end + "," + } + + if len(keys) > 0 { + for _, key := range keys { + querystr += "\n\t" + key.Type + if key.Type != "unique" { + querystr += " key" + } + querystr += "(" + for _, column := range strings.Split(key.Columns,",") { + querystr += "`" + column + "`," + } + querystr = querystr[0:len(querystr) - 1] + ")," + } + } + + querystr = querystr[0:len(querystr) - 1] + "\n);" + adapter.push_statement(name,"create-table",querystr) + return querystr, nil +} + +// TO-DO: Implement this +func (adapter *Pgsql_Adapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleInsert") + } + if len(fields) == 0 { + return "", errors.New("No input data found for SimpleInsert") + } + return "", nil +} + +// TO-DO: Implement this +func (adapter *Pgsql_Adapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleInsert") + } + if len(fields) == 0 { + return "", errors.New("No input data found for SimpleInsert") + } + return "", nil +} + +// TO-DO: Implemented, but we need CreateTable and a better installer to *test* it +func (adapter *Pgsql_Adapter) SimpleUpdate(name string, table string, set string, where string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if set == "" { + return "", errors.New("You need to set data in this update statement") + } + var querystr string = "UPDATE `" + table + "` SET " + for _, item := range _process_set(set) { + querystr += "`" + item.Column + "` =" + for _, token := range item.Expr { + switch(token.Type) { + case "function","operator","number","substitute": + querystr += " " + token.Contents + "" + case "column": + querystr += " `" + token.Contents + "`" + case "string": + querystr += " '" + token.Contents + "'" + } + } + querystr += "," + } + + // Remove the trailing comma + querystr = querystr[0:len(querystr) - 1] + + // Add support for BETWEEN x.x + if len(where) != 0 { + querystr += " WHERE" + for _, loc := range _process_where(where) { + for _, token := range loc.Expr { + switch(token.Type) { + case "function","operator","number","substitute": + querystr += " " + token.Contents + "" + case "column": + querystr += " `" + token.Contents + "`" + case "string": + querystr += " '" + token.Contents + "'" + default: + panic("This token doesn't exist o_o") + } + } + querystr += " AND" + } + querystr = querystr[0:len(querystr) - 4] + } + + adapter.push_statement(name,"update",querystr) + return querystr, nil +} + +// TO-DO: Implement this +func (adapter *Pgsql_Adapter) SimpleDelete(name string, table string, where string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if where == "" { + return "", errors.New("You need to specify what data you want to delete") + } + return "", nil +} + +// TO-DO: Implement this +// We don't want to accidentally wipe tables, so we'll have a seperate method for purging tables instead +func (adapter *Pgsql_Adapter) Purge(name string, table string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + return "", nil +} + +// TO-DO: Implement this +func (adapter *Pgsql_Adapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleSelect") + } + return "", nil +} + +// TO-DO: Implement this +func (adapter *Pgsql_Adapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table1 == "" { + return "", errors.New("You need a name for the left table") + } + if table2 == "" { + return "", errors.New("You need a name for the right table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleLeftJoin") + } + if len(joiners) == 0 { + return "", errors.New("No joiners found for SimpleLeftJoin") + } + return "", nil +} + +// TO-DO: Implement this +func (adapter *Pgsql_Adapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table1 == "" { + return "", errors.New("You need a name for the left table") + } + if table2 == "" { + return "", errors.New("You need a name for the right table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleInnerJoin") + } + if len(joiners) == 0 { + return "", errors.New("No joiners found for SimpleInnerJoin") + } + return "", nil +} + +// TO-DO: Implement this +func (adapter *Pgsql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel DB_Select) (string, error) { + return "", nil +} + +// TO-DO: Implement this +func (adapter *Pgsql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, sel DB_Join) (string, error) { + return "", nil +} + +// TO-DO: Implement this +func (adapter *Pgsql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert, sel DB_Join) (string, error) { + return "", nil +} + +// TO-DO: Implement this +func (adapter *Pgsql_Adapter) SimpleCount(name string, table string, where string, limit string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + return "", nil +} + +func (adapter *Pgsql_Adapter) Write() error { + var stmts, body string + for _, name := range adapter.BufferOrder { + stmt := adapter.Buffer[name] + // TO-DO: Add support for create-table? Table creation might be a little complex for Go to do outside a SQL file :( + if stmt.Type != "create-table" { + stmts += "var " + name + "_stmt *sql.Stmt\n" + body += ` + log.Print("Preparing ` + name + ` statement.") + ` + name + `_stmt, err = db.Prepare("` + stmt.Contents + `") + if err != nil { + return err + } + ` + } + } + + out := `// +build pgsql + +// This file was generated by Gosora's Query Generator. Please try to avoid modifying this file, as it might change at any time. +package main + +import "log" +import "database/sql" + +` + stmts + ` +func _gen_pgsql() (err error) { + if debug_mode { + log.Print("Building the generated statements") + } +` + body + ` + return nil +} +` + return write_file("./gen_pgsql.go", out) +} + +// Internal methods, not exposed in the interface +func (adapter *Pgsql_Adapter) push_statement(name string, stype string, querystr string) { + adapter.Buffer[name] = DB_Stmt{querystr,stype} + adapter.BufferOrder = append(adapter.BufferOrder,name) +} + +func (adapter *Pgsql_Adapter) stringy_type(ctype string) bool { + ctype = strings.ToLower(ctype) + return ctype == "char" || ctype == "varchar" || ctype == "timestamp" || ctype == "text" +} diff --git a/query_gen/lib/querygen.go b/query_gen/lib/querygen.go index 572dbe27..1dac170e 100644 --- a/query_gen/lib/querygen.go +++ b/query_gen/lib/querygen.go @@ -98,6 +98,12 @@ type DB_Limit struct { MaxCount string // ? or int } +type DB_Stmt struct +{ + Contents string + Type string // create-table, insert, update, delete +} + type DB_Adapter interface { GetName() string CreateTable(name string, table string, charset string, collation string, columns []DB_Table_Column, keys []DB_Table_Key) (string, error) diff --git a/query_gen/main.go b/query_gen/main.go index a09a9186..032d1084 100644 --- a/query_gen/main.go +++ b/query_gen/main.go @@ -8,13 +8,25 @@ func main() { log.Println("Running the query generator") for _, adapter := range qgen.DB_Registry { log.Println("Building the queries for the " + adapter.GetName() + " adapter") + qgen.Install.SetAdapterInstance(adapter) write_statements(adapter) + qgen.Install.Write() adapter.Write() } } func write_statements(adapter qgen.DB_Adapter) error { - err := write_selects(adapter) + err := create_tables(adapter) + if err != nil { + return err + } + + err = seed_tables(adapter) + if err != nil { + return err + } + + err = write_selects(adapter) if err != nil { return err } @@ -72,6 +84,45 @@ func write_statements(adapter qgen.DB_Adapter) error { return nil } +func create_tables(adapter qgen.DB_Adapter) error { + qgen.Install.CreateTable("users","utf8mb4","utf8mb4_general_ci", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"uid","int",0,false,true,""}, + qgen.DB_Table_Column{"name","varchar",100,false,false,""}, + qgen.DB_Table_Column{"password","varchar",100,false,false,""}, + qgen.DB_Table_Column{"salt","varchar",80,false,false,"''"}, + qgen.DB_Table_Column{"group","int",0,false,false,""}, + qgen.DB_Table_Column{"active","boolean",0,false,false,"0"}, + qgen.DB_Table_Column{"is_super_admin","boolean",0,false,false,"0"}, + qgen.DB_Table_Column{"createdAt","createdAt",0,false,false,""}, + qgen.DB_Table_Column{"lastActiveAt","datetime",0,false,false,""}, + qgen.DB_Table_Column{"session","varchar",200,false,false,"''"}, + qgen.DB_Table_Column{"last_ip","varchar",200,false,false,"0.0.0.0.0"}, + qgen.DB_Table_Column{"email","varchar",200,false,false,"''"}, + qgen.DB_Table_Column{"avatar","varchar",100,false,false,"''"}, + qgen.DB_Table_Column{"message","text",0,false,false,"''"}, + qgen.DB_Table_Column{"url_prefix","varchar",20,false,false,"''"}, + qgen.DB_Table_Column{"url_name","varchar",100,false,false,"''"}, + qgen.DB_Table_Column{"level","smallint",0,false,false,"0"}, + qgen.DB_Table_Column{"score","int",0,false,false,"0"}, + qgen.DB_Table_Column{"posts","int",0,false,false,"0"}, + qgen.DB_Table_Column{"bigposts","int",0,false,false,"0"}, + qgen.DB_Table_Column{"megaposts","int",0,false,false,"0"}, + qgen.DB_Table_Column{"topics","int",0,false,false,"0"}, + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"uid","primary"}, + qgen.DB_Table_Key{"name","unique"}, + }, + ) + + return nil +} + +func seed_tables(adapter qgen.DB_Adapter) error { + return nil +} + func write_selects(adapter qgen.DB_Adapter) error { // url_prefix and url_name will be removed from this query in a later commit adapter.SimpleSelect("get_user","users","name, group, is_super_admin, avatar, message, url_prefix, url_name, level","uid = ?","","") diff --git a/routes.go b/routes.go index b1ab4045..c8e683fb 100644 --- a/routes.go +++ b/routes.go @@ -17,7 +17,6 @@ import ( "html/template" "./query_gen/lib" - "golang.org/x/crypto/bcrypt" ) // A blank list to fill out that parameter in Page for routes which don't use it @@ -68,22 +67,28 @@ func route_fstatic(w http.ResponseWriter, r *http.Request){ http.ServeFile(w,r,r.URL.Path) }*/ -func route_overview(w http.ResponseWriter, r *http.Request){ - user, headerVars, ok := SessionCheck(w,r) +func route_overview(w http.ResponseWriter, r *http.Request, user User){ + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } - BuildWidgets("overview",nil,&headerVars) + BuildWidgets("overview",nil,&headerVars,r) pi := Page{"Overview",user,headerVars,tList,nil} + if pre_render_hooks["pre_render_overview"] != nil { + if run_pre_render_hook("pre_render_overview", w, r, &user, &pi) { + return + } + } + err := templates.ExecuteTemplate(w,"overview.html",pi) if err != nil { InternalError(err,w,r) } } -func route_custom_page(w http.ResponseWriter, r *http.Request){ - user, headerVars, ok := SessionCheck(w,r) +func route_custom_page(w http.ResponseWriter, r *http.Request, user User){ + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -93,20 +98,27 @@ func route_custom_page(w http.ResponseWriter, r *http.Request){ NotFound(w,r) return } - BuildWidgets("custom_page",name,&headerVars) + BuildWidgets("custom_page",name,&headerVars,r) - err := templates.ExecuteTemplate(w,"page_" + name,Page{"Page",user,headerVars,tList,nil}) + pi := Page{"Page",user,headerVars,tList,nil} + if pre_render_hooks["pre_render_custom_page"] != nil { + if run_pre_render_hook("pre_render_custom_page", w, r, &user, &pi) { + return + } + } + + err := templates.ExecuteTemplate(w,"page_" + name,pi) if err != nil { InternalError(err,w,r) } } -func route_topics(w http.ResponseWriter, r *http.Request){ - user, headerVars, ok := SessionCheck(w,r) +func route_topics(w http.ResponseWriter, r *http.Request, user User){ + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } - BuildWidgets("topics",nil,&headerVars) + BuildWidgets("topics",nil,&headerVars,r) var qlist string var fidList []interface{} @@ -151,8 +163,10 @@ func route_topics(w http.ResponseWriter, r *http.Request){ topicItem.Avatar = strings.Replace(noavatar,"{id}",strconv.Itoa(topicItem.CreatedBy),1) } + forum := fstore.DirtyGet(topicItem.ParentID) if topicItem.ParentID >= 0 { - topicItem.ForumName = fstore.DirtyGet(topicItem.ParentID).Name + topicItem.ForumName = forum.Name + topicItem.ForumLink = forum.Link } else { topicItem.ForumName = "" } @@ -166,8 +180,8 @@ func route_topics(w http.ResponseWriter, r *http.Request){ InternalError(err,w,r) } - if hooks["trow_assign"] != nil { - topicItem = run_hook("trow_assign", topicItem).(TopicsRow) + if hooks["topics_trow_assign"] != nil { + run_vhook("topics_trow_assign", &topicItem, &forum) } topicList = append(topicList, topicItem) } @@ -179,6 +193,12 @@ func route_topics(w http.ResponseWriter, r *http.Request){ rows.Close() pi := TopicsPage{"Topic List",user,headerVars,topicList,extData} + if pre_render_hooks["pre_render_topic_list"] != nil { + if run_pre_render_hook("pre_render_topic_list", w, r, &user, &pi) { + return + } + } + if template_topics_handle != nil { template_topics_handle(pi,w) } else { @@ -193,7 +213,7 @@ func route_topics(w http.ResponseWriter, r *http.Request){ } } -func route_forum(w http.ResponseWriter, r *http.Request, sfid string){ +func route_forum(w http.ResponseWriter, r *http.Request, user User, sfid string){ page, _ := strconv.Atoi(r.FormValue("page")) // SEO URLs... @@ -207,7 +227,7 @@ func route_forum(w http.ResponseWriter, r *http.Request, sfid string){ return } - user, headerVars, ok := ForumSessionCheck(w,r,fid) + headerVars, ok := ForumSessionCheck(w,r,&user,fid) if !ok { return } @@ -227,7 +247,7 @@ func route_forum(w http.ResponseWriter, r *http.Request, sfid string){ return } - BuildWidgets("view_forum",&forum,&headerVars) + BuildWidgets("view_forum",forum,&headerVars,r) // Calculate the offset var offset int @@ -271,8 +291,8 @@ func route_forum(w http.ResponseWriter, r *http.Request, sfid string){ InternalError(err,w,r) } - if hooks["trow_assign"] != nil { - topicItem = run_hook("trow_assign", topicItem).(TopicUser) + if hooks["forum_trow_assign"] != nil { + run_vhook("forum_trow_assign", &topicItem, &forum) } topicList = append(topicList, topicItem) } @@ -284,6 +304,12 @@ func route_forum(w http.ResponseWriter, r *http.Request, sfid string){ rows.Close() pi := ForumPage{forum.Name,user,headerVars,topicList,*forum,page,last_page,extData} + if pre_render_hooks["pre_render_view_forum"] != nil { + if run_pre_render_hook("pre_render_view_forum", w, r, &user, &pi) { + return + } + } + if template_forum_handle != nil { template_forum_handle(pi,w) } else { @@ -298,21 +324,33 @@ func route_forum(w http.ResponseWriter, r *http.Request, sfid string){ } } -func route_forums(w http.ResponseWriter, r *http.Request){ - user, headerVars, ok := SessionCheck(w,r) +func route_forums(w http.ResponseWriter, r *http.Request, user User){ + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } - BuildWidgets("forums",nil,&headerVars) + BuildWidgets("forums",nil,&headerVars,r) - var forumList []Forum var err error - group := groups[user.Group] - //fmt.Println(group.CanSee) - for _, fid := range group.CanSee { + var forumList []Forum + var canSee []int + if user.Is_Super_Admin { + canSee, err = fstore.GetAllIDs() + if err != nil { + InternalError(err,w,r) + return + } + //fmt.Println("canSee",canSee) + } else { + group := groups[user.Group] + canSee = group.CanSee + //fmt.Println("group.CanSee",group.CanSee) + } + + for _, fid := range canSee { //fmt.Println(forums[fid]) var forum Forum = *fstore.DirtyGet(fid) - if forum.Active && forum.Name != "" { + if forum.Active && forum.Name != "" && forum.ParentID == 0 { if forum.LastTopicID != 0 { forum.LastTopicTime, err = relative_time(forum.LastTopicTime) if err != nil { @@ -322,11 +360,20 @@ func route_forums(w http.ResponseWriter, r *http.Request){ forum.LastTopic = "None" forum.LastTopicTime = "" } + if hooks["forums_frow_assign"] != nil { + run_hook("forums_frow_assign", &forum) + } forumList = append(forumList, forum) } } pi := ForumsPage{"Forum List",user,headerVars,forumList,extData} + if pre_render_hooks["pre_render_forum_list"] != nil { + if run_pre_render_hook("pre_render_forum_list", w, r, &user, &pi) { + return + } + } + if template_forums_handle != nil { template_forums_handle(pi,w) } else { @@ -341,7 +388,7 @@ func route_forums(w http.ResponseWriter, r *http.Request){ } } -func route_topic_id(w http.ResponseWriter, r *http.Request){ +func route_topic_id(w http.ResponseWriter, r *http.Request, user User){ var err error var page, offset int var replyList []Reply @@ -371,7 +418,7 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){ } topic.Css = no_css_tmpl - user, headerVars, ok := ForumSessionCheck(w,r,topic.ParentID) + headerVars, ok := ForumSessionCheck(w,r,&user,topic.ParentID) if !ok { return } @@ -381,7 +428,7 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){ return } - BuildWidgets("view_topic",&topic,&headerVars) + BuildWidgets("view_topic",&topic,&headerVars,r) topic.Content = parse_message(topic.Content) topic.ContentLines = strings.Count(topic.Content,"\n") @@ -500,8 +547,9 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){ } replyItem.Liked = false + // TO-DO: Rename this to topic_rrow_assign if hooks["rrow_assign"] != nil { - replyItem = run_hook("rrow_assign", replyItem).(Reply) + run_hook("rrow_assign", &replyItem) } replyList = append(replyList, replyItem) } @@ -513,6 +561,12 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){ rows.Close() tpage := TopicPage{topic.Title,user,headerVars,replyList,topic,page,last_page,extData} + if pre_render_hooks["pre_render_view_topic"] != nil { + if run_pre_render_hook("pre_render_view_topic", w, r, &user, &tpage) { + return + } + } + if template_topic_handle != nil { template_topic_handle(tpage,w) } else { @@ -527,8 +581,8 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){ } } -func route_profile(w http.ResponseWriter, r *http.Request){ - user, headerVars, ok := SessionCheck(w,r) +func route_profile(w http.ResponseWriter, r *http.Request, user User){ + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -607,6 +661,8 @@ func route_profile(w http.ResponseWriter, r *http.Request){ replyLiked := false replyLikeCount := 0 + // TO-DO: Add a hook here + replyList = append(replyList, Reply{rid,puser.ID,replyContent,parse_message(replyContent),replyCreatedBy,name_to_slug(replyCreatedByName),replyCreatedByName,replyGroup,replyCreatedAt,replyLastEdit,replyLastEditBy,replyAvatar,replyCss,replyLines,replyTag,"","","",0,"",replyLiked,replyLikeCount,"",""}) } err = rows.Err() @@ -616,6 +672,12 @@ func route_profile(w http.ResponseWriter, r *http.Request){ } ppage := ProfilePage{puser.Name + "'s Profile",user,headerVars,replyList,*puser,extData} + if pre_render_hooks["pre_render_profile"] != nil { + if run_pre_render_hook("pre_render_profile", w, r, &user, &ppage) { + return + } + } + if template_profile_handle != nil { template_profile_handle(ppage,w) } else { @@ -626,7 +688,7 @@ func route_profile(w http.ResponseWriter, r *http.Request){ } } -func route_topic_create(w http.ResponseWriter, r *http.Request, sfid string){ +func route_topic_create(w http.ResponseWriter, r *http.Request, user User, sfid string){ var fid int var err error if sfid != "" { @@ -637,7 +699,7 @@ func route_topic_create(w http.ResponseWriter, r *http.Request, sfid string){ } } - user, headerVars, ok := ForumSessionCheck(w,r,fid) + headerVars, ok := ForumSessionCheck(w,r,&user,fid) if !ok { return } @@ -646,16 +708,56 @@ func route_topic_create(w http.ResponseWriter, r *http.Request, sfid string){ return } + BuildWidgets("create_topic",nil,&headerVars,r) + + // Lock this to the forum being linked? + // Should we always put it in strictmode when it's linked from another forum? Well, the user might end up changing their mind on what forum they want to post in and it would be a hassle, if they had to switch pages, even if it is a single click for many (exc. mobile) + var strictmode bool + if vhooks["topic_create_pre_loop"] != nil { + run_vhook("topic_create_pre_loop", w, r, fid, &headerVars, &user, &strictmode) + } + var forumList []Forum - group := groups[user.Group] - for _, fid := range group.CanSee { - forum := fstore.DirtyGet(fid) + var canSee []int + if user.Is_Super_Admin { + canSee, err = fstore.GetAllIDs() + if err != nil { + InternalError(err,w,r) + return + } + } else { + group := groups[user.Group] + canSee = group.CanSee + } + + // TO-DO: plugin_superadmin needs to be able to override this loop. Skip flag on topic_create_pre_loop? + for _, ffid := range canSee { + // TO-DO: Surely, there's a better way of doing this. I've added it in for now to support plugin_socialgroups, but we really need to clean this up + if strictmode && ffid != fid { + continue + } + + // Do a bulk forum fetch, just in case it's the SqlForumStore? + forum := fstore.DirtyGet(ffid) if forum.Active && forum.Name != "" { - forumList = append(forumList, *forum) + fcopy := *forum + if hooks["topic_create_frow_assign"] != nil { + // TO-DO: Add the skip feature to all the other row based hooks? + if run_hook("topic_create_frow_assign", &fcopy).(bool) { + continue + } + } + forumList = append(forumList, fcopy) } } ctpage := CreateTopicPage{"Create Topic",user,headerVars,forumList,fid,extData} + if pre_render_hooks["pre_render_create_topic"] != nil { + if run_pre_render_hook("pre_render_create_topic", w, r, &user, &ctpage) { + return + } + } + if template_create_topic_handle != nil { template_create_topic_handle(ctpage,w) } else { @@ -667,7 +769,7 @@ func route_topic_create(w http.ResponseWriter, r *http.Request, sfid string){ } // POST functions. Authorised users only. -func route_create_topic(w http.ResponseWriter, r *http.Request) { +func route_topic_create_submit(w http.ResponseWriter, r *http.Request, user User) { err := r.ParseForm() if err != nil { PreError("Bad Form",w,r) @@ -680,7 +782,7 @@ func route_create_topic(w http.ResponseWriter, r *http.Request) { return } - user, ok := SimpleForumSessionCheck(w,r,fid) + ok := SimpleForumSessionCheck(w,r,&user,fid) if !ok { return } @@ -734,7 +836,7 @@ func route_create_topic(w http.ResponseWriter, r *http.Request) { } } -func route_create_reply(w http.ResponseWriter, r *http.Request) { +func route_create_reply(w http.ResponseWriter, r *http.Request, user User) { err := r.ParseForm() if err != nil { PreError("Bad Form",w,r) @@ -755,7 +857,7 @@ func route_create_reply(w http.ResponseWriter, r *http.Request) { return } - user, ok := SimpleForumSessionCheck(w,r,topic.ParentID) + ok := SimpleForumSessionCheck(w,r,&user,topic.ParentID) if !ok { return } @@ -829,7 +931,7 @@ func route_create_reply(w http.ResponseWriter, r *http.Request) { } } -func route_like_topic(w http.ResponseWriter, r *http.Request) { +func route_like_topic(w http.ResponseWriter, r *http.Request, user User) { err := r.ParseForm() if err != nil { PreError("Bad Form",w,r) @@ -851,7 +953,7 @@ func route_like_topic(w http.ResponseWriter, r *http.Request) { return } - user, ok := SimpleForumSessionCheck(w,r,topic.ParentID) + ok := SimpleForumSessionCheck(w,r,&user,topic.ParentID) if !ok { return } @@ -929,7 +1031,7 @@ func route_like_topic(w http.ResponseWriter, r *http.Request) { http.Redirect(w,r,"/topic/" + strconv.Itoa(tid),http.StatusSeeOther) } -func route_reply_like_submit(w http.ResponseWriter, r *http.Request) { +func route_reply_like_submit(w http.ResponseWriter, r *http.Request, user User) { err := r.ParseForm() if err != nil { PreError("Bad Form",w,r) @@ -961,7 +1063,7 @@ func route_reply_like_submit(w http.ResponseWriter, r *http.Request) { return } - user, ok := SimpleForumSessionCheck(w,r,fid) + ok := SimpleForumSessionCheck(w,r,&user,fid) if !ok { return } @@ -1029,11 +1131,7 @@ func route_reply_like_submit(w http.ResponseWriter, r *http.Request) { http.Redirect(w,r,"/topic/" + strconv.Itoa(reply.ParentID),http.StatusSeeOther) } -func route_profile_reply_create(w http.ResponseWriter, r *http.Request) { - user, ok := SimpleSessionCheck(w,r) - if !ok { - return - } +func route_profile_reply_create(w http.ResponseWriter, r *http.Request, user User) { if !user.Loggedin || !user.Perms.CreateReply { NoPermissions(w,r,user) return @@ -1075,11 +1173,7 @@ func route_profile_reply_create(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/user/" + strconv.Itoa(uid), http.StatusSeeOther) } -func route_report_submit(w http.ResponseWriter, r *http.Request, sitem_id string) { - user, ok := SimpleSessionCheck(w,r) - if !ok { - return - } +func route_report_submit(w http.ResponseWriter, r *http.Request, user User, sitem_id string) { if !user.Loggedin { LoginRequired(w,r,user) return @@ -1216,8 +1310,8 @@ func route_report_submit(w http.ResponseWriter, r *http.Request, sitem_id string http.Redirect(w,r,"/topic/" + strconv.FormatInt(lastId, 10), http.StatusSeeOther) } -func route_account_own_edit_critical(w http.ResponseWriter, r *http.Request) { - user, headerVars, ok := SessionCheck(w,r) +func route_account_own_edit_critical(w http.ResponseWriter, r *http.Request, user User) { + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -1225,12 +1319,18 @@ func route_account_own_edit_critical(w http.ResponseWriter, r *http.Request) { LocalError("You need to login to edit your account.",w,r,user) return } + pi := Page{"Edit Password",user,headerVars,tList,nil} + if pre_render_hooks["pre_render_account_own_edit_critical"] != nil { + if run_pre_render_hook("pre_render_account_own_edit_critical", w, r, &user, &pi) { + return + } + } templates.ExecuteTemplate(w,"account-own-edit.html", pi) } -func route_account_own_edit_critical_submit(w http.ResponseWriter, r *http.Request) { - user, headerVars, ok := SessionCheck(w,r) +func route_account_own_edit_critical_submit(w http.ResponseWriter, r *http.Request, user User) { + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -1259,9 +1359,8 @@ func route_account_own_edit_critical_submit(w http.ResponseWriter, r *http.Reque return } - current_password = current_password + salt - err = bcrypt.CompareHashAndPassword([]byte(real_password), []byte(current_password)) - if err == bcrypt.ErrMismatchedHashAndPassword { + err = CheckPassword(real_password,current_password,salt) + if err == ErrMismatchedHashAndPassword { LocalError("That's not the correct password.",w,r,user) return } else if err != nil { @@ -1279,11 +1378,16 @@ func route_account_own_edit_critical_submit(w http.ResponseWriter, r *http.Reque headerVars.NoticeList = append(headerVars.NoticeList,"Your password was successfully updated") pi := Page{"Edit Password",user,headerVars,tList,nil} + if pre_render_hooks["pre_render_account_own_edit_critical"] != nil { + if run_pre_render_hook("pre_render_account_own_edit_critical", w, r, &user, &pi) { + return + } + } templates.ExecuteTemplate(w,"account-own-edit.html", pi) } -func route_account_own_edit_avatar(w http.ResponseWriter, r *http.Request) { - user, headerVars, ok := SessionCheck(w,r) +func route_account_own_edit_avatar(w http.ResponseWriter, r *http.Request, user User) { + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -1292,17 +1396,22 @@ func route_account_own_edit_avatar(w http.ResponseWriter, r *http.Request) { return } pi := Page{"Edit Avatar",user,headerVars,tList,nil} + if pre_render_hooks["pre_render_account_own_edit_avatar"] != nil { + if run_pre_render_hook("pre_render_account_own_edit_avatar", w, r, &user, &pi) { + return + } + } templates.ExecuteTemplate(w,"account-own-edit-avatar.html",pi) } -func route_account_own_edit_avatar_submit(w http.ResponseWriter, r *http.Request) { +func route_account_own_edit_avatar_submit(w http.ResponseWriter, r *http.Request, user User) { 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, headerVars, ok := SessionCheck(w,r) + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -1385,11 +1494,16 @@ func route_account_own_edit_avatar_submit(w http.ResponseWriter, r *http.Request headerVars.NoticeList = append(headerVars.NoticeList, "Your avatar was successfully updated") pi := Page{"Edit Avatar",user,headerVars,tList,nil} + if pre_render_hooks["pre_render_account_own_edit_avatar"] != nil { + if run_pre_render_hook("pre_render_account_own_edit_avatar", w, r, &user, &pi) { + return + } + } templates.ExecuteTemplate(w,"account-own-edit-avatar.html", pi) } -func route_account_own_edit_username(w http.ResponseWriter, r *http.Request) { - user, headerVars, ok := SessionCheck(w,r) +func route_account_own_edit_username(w http.ResponseWriter, r *http.Request, user User) { + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -1398,11 +1512,16 @@ func route_account_own_edit_username(w http.ResponseWriter, r *http.Request) { return } pi := Page{"Edit Username",user,headerVars,tList,user.Name} + if pre_render_hooks["pre_render_account_own_edit_username"] != nil { + if run_pre_render_hook("pre_render_account_own_edit_username", w, r, &user, &pi) { + return + } + } templates.ExecuteTemplate(w,"account-own-edit-username.html",pi) } -func route_account_own_edit_username_submit(w http.ResponseWriter, r *http.Request) { - user, headerVars, ok := SessionCheck(w,r) +func route_account_own_edit_username_submit(w http.ResponseWriter, r *http.Request, user User) { + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -1432,11 +1551,16 @@ func route_account_own_edit_username_submit(w http.ResponseWriter, r *http.Reque headerVars.NoticeList = append(headerVars.NoticeList,"Your username was successfully updated") pi := Page{"Edit Username",user,headerVars,tList,nil} + if pre_render_hooks["pre_render_account_own_edit_username"] != nil { + if run_pre_render_hook("pre_render_account_own_edit_username", w, r, &user, &pi) { + return + } + } templates.ExecuteTemplate(w,"account-own-edit-username.html", pi) } -func route_account_own_edit_email(w http.ResponseWriter, r *http.Request) { - user, headerVars, ok := SessionCheck(w,r) +func route_account_own_edit_email(w http.ResponseWriter, r *http.Request, user User) { + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -1482,11 +1606,16 @@ func route_account_own_edit_email(w http.ResponseWriter, r *http.Request) { headerVars.NoticeList = append(headerVars.NoticeList,"The mail system is currently disabled.") } pi := Page{"Email Manager",user,headerVars,emailList,nil} + if pre_render_hooks["pre_render_account_own_edit_email"] != nil { + if run_pre_render_hook("pre_render_account_own_edit_email", w, r, &user, &pi) { + return + } + } templates.ExecuteTemplate(w,"account-own-edit-email.html", pi) } -func route_account_own_edit_email_token_submit(w http.ResponseWriter, r *http.Request) { - user, headerVars, ok := SessionCheck(w,r) +func route_account_own_edit_email_token_submit(w http.ResponseWriter, r *http.Request, user User) { + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -1556,14 +1685,15 @@ func route_account_own_edit_email_token_submit(w http.ResponseWriter, r *http.Re } headerVars.NoticeList = append(headerVars.NoticeList,"Your email was successfully verified") pi := Page{"Email Manager",user,headerVars,emailList,nil} + if pre_render_hooks["pre_render_account_own_edit_email"] != nil { + if run_pre_render_hook("pre_render_account_own_edit_email", w, r, &user, &pi) { + return + } + } 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 { - return - } +func route_logout(w http.ResponseWriter, r *http.Request, user User) { if !user.Loggedin { LocalError("You can't logout without logging in first.",w,r,user) return @@ -1572,8 +1702,8 @@ func route_logout(w http.ResponseWriter, r *http.Request) { http.Redirect(w,r, "/", http.StatusSeeOther) } -func route_login(w http.ResponseWriter, r *http.Request) { - user, headerVars, ok := SessionCheck(w,r) +func route_login(w http.ResponseWriter, r *http.Request, user User) { + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -1582,14 +1712,15 @@ func route_login(w http.ResponseWriter, r *http.Request) { return } pi := Page{"Login",user,headerVars,tList,nil} + if pre_render_hooks["pre_render_login"] != nil { + if run_pre_render_hook("pre_render_login", w, r, &user, &pi) { + return + } + } templates.ExecuteTemplate(w,"login.html",pi) } -func route_login_submit(w http.ResponseWriter, r *http.Request) { - user, ok := SimpleSessionCheck(w,r) - if !ok { - return - } +func route_login_submit(w http.ResponseWriter, r *http.Request, user User) { if user.Loggedin { LocalError("You're already logged in.",w,r,user) return @@ -1621,8 +1752,8 @@ func route_login_submit(w http.ResponseWriter, r *http.Request) { http.Redirect(w,r,"/",http.StatusSeeOther) } -func route_register(w http.ResponseWriter, r *http.Request) { - user, headerVars, ok := SessionCheck(w,r) +func route_register(w http.ResponseWriter, r *http.Request, user User) { + headerVars, ok := SessionCheck(w,r,&user) if !ok { return } @@ -1630,14 +1761,16 @@ func route_register(w http.ResponseWriter, r *http.Request) { LocalError("You're already logged in.",w,r,user) return } - templates.ExecuteTemplate(w,"register.html",Page{"Registration",user,headerVars,tList,nil}) + pi := Page{"Registration",user,headerVars,tList,nil} + if pre_render_hooks["pre_render_register"] != nil { + if run_pre_render_hook("pre_render_register", w, r, &user, &pi) { + return + } + } + templates.ExecuteTemplate(w,"register.html",pi) } -func route_register_submit(w http.ResponseWriter, r *http.Request) { - user, ok := SimpleSessionCheck(w,r) - if !ok { - return - } +func route_register_submit(w http.ResponseWriter, r *http.Request, user User) { err := r.ParseForm() if err != nil { LocalError("Bad Form",w,r,user) @@ -1734,9 +1867,10 @@ func route_register_submit(w http.ResponseWriter, r *http.Request) { } var phrase_login_alerts []byte = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}]}`) -func route_api(w http.ResponseWriter, r *http.Request) { +func route_api(w http.ResponseWriter, r *http.Request, user User) { err := r.ParseForm() format := r.FormValue("format") + // TO-DO: Change is_js from a string to a boolean value var is_js string if format == "json" { is_js = "1" @@ -1748,11 +1882,6 @@ func route_api(w http.ResponseWriter, r *http.Request) { return } - user, ok := SimpleSessionCheck(w,r) - if !ok { - return - } - action := r.FormValue("action") if action != "get" && action != "set" { PreErrorJSQ("Invalid Action",w,r,is_js) diff --git a/schema/mysql/inserts.sql b/schema/mysql/inserts.sql new file mode 100644 index 00000000..e69de29b diff --git a/schema/mysql/query_users.sql b/schema/mysql/query_users.sql new file mode 100644 index 00000000..b66643b4 --- /dev/null +++ b/schema/mysql/query_users.sql @@ -0,0 +1,26 @@ +CREATE TABLE `users` ( + `uid` int not null AUTO_INCREMENT, + `name` varchar(100) not null, + `password` varchar(100) not null, + `salt` varchar(80) DEFAULT '' not null, + `group` int not null, + `active` boolean DEFAULT 0 not null, + `is_super_admin` boolean DEFAULT 0 not null, + `createdAt` datetime not null, + `lastActiveAt` datetime not null, + `session` varchar(200) DEFAULT '' not null, + `last_ip` varchar(200) DEFAULT '0.0.0.0.0' not null, + `email` varchar(200) DEFAULT '' not null, + `avatar` varchar(100) DEFAULT '' not null, + `message` text not null, + `url_prefix` varchar(20) DEFAULT '' not null, + `url_name` varchar(100) DEFAULT '' not null, + `level` smallint DEFAULT 0 not null, + `score` int DEFAULT 0 not null, + `posts` int DEFAULT 0 not null, + `bigposts` int DEFAULT 0 not null, + `megaposts` int DEFAULT 0 not null, + `topics` int DEFAULT 0 not null, + primary key(`uid`), + unique(`name`) +) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; \ No newline at end of file diff --git a/schema/pgsql/inserts.sql b/schema/pgsql/inserts.sql new file mode 100644 index 00000000..e69de29b diff --git a/schema/pgsql/query_users.sql b/schema/pgsql/query_users.sql new file mode 100644 index 00000000..a83cd243 --- /dev/null +++ b/schema/pgsql/query_users.sql @@ -0,0 +1,26 @@ +CREATE TABLE `users` ( + `uid` serial not null, + `name` varchar (100) not null, + `password` varchar (100) not null, + `salt` varchar (80) DEFAULT '' not null, + `group` int not null, + `active` boolean DEFAULT 0 not null, + `is_super_admin` boolean DEFAULT 0 not null, + `createdAt` timestamp not null, + `lastActiveAt` timestamp not null, + `session` varchar (200) DEFAULT '' not null, + `last_ip` varchar (200) DEFAULT '0.0.0.0.0' not null, + `email` varchar (200) DEFAULT '' not null, + `avatar` varchar (100) DEFAULT '' not null, + `message` text DEFAULT '' not null, + `url_prefix` varchar (20) DEFAULT '' not null, + `url_name` varchar (100) DEFAULT '' not null, + `level` smallint DEFAULT 0 not null, + `score` int DEFAULT 0 not null, + `posts` int DEFAULT 0 not null, + `bigposts` int DEFAULT 0 not null, + `megaposts` int DEFAULT 0 not null, + `topics` int DEFAULT 0 not null, + primary key(`uid`), + unique(`name`) +); \ No newline at end of file diff --git a/templates/socialgroups_member_list.html b/templates/socialgroups_member_list.html index b238fd62..b5f0025a 100644 --- a/templates/socialgroups_member_list.html +++ b/templates/socialgroups_member_list.html @@ -9,9 +9,9 @@
diff --git a/templates/socialgroups_view_group.html b/templates/socialgroups_view_group.html index acb865d5..ca4a80f0 100644 --- a/templates/socialgroups_view_group.html +++ b/templates/socialgroups_view_group.html @@ -9,10 +9,11 @@
diff --git a/themes/cosmo-conflux/public/main.css b/themes/cosmo-conflux/public/main.css index cb97ec9e..b5d94969 100644 --- a/themes/cosmo-conflux/public/main.css +++ b/themes/cosmo-conflux/public/main.css @@ -151,7 +151,6 @@ li:hover { hr { color: silver; border: 1px solid silver; } -/* I HATE CSS for being so incompetently designed that I have to declare this for THREE different elements rather than just having a statement go back up the tree. What on earth is the W3C doing?! */ .rowhead .rowitem, .colstack_head .rowitem, .opthead .rowitem { border-top: none; font-weight: bold; diff --git a/themes/cosmo/public/main.css b/themes/cosmo/public/main.css index dcababf3..7339c976 100644 --- a/themes/cosmo/public/main.css +++ b/themes/cosmo/public/main.css @@ -139,7 +139,6 @@ li:hover { hr { color: silver; border: 1px solid silver; } -/* I HATE CSS for being so incompetently designed that I have to declare this for THREE different elements rather than just having a statement go back up the tree. What on earth is the W3C doing?! */ .rowhead .rowitem, .opthead .rowitem, .colstack_head .rowitem { border-top: none; font-weight: bold; diff --git a/update-deps.bat b/update-deps.bat index da23d320..38da4f89 100644 --- a/update-deps.bat +++ b/update-deps.bat @@ -19,6 +19,13 @@ if %errorlevel% neq 0 ( exit /b %errorlevel% ) +echo Updating /x/system/windows (dependency for gopsutil) +go get -u golang.org/x/sys/windows +if %errorlevel% neq 0 ( + pause + exit /b %errorlevel% +) + echo Updating wmi (dependency for gopsutil) go get -u github.com/StackExchange/wmi if %errorlevel% neq 0 ( @@ -41,4 +48,4 @@ if %errorlevel% neq 0 ( ) echo The dependencies were successfully updated -pause \ No newline at end of file +pause diff --git a/user.go b/user.go index 256bae8b..5da904ac 100644 --- a/user.go +++ b/user.go @@ -12,13 +12,16 @@ import ( ) var guest_user User = User{ID:0,Group:6,Perms:GuestPerms} + var PreRoute func(http.ResponseWriter, *http.Request) (User,bool) = _pre_route var PanelSessionCheck func(http.ResponseWriter, *http.Request, *User) (HeaderVars,bool) = _panel_session_check var SimplePanelSessionCheck func(http.ResponseWriter, *http.Request, *User) bool = _simple_panel_session_check var SimpleForumSessionCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (success bool) = _simple_forum_session_check var ForumSessionCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (headerVars HeaderVars, success bool) = _forum_session_check var SessionCheck func(w http.ResponseWriter, r *http.Request, user *User) (headerVars HeaderVars, success bool) = _session_check + var CheckPassword func(real_password string, password string, salt string) (err error) = BcryptCheckPassword +var GeneratePassword func(password string) (hashed_password string, salt string, err error) = BcryptGeneratePassword type User struct { @@ -60,19 +63,35 @@ func BcryptCheckPassword(real_password string, password string, salt string) (er return bcrypt.CompareHashAndPassword([]byte(real_password), []byte(password + salt)) } -func SetPassword(uid int, password string) (error) { - salt, err := GenerateSafeString(saltLength) +// Investigate. Do we need the extra salt? +func BcryptGeneratePassword(password string) (hashed_password string, salt string, err error) { + salt, err = GenerateSafeString(saltLength) if err != nil { - return err + return "", "", err } password = password + salt + hashed_password, err = BcryptGeneratePasswordNoSalt(password) + if err != nil { + return "", "", err + } + return hashed_password, salt, nil +} + +func BcryptGeneratePasswordNoSalt(password string) (hash string, err error) { hashed_password, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashed_password), nil +} + +func SetPassword(uid int, password string) error { + hashed_password, salt, err := GeneratePassword(password) if err != nil { return err } - - _, err = set_password_stmt.Exec(string(hashed_password), salt, uid) + _, err = set_password_stmt.Exec(hashed_password, salt, uid) if err != nil { return err } @@ -120,6 +139,7 @@ func _simple_forum_session_check(w http.ResponseWriter, r *http.Request, user *U PreError("The target forum doesn't exist.",w,r) return false } + success = true // Is there a better way of doing the skip AND the success flag on this hook like multiple returns? if vhooks["simple_forum_check_pre_perms"] != nil {