diff --git a/.gitignore b/.gitignore index 13a268d5..a962e99b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,15 @@ tmp.txt run_notemplategen.bat brun.bat +attachs/* +!attachs/filler.txt uploads/avatar_* uploads/socialgroup_* backups/*.sql +node_modules/* bin/* out/* +logs/* *.exe *.exe~ *.prof diff --git a/attachs/filler.txt b/attachs/filler.txt new file mode 100644 index 00000000..20e14b1e --- /dev/null +++ b/attachs/filler.txt @@ -0,0 +1 @@ +This file is here so that Git will include this folder in the repository. \ No newline at end of file diff --git a/auth.go b/auth.go index 45b18224..20e8115e 100644 --- a/auth.go +++ b/auth.go @@ -117,6 +117,7 @@ func (auth *DefaultAuth) SetCookies(w http.ResponseWriter, uid int, session stri http.SetCookie(w, &cookie) } +// GetCookies fetches the current user's session cookies func (auth *DefaultAuth) GetCookies(r *http.Request) (uid int, session string, err error) { // Are there any session cookies..? cookie, err := r.Cookie("uid") @@ -134,6 +135,7 @@ func (auth *DefaultAuth) GetCookies(r *http.Request) (uid int, session string, e return uid, cookie.Value, err } +// SessionCheck checks if a user has session cookies and whether they're valid func (auth *DefaultAuth) SessionCheck(w http.ResponseWriter, r *http.Request) (user *User, halt bool) { uid, session, err := auth.GetCookies(r) if err != nil { @@ -156,6 +158,7 @@ func (auth *DefaultAuth) SessionCheck(w http.ResponseWriter, r *http.Request) (u return user, false } +// CreateSession generates a new session to allow a remote client to stay logged in as a specific user func (auth *DefaultAuth) CreateSession(uid int) (session string, err error) { session, err = GenerateSafeString(sessionLength) if err != nil { diff --git a/cache.go b/cache.go index 692b1652..34dd65c4 100644 --- a/cache.go +++ b/cache.go @@ -8,6 +8,7 @@ const CACHE_STATIC int = 0 const CACHE_DYNAMIC int = 1 const CACHE_SQL int = 2 +// nolint // ErrCacheDesync is thrown whenever a piece of data, for instance, a user is out of sync with the database. Currently unused. var ErrCacheDesync = errors.New("The cache is out of sync with the database.") // TODO: A cross-server synchronisation mechanism diff --git a/config.go b/config.go index b9846e8f..20c4daea 100644 --- a/config.go +++ b/config.go @@ -2,7 +2,8 @@ package main func init() { // Site Info - site.Name = "TS" + site.ShortName = "TS" // This should be less than three letters to fit in the navbar + site.Name = "Test Site" site.Email = "" site.URL = "localhost" site.Port = "8080" // 8080 @@ -47,7 +48,7 @@ func init() { config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png" config.ItemsPerPage = 25 - // Developer flag + // Developer flags dev.DebugMode = true //dev.SuperDebug = true //dev.TemplateDebug = true diff --git a/errors.go b/errors.go index dc501904..becb9d5b 100644 --- a/errors.go +++ b/errors.go @@ -230,6 +230,7 @@ func SecurityError(w http.ResponseWriter, r *http.Request, user User) { } } +// NotFound is used when the requested page doesn't exist // ? - Add a JSQ and JS version of this? // ? - Add a user parameter? func NotFound(w http.ResponseWriter, r *http.Request) { @@ -243,7 +244,7 @@ func NotFound(w http.ResponseWriter, r *http.Request) { } } -// nolint +// CustomError lets us make custom error types which aren't covered by the generic functions above func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User) { w.WriteHeader(errcode) pi := Page{errtitle, user, getDefaultHeaderVar(), tList, errmsg} @@ -258,7 +259,7 @@ func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWri } } -// nolint +// CustomErrorJSQ is a version of CustomError which lets us handle both JSON and regular pages depending on how it's being accessed func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User, isJs bool) { if !isJs { CustomError(errmsg, errcode, errtitle, w, r, user) @@ -267,7 +268,7 @@ func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.Response } } -// nolint +// CustomErrorJS is the pure JSON version of CustomError func CustomErrorJS(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User) { w.WriteHeader(errcode) _, _ = w.Write([]byte(`{"errmsg":"` + errmsg + `"}`)) diff --git a/experimental/center-experiment.css b/experimental/center-experiment.css deleted file mode 100644 index 22be1bae..00000000 --- a/experimental/center-experiment.css +++ /dev/null @@ -1,333 +0,0 @@ -* { - box-sizing: border-box; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; -} - -body -{ - font-family: arial; -} - -@font-face { - font-family: 'EmojiFont'; - src: url('https://github.com/Ranks/emojione/raw/master/assets/fonts/emojione-svg.woff2') format('woff2'), - url('https://github.com/Ranks/emojione/raw/master/assets/fonts/emojione-svg.woff') format('woff'), local("arial"); -} - -@supports (-ms-ime-align:auto) { -.user_content -{ - font-family: EmojiFont, arial; -} -} -@-moz-document url-prefix() { -.user_content -{ - font-family: EmojiFont, arial; -} -} - -.move_left -{ - float: left; - position: relative; - left: 50%; -} -.move_right -{ - float: left; - position: relative; - left: -50%; -} -ul -{ - padding-left: 0px; - padding-right: 0px; - height: 28px; - list-style-type: none; -} -li -{ - height: 28px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; - padding-left: 10px; - padding-top: 5px; - padding-bottom: 5px; - font-weight: bold; - text-transform: uppercase; -} -li a -{ - text-decoration: none; - color: #515151; -} -li a:hover -{ - color: #7a7a7a; -} -.menu_left -{ - float: left; - border-right: 1px solid #ccc; - padding-right: 10px; -} -.menu_left:first-child -{ - border-left: 1px solid #ccc -} -.menu_right -{ - float: right; - border-left: 1px solid #ccc; - padding-right: 10px; -} - -.container -{ - width: 90%; - padding: 0px; - margin-left: auto; - margin-right: auto; -} - -.rowblock -{ - border: 1px solid #ccc; - width: 100%; - padding: 0px; - padding-top: 0px; -} -.rowblock:empty -{ - display: none; -} - -.colblock_left -{ - border: 1px solid #ccc; - padding: 0px; - padding-top: 0px; - width: 30%; - float: left; - margin-right: 8px; -} -.colblock_right -{ - border: 1px solid #ccc; - padding: 0px; - padding-top: 0px; - width: 65%; - overflow: hidden; - word-wrap: break-word; -} -.colblock_left:empty -{ - display: none; -} -.colblock_right:empty -{ - display: none; -} - -.rowitem -{ - width: 100%; - padding-left: 8px; - padding-right: 8px; - padding-top: 17px; - padding-bottom: 12px; - font-weight: bold; - text-transform: uppercase; -} -.rowitem.passive -{ - font-weight: normal; - text-transform: none; -} -.rowitem:not(:last-child)/*:not(:only-child)*/ -{ - border-bottom: 1px dotted #ccc; -} -.rowitem a -{ - text-decoration: none; - color: black; -} -.rowitem a:hover -{ - color: silver; -} - -.col_left -{ - width: 30%; - float: left; -} -.col_right -{ - width: 69%; - overflow: hidden; -} -.colitem -{ - padding-left: 8px; - padding-right: 8px; - padding-top: 17px; - padding-bottom: 12px; - font-weight: bold; - text-transform: uppercase; -} -.colitem.passive -{ - font-weight: normal; - text-transform: none; -} -.colitem a -{ - text-decoration: none; - color: black; -} -.colitem a:hover -{ - color: silver; -} - -.formrow -{ - /*height: 40px;*/ - width: 100%; -} - -/*Clearfix*/ -.formrow:before, -.formrow:after { - content: " "; - display: table; -} - -.formrow:after { - clear: both; -} - -.formrow:not(:last-child) -{ - border-bottom: 1px dotted #ccc; -} - -.formitem -{ - float: left; - padding-left: 8px; - padding-right: 8px; - padding-top: 13px; - padding-bottom: 8px; - font-weight: bold; -} - -.formitem:first-child -{ - font-weight: bold; -} - -.formitem:not(:last-child) -{ - border-right: 1px dotted #ccc; -} - -.formitem.invisible_border -{ - border: none; -} - -/* Mostly for textareas */ -.formitem:only-child -{ - width: 97%; -} -.formitem textarea -{ - width: 100%; - height: 100px; -} -.formitem:has-child() -{ - margin: 0 auto; - float: none; -} - -button -{ - background: white; - border: 1px solid #8e8e8e; -} - -/* Topics */ -.topic_status -{ - text-transform: none; - margin-left: 8px; - padding-left: 2px; - padding-right: 2px; - padding-top: 2px; - padding-bottom: 2px; - background-color: #E8E8E8; /* 232,232,232. All three RGB colours being the same seems to create a shade of gray */ - color: #505050; /* 80,80,80 */ - border-radius: 2px; -} - -.topic_status:empty -{ - display: none; -} - -.username -{ - text-transform: none; - margin-left: 0px; - padding-left: 4px; - padding-right: 4px; - padding-top: 2px; - padding-bottom: 2px; - color: #505050; /* 80,80,80 */ - background-color: #FFFFFF; - border-style: dotted; - border-color: #505050; /* 232,232,232. All three RGB colours being the same seems to create a shade of gray */ - border-width: 1px; - font-size: 15px; -} -button.username -{ - position: relative; - top: -0.25px; -} - -.show_on_edit -{ - display: none; -} - -.alert -{ - display: block; - padding: 5px; - margin-bottom: 10px; - border: 1px solid #ccc; -} -.alert_success -{ - display: block; - padding: 5px; - border: 1px solid A2FC00; - margin-bottom: 10px; - background-color: DAF7A6; -} -.alert_error -{ - display: block; - padding: 5px; - border: 1px solid #FF004B; - margin-bottom: 8px; - background-color: #FEB7CC; -} \ No newline at end of file diff --git a/extend.go b/extend.go index f91980a6..02b5422d 100644 --- a/extend.go +++ b/extend.go @@ -32,6 +32,28 @@ var vhooks = map[string]func(...interface{}) interface{}{ "topic_create_pre_loop": nil, } +// Coming Soon: +type Message interface { + ID() int + Poster() int + Contents() string + ParsedContents() string +} + +// While the idea is nice, this might result in too much code duplication, as we have seventy billion page structs, what else could we do to get static typing with these in plugins? +type PageInt interface { + Title() string + HeaderVars() *HeaderVars + CurrentUser() *User + GetExtData(name string) interface{} + SetExtData(name string, contents interface{}) +} + +// Coming Soon: +var messageHooks = map[string][]func(Message, PageInt, ...interface{}) interface{}{ + "topic_reply_row_assign": nil, +} + // Hooks which take in and spit out a string. This is usually used for parser components var sshooks = map[string][]func(string) string{ "preparse_preassign": nil, diff --git a/forum.go b/forum.go index bb2c2728..736124a2 100644 --- a/forum.go +++ b/forum.go @@ -46,6 +46,7 @@ type ForumSimple struct { Preset string } +// Copy gives you a non-pointer concurrency safe copy of the forum func (forum *Forum) Copy() (fcopy Forum) { //forum.LastLock.RLock() fcopy = *forum diff --git a/forum_store.go b/forum_store.go index fa1144dc..fbac8d6c 100644 --- a/forum_store.go +++ b/forum_store.go @@ -43,14 +43,14 @@ type ForumStore interface { //GetFirstChild(parentID int, parentType string) (*Forum,error) Create(forumName string, forumDesc string, active bool, preset string) (int, error) - GetGlobalCount() int + GlobalCount() int } type ForumCache interface { CacheGet(id int) (*Forum, error) CacheSet(forum *Forum) error CacheDelete(id int) - GetLength() int + Length() int } // MemoryForumStore is a struct which holds an arbitrary number of forums in memory, usually all of them, although we might introduce functionality to hold a smaller subset in memory for sites with an extremely large number of forums @@ -385,7 +385,8 @@ func (mfs *MemoryForumStore) Create(forumName string, forumDesc string, active b } // ! Might be slightly inaccurate, if the sync.Map is constantly shifting and churning, but it'll stabilise eventually. Also, slow. Don't use this on every request x.x -func (mfs *MemoryForumStore) GetLength() (length int) { +// Length returns the number of forums in the memory cache +func (mfs *MemoryForumStore) Length() (length int) { mfs.forums.Range(func(_ interface{}, value interface{}) bool { length++ return true @@ -394,8 +395,8 @@ func (mfs *MemoryForumStore) GetLength() (length int) { } // TODO: Get the total count of forums in the forum store minus the blanked forums rather than doing a heavy query for this? -// GetGlobalCount returns the total number of forums -func (mfs *MemoryForumStore) GetGlobalCount() (fcount int) { +// GlobalCount returns the total number of forums +func (mfs *MemoryForumStore) GlobalCount() (fcount int) { err := mfs.getForumCount.QueryRow().Scan(&fcount) if err != nil { LogError(err) diff --git a/gen_mysql.go b/gen_mysql.go index 90afab99..a286023d 100644 --- a/gen_mysql.go +++ b/gen_mysql.go @@ -43,6 +43,7 @@ var groupEntryExistsStmt *sql.Stmt var getForumTopicsOffsetStmt *sql.Stmt var getExpiredScheduledGroupsStmt *sql.Stmt var getSyncStmt *sql.Stmt +var getAttachmentStmt *sql.Stmt var getTopicRepliesOffsetStmt *sql.Stmt var getTopicListStmt *sql.Stmt var getTopicUserStmt *sql.Stmt @@ -68,6 +69,7 @@ var addThemeStmt *sql.Stmt var createGroupStmt *sql.Stmt var addModlogEntryStmt *sql.Stmt var addAdminlogEntryStmt *sql.Stmt +var addAttachmentStmt *sql.Stmt var createWordFilterStmt *sql.Stmt var addForumPermsToGroupStmt *sql.Stmt var replaceScheduleGroupStmt *sql.Stmt @@ -341,6 +343,12 @@ func _gen_mysql() (err error) { return err } + log.Print("Preparing getAttachment statement.") + getAttachmentStmt, err = db.Prepare("SELECT `sectionID`,`sectionTable`,`originID`,`originTable`,`uploadedBy`,`path` FROM `attachments` WHERE `path` = ? AND `sectionID` = ? AND `sectionTable` = ?") + if err != nil { + return err + } + log.Print("Preparing getTopicRepliesOffset statement.") getTopicRepliesOffsetStmt, err = db.Prepare("SELECT `replies`.`rid`,`replies`.`content`,`replies`.`createdBy`,`replies`.`createdAt`,`replies`.`lastEdit`,`replies`.`lastEditBy`,`users`.`avatar`,`users`.`name`,`users`.`group`,`users`.`url_prefix`,`users`.`url_name`,`users`.`level`,`replies`.`ipaddress`,`replies`.`likeCount`,`replies`.`actionType` FROM `replies` LEFT JOIN `users` ON `replies`.`createdBy` = `users`.`uid` WHERE `tid` = ? LIMIT ?,?") if err != nil { @@ -491,6 +499,12 @@ func _gen_mysql() (err error) { return err } + log.Print("Preparing addAttachment statement.") + addAttachmentStmt, err = db.Prepare("INSERT INTO `attachments`(`sectionID`,`sectionTable`,`originID`,`originTable`,`uploadedBy`,`path`) VALUES (?,?,?,?,?,?)") + if err != nil { + return err + } + log.Print("Preparing createWordFilter statement.") createWordFilterStmt, err = db.Prepare("INSERT INTO `word_filters`(`find`,`replacement`) VALUES (?,?)") if err != nil { diff --git a/gen_router.go b/gen_router.go index 8fe0464e..71fe7cf0 100644 --- a/gen_router.go +++ b/gen_router.go @@ -107,6 +107,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { case "/theme": routeChangeTheme(w,req,user) return + case "/attachs": + routeShowAttachment(w,req,user,extra_data) + return case "/report": switch(req.URL.Path) { case "/report/submit/": diff --git a/general_test.go b/general_test.go index 8aa39f7c..f8fbda1b 100644 --- a/general_test.go +++ b/general_test.go @@ -895,36 +895,37 @@ func BenchmarkCustomRouterSerial(b *testing.B) { }) }*/ +// TODO: Take the attachment system into account in these parser benches func BenchmarkParserSerial(b *testing.B) { b.ReportAllocs() b.Run("empty_post", func(b *testing.B) { for i := 0; i < b.N; i++ { - _ = parseMessage("") + _ = parseMessage("", 0, "") } }) b.Run("short_post", func(b *testing.B) { for i := 0; i < b.N; i++ { - _ = parseMessage("Hey everyone, how's it going?") + _ = parseMessage("Hey everyone, how's it going?", 0, "") } }) b.Run("one_smily", func(b *testing.B) { for i := 0; i < b.N; i++ { - _ = parseMessage("Hey everyone, how's it going? :)") + _ = parseMessage("Hey everyone, how's it going? :)", 0, "") } }) b.Run("five_smilies", func(b *testing.B) { for i := 0; i < b.N; i++ { - _ = parseMessage("Hey everyone, how's it going? :):):):):)") + _ = parseMessage("Hey everyone, how's it going? :):):):):)", 0, "") } }) b.Run("ten_smilies", func(b *testing.B) { for i := 0; i < b.N; i++ { - _ = parseMessage("Hey everyone, how's it going? :):):):):):):):):):)") + _ = parseMessage("Hey everyone, how's it going? :):):):):):):):):):)", 0, "") } }) b.Run("twenty_smilies", func(b *testing.B) { for i := 0; i < b.N; i++ { - _ = parseMessage("Hey everyone, how's it going? :):):):):):):):):):):):):):):):):):):):)") + _ = parseMessage("Hey everyone, how's it going? :):):):):):):):):):):):):):):):):):):):)", 0, "") } }) } diff --git a/group.go b/group.go index 54c40686..640f9264 100644 --- a/group.go +++ b/group.go @@ -27,6 +27,8 @@ type Group struct { CanSee []int // The IDs of the forums this group can see } +// ! Ahem, don't listen to the comment below. It's not concurrency safe right now. +// Copy gives you a non-pointer concurrency safe copy of the group func (group *Group) Copy() Group { return *group } diff --git a/group_store.go b/group_store.go index ac623176..ad95ff74 100644 --- a/group_store.go +++ b/group_store.go @@ -12,6 +12,7 @@ var groupCreateMutex sync.Mutex var groupUpdateMutex sync.Mutex var gstore GroupStore +// ? - We could fallback onto the database when an item can't be found in the cache? type GroupStore interface { LoadGroups() error DirtyGet(id int) *Group @@ -23,6 +24,10 @@ type GroupStore interface { GetRange(lower int, higher int) ([]*Group, error) } +type GroupCache interface { + Length() int +} + type MemoryGroupStore struct { groups []*Group // TODO: Use a sync.Map instead of a slice groupCapCount int @@ -203,3 +208,7 @@ func (mgs *MemoryGroupStore) GetRange(lower int, higher int) (groups []*Group, e } return groups, nil } + +func (mgs *MemoryGroupStore) Length() int { + return len(mgs.groups) +} diff --git a/images/tempra-conflux-topic-list.png b/images/tempra-conflux-topic-list.png index b447237b..87a52cdf 100644 Binary files a/images/tempra-conflux-topic-list.png and b/images/tempra-conflux-topic-list.png differ diff --git a/images/tempra-conflux.png b/images/tempra-conflux.png index a01e09b5..8c7bf018 100644 Binary files a/images/tempra-conflux.png and b/images/tempra-conflux.png differ diff --git a/install/install.go b/install/install.go index 55cf90a8..ee640de0 100644 --- a/install/install.go +++ b/install/install.go @@ -28,12 +28,17 @@ var dbUsername string var dbPassword string var dbName string var dbPort string -var siteName, siteURL, serverPort string + +var siteShortName string +var siteName string +var siteURL string +var serverPort string var defaultAdapter = "mysql" var defaultHost = "localhost" var defaultUsername = "root" var defaultDbname = "gosora" +var defaultSiteShortName = "SN" var defaultSiteName = "Site Name" var defaultsiteURL = "localhost" var defaultServerPort = "80" // 8080's a good one, if you're testing and don't want it to clash with port 80 @@ -145,57 +150,58 @@ func main() { configContents := []byte(`package main func init() { -// Site Info -site.Name = "` + siteName + `" -site.Email = "" -site.URL = "` + siteURL + `" -site.Port = "` + serverPort + `" -site.EnableSsl = false -site.EnableEmails = false -site.HasProxy = false // Cloudflare counts as this, if it's sitting in the middle -config.SslPrivkey = "" -config.SslFullchain = "" -site.Language = "english" + // Site Info + site.ShortName = "` + siteShortName + `" // This should be less than three letters to fit in the navbar + site.Name = "` + siteName + `" + site.Email = "" + site.URL = "` + siteURL + `" + site.Port = "` + serverPort + `" + site.EnableSsl = false + site.EnableEmails = false + site.HasProxy = false // Cloudflare counts as this, if it's sitting in the middle + config.SslPrivkey = "" + config.SslFullchain = "" + site.Language = "english" -// Database details -dbConfig.Host = "` + dbHost + `" -dbConfig.Username = "` + dbUsername + `" -dbConfig.Password = "` + dbPassword + `" -dbConfig.Dbname = "` + dbName + `" -dbConfig.Port = "` + dbPort + `" // You probably won't need to change this + // Database details + dbConfig.Host = "` + dbHost + `" + dbConfig.Username = "` + dbUsername + `" + dbConfig.Password = "` + dbPassword + `" + dbConfig.Dbname = "` + dbName + `" + dbConfig.Port = "` + dbPort + `" // You probably won't need to change this -// Limiters -config.MaxRequestSize = 5 * megabyte + // Limiters + config.MaxRequestSize = 5 * megabyte -// Caching -config.CacheTopicUser = CACHE_STATIC -config.UserCacheCapacity = 120 // The max number of users held in memory -config.TopicCacheCapacity = 200 // The max number of topics held in memory + // Caching + config.CacheTopicUser = CACHE_STATIC + config.UserCacheCapacity = 120 // The max number of users held in memory + config.TopicCacheCapacity = 200 // The max number of topics held in memory -// Email -config.SMTPServer = "" -config.SMTPUsername = "" -config.SMTPPassword = "" -config.SMTPPort = "25" + // Email + config.SMTPServer = "" + config.SMTPUsername = "" + config.SMTPPassword = "" + config.SMTPPort = "25" -// Misc -config.DefaultRoute = routeTopics -config.DefaultGroup = 3 // Should be a setting in the database -config.ActivationGroup = 5 // Should be a setting in the database -config.StaffCSS = "staff_post" -config.DefaultForum = 2 -config.MinifyTemplates = true -config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features + // Misc + config.DefaultRoute = routeTopics + config.DefaultGroup = 3 // Should be a setting in the database + config.ActivationGroup = 5 // Should be a setting in the database + config.StaffCSS = "staff_post" + config.DefaultForum = 2 + config.MinifyTemplates = true + config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features -//config.Noavatar = "https://api.adorable.io/avatars/{width}/{id}@{site_url}.png" -config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png" -config.ItemsPerPage = 25 + //config.Noavatar = "https://api.adorable.io/avatars/{width}/{id}@{site_url}.png" + config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png" + config.ItemsPerPage = 25 -// Developer flag -dev.DebugMode = true -//dev.SuperDebug = true -//dev.TemplateDebug = true -//dev.Profiling = true + // Developer flags + dev.DebugMode = true + //dev.SuperDebug = true + //dev.TemplateDebug = true + //dev.Profiling = true } `) @@ -294,6 +300,17 @@ func getSiteDetails() bool { } fmt.Println("Set the site name to " + siteName) + // ? - We could compute this based on the first letter of each word in the site's name, if it's name spans multiple words. I'm not sure how to do this for single word names. + fmt.Println("Can we have a short abbreviation for your site? Default: " + defaultSiteShortName) + if !scanner.Scan() { + return false + } + siteShortName = scanner.Text() + if siteShortName == "" { + siteShortName = defaultSiteShortName + } + fmt.Println("Set the site name to " + siteShortName) + fmt.Println("What's your site's url? Default: " + defaultsiteURL) if !scanner.Scan() { return false diff --git a/langs/english.json b/langs/english.json index 80f8e589..afc9d649 100644 --- a/langs/english.json +++ b/langs/english.json @@ -23,7 +23,9 @@ "ManageThemes": "Can manage themes", "ManagePlugins": "Can manage plugins", "ViewAdminLogs": "Can view the administrator action logs", - "ViewIPs": "Can view IP addresses" + "ViewIPs": "Can view IP addresses", + + "UploadFiles": "Can upload files" }, "LocalPerms": { "ViewTopic": "Can view topics", diff --git a/main.go b/main.go index 2079420c..aee61d2a 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ const kilobyte int = 1024 const megabyte int = kilobyte * 1024 const gigabyte int = megabyte * 1024 const terabyte int = gigabyte * 1024 +const petabyte int = terabyte * 1024 const saltLength int = 32 const sessionLength int = 80 @@ -37,6 +38,30 @@ var startTime time.Time var externalSites = map[string]string{ "YT": "https://www.youtube.com/", } + +type StringList []string + +// ? - Should we allow users to upload .php or .go files? It could cause security issues. We could store them with a mangled extension to render them inert +// TODO: Let admins manage this from the Control Panel +var allowedFileExts = StringList{ + "png", "jpg", "jpeg", "svg", "bmp", "gif", + "txt", "xml", "json", "yaml", "js", "py", "rb", + "mp3", "mp4", "avi", "wmv", +} +var imageFileExts = StringList{ + "png", "jpg", "jpeg", "svg", "bmp", "gif", +} + +// TODO: Write a test for this +func (slice StringList) Contains(needle string) bool { + for _, item := range slice { + if item == needle { + return true + } + } + return false +} + var staticFiles = make(map[string]SFile) var logWriter = io.MultiWriter(os.Stderr) diff --git a/member_routes.go b/member_routes.go index 3d8b5f8a..ada8cbea 100644 --- a/member_routes.go +++ b/member_routes.go @@ -1,12 +1,15 @@ package main import ( + "crypto/sha256" + "encoding/hex" "html" "io" "log" "net" "net/http" "os" + "path/filepath" "regexp" "strconv" "strings" @@ -101,9 +104,17 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid st // POST functions. Authorised users only. func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { - err := r.ParseForm() + // TODO: Reduce this to 1MB for attachments for each file? + if r.ContentLength > int64(config.MaxRequestSize) { + size, unit := convertByteUnit(float64(config.MaxRequestSize)) + CustomError("Your attachments are too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, user) + return + } + r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxRequestSize)) + + err := r.ParseMultipartForm(int64(megabyte)) if err != nil { - PreError("Bad Form", w, r) + LocalError("Unable to parse the form", w, r, user) return } @@ -131,35 +142,110 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { return } - wcount := wordCount(content) - res, err := createTopicStmt.Exec(fid, topicName, content, parseMessage(content), user.ID, ipaddress, wcount, user.ID) + tid, err := topics.Create(fid, topicName, content, user.ID, ipaddress) if err != nil { - InternalError(err, w) + switch err { + case ErrNoRows: + LocalError("Something went wrong, perhaps the forum got deleted?", w, r, user) + case ErrNoTitle: + LocalError("This topic doesn't have a title", w, r, user) + case ErrNoBody: + LocalError("This topic doesn't have a body", w, r, user) + default: + InternalError(err, w) + } return } - lastID, err := res.LastInsertId() + + _, err = addSubscriptionStmt.Exec(user.ID, tid, "topic") if err != nil { InternalError(err, w) return } - _, err = addSubscriptionStmt.Exec(user.ID, lastID, "topic") + err = user.increasePostStats(wordCount(content), true) if err != nil { InternalError(err, w) return } - http.Redirect(w, r, "/topic/"+strconv.FormatInt(lastID, 10), http.StatusSeeOther) - err = user.increasePostStats(wcount, true) - if err != nil { - InternalError(err, w) - return + // Handle the file attachments + if user.Perms.UploadFiles { + var mpartFiles = r.MultipartForm.File + if len(mpartFiles) > 5 { + LocalError("You can't attach more than five files", w, r, user) + return + } + + for _, fheaders := range r.MultipartForm.File { + for _, hdr := range fheaders { + log.Print("hdr.Filename ", hdr.Filename) + extarr := strings.Split(hdr.Filename, ".") + if len(extarr) < 2 { + LocalError("Bad file", w, r, user) + return + } + ext := extarr[len(extarr)-1] + + // TODO: Can we do this without a regex? + reg, err := regexp.Compile("[^A-Za-z0-9]+") + if err != nil { + LocalError("Bad file extension", w, r, user) + return + } + ext = strings.ToLower(reg.ReplaceAllString(ext, "")) + if !allowedFileExts.Contains(ext) { + LocalError("You're not allowed this upload files with this extension", w, r, user) + return + } + + infile, err := hdr.Open() + if err != nil { + LocalError("Upload failed", w, r, user) + return + } + defer infile.Close() + + hasher := sha256.New() + _, err = io.Copy(hasher, infile) + if err != nil { + LocalError("Upload failed [Hashing Failed]", w, r, user) + return + } + infile.Close() + + checksum := hex.EncodeToString(hasher.Sum(nil)) + filename := checksum + "." + ext + outfile, err := os.Create("." + "/attachs/" + filename) + if err != nil { + LocalError("Upload failed [File Creation Failed]", w, r, user) + return + } + defer outfile.Close() + + infile, err = hdr.Open() + if err != nil { + LocalError("Upload failed", w, r, user) + return + } + defer infile.Close() + + _, err = io.Copy(outfile, infile) + if err != nil { + LocalError("Upload failed [Copy Failed]", w, r, user) + return + } + + _, err = addAttachmentStmt.Exec(fid, "forums", tid, "topics", user.ID, filename) + if err != nil { + InternalError(err, w) + return + } + } + } } - err = fstore.AddTopic(int(lastID), user.ID, fid) - if err != nil && err != ErrNoRows { - InternalError(err, w) - } + http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) } func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { @@ -201,7 +287,7 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { } wcount := wordCount(content) - _, err = createReplyStmt.Exec(tid, content, parseMessage(content), ipaddress, wcount, user.ID) + _, err = createReplyStmt.Exec(tid, content, parseMessage(content, topic.ParentID, "forums"), ipaddress, wcount, user.ID) if err != nil { InternalError(err, w) return @@ -475,7 +561,8 @@ func routeProfileReplyCreate(w http.ResponseWriter, r *http.Request, user User) return } - _, err = createProfileReplyStmt.Exec(uid, html.EscapeString(preparseMessage(r.PostFormValue("reply-content"))), parseMessage(html.EscapeString(preparseMessage(r.PostFormValue("reply-content")))), user.ID, ipaddress) + content := html.EscapeString(preparseMessage(r.PostFormValue("reply-content"))) + _, err = createProfileReplyStmt.Exec(uid, content, parseMessage(content, 0, ""), user.ID, ipaddress) if err != nil { InternalError(err, w) return @@ -605,8 +692,9 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI return } + // TODO: Repost attachments in the reports forum, so that the mods can see them // ? - Can we do this via the TopicStore? - res, err := createReportStmt.Exec(title, content, parseMessage(content), user.ID, itemType+"_"+strconv.Itoa(itemID)) + res, err := createReportStmt.Exec(title, content, parseMessage(content, 0, ""), user.ID, itemType+"_"+strconv.Itoa(itemID)) if err != nil { InternalError(err, w) return @@ -728,7 +816,8 @@ func routeAccountOwnEditAvatar(w http.ResponseWriter, r *http.Request, user User func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user User) { if r.ContentLength > int64(config.MaxRequestSize) { - http.Error(w, "Request too large", http.StatusExpectationFailed) + size, unit := convertByteUnit(float64(config.MaxRequestSize)) + CustomError("Your avatar's too big. Avatars must be smaller than "+strconv.Itoa(int(size))+unit, http.StatusExpectationFailed, "Error", w, r, user) return } r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxRequestSize)) @@ -742,14 +831,13 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use return } - err := r.ParseMultipartForm(int64(config.MaxRequestSize)) + err := r.ParseMultipartForm(int64(megabyte)) if err != nil { LocalError("Upload failed", w, r, user) return } - var filename string - var ext string + var filename, ext string for _, fheaders := range r.MultipartForm.File { for _, hdr := range fheaders { infile, err := hdr.Open() @@ -760,6 +848,7 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use defer infile.Close() // We don't want multiple files + // TODO: Check the length of r.MultipartForm.File and error rather than doing this x.x if filename != "" { if filename != hdr.Filename { os.Remove("./uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext) @@ -778,6 +867,7 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use } ext = extarr[len(extarr)-1] + // TODO: Can we do this without a regex? reg, err := regexp.Compile("[^A-Za-z0-9]+") if err != nil { LocalError("Bad file extension", w, r, user) @@ -802,16 +892,12 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use } } - _, err = setAvatarStmt.Exec("."+ext, strconv.Itoa(user.ID)) + err = user.ChangeAvatar("." + ext) if err != nil { InternalError(err, w) return } user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext - ucache, ok := users.(UserCache) - if ok { - ucache.CacheRemove(user.ID) - } headerVars.NoticeList = append(headerVars.NoticeList, "Your avatar was successfully updated") pi := Page{"Edit Avatar", user, headerVars, tList, nil} @@ -857,18 +943,12 @@ func routeAccountOwnEditUsernameSubmit(w http.ResponseWriter, r *http.Request, u } newUsername := html.EscapeString(r.PostFormValue("account-new-username")) - _, err = setUsernameStmt.Exec(newUsername, strconv.Itoa(user.ID)) + err = user.ChangeName(newUsername) if err != nil { LocalError("Unable to change the username. Does someone else already have this name?", w, r, user) return } - - // TODO: Use the reloaded data instead for the name? user.Name = newUsername - ucache, ok := users.(UserCache) - if ok { - ucache.CacheRemove(user.ID) - } headerVars.NoticeList = append(headerVars.NoticeList, "Your username was successfully updated") pi := Page{"Edit Username", user, headerVars, tList, nil} @@ -1022,3 +1102,60 @@ func routeLogout(w http.ResponseWriter, r *http.Request, user User) { auth.Logout(w, user.ID) http.Redirect(w, r, "/", http.StatusSeeOther) } + +func routeShowAttachment(w http.ResponseWriter, r *http.Request, user User, filename string) { + err := r.ParseForm() + if err != nil { + PreError("Bad Form", w, r) + return + } + + filename = Stripslashes(filename) + var ext = filepath.Ext("./attachs/" + filename) + //log.Print("ext ", ext) + //log.Print("filename ", filename) + if !allowedFileExts.Contains(strings.TrimPrefix(ext, ".")) { + LocalError("Bad extension", w, r, user) + return + } + + sectionID, err := strconv.Atoi(r.FormValue("sectionID")) + if err != nil { + LocalError("The sectionID is not an integer", w, r, user) + return + } + var sectionTable = r.FormValue("sectionType") + + var originTable string + var originID, uploadedBy int + err = getAttachmentStmt.QueryRow(filename, sectionID, sectionTable).Scan(§ionID, §ionTable, &originID, &originTable, &uploadedBy, &filename) + if err == ErrNoRows { + NotFound(w, r) + return + } else if err != nil { + InternalError(err, w) + return + } + + if sectionTable == "forums" { + _, ok := SimpleForumUserCheck(w, r, &user, sectionID) + if !ok { + return + } + if !user.Perms.ViewTopic { + NoPermissions(w, r, user) + return + } + } else { + LocalError("Unknown section", w, r, user) + return + } + + if originTable != "topics" && originTable != "replies" { + LocalError("Unknown origin", w, r, user) + return + } + + // TODO: Fix the problem where non-existent files aren't greeted with custom 404s on ServeFile()'s side + http.ServeFile(w, r, "./attachs/"+filename) +} diff --git a/misc_test.go b/misc_test.go index 8e70a6d5..018c2dde 100644 --- a/misc_test.go +++ b/misc_test.go @@ -28,7 +28,7 @@ func userStoreTest(t *testing.T) { var length int ucache, hasCache := users.(UserCache) - if hasCache && ucache.GetLength() != 0 { + if hasCache && ucache.Length() != 0 { t.Error("Initial ucache length isn't zero") } @@ -39,7 +39,7 @@ func userStoreTest(t *testing.T) { t.Fatal(err) } - if hasCache && ucache.GetLength() != 0 { + if hasCache && ucache.Length() != 0 { t.Error("There shouldn't be anything in the user cache") } @@ -50,7 +50,7 @@ func userStoreTest(t *testing.T) { t.Fatal(err) } - if hasCache && ucache.GetLength() != 0 { + if hasCache && ucache.Length() != 0 { t.Error("There shouldn't be anything in the user cache") } @@ -66,7 +66,7 @@ func userStoreTest(t *testing.T) { } if hasCache { - length = ucache.GetLength() + length = ucache.Length() if length != 1 { t.Error("User cache length should be 1, not " + strconv.Itoa(length)) } @@ -83,7 +83,7 @@ func userStoreTest(t *testing.T) { } ucache.Flush() - length = ucache.GetLength() + length = ucache.Length() if length != 0 { t.Error("User cache length should be 0, not " + strconv.Itoa(length)) } @@ -97,7 +97,7 @@ func userStoreTest(t *testing.T) { } if hasCache { - length = ucache.GetLength() + length = ucache.Length() if length != 0 { t.Error("User cache length should be 0, not " + strconv.Itoa(length)) } @@ -109,7 +109,7 @@ func userStoreTest(t *testing.T) { } if hasCache { - length = ucache.GetLength() + length = ucache.Length() if length != 0 { t.Error("User cache length should be 0, not " + strconv.Itoa(length)) } @@ -133,7 +133,7 @@ func userStoreTest(t *testing.T) { } if hasCache { - length = ucache.GetLength() + length = ucache.Length() if length != 1 { t.Error("User cache length should be 1, not " + strconv.Itoa(length)) } @@ -168,13 +168,13 @@ func userStoreTest(t *testing.T) { } if hasCache { - length = ucache.GetLength() + length = ucache.Length() if length != 0 { t.Error("User cache length should be 0, not " + strconv.Itoa(length)) } } - count := users.GetGlobalCount() + count := users.GlobalCount() if count <= 0 { t.Error("The number of users should be bigger than zero") t.Error("count", count) @@ -243,7 +243,7 @@ func topicStoreTest(t *testing.T) { t.Error("TID #1 should exist") } - count := topics.GetGlobalCount() + count := topics.GlobalCount() if count <= 0 { t.Error("The number of topics should be bigger than zero") t.Error("count", count) diff --git a/mod_routes.go b/mod_routes.go index 06ca5d5e..b77c2d6b 100644 --- a/mod_routes.go +++ b/mod_routes.go @@ -345,13 +345,6 @@ func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) { return } - content := html.EscapeString(preparseMessage(r.PostFormValue("edit_item"))) - _, err = editReplyStmt.Exec(content, parseMessage(content), rid) - if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return - } - // Get the Reply ID.. var tid int err = getReplyTIDStmt.QueryRow(rid).Scan(&tid) @@ -380,6 +373,13 @@ func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) { return } + content := html.EscapeString(preparseMessage(r.PostFormValue("edit_item"))) + _, err = editReplyStmt.Exec(content, parseMessage(content, fid, "forums"), rid) + if err != nil { + InternalErrorJSQ(err, w, r, isJs) + return + } + if !isJs { http.Redirect(w, r, "/topic/"+strconv.Itoa(tid)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther) } else { @@ -504,7 +504,7 @@ func routeProfileReplyEditSubmit(w http.ResponseWriter, r *http.Request, user Us } content := html.EscapeString(preparseMessage(r.PostFormValue("edit_item"))) - _, err = editProfileReplyStmt.Exec(content, parseMessage(content), rid) + _, err = editProfileReplyStmt.Exec(content, parseMessage(content, 0, ""), rid) if err != nil { InternalErrorJSQ(err, w, r, isJs) return diff --git a/mysql.sql b/mysql.sql index c0c77e1f..28eff700 100644 --- a/mysql.sql +++ b/mysql.sql @@ -78,6 +78,17 @@ CREATE TABLE `replies`( primary key(`rid`) ) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; +CREATE TABLE `attachments`( + `attachID` int not null AUTO_INCREMENT, + `sectionID` int DEFAULT 0 not null, /* section ID */ + `sectionTable` varchar(200) DEFAULT 'forums' not null, /* section table */ + `originID` int not null, + `originTable` varchar(200) DEFAULT 'replies' not null, + `uploadedBy` int not null, + `path` varchar(200) not null, + primary key(`attachID`) +) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; + CREATE TABLE `revisions`( `index` int not null, `content` text not null, @@ -190,6 +201,7 @@ INSERT INTO emails(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1); /* The Permissions: +Global Permissions: BanUsers ActivateUsers EditUser @@ -210,6 +222,10 @@ ManagePlugins ViewAdminLogs ViewIPs +Non-staff Global Permissions: +UploadFiles + +Forum Permissions: ViewTopic LikeItem CreateTopic @@ -222,9 +238,9 @@ PinTopic CloseTopic */ -INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`tag`) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,1,"Admin"); -INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`is_mod`,`tag`) VALUES ('Moderator','{"BanUsers":true,"ActivateUsers":false,"EditUser":true,"EditUserEmail":false,"EditUserGroup":true,"ViewIPs":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,"Mod"); -INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`) VALUES ('Member','{"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"CreateReply":true}','{}'); +INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`tag`) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,1,"Admin"); +INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`is_mod`,`tag`) VALUES ('Moderator','{"BanUsers":true,"ActivateUsers":false,"EditUser":true,"EditUserEmail":false,"EditUserGroup":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,"Mod"); +INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`) VALUES ('Member','{"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"CreateReply":true}','{}'); INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`is_banned`) VALUES ('Banned','{"ViewTopic":true}','{}',1); INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`) VALUES ('Awaiting Activation','{"ViewTopic":true}','{}'); INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`tag`) VALUES ('Not Loggedin','{"ViewTopic":true}','{}','Guest'); diff --git a/pages.go b/pages.go index feb273d7..acd84cee 100644 --- a/pages.go +++ b/pages.go @@ -4,6 +4,7 @@ import ( //"fmt" "bytes" "html/template" + "net/url" "regexp" "strconv" "strings" @@ -61,10 +62,12 @@ type TopicPage struct { } type TopicsPage struct { - Title string - CurrentUser User - Header *HeaderVars - ItemList []*TopicsRow + Title string + CurrentUser User + Header *HeaderVars + TopicList []*TopicsRow + ForumList []Forum + DefaultForum int } type ForumPage struct { @@ -287,12 +290,16 @@ var invalidURL = []byte("[Invalid URL]") var invalidTopic = []byte("[Invalid Topic]") var invalidProfile = []byte("[Invalid Profile]") var invalidForum = []byte("[Invalid Forum]") +var unknownMedia = []byte("[Unknown Media]") var urlOpen = []byte("") var bytesSinglequote = []byte("'") var bytesGreaterthan = []byte(">") var urlMention = []byte(" class='mention'") var urlClose = []byte("") +var imageOpen = []byte("") var urlpattern = `(?s)([ {1}])((http|https|ftp|mailto)*)(:{??)\/\/([\.a-zA-Z\/]+)([ {1}])` var urlReg *regexp.Regexp @@ -440,7 +447,8 @@ func preparseMessage(msg string) string { } // TODO: Write a test for this -func parseMessage(msg string /*, user User*/) string { +// TODO: We need a lot more hooks here. E.g. To add custom media types and handlers. +func parseMessage(msg string, sectionID int, sectionType string /*, user User*/) string { msg = strings.Replace(msg, ":)", "😀", -1) msg = strings.Replace(msg, ":(", "😞", -1) msg = strings.Replace(msg, ":D", "😃", -1) @@ -461,19 +469,13 @@ func parseMessage(msg string /*, user User*/) string { var msgbytes = []byte(msg) var outbytes []byte msgbytes = append(msgbytes, spaceGap...) - //log.Print(`"`+string(msgbytes)+`"`) - lastItem := 0 - i := 0 + //log.Printf("string(msgbytes) %+v\n", `"`+string(msgbytes)+`"`) + var lastItem = 0 + var i = 0 for ; len(msgbytes) > (i + 1); i++ { //log.Print("Index:",i) - //log.Print("Index Item:",msgbytes[i]) - //if msgbytes[i] == 10 { - // log.Print("NEWLINE") - //} else if msgbytes[i] == 32 { - // log.Print("SPACE") - //} else { - // log.Print("string(msgbytes[i])",string(msgbytes[i])) - //} + //log.Print("Index Item: ",msgbytes[i]) + //log.Print("string(msgbytes[i]): ",string(msgbytes[i])) //log.Print("End Index") if (i == 0 && (msgbytes[0] > 32)) || ((msgbytes[i] < 33) && (msgbytes[i+1] > 32)) { //log.Print("IN") @@ -507,12 +509,12 @@ func parseMessage(msg string /*, user User*/) string { outbytes = append(outbytes, urlClose...) lastItem = i - //log.Print("string(msgbytes)",string(msgbytes)) - //log.Print(msgbytes) + //log.Print("string(msgbytes) ",string(msgbytes)) + //log.Print("msgbytes ",msgbytes) //log.Print(msgbytes[lastItem - 1]) //log.Print(lastItem - 1) //log.Print(msgbytes[lastItem]) - //log.Print(lastItem) + //log.Print("lastItem ",lastItem) } else if bytes.Equal(msgbytes[i+1:i+5], []byte("rid-")) { outbytes = append(outbytes, msgbytes[lastItem:i]...) i += 5 @@ -611,11 +613,57 @@ func parseMessage(msg string /*, user User*/) string { outbytes = append(outbytes, msgbytes[lastItem:i]...) urlLen := partialURLBytesLen(msgbytes[i:]) - if msgbytes[i+urlLen] != ' ' && msgbytes[i+urlLen] != 10 { + if msgbytes[i+urlLen] > 32 { // space and invisibles outbytes = append(outbytes, invalidURL...) i += urlLen continue } + outbytes = append(outbytes, urlOpen...) + outbytes = append(outbytes, msgbytes[i:i+urlLen]...) + outbytes = append(outbytes, urlOpen2...) + outbytes = append(outbytes, msgbytes[i:i+urlLen]...) + outbytes = append(outbytes, urlClose...) + i += urlLen + lastItem = i + } else if msgbytes[i] == '/' && msgbytes[i+1] == '/' { + outbytes = append(outbytes, msgbytes[lastItem:i]...) + urlLen := partialURLBytesLen(msgbytes[i:]) + if msgbytes[i+urlLen] > 32 { // space and invisibles + //log.Print("INVALID URL") + //log.Print("msgbytes[i+urlLen]", msgbytes[i+urlLen]) + //log.Print("string(msgbytes[i+urlLen])", string(msgbytes[i+urlLen])) + //log.Print("msgbytes[i:i+urlLen]", msgbytes[i:i+urlLen]) + //log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen])) + outbytes = append(outbytes, invalidURL...) + i += urlLen + continue + } + + //log.Print("VALID URL") + //log.Print("msgbytes[i:i+urlLen]", msgbytes[i:i+urlLen]) + //log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen])) + media, ok := parseMediaBytes(msgbytes[i : i+urlLen]) + if !ok { + outbytes = append(outbytes, invalidURL...) + i += urlLen + continue + } + + if media.Type == "image" { + outbytes = append(outbytes, imageOpen...) + outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"§ionType="+sectionType)...) + outbytes = append(outbytes, imageOpen2...) + outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"§ionType="+sectionType)...) + outbytes = append(outbytes, imageClose...) + i += urlLen + lastItem = i + continue + } else if media.Type != "" { + outbytes = append(outbytes, unknownMedia...) + i += urlLen + continue + } + outbytes = append(outbytes, urlOpen...) outbytes = append(outbytes, msgbytes[i:i+urlLen]...) outbytes = append(outbytes, urlOpen2...) @@ -628,13 +676,10 @@ func parseMessage(msg string /*, user User*/) string { } if lastItem != i && len(outbytes) != 0 { - //log.Print("lastItem:",msgbytes[lastItem]) - //log.Print("lastItem index:") - //log.Print(lastItem) - //log.Print("i:") - //log.Print(i) - //log.Print("lastItem to end:") - //log.Print(msgbytes[lastItem:]) + //log.Print("lastItem: ",msgbytes[lastItem]) + //log.Print("lastItem index: ",lastItem) + //log.Print("i: ",i) + //log.Print("lastItem to end: ",msgbytes[lastItem:]) //log.Print("-----") calclen := len(msgbytes) - 10 if calclen <= lastItem { @@ -666,8 +711,8 @@ func regexParseMessage(msg string) string { return msg } -// 6, 7, 8, 6, 7 -// ftp://, http://, https:// git://, mailto: (not a URL, just here for length comparison purposes) +// 6, 7, 8, 6, 2, 7 +// ftp://, http://, https:// git://, //, mailto: (not a URL, just here for length comparison purposes) // TODO: Write a test for this func validateURLBytes(data []byte) bool { datalen := len(data) @@ -681,10 +726,13 @@ func validateURLBytes(data []byte) bool { } else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) { i = 8 } + } else if datalen >= 2 && data[0] == '/' && data[1] == '/' { + i = 2 } + // ? - There should only be one : and that's only if the URL is on a non-standard port for ; datalen > i; i++ { - if data[i] != '\\' && data[i] != '_' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { + if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { return false } } @@ -704,10 +752,13 @@ func validatedURLBytes(data []byte) (url []byte) { } else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) { i = 8 } + } else if datalen >= 2 && data[0] == '/' && data[1] == '/' { + i = 2 } + // ? - There should only be one : and that's only if the URL is on a non-standard port for ; datalen > i; i++ { - if data[i] != '\\' && data[i] != '_' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { + if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { return invalidURL } } @@ -730,10 +781,13 @@ func partialURLBytes(data []byte) (url []byte) { } else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) { i = 8 } + } else if datalen >= 2 && data[0] == '/' && data[1] == '/' { + i = 2 } + // ? - There should only be one : and that's only if the URL is on a non-standard port for ; end >= i; i++ { - if data[i] != '\\' && data[i] != '_' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { + if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { end = i } } @@ -756,47 +810,80 @@ func partialURLBytesLen(data []byte) int { } else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) { i = 8 } + } else if datalen >= 2 && data[0] == '/' && data[1] == '/' { + i = 2 } + // ? - There should only be one : and that's only if the URL is on a non-standard port for ; datalen > i; i++ { - if data[i] != '\\' && data[i] != '_' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { - //log.Print("Bad Character:",data[i]) + if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { + //log.Print("Bad Character: ", data[i]) return i } } - - //log.Print("Data Length:",datalen) + //log.Print("Data Length: ",datalen) return datalen } +type MediaEmbed struct { + Type string //image + URL string +} + // TODO: Write a test for this -func parseMediaBytes(data []byte) (protocol []byte, url []byte) { - datalen := len(data) - i := 0 +func parseMediaBytes(data []byte) (media MediaEmbed, ok bool) { + if !validateURLBytes(data) { + return media, false + } + url, err := parseURL(data) + if err != nil { + return media, false + } - if datalen >= 6 { - if bytes.Equal(data[0:6], []byte("ftp://")) || bytes.Equal(data[0:6], []byte("git://")) { - i = 6 - protocol = data[0:2] - } else if datalen >= 7 && bytes.Equal(data[0:7], httpProtBytes) { - i = 7 - protocol = []byte("http") - } else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) { - i = 8 - protocol = []byte("https") + //log.Print("url ", url) + hostname := url.Hostname() + scheme := url.Scheme + port := url.Port() + //log.Print("hostname ", hostname) + //log.Print("scheme ", scheme) + + var samesite = hostname == "localhost" || hostname == site.URL + if samesite { + //log.Print("samesite") + hostname = strings.Split(site.URL, ":")[0] + // ?- Test this as I'm not sure it'll do what it should. If someone's running SSL on port 80 or non-SSL on port 443 then... Well... They're in far worse trouble than this... + port = site.Port + if scheme == "" && site.EnableSsl { + scheme = "https" } } + if scheme == "" { + scheme = "http" + } - for ; datalen > i; i++ { - if data[i] != '\\' && data[i] != '_' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { - return []byte(""), invalidURL + path := url.EscapedPath() + //log.Print("path", path) + pathFrags := strings.Split(path, "/") + //log.Printf("pathFrags %+v\n", pathFrags) + //log.Print("scheme ", scheme) + //log.Print("hostname ", hostname) + if len(pathFrags) >= 2 { + if samesite && pathFrags[1] == "attachs" && (scheme == "http" || scheme == "https") { + //log.Print("Attachment") + media.Type = "image" + var sport string + // ? - Assumes the sysadmin hasn't mixed up the two standard ports + if port != "443" && port != "80" { + sport = ":" + port + } + media.URL = scheme + "://" + hostname + sport + path } } + return media, true +} - if len(protocol) == 0 { - protocol = []byte("http") - } - return protocol, data[i:] +func parseURL(data []byte) (*url.URL, error) { + return url.Parse(string(data)) } // TODO: Write a test for this diff --git a/panel_routes.go b/panel_routes.go index 9e2edac7..d9488c19 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -1531,6 +1531,7 @@ func routePanelGroupsEditPerms(w http.ResponseWriter, r *http.Request, user User globalPerms = append(globalPerms, NameLangToggle{"ManagePlugins", GetGlobalPermPhrase("ManagePlugins"), group.Perms.ManagePlugins}) globalPerms = append(globalPerms, NameLangToggle{"ViewAdminLogs", GetGlobalPermPhrase("ViewAdminLogs"), group.Perms.ViewAdminLogs}) globalPerms = append(globalPerms, NameLangToggle{"ViewIPs", GetGlobalPermPhrase("ViewIPs"), group.Perms.ViewIPs}) + globalPerms = append(globalPerms, NameLangToggle{"UploadFiles", GetGlobalPermPhrase("UploadFiles"), group.Perms.UploadFiles}) pi := PanelEditGroupPermsPage{"Group Editor", user, headerVars, stats, group.ID, group.Name, localPerms, globalPerms} if preRenderHooks["pre_render_panel_edit_group_perms"] != nil { diff --git a/permissions.go b/permissions.go index 95ed3a0a..6a42ec6c 100644 --- a/permissions.go +++ b/permissions.go @@ -16,6 +16,8 @@ var AllPerms Perms var AllForumPerms ForumPerms var AllPluginPerms = make(map[string]bool) +// ? - Can we avoid duplicating the items in this list in a bunch of places? + var LocalPermList = []string{ "ViewTopic", "LikeItem", @@ -29,6 +31,7 @@ var LocalPermList = []string{ "CloseTopic", } +// ? - Can we avoid duplicating the items in this list in a bunch of places? var GlobalPermList = []string{ "BanUsers", "ActivateUsers", @@ -49,6 +52,7 @@ var GlobalPermList = []string{ "ManagePlugins", "ViewAdminLogs", "ViewIPs", + "UploadFiles", } // Permission Structure: ActionComponent[Subcomponent]Flag @@ -74,6 +78,10 @@ type Perms struct { ViewAdminLogs bool ViewIPs bool + // Global non-staff permissions + UploadFiles bool + // TODO: Add a permission for enabling avatars + // Forum permissions ViewTopic bool LikeItem bool @@ -147,6 +155,8 @@ func init() { ViewAdminLogs: true, ViewIPs: true, + UploadFiles: true, + ViewTopic: true, LikeItem: true, CreateTopic: true, diff --git a/public/global.js b/public/global.js index 96deddb6..9e034334 100644 --- a/public/global.js +++ b/public/global.js @@ -78,6 +78,7 @@ function load_alerts(menu_alerts) bind_to_alerts(); }, error: function(magic,theStatus,error) { + var errtxt try { var data = JSON.parse(magic.responseText); if("errmsg" in data) errtxt = data.errmsg; @@ -94,16 +95,16 @@ function load_alerts(menu_alerts) function SplitN(data,ch,n) { var out = []; - if(data.length == 0) return out; + if(data.length === 0) return out; var lastIndex = 0; var j = 0; var lastN = 1; - for(var i = 0; i < data.length; i++) { - if(data[i] == ch) { + for(let i = 0; i < data.length; i++) { + if(data[i] === ch) { out[j++] = data.substring(lastIndex,i); lastIndex = i; - if(lastN == n) break; + if(lastN === n) break; lastN++; } } @@ -118,19 +119,23 @@ $(document).ready(function(){ else conn = new WebSocket("ws://" + document.location.host + "/ws/"); conn.onopen = function() { + console.log("The WebSockets connection was opened"); conn.send("page " + document.location.pathname + '\r'); // TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on Notification.requestPermission(); } conn.onclose = function() { conn = false; + console.log("The WebSockets connection was closed"); } conn.onmessage = function(event) { //console.log("WS_Message: ",event.data); if(event.data[0] == "{") { try { var data = JSON.parse(event.data); - } catch(err) { console.log(err); } + } catch(err) { + console.log(err); + } if ("msg" in data) { var msg = data.msg @@ -175,11 +180,11 @@ $(document).ready(function(){ //console.log(messages[i]); if(messages[i].startsWith("set ")) { //msgblocks = messages[i].split(' ',3); - msgblocks = SplitN(messages[i]," ",3); + let msgblocks = SplitN(messages[i]," ",3); if(msgblocks.length < 3) continue; document.querySelector(msgblocks[1]).innerHTML = msgblocks[2]; } else if(messages[i].startsWith("set-class ")) { - msgblocks = SplitN(messages[i]," ",3); + let msgblocks = SplitN(messages[i]," ",3); if(msgblocks.length < 3) continue; document.querySelector(msgblocks[1]).className = msgblocks[2]; } @@ -328,7 +333,7 @@ $(document).ready(function(){ //console.log("running .submit_edit event"); var out_data = {isJs: "1"} var block_parent = $(this).closest('.editable_parent'); - var block = block_parent.find('.editable_block').each(function(){ + block_parent.find('.editable_block').each(function(){ var field_name = this.getAttribute("data-field"); var field_type = this.getAttribute("data-type"); if(field_type=="list") { @@ -397,6 +402,71 @@ $(document).ready(function(){ event.stopPropagation(); }) + $(".create_topic_link").click(function(event){ + event.preventDefault(); + $(".topic_create_form").show(); + }); + $(".topic_create_form .close_form").click(function(){ + event.preventDefault(); + $(".topic_create_form").hide(); + }); + + function uploadFileHandler() { + var fileList = this.files; + + // Truncate the number of files to 5 + let files = []; + for(var i = 0; i < fileList.length && i < 5; i++) + files[i] = fileList[i]; + + // Iterate over the files + for(let i = 0; i < files.length; i++) { + console.log("files[" + i + "]",files[i]); + let reader = new FileReader(); + reader.onload = function(e) { + var fileDock = document.getElementById("upload_file_dock"); + var fileItem = document.createElement("label"); + console.log("fileItem",fileItem); + + if(!files[i]["name"].indexOf('.' > -1)) { + // TODO: Surely, there's a prettier and more elegant way of doing this? + alert("This file doesn't have an extension"); + return; + } + + var ext = files[i]["name"].split('.').pop(); + fileItem.innerText = "." + ext; + fileItem.className = "formbutton uploadItem"; + fileItem.style.backgroundImage = "url("+e.target.result+")"; + + fileDock.appendChild(fileItem); + + let reader = new FileReader(); + reader.onload = function(e) { + crypto.subtle.digest('SHA-256',e.target.result).then(function(hash) { + const hashArray = Array.from(new Uint8Array(hash)) + return hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('') + }).then(function(hash) { + console.log("hash",hash); + let content = document.getElementById("topic_content") + console.log("content.value",content.value); + + if(content.value == "") content.value = content.value + "//" + siteURL + "/attachs/" + hash + "." + ext; + else content.value = content.value + "\r\n//" + siteURL + "/attachs/" + hash + "." + ext; + console.log("content.value",content.value); + }); + } + reader.readAsArrayBuffer(files[i]); + } + reader.readAsDataURL(files[i]); + } + } + + var uploadFiles = document.getElementById("quick_topic_upload_files"); + if(uploadFiles != null) { + uploadFiles.addEventListener("change", uploadFileHandler, false); + } + $("#themeSelectorSelect").change(function(){ console.log("Changing the theme to " + this.options[this.selectedIndex].getAttribute("val")); $.ajax({ @@ -408,6 +478,7 @@ $(document).ready(function(){ console.log("Theme successfully switched"); console.log("data",data); console.log("status",status); + console.log("xhr",xhr); window.location.reload(); }, // TODO: Use a standard error handler for the AJAX calls in here which throws up the response (if JSON) in a .notice? Might be difficult to trace errors in the console, if we reuse the same function every-time diff --git a/public/test_bg2.svg b/public/test_bg2.svg deleted file mode 100644 index ce7769a0..00000000 --- a/public/test_bg2.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/public/test_bg3.svg b/public/test_bg3.svg deleted file mode 100644 index 5c2ab9f9..00000000 --- a/public/test_bg3.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/query_gen/lib/mysql.go b/query_gen/lib/mysql.go index f87bb164..7f2a69b3 100644 --- a/query_gen/lib/mysql.go +++ b/query_gen/lib/mysql.go @@ -13,11 +13,12 @@ func init() { } type Mysql_Adapter struct { - Name string + Name string // ? - Do we really need this? Can't we hard-code this? 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 } +// GetName gives you the name of the database adapter. In this case, it's mysql func (adapter *Mysql_Adapter) GetName() string { return adapter.Name } @@ -120,7 +121,7 @@ func (adapter *Mysql_Adapter) SimpleInsert(name string, table string, columns st var querystr = "INSERT INTO `" + table + "`(" // Escape the column names, just in case we've used a reserved keyword - for _, column := range _process_columns(columns) { + for _, column := range processColumns(columns) { if column.Type == "function" { querystr += column.Left + "," } else { @@ -132,7 +133,7 @@ func (adapter *Mysql_Adapter) SimpleInsert(name string, table string, columns st querystr = querystr[0 : len(querystr)-1] querystr += ") VALUES (" - for _, field := range _processFields(fields) { + for _, field := range processFields(fields) { querystr += field.Name + "," } querystr = querystr[0 : len(querystr)-1] @@ -158,7 +159,7 @@ func (adapter *Mysql_Adapter) SimpleReplace(name string, table string, columns s var querystr = "REPLACE INTO `" + table + "`(" // Escape the column names, just in case we've used a reserved keyword - for _, column := range _process_columns(columns) { + for _, column := range processColumns(columns) { if column.Type == "function" { querystr += column.Left + "," } else { @@ -169,7 +170,7 @@ func (adapter *Mysql_Adapter) SimpleReplace(name string, table string, columns s querystr = querystr[0 : len(querystr)-1] querystr += ") VALUES (" - for _, field := range _processFields(fields) { + for _, field := range processFields(fields) { querystr += field.Name + "," } querystr = querystr[0 : len(querystr)-1] @@ -190,7 +191,7 @@ func (adapter *Mysql_Adapter) SimpleUpdate(name string, table string, set string } var querystr = "UPDATE `" + table + "` SET " - for _, item := range _process_set(set) { + for _, item := range processSet(set) { querystr += "`" + item.Column + "` =" for _, token := range item.Expr { switch token.Type { @@ -211,7 +212,7 @@ func (adapter *Mysql_Adapter) SimpleUpdate(name string, table string, set string // Add support for BETWEEN x.x if len(where) != 0 { querystr += " WHERE" - for _, loc := range _processWhere(where) { + for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { case "function", "operator", "number", "substitute": @@ -247,7 +248,7 @@ func (adapter *Mysql_Adapter) SimpleDelete(name string, table string, where stri var querystr = "DELETE FROM `" + table + "` WHERE" // Add support for BETWEEN x.x - for _, loc := range _processWhere(where) { + for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { case "function", "operator", "number", "substitute": @@ -308,7 +309,7 @@ func (adapter *Mysql_Adapter) SimpleSelect(name string, table string, columns st // Add support for BETWEEN x.x if len(where) != 0 { querystr += " WHERE" - for _, loc := range _processWhere(where) { + for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { case "function", "operator", "number", "substitute": @@ -328,7 +329,7 @@ func (adapter *Mysql_Adapter) SimpleSelect(name string, table string, columns st if len(orderby) != 0 { querystr += " ORDER BY " - for _, column := range _process_orderby(orderby) { + for _, column := range processOrderby(orderby) { querystr += column.Column + " " + strings.ToUpper(column.Order) + "," } querystr = querystr[0 : len(querystr)-1] @@ -362,7 +363,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2 var querystr = "SELECT " - for _, column := range _process_columns(columns) { + for _, column := range processColumns(columns) { var source, alias string // Escape the column names, just in case we've used a reserved keyword @@ -384,7 +385,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2 querystr = querystr[0 : len(querystr)-1] querystr += " FROM `" + table1 + "` LEFT JOIN `" + table2 + "` ON " - for _, joiner := range _processJoiner(joiners) { + for _, joiner := range processJoiner(joiners) { querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND " } // Remove the trailing AND @@ -393,7 +394,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2 // Add support for BETWEEN x.x if len(where) != 0 { querystr += " WHERE" - for _, loc := range _processWhere(where) { + for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { case "function", "operator", "number", "substitute": @@ -418,7 +419,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2 if len(orderby) != 0 { querystr += " ORDER BY " - for _, column := range _process_orderby(orderby) { + for _, column := range processOrderby(orderby) { querystr += column.Column + " " + strings.ToUpper(column.Order) + "," } querystr = querystr[0 : len(querystr)-1] @@ -452,7 +453,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2 var querystr = "SELECT " - for _, column := range _process_columns(columns) { + for _, column := range processColumns(columns) { var source, alias string // Escape the column names, just in case we've used a reserved keyword @@ -474,7 +475,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2 querystr = querystr[0 : len(querystr)-1] querystr += " FROM `" + table1 + "` INNER JOIN `" + table2 + "` ON " - for _, joiner := range _processJoiner(joiners) { + for _, joiner := range processJoiner(joiners) { querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND " } // Remove the trailing AND @@ -483,7 +484,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2 // Add support for BETWEEN x.x if len(where) != 0 { querystr += " WHERE" - for _, loc := range _processWhere(where) { + for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { case "function", "operator", "number", "substitute": @@ -508,7 +509,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2 if len(orderby) != 0 { querystr += " ORDER BY " - for _, column := range _process_orderby(orderby) { + for _, column := range processOrderby(orderby) { querystr += column.Column + " " + strings.ToUpper(column.Order) + "," } querystr = querystr[0 : len(querystr)-1] @@ -529,7 +530,7 @@ func (adapter *Mysql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel var querystr = "INSERT INTO `" + ins.Table + "`(" // Escape the column names, just in case we've used a reserved keyword - for _, column := range _process_columns(ins.Columns) { + for _, column := range processColumns(ins.Columns) { if column.Type == "function" { querystr += column.Left + "," } else { @@ -540,7 +541,7 @@ func (adapter *Mysql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel /* Select Portion */ - for _, column := range _process_columns(sel.Columns) { + for _, column := range processColumns(sel.Columns) { var source, alias string // Escape the column names, just in case we've used a reserved keyword @@ -562,7 +563,7 @@ func (adapter *Mysql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel // Add support for BETWEEN x.x if len(sel.Where) != 0 { querystr += " WHERE" - for _, loc := range _processWhere(sel.Where) { + for _, loc := range processWhere(sel.Where) { for _, token := range loc.Expr { switch token.Type { case "function", "operator", "number", "substitute": @@ -582,7 +583,7 @@ func (adapter *Mysql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel if len(sel.Orderby) != 0 { querystr += " ORDER BY " - for _, column := range _process_orderby(sel.Orderby) { + for _, column := range processOrderby(sel.Orderby) { querystr += column.Column + " " + strings.ToUpper(column.Order) + "," } querystr = querystr[0 : len(querystr)-1] @@ -603,7 +604,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s var querystr = "INSERT INTO `" + ins.Table + "`(" // Escape the column names, just in case we've used a reserved keyword - for _, column := range _process_columns(ins.Columns) { + for _, column := range processColumns(ins.Columns) { if column.Type == "function" { querystr += column.Left + "," } else { @@ -614,7 +615,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s /* Select Portion */ - for _, column := range _process_columns(sel.Columns) { + for _, column := range processColumns(sel.Columns) { var source, alias string // Escape the column names, just in case we've used a reserved keyword @@ -634,7 +635,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s querystr = querystr[0 : len(querystr)-1] querystr += " FROM `" + sel.Table1 + "` LEFT JOIN `" + sel.Table2 + "` ON " - for _, joiner := range _processJoiner(sel.Joiners) { + for _, joiner := range processJoiner(sel.Joiners) { querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND " } querystr = querystr[0 : len(querystr)-4] @@ -642,7 +643,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s // Add support for BETWEEN x.x if len(sel.Where) != 0 { querystr += " WHERE" - for _, loc := range _processWhere(sel.Where) { + for _, loc := range processWhere(sel.Where) { for _, token := range loc.Expr { switch token.Type { case "function", "operator", "number", "substitute": @@ -667,7 +668,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s if len(sel.Orderby) != 0 { querystr += " ORDER BY " - for _, column := range _process_orderby(sel.Orderby) { + for _, column := range processOrderby(sel.Orderby) { querystr += column.Column + " " + strings.ToUpper(column.Order) + "," } querystr = querystr[0 : len(querystr)-1] @@ -688,7 +689,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert, var querystr = "INSERT INTO `" + ins.Table + "`(" // Escape the column names, just in case we've used a reserved keyword - for _, column := range _process_columns(ins.Columns) { + for _, column := range processColumns(ins.Columns) { if column.Type == "function" { querystr += column.Left + "," } else { @@ -699,7 +700,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert, /* Select Portion */ - for _, column := range _process_columns(sel.Columns) { + for _, column := range processColumns(sel.Columns) { var source, alias string // Escape the column names, just in case we've used a reserved keyword @@ -719,7 +720,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert, querystr = querystr[0 : len(querystr)-1] querystr += " FROM `" + sel.Table1 + "` INNER JOIN `" + sel.Table2 + "` ON " - for _, joiner := range _processJoiner(sel.Joiners) { + for _, joiner := range processJoiner(sel.Joiners) { querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND " } querystr = querystr[0 : len(querystr)-4] @@ -727,7 +728,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert, // Add support for BETWEEN x.x if len(sel.Where) != 0 { querystr += " WHERE" - for _, loc := range _processWhere(sel.Where) { + for _, loc := range processWhere(sel.Where) { for _, token := range loc.Expr { switch token.Type { case "function", "operator", "number", "substitute": @@ -752,7 +753,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert, if len(sel.Orderby) != 0 { querystr += " ORDER BY " - for _, column := range _process_orderby(sel.Orderby) { + for _, column := range processOrderby(sel.Orderby) { querystr += column.Column + " " + strings.ToUpper(column.Order) + "," } querystr = querystr[0 : len(querystr)-1] @@ -783,7 +784,7 @@ func (adapter *Mysql_Adapter) SimpleCount(name string, table string, where strin //fmt.Println("SimpleCount:",name) //fmt.Println("where:",where) //fmt.Println("_process_where:",_process_where(where)) - for _, loc := range _processWhere(where) { + for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { case "function", "operator", "number", "substitute": diff --git a/query_gen/lib/pgsql.go b/query_gen/lib/pgsql.go index 3a2b1acf..1c74ce2a 100644 --- a/query_gen/lib/pgsql.go +++ b/query_gen/lib/pgsql.go @@ -12,11 +12,12 @@ func init() { } type Pgsql_Adapter struct { - Name string + Name string // ? - Do we really need this? Can't we hard-code this? 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 } +// GetName gives you the name of the database adapter. In this case, it's pgsql func (adapter *Pgsql_Adapter) GetName() string { return adapter.Name } @@ -139,7 +140,7 @@ func (adapter *Pgsql_Adapter) SimpleUpdate(name string, table string, set string return "", errors.New("You need to set data in this update statement") } var querystr = "UPDATE `" + table + "` SET " - for _, item := range _process_set(set) { + for _, item := range processSet(set) { querystr += "`" + item.Column + "` =" for _, token := range item.Expr { switch token.Type { @@ -166,7 +167,7 @@ func (adapter *Pgsql_Adapter) SimpleUpdate(name string, table string, set string // Add support for BETWEEN x.x if len(where) != 0 { querystr += " WHERE" - for _, loc := range _processWhere(where) { + for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { case "function": diff --git a/query_gen/lib/utils.go b/query_gen/lib/utils.go index a2de942d..bab8ffbd 100644 --- a/query_gen/lib/utils.go +++ b/query_gen/lib/utils.go @@ -1,25 +1,31 @@ -/* WIP Under Construction */ +/* +* +* Query Generator Library +* WIP Under Construction +* Copyright Azareal 2017 - 2018 +* + */ package qgen //import "fmt" import "strings" import "os" -func _process_columns(colstr string) (columns []DB_Column) { +func processColumns(colstr string) (columns []DB_Column) { if colstr == "" { return columns } colstr = strings.Replace(colstr, " as ", " AS ", -1) for _, segment := range strings.Split(colstr, ",") { var outcol DB_Column - dothalves := strings.Split(strings.TrimSpace(segment), ".") + dotHalves := strings.Split(strings.TrimSpace(segment), ".") var halves []string - if len(dothalves) == 2 { - outcol.Table = dothalves[0] - halves = strings.Split(dothalves[1], " AS ") + if len(dotHalves) == 2 { + outcol.Table = dotHalves[0] + halves = strings.Split(dotHalves[1], " AS ") } else { - halves = strings.Split(dothalves[0], " AS ") + halves = strings.Split(dotHalves[0], " AS ") } halves[0] = strings.TrimSpace(halves[0]) @@ -40,7 +46,7 @@ func _process_columns(colstr string) (columns []DB_Column) { return columns } -func _process_orderby(orderstr string) (order []DB_Order) { +func processOrderby(orderstr string) (order []DB_Order) { if orderstr == "" { return order } @@ -57,7 +63,7 @@ func _process_orderby(orderstr string) (order []DB_Order) { return order } -func _processJoiner(joinstr string) (joiner []DB_Joiner) { +func processJoiner(joinstr string) (joiner []DB_Joiner) { if joinstr == "" { return joiner } @@ -68,9 +74,9 @@ func _processJoiner(joinstr string) (joiner []DB_Joiner) { var parseOffset int var left, right string - left, parseOffset = _getIdentifier(segment, parseOffset) - outjoin.Operator, parseOffset = _getOperator(segment, parseOffset+1) - right, parseOffset = _getIdentifier(segment, parseOffset+1) + left, parseOffset = getIdentifier(segment, parseOffset) + outjoin.Operator, parseOffset = getOperator(segment, parseOffset+1) + right, parseOffset = getIdentifier(segment, parseOffset+1) left_column := strings.Split(left, ".") right_column := strings.Split(right, ".") @@ -84,7 +90,7 @@ func _processJoiner(joinstr string) (joiner []DB_Joiner) { return joiner } -func _processWhere(wherestr string) (where []DB_Where) { +func processWhere(wherestr string) (where []DB_Where) { if wherestr == "" { return where } @@ -144,7 +150,7 @@ func _processWhere(wherestr string) (where []DB_Where) { //fmt.Println("len(halves)",len(halves[1])) //fmt.Println("preI",string(halves[1][preI])) //fmt.Println("msg prior to preI",halves[1][0:preI]) - i = _skipFunctionCall(segment, i-1) + i = skipFunctionCall(segment, i-1) //fmt.Println("i",i) //fmt.Println("msg prior to i-1",halves[1][0:i-1]) //fmt.Println("string(i-1)",string(halves[1][i-1])) @@ -179,7 +185,7 @@ func _processWhere(wherestr string) (where []DB_Where) { return where } -func _process_set(setstr string) (setter []DB_Setter) { +func processSet(setstr string) (setter []DB_Setter) { if setstr == "" { return setter } @@ -192,7 +198,7 @@ func _process_set(setstr string) (setter []DB_Setter) { setstr += "," for i := 0; i < len(setstr); i++ { if setstr[i] == '(' { - i = _skipFunctionCall(setstr, i-1) + i = skipFunctionCall(setstr, i-1) setset = append(setset, setstr[lastItem:i+1]) buffer = "" lastItem = i + 2 @@ -208,12 +214,12 @@ func _process_set(setstr string) (setter []DB_Setter) { // Second pass. Break this setitem into manageable chunks buffer = "" for _, setitem := range setset { - var tmp_setter DB_Setter + var tmpSetter DB_Setter halves := strings.Split(setitem, "=") if len(halves) != 2 { continue } - tmp_setter.Column = strings.TrimSpace(halves[0]) + tmpSetter.Column = strings.TrimSpace(halves[0]) halves[1] += ")" var optype int // 0: None, 1: Number, 2: Column, 3: Function, 4: String, 5: Operator @@ -237,7 +243,7 @@ func _process_set(setstr string) (setter []DB_Setter) { buffer = string(char) } else if char == '?' { //fmt.Println("Expr:","?") - tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{"?", "substitute"}) + tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{"?", "substitute"}) } case 1: // number if '0' <= char && char <= '9' { @@ -246,7 +252,7 @@ func _process_set(setstr string) (setter []DB_Setter) { optype = 0 i-- //fmt.Println("Expr:",buffer) - tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{buffer, "number"}) + tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{buffer, "number"}) } case 2: // column if ('a' <= char && char <= 'z') || ('A' <= char && char <= 'Z') || char == '_' { @@ -258,7 +264,7 @@ func _process_set(setstr string) (setter []DB_Setter) { optype = 0 i-- //fmt.Println("Expr:",buffer) - tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{buffer, "column"}) + tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{buffer, "column"}) } case 3: // function var preI = i @@ -266,14 +272,14 @@ func _process_set(setstr string) (setter []DB_Setter) { //fmt.Println("len(halves)",len(halves[1])) //fmt.Println("preI",string(halves[1][preI])) //fmt.Println("msg prior to preI",halves[1][0:preI]) - i = _skipFunctionCall(halves[1], i-1) + i = skipFunctionCall(halves[1], i-1) //fmt.Println("i",i) //fmt.Println("msg prior to i-1",halves[1][0:i-1]) //fmt.Println("string(i-1)",string(halves[1][i-1])) //fmt.Println("string(i)",string(halves[1][i])) buffer += halves[1][preI:i] + string(halves[1][i]) //fmt.Println("Expr:",buffer) - tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{buffer, "function"}) + tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{buffer, "function"}) optype = 0 case 4: // string if char != '\'' { @@ -281,7 +287,7 @@ func _process_set(setstr string) (setter []DB_Setter) { } else { optype = 0 //fmt.Println("Expr:",buffer) - tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{buffer, "string"}) + tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{buffer, "string"}) } case 5: // operator if _is_op_byte(char) { @@ -290,19 +296,19 @@ func _process_set(setstr string) (setter []DB_Setter) { optype = 0 i-- //fmt.Println("Expr:",buffer) - tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{buffer, "operator"}) + tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{buffer, "operator"}) } default: panic("Bad optype in _process_set") } } - setter = append(setter, tmp_setter) + setter = append(setter, tmpSetter) } //fmt.Println("setter",setter) return setter } -func _processLimit(limitstr string) (limiter DB_Limit) { +func processLimit(limitstr string) (limiter DB_Limit) { halves := strings.Split(limitstr, ",") if len(halves) == 2 { limiter.Offset = halves[0] @@ -321,7 +327,7 @@ func _is_op_rune(char rune) bool { return char == '<' || char == '>' || char == '=' || char == '!' || char == '*' || char == '%' || char == '+' || char == '-' || char == '/' } -func _processFields(fieldstr string) (fields []DB_Field) { +func processFields(fieldstr string) (fields []DB_Field) { if fieldstr == "" { return fields } @@ -330,12 +336,12 @@ func _processFields(fieldstr string) (fields []DB_Field) { fieldstr += "," for i := 0; i < len(fieldstr); i++ { if fieldstr[i] == '(' { - i = _skipFunctionCall(fieldstr, i-1) - fields = append(fields, DB_Field{Name: fieldstr[lastItem : i+1], Type: _getIdentifierType(fieldstr[lastItem : i+1])}) + i = skipFunctionCall(fieldstr, i-1) + fields = append(fields, DB_Field{Name: fieldstr[lastItem : i+1], Type: getIdentifierType(fieldstr[lastItem : i+1])}) buffer = "" lastItem = i + 2 } else if fieldstr[i] == ',' && buffer != "" { - fields = append(fields, DB_Field{Name: buffer, Type: _getIdentifierType(buffer)}) + fields = append(fields, DB_Field{Name: buffer, Type: getIdentifierType(buffer)}) buffer = "" lastItem = i + 1 } else if (fieldstr[i] > 32) && fieldstr[i] != ',' && fieldstr[i] != ')' { @@ -345,7 +351,7 @@ func _processFields(fieldstr string) (fields []DB_Field) { return fields } -func _getIdentifierType(identifier string) string { +func getIdentifierType(identifier string) string { if ('a' <= identifier[0] && identifier[0] <= 'z') || ('A' <= identifier[0] && identifier[0] <= 'Z') { if identifier[len(identifier)-1] == ')' { return "function" @@ -358,12 +364,12 @@ func _getIdentifierType(identifier string) string { return "literal" } -func _getIdentifier(segment string, startOffset int) (out string, i int) { +func getIdentifier(segment string, startOffset int) (out string, i int) { segment = strings.TrimSpace(segment) segment += " " // Avoid overflow bugs with slicing for i = startOffset; i < len(segment); i++ { if segment[i] == '(' { - i = _skipFunctionCall(segment, i) + i = skipFunctionCall(segment, i) return strings.TrimSpace(segment[startOffset:i]), (i - 1) } if (segment[i] == ' ' || _is_op_byte(segment[i])) && i != startOffset { @@ -373,7 +379,7 @@ func _getIdentifier(segment string, startOffset int) (out string, i int) { return strings.TrimSpace(segment[startOffset:]), (i - 1) } -func _getOperator(segment string, startOffset int) (out string, i int) { +func getOperator(segment string, startOffset int) (out string, i int) { segment = strings.TrimSpace(segment) segment += " " // Avoid overflow bugs with slicing for i = startOffset; i < len(segment); i++ { @@ -384,7 +390,7 @@ func _getOperator(segment string, startOffset int) (out string, i int) { return strings.TrimSpace(segment[startOffset:]), (i - 1) } -func _skipFunctionCall(data string, index int) int { +func skipFunctionCall(data string, index int) int { var braceCount int for ; index < len(data); index++ { char := data[index] diff --git a/query_gen/main.go b/query_gen/main.go index f13cefd3..07fd689e 100644 --- a/query_gen/main.go +++ b/query_gen/main.go @@ -269,6 +269,8 @@ func write_selects(adapter qgen.DB_Adapter) error { adapter.SimpleSelect("getSync", "sync", "last_update", "", "", "") + adapter.SimpleSelect("getAttachment", "attachments", "sectionID, sectionTable, originID, originTable, uploadedBy, path", "path = ? AND sectionID = ? AND sectionTable = ?", "", "") + return nil } @@ -334,6 +336,8 @@ func write_inserts(adapter qgen.DB_Adapter) error { adapter.SimpleInsert("addAdminlogEntry", "administration_logs", "action, elementID, elementType, ipaddress, actorID, doneAt", "?,?,?,?,?,UTC_TIMESTAMP()") + adapter.SimpleInsert("addAttachment", "attachments", "sectionID, sectionTable, originID, originTable, uploadedBy, path", "?,?,?,?,?,?") + adapter.SimpleInsert("createWordFilter", "word_filters", "find, replacement", "?,?") return nil diff --git a/reply.go b/reply.go index 3ac90fa5..78f42293 100644 --- a/reply.go +++ b/reply.go @@ -50,6 +50,7 @@ type Reply struct { LikeCount int } +// Copy gives you a non-pointer concurrency safe copy of the reply func (reply *Reply) Copy() Reply { return *reply } diff --git a/router_gen/main.go b/router_gen/main.go index 8f7c77c8..ce1eda66 100644 --- a/router_gen/main.go +++ b/router_gen/main.go @@ -6,8 +6,8 @@ import "log" //import "strings" import "os" -var route_list []Route -var route_groups []RouteGroup +var routeList []Route +var routeGroups []RouteGroup func main() { log.Println("Generating the router...") @@ -16,9 +16,9 @@ func main() { routes() var out string - var fdata = "// Code generated by. DO NOT EDIT.\n/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */\n" + var fileData = "// Code generated by. DO NOT EDIT.\n/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */\n" - for _, route := range route_list { + for _, route := range routeList { var end int if route.Path[len(route.Path)-1] == '/' { end = len(route.Path) - 1 @@ -36,7 +36,7 @@ func main() { out += ")\n\t\t\treturn" } - for _, group := range route_groups { + for _, group := range routeGroups { var end int if group.Path[len(group.Path)-1] == '/' { end = len(group.Path) - 1 @@ -46,10 +46,10 @@ func main() { out += ` case "` + group.Path[0:end] + `": switch(req.URL.Path) {` - var default_route Route + var defaultRoute Route for _, route := range group.Routes { if group.Path == route.Path { - default_route = route + defaultRoute = route continue } @@ -64,13 +64,13 @@ func main() { out += ")\n\t\t\t\t\treturn" } - if default_route.Name != "" { + if defaultRoute.Name != "" { out += "\n\t\t\t\tdefault:" - if default_route.Before != "" { - out += "\n\t\t\t\t\t" + default_route.Before + if defaultRoute.Before != "" { + out += "\n\t\t\t\t\t" + defaultRoute.Before } - out += "\n\t\t\t\t\t" + default_route.Name + "(w,req,user" - for _, item := range default_route.Vars { + out += "\n\t\t\t\t\t" + defaultRoute.Name + "(w,req,user" + for _, item := range defaultRoute.Vars { out += ", " + item } out += ")\n\t\t\t\t\treturn" @@ -78,7 +78,7 @@ func main() { out += "\n\t\t\t}" } - fdata += `package main + fileData += `package main import "log" import "strings" @@ -209,11 +209,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { NotFound(w,req) } ` - write_file("./gen_router.go", fdata) + writeFile("./gen_router.go", fileData) log.Println("Successfully generated the router") } -func write_file(name string, content string) { +func writeFile(name string, content string) { f, err := os.Create(name) if err != nil { log.Fatal(err) diff --git a/router_gen/routes.go b/router_gen/routes.go index f3619696..890e710d 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -13,11 +13,11 @@ type RouteGroup struct { } func addRoute(fname string, path string, before string, vars ...string) { - route_list = append(route_list, Route{fname, path, before, vars}) + routeList = append(routeList, Route{fname, path, before, vars}) } func addRouteGroup(path string, routes ...Route) { - route_groups = append(route_groups, RouteGroup{path, routes}) + routeGroups = append(routeGroups, RouteGroup{path, routes}) } func routes() { @@ -31,6 +31,7 @@ func routes() { //addRoute("routeTopicCreate","/topics/create/","","extra_data") //addRoute("routeTopics","/topics/",""/*,"&groups","&forums"*/) addRoute("routeChangeTheme", "/theme/", "") + addRoute("routeShowAttachment", "/attachs/", "", "extra_data") addRouteGroup("/report/", Route{"routeReportSubmit", "/report/submit/", "", []string{"extra_data"}}, diff --git a/routes.go b/routes.go index dafc9574..05a04c7f 100644 --- a/routes.go +++ b/routes.go @@ -26,7 +26,7 @@ var tList []interface{} //var nList []string var successJSONBytes = []byte(`{"success":"1"}`) -var cacheControlMaxAge = "max-age=" + strconv.Itoa(day) +var cacheControlMaxAge = "max-age=" + strconv.Itoa(day) // TODO: Make this a config value // HTTPSRedirect is a connection handler which redirects all HTTP requests to HTTPS type HTTPSRedirect struct { @@ -171,11 +171,25 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user User) { canSee = group.CanSee } + // We need a list of the visible forums for Quick Topic + var forumList []Forum + for _, fid := range canSee { forum := fstore.DirtyGet(fid) if forum.Name != "" && forum.Active { + if forum.ParentType == "" || forum.ParentType == "forum" { + // Optimise Quick Topic away for guests + if user.Loggedin { + fcopy := forum.Copy() + // TODO: Add a hook here for plugin_socialgroups + forumList = append(forumList, fcopy) + } + } + // ? - Should we be showing plugin_socialgroups posts on /topics/? + // ? - Would it be useful, if we could post in social groups from /topics/? fidList = append(fidList, strconv.Itoa(fid)) qlist += "?," + } } @@ -265,7 +279,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user User) { topicItem.LastUser = userList[topicItem.LastReplyBy] } - pi := TopicsPage{"Topic List", user, headerVars, topicList} + pi := TopicsPage{"All Topics", user, headerVars, topicList, forumList, config.DefaultForum} if preRenderHooks["pre_render_topic_list"] != nil { if runPreRenderHook("pre_render_topic_list", w, r, &user, &pi) { return @@ -495,7 +509,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) { BuildWidgets("view_topic", &topic, headerVars, r) - topic.ContentHTML = parseMessage(topic.Content) + topic.ContentHTML = parseMessage(topic.Content, topic.ParentID, "forums") topic.ContentLines = strings.Count(topic.Content, "\n") // We don't want users posting in locked topics... @@ -573,7 +587,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) { replyItem.UserLink = buildProfileURL(nameToSlug(replyItem.CreatedByName), replyItem.CreatedBy) replyItem.ParentID = topic.ID - replyItem.ContentHtml = parseMessage(replyItem.Content) + replyItem.ContentHtml = parseMessage(replyItem.Content, topic.ParentID, "forums") replyItem.ContentLines = strings.Count(replyItem.Content, "\n") postGroup, err = gstore.Get(replyItem.Group) @@ -744,7 +758,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user User) { // TODO: Add a hook here - replyList = append(replyList, ReplyUser{rid, puser.ID, replyContent, parseMessage(replyContent), replyCreatedBy, buildProfileURL(nameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""}) + replyList = append(replyList, ReplyUser{rid, puser.ID, replyContent, parseMessage(replyContent, 0, ""), replyCreatedBy, buildProfileURL(nameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""}) } err = rows.Err() if err != nil { diff --git a/routes_common.go b/routes_common.go index 2891c24b..210d541d 100644 --- a/routes_common.go +++ b/routes_common.go @@ -192,8 +192,8 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerV return headerVars, stats, false } - stats.Users = users.GetGlobalCount() - stats.Forums = fstore.GetGlobalCount() // TODO: Stop it from showing the blanked forums + stats.Users = users.GlobalCount() + stats.Forums = fstore.GlobalCount() // TODO: Stop it from showing the blanked forums stats.Settings = len(headerVars.Settings) stats.WordFilters = len(wordFilterBox.Load().(WordFilterBox)) stats.Themes = len(themes) diff --git a/site.go b/site.go index 9b6f5a39..d741eae1 100644 --- a/site.go +++ b/site.go @@ -12,6 +12,7 @@ var config Config var dev DevConfig type Site struct { + ShortName string Name string // ? - Move this into the settings table? Should we make a second version of this for the abbreviation shown in the navbar? Email string // ? - Move this into the settings table? URL string diff --git a/template_forum.go b/template_forum.go index ab115c63..2b945bf5 100644 --- a/template_forum.go +++ b/template_forum.go @@ -20,33 +20,37 @@ func template_forum(tmpl_forum_vars ForumPage, w http.ResponseWriter) { w.Write(header_0) w.Write([]byte(tmpl_forum_vars.Title)) w.Write(header_1) -w.Write([]byte(tmpl_forum_vars.Header.ThemeName)) +w.Write([]byte(tmpl_forum_vars.Header.Site.Name)) w.Write(header_2) +w.Write([]byte(tmpl_forum_vars.Header.ThemeName)) +w.Write(header_3) if len(tmpl_forum_vars.Header.Stylesheets) != 0 { for _, item := range tmpl_forum_vars.Header.Stylesheets { -w.Write(header_3) -w.Write([]byte(item)) w.Write(header_4) -} -} +w.Write([]byte(item)) w.Write(header_5) +} +} +w.Write(header_6) if len(tmpl_forum_vars.Header.Scripts) != 0 { for _, item := range tmpl_forum_vars.Header.Scripts { -w.Write(header_6) -w.Write([]byte(item)) w.Write(header_7) -} -} +w.Write([]byte(item)) w.Write(header_8) -w.Write([]byte(tmpl_forum_vars.CurrentUser.Session)) -w.Write(header_9) -if !tmpl_forum_vars.CurrentUser.IsSuperMod { -w.Write(header_10) } +} +w.Write(header_9) +w.Write([]byte(tmpl_forum_vars.CurrentUser.Session)) +w.Write(header_10) +w.Write([]byte(tmpl_forum_vars.Header.Site.URL)) w.Write(header_11) +if !tmpl_forum_vars.CurrentUser.IsSuperMod { +w.Write(header_12) +} +w.Write(header_13) w.Write(menu_0) w.Write(menu_1) -w.Write([]byte(tmpl_forum_vars.Header.Site.Name)) +w.Write([]byte(tmpl_forum_vars.Header.Site.ShortName)) w.Write(menu_2) if tmpl_forum_vars.CurrentUser.Loggedin { w.Write(menu_3) @@ -58,16 +62,16 @@ w.Write(menu_5) w.Write(menu_6) } w.Write(menu_7) -w.Write(header_12) -if tmpl_forum_vars.Header.Widgets.RightSidebar != "" { -w.Write(header_13) -} w.Write(header_14) +if tmpl_forum_vars.Header.Widgets.RightSidebar != "" { +w.Write(header_15) +} +w.Write(header_16) if len(tmpl_forum_vars.Header.NoticeList) != 0 { for _, item := range tmpl_forum_vars.Header.NoticeList { -w.Write(header_15) +w.Write(header_17) w.Write([]byte(item)) -w.Write(header_16) +w.Write(header_18) } } if tmpl_forum_vars.Page > 1 { @@ -106,65 +110,75 @@ w.Write(forum_14) w.Write(forum_15) } w.Write(forum_16) -if len(tmpl_forum_vars.ItemList) != 0 { -for _, item := range tmpl_forum_vars.ItemList { +if tmpl_forum_vars.CurrentUser.Perms.CreateTopic { w.Write(forum_17) -if item.Sticky { +w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) w.Write(forum_18) -} else { -if item.IsClosed { +if tmpl_forum_vars.CurrentUser.Perms.UploadFiles { w.Write(forum_19) } -} w.Write(forum_20) -if item.Creator.Avatar != "" { -w.Write(forum_21) -w.Write([]byte(item.Creator.Avatar)) -w.Write(forum_22) } +w.Write(forum_21) +if len(tmpl_forum_vars.ItemList) != 0 { +for _, item := range tmpl_forum_vars.ItemList { +w.Write(forum_22) +if item.Sticky { w.Write(forum_23) -w.Write([]byte(strconv.Itoa(item.PostCount))) -w.Write(forum_24) -w.Write([]byte(item.LastReplyAt)) -w.Write(forum_25) -w.Write([]byte(item.Link)) -w.Write(forum_26) -w.Write([]byte(item.Title)) -w.Write(forum_27) -w.Write([]byte(item.Creator.Link)) -w.Write(forum_28) -w.Write([]byte(item.Creator.Name)) -w.Write(forum_29) +} else { if item.IsClosed { +w.Write(forum_24) +} +} +w.Write(forum_25) +if item.Creator.Avatar != "" { +w.Write(forum_26) +w.Write([]byte(item.Creator.Avatar)) +w.Write(forum_27) +} +w.Write(forum_28) +w.Write([]byte(strconv.Itoa(item.PostCount))) +w.Write(forum_29) +w.Write([]byte(item.LastReplyAt)) w.Write(forum_30) +w.Write([]byte(item.Link)) +w.Write(forum_31) +w.Write([]byte(item.Title)) +w.Write(forum_32) +w.Write([]byte(item.Creator.Link)) +w.Write(forum_33) +w.Write([]byte(item.Creator.Name)) +w.Write(forum_34) +if item.IsClosed { +w.Write(forum_35) } if item.Sticky { -w.Write(forum_31) -} -w.Write(forum_32) -if item.LastUser.Avatar != "" { -w.Write(forum_33) -w.Write([]byte(item.LastUser.Avatar)) -w.Write(forum_34) -} -w.Write(forum_35) -w.Write([]byte(item.LastUser.Link)) w.Write(forum_36) -w.Write([]byte(item.LastUser.Name)) +} w.Write(forum_37) -w.Write([]byte(item.LastReplyAt)) +if item.LastUser.Avatar != "" { w.Write(forum_38) +w.Write([]byte(item.LastUser.Avatar)) +w.Write(forum_39) +} +w.Write(forum_40) +w.Write([]byte(item.LastUser.Link)) +w.Write(forum_41) +w.Write([]byte(item.LastUser.Name)) +w.Write(forum_42) +w.Write([]byte(item.LastReplyAt)) +w.Write(forum_43) } } else { -w.Write(forum_39) +w.Write(forum_44) if tmpl_forum_vars.CurrentUser.Perms.CreateTopic { -w.Write(forum_40) +w.Write(forum_45) w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) -w.Write(forum_41) +w.Write(forum_46) } -w.Write(forum_42) +w.Write(forum_47) } -w.Write(forum_43) +w.Write(forum_48) w.Write(footer_0) if len(tmpl_forum_vars.Header.Themes) != 0 { for _, item := range tmpl_forum_vars.Header.Themes { diff --git a/template_forums.go b/template_forums.go index 00501d12..62bb9835 100644 --- a/template_forums.go +++ b/template_forums.go @@ -19,33 +19,37 @@ func template_forums(tmpl_forums_vars ForumsPage, w http.ResponseWriter) { w.Write(header_0) w.Write([]byte(tmpl_forums_vars.Title)) w.Write(header_1) -w.Write([]byte(tmpl_forums_vars.Header.ThemeName)) +w.Write([]byte(tmpl_forums_vars.Header.Site.Name)) w.Write(header_2) +w.Write([]byte(tmpl_forums_vars.Header.ThemeName)) +w.Write(header_3) if len(tmpl_forums_vars.Header.Stylesheets) != 0 { for _, item := range tmpl_forums_vars.Header.Stylesheets { -w.Write(header_3) -w.Write([]byte(item)) w.Write(header_4) -} -} +w.Write([]byte(item)) w.Write(header_5) +} +} +w.Write(header_6) if len(tmpl_forums_vars.Header.Scripts) != 0 { for _, item := range tmpl_forums_vars.Header.Scripts { -w.Write(header_6) -w.Write([]byte(item)) w.Write(header_7) -} -} +w.Write([]byte(item)) w.Write(header_8) -w.Write([]byte(tmpl_forums_vars.CurrentUser.Session)) -w.Write(header_9) -if !tmpl_forums_vars.CurrentUser.IsSuperMod { -w.Write(header_10) } +} +w.Write(header_9) +w.Write([]byte(tmpl_forums_vars.CurrentUser.Session)) +w.Write(header_10) +w.Write([]byte(tmpl_forums_vars.Header.Site.URL)) w.Write(header_11) +if !tmpl_forums_vars.CurrentUser.IsSuperMod { +w.Write(header_12) +} +w.Write(header_13) w.Write(menu_0) w.Write(menu_1) -w.Write([]byte(tmpl_forums_vars.Header.Site.Name)) +w.Write([]byte(tmpl_forums_vars.Header.Site.ShortName)) w.Write(menu_2) if tmpl_forums_vars.CurrentUser.Loggedin { w.Write(menu_3) @@ -57,16 +61,16 @@ w.Write(menu_5) w.Write(menu_6) } w.Write(menu_7) -w.Write(header_12) -if tmpl_forums_vars.Header.Widgets.RightSidebar != "" { -w.Write(header_13) -} w.Write(header_14) +if tmpl_forums_vars.Header.Widgets.RightSidebar != "" { +w.Write(header_15) +} +w.Write(header_16) if len(tmpl_forums_vars.Header.NoticeList) != 0 { for _, item := range tmpl_forums_vars.Header.NoticeList { -w.Write(header_15) +w.Write(header_17) w.Write([]byte(item)) -w.Write(header_16) +w.Write(header_18) } } w.Write(forums_0) diff --git a/template_init.go b/template_init.go index eae8398b..5b536766 100644 --- a/template_init.go +++ b/template_init.go @@ -82,6 +82,7 @@ var template_create_topic_handle func(CreateTopicPage, http.ResponseWriter) = fu } } +// ? - Add template hooks? func compileTemplates() error { var c CTemplateSet @@ -128,6 +129,7 @@ func compileTemplates() error { return err } + // TODO: Use a dummy forum list to avoid o(n) problems var forumList []Forum forums, err := fstore.GetAll() if err != nil { @@ -147,7 +149,7 @@ func compileTemplates() error { var topicsList []*TopicsRow topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, "Date", "Date", user3.ID, 1, "", "127.0.0.1", 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}) - topicsPage := TopicsPage{"Topic List", user, headerVars, topicsList} + topicsPage := TopicsPage{"Topic List", user, headerVars, topicsList, forumList, config.DefaultForum} topicsTmpl, err := c.compileTemplate("topics.html", "templates/", "TopicsPage", topicsPage, varList) if err != nil { return err diff --git a/template_list.go b/template_list.go index a0aaf3fa..b72b08b9 100644 --- a/template_list.go +++ b/template_list.go @@ -5,31 +5,36 @@ var header_0 = []byte(` `) -var header_1 = []byte(` +var header_1 = []byte(` | `) +var header_2 = []byte(` +var header_3 = []byte(`/main.css" rel="stylesheet" type="text/css"> `) -var header_3 = []byte(` +var header_4 = []byte(` - `) -var header_5 = []byte(` - +var header_5 = []byte(`" rel="stylesheet" type="text/css"> `) var header_6 = []byte(` - + `) -var header_8 = []byte(` - +var header_7 = []byte(` + + `) +var header_9 = []byte(` + +var header_12 = []byte(`.supermod_only { display: none !important; }`) +var header_13 = []byte(`
`) var menu_0 = []byte(` `) -var header_12 = []byte(` +var header_14 = []byte(`
+var header_15 = []byte(`class="shrink_main"`) +var header_16 = []byte(`> `) -var header_15 = []byte(`
`) -var header_16 = []byte(`
`) +var header_17 = []byte(`
`) +var header_18 = []byte(`
`) var topic_0 = []byte(`
`) -var topic_35 = []byte(``) -var topic_37 = []byte(``) -var topic_39 = []byte(``) -var topic_41 = []byte(``) -var topic_43 = []byte(``) -var topic_45 = []byte(``) -var topic_47 = []byte(` +var topic_32 = []byte(` style="background-color:#D6FFD6;"`) +var topic_33 = []byte(`>`) +var topic_34 = []byte(``) +var topic_36 = []byte(``) +var topic_38 = []byte(``) +var topic_40 = []byte(``) +var topic_42 = []byte(``) +var topic_44 = []byte(``) +var topic_46 = []byte(``) +var topic_48 = []byte(` +var topic_49 = []byte(`?session=`) +var topic_50 = []byte(`&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag Topic"> `) -var topic_50 = []byte(``) -var topic_52 = []byte(``) -var topic_53 = []byte(``) -var topic_54 = []byte(``) -var topic_55 = []byte(``) -var topic_56 = []byte(` +var topic_51 = []byte(``) +var topic_53 = []byte(``) +var topic_54 = []byte(``) +var topic_55 = []byte(``) +var topic_56 = []byte(``) +var topic_57 = []byte(`
`) -var topic_57 = []byte(` +var topic_58 = []byte(`
`) -var topic_58 = []byte(` - `) var topic_59 = []byte(` + `) +var topic_60 = []byte(`
`) -var topic_60 = []byte(` +var topic_61 = []byte(`
+var topic_62 = []byte(`" style="`) +var topic_63 = []byte(`background-image:url(`) +var topic_64 = []byte(`), url(/static/`) +var topic_65 = []byte(`/post-avatar-bg.jpg);background-position: 0px `) +var topic_66 = []byte(`-1`) +var topic_67 = []byte(`0px;background-repeat:no-repeat, repeat-y;`) +var topic_68 = []byte(`"> `) -var topic_67 = []byte(` +var topic_69 = []byte(`

`) -var topic_68 = []byte(`

+var topic_70 = []byte(`

`) -var topic_70 = []byte(`   +var topic_71 = []byte(`" class="username real_username">`) +var topic_72 = []byte(`   `) -var topic_71 = []byte(``) -var topic_75 = []byte(``) -var topic_77 = []byte(``) -var topic_79 = []byte(``) -var topic_81 = []byte(` +var topic_73 = []byte(``) +var topic_77 = []byte(``) +var topic_79 = []byte(``) +var topic_81 = []byte(``) +var topic_83 = []byte(` +var topic_84 = []byte(`?session=`) +var topic_85 = []byte(`&type=reply" class="mod_button report_item" title="Flag Reply"> `) -var topic_84 = []byte(``) -var topic_86 = []byte(``) -var topic_87 = []byte(``) -var topic_88 = []byte(``) -var topic_89 = []byte(``) -var topic_90 = []byte(` +var topic_86 = []byte(``) +var topic_88 = []byte(``) +var topic_89 = []byte(``) +var topic_90 = []byte(``) +var topic_91 = []byte(``) +var topic_92 = []byte(`
`) -var topic_91 = []byte(`
+var topic_93 = []byte(`
`) -var topic_92 = []byte(` +var topic_94 = []byte(`
+var topic_95 = []byte(`' type="hidden" />
@@ -240,7 +246,7 @@ var topic_93 = []byte(`' type="hidden" />
`) -var topic_94 = []byte(` +var topic_96 = []byte(` @@ -654,60 +660,111 @@ var topics_0 = []byte(`
-

Topic List

-
-
+

All Topics

`) -var topics_1 = []byte(`
+var topics_3 = []byte(` +
+ `) +var topics_4 = []byte(`
`) +var topics_5 = []byte(` +
+ `) +var topics_6 = []byte(` +
+`) +var topics_7 = []byte(` + + `) +var topics_16 = []byte(` +
+ `) +var topics_17 = []byte(`
`) -var topics_8 = []byte(` replies
+var topics_24 = []byte(` replies

`) -var topics_9 = []byte(` +var topics_25 = []byte(` `) -var topics_11 = []byte(` `) -var topics_12 = []byte(``) -var topics_14 = []byte(``) -var topics_15 = []byte(` +var topics_26 = []byte(`">`) +var topics_27 = []byte(` `) +var topics_28 = []byte(``) +var topics_30 = []byte(``) +var topics_31 = []byte(`
Starter: `) -var topics_17 = []byte(` +var topics_32 = []byte(`">Starter: `) +var topics_33 = []byte(` `) -var topics_18 = []byte(` | 🔒︎`) -var topics_19 = []byte(` | 📍︎`) -var topics_20 = []byte(` +var topics_34 = []byte(` | 🔒︎`) +var topics_35 = []byte(` | 📍︎`) +var topics_36 = []byte(`
+var topics_37 = []byte(`topic_sticky`) +var topics_38 = []byte(`topic_closed`) +var topics_39 = []byte(`" style="`) +var topics_40 = []byte(`background-image: url(`) +var topics_41 = []byte(`);background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;`) +var topics_42 = []byte(`"> `) -var topics_28 = []byte(`
+var topics_43 = []byte(`" class="lastName" style="font-size: 14px;">`) +var topics_44 = []byte(`
Last: `) -var topics_29 = []byte(` +var topics_45 = []byte(`
`) -var topics_30 = []byte(`
There aren't any topics yet.`) -var topics_31 = []byte(` Start one?`) -var topics_32 = []byte(`
`) -var topics_33 = []byte(` +var topics_46 = []byte(`
There aren't any topics yet.`) +var topics_47 = []byte(` Start one?`) +var topics_48 = []byte(`
`) +var topics_49 = []byte(`
@@ -733,7 +790,7 @@ var forum_11 = []byte(`
`) var forum_12 = []byte(` -
`) var forum_14 = []byte(`
`) @@ -742,52 +799,83 @@ var forum_15 = []byte(` `) var forum_16 = []byte(` +`) +var forum_17 = []byte(` + +`) +var forum_21 = []byte(`
`) -var forum_17 = []byte(`
+var forum_22 = []byte(`
`) -var forum_24 = []byte(` replies
+var forum_29 = []byte(` replies

`) -var forum_25 = []byte(` +var forum_30 = []byte(` `) -var forum_27 = []byte(` +var forum_31 = []byte(`">`) +var forum_32 = []byte(`
Starter: `) -var forum_29 = []byte(` +var forum_33 = []byte(`">Starter: `) +var forum_34 = []byte(` `) -var forum_30 = []byte(` | 🔒︎`) -var forum_31 = []byte(` | 📍︎`) -var forum_32 = []byte(` +var forum_35 = []byte(` | 🔒︎`) +var forum_36 = []byte(` | 📍︎`) +var forum_37 = []byte(`
+var forum_38 = []byte(`background-image: url(`) +var forum_39 = []byte(`);background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;`) +var forum_40 = []byte(`"> `) -var forum_37 = []byte(`
+var forum_41 = []byte(`" class="lastName" style="font-size: 14px;">`) +var forum_42 = []byte(`
Last: `) -var forum_38 = []byte(` +var forum_43 = []byte(`
`) -var forum_39 = []byte(`
There aren't any topics in this forum yet.`) -var forum_40 = []byte(` Start one?`) -var forum_42 = []byte(`
`) -var forum_43 = []byte(` +var forum_44 = []byte(`
There aren't any topics in this forum yet.`) +var forum_45 = []byte(` Start one?`) +var forum_47 = []byte(`
`) +var forum_48 = []byte(`
diff --git a/template_profile.go b/template_profile.go index 4b25b1d7..30c8b4f8 100644 --- a/template_profile.go +++ b/template_profile.go @@ -20,33 +20,37 @@ func template_profile(tmpl_profile_vars ProfilePage, w http.ResponseWriter) { w.Write(header_0) w.Write([]byte(tmpl_profile_vars.Title)) w.Write(header_1) -w.Write([]byte(tmpl_profile_vars.Header.ThemeName)) +w.Write([]byte(tmpl_profile_vars.Header.Site.Name)) w.Write(header_2) +w.Write([]byte(tmpl_profile_vars.Header.ThemeName)) +w.Write(header_3) if len(tmpl_profile_vars.Header.Stylesheets) != 0 { for _, item := range tmpl_profile_vars.Header.Stylesheets { -w.Write(header_3) -w.Write([]byte(item)) w.Write(header_4) -} -} +w.Write([]byte(item)) w.Write(header_5) +} +} +w.Write(header_6) if len(tmpl_profile_vars.Header.Scripts) != 0 { for _, item := range tmpl_profile_vars.Header.Scripts { -w.Write(header_6) -w.Write([]byte(item)) w.Write(header_7) -} -} +w.Write([]byte(item)) w.Write(header_8) -w.Write([]byte(tmpl_profile_vars.CurrentUser.Session)) -w.Write(header_9) -if !tmpl_profile_vars.CurrentUser.IsSuperMod { -w.Write(header_10) } +} +w.Write(header_9) +w.Write([]byte(tmpl_profile_vars.CurrentUser.Session)) +w.Write(header_10) +w.Write([]byte(tmpl_profile_vars.Header.Site.URL)) w.Write(header_11) +if !tmpl_profile_vars.CurrentUser.IsSuperMod { +w.Write(header_12) +} +w.Write(header_13) w.Write(menu_0) w.Write(menu_1) -w.Write([]byte(tmpl_profile_vars.Header.Site.Name)) +w.Write([]byte(tmpl_profile_vars.Header.Site.ShortName)) w.Write(menu_2) if tmpl_profile_vars.CurrentUser.Loggedin { w.Write(menu_3) @@ -58,16 +62,16 @@ w.Write(menu_5) w.Write(menu_6) } w.Write(menu_7) -w.Write(header_12) -if tmpl_profile_vars.Header.Widgets.RightSidebar != "" { -w.Write(header_13) -} w.Write(header_14) +if tmpl_profile_vars.Header.Widgets.RightSidebar != "" { +w.Write(header_15) +} +w.Write(header_16) if len(tmpl_profile_vars.Header.NoticeList) != 0 { for _, item := range tmpl_profile_vars.Header.NoticeList { -w.Write(header_15) +w.Write(header_17) w.Write([]byte(item)) -w.Write(header_16) +w.Write(header_18) } } w.Write(profile_0) diff --git a/template_topic.go b/template_topic.go index d4830189..b811008e 100644 --- a/template_topic.go +++ b/template_topic.go @@ -20,33 +20,37 @@ func template_topic(tmpl_topic_vars TopicPage, w http.ResponseWriter) { w.Write(header_0) w.Write([]byte(tmpl_topic_vars.Title)) w.Write(header_1) -w.Write([]byte(tmpl_topic_vars.Header.ThemeName)) +w.Write([]byte(tmpl_topic_vars.Header.Site.Name)) w.Write(header_2) +w.Write([]byte(tmpl_topic_vars.Header.ThemeName)) +w.Write(header_3) if len(tmpl_topic_vars.Header.Stylesheets) != 0 { for _, item := range tmpl_topic_vars.Header.Stylesheets { -w.Write(header_3) -w.Write([]byte(item)) w.Write(header_4) -} -} +w.Write([]byte(item)) w.Write(header_5) +} +} +w.Write(header_6) if len(tmpl_topic_vars.Header.Scripts) != 0 { for _, item := range tmpl_topic_vars.Header.Scripts { -w.Write(header_6) -w.Write([]byte(item)) w.Write(header_7) -} -} +w.Write([]byte(item)) w.Write(header_8) -w.Write([]byte(tmpl_topic_vars.CurrentUser.Session)) -w.Write(header_9) -if !tmpl_topic_vars.CurrentUser.IsSuperMod { -w.Write(header_10) } +} +w.Write(header_9) +w.Write([]byte(tmpl_topic_vars.CurrentUser.Session)) +w.Write(header_10) +w.Write([]byte(tmpl_topic_vars.Header.Site.URL)) w.Write(header_11) +if !tmpl_topic_vars.CurrentUser.IsSuperMod { +w.Write(header_12) +} +w.Write(header_13) w.Write(menu_0) w.Write(menu_1) -w.Write([]byte(tmpl_topic_vars.Header.Site.Name)) +w.Write([]byte(tmpl_topic_vars.Header.Site.ShortName)) w.Write(menu_2) if tmpl_topic_vars.CurrentUser.Loggedin { w.Write(menu_3) @@ -58,16 +62,16 @@ w.Write(menu_5) w.Write(menu_6) } w.Write(menu_7) -w.Write(header_12) -if tmpl_topic_vars.Header.Widgets.RightSidebar != "" { -w.Write(header_13) -} w.Write(header_14) +if tmpl_topic_vars.Header.Widgets.RightSidebar != "" { +w.Write(header_15) +} +w.Write(header_16) if len(tmpl_topic_vars.Header.NoticeList) != 0 { for _, item := range tmpl_topic_vars.Header.NoticeList { -w.Write(header_15) +w.Write(header_17) w.Write([]byte(item)) -w.Write(header_16) +w.Write(header_18) } } w.Write(topic_0) @@ -117,169 +121,173 @@ if tmpl_topic_vars.Topic.Avatar != "" { w.Write(topic_20) w.Write([]byte(tmpl_topic_vars.Topic.Avatar)) w.Write(topic_21) -if tmpl_topic_vars.Topic.ContentLines <= 5 { +w.Write([]byte(tmpl_topic_vars.Header.ThemeName)) w.Write(topic_22) -} +if tmpl_topic_vars.Topic.ContentLines <= 5 { w.Write(topic_23) } w.Write(topic_24) -w.Write([]byte(tmpl_topic_vars.Topic.ContentHTML)) -w.Write(topic_25) -w.Write([]byte(tmpl_topic_vars.Topic.Content)) -w.Write(topic_26) -w.Write([]byte(tmpl_topic_vars.Topic.UserLink)) -w.Write(topic_27) -w.Write([]byte(tmpl_topic_vars.Topic.CreatedByName)) -w.Write(topic_28) -if tmpl_topic_vars.CurrentUser.Perms.LikeItem { -w.Write(topic_29) -w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) -w.Write(topic_30) -if tmpl_topic_vars.Topic.Liked { -w.Write(topic_31) } +w.Write(topic_25) +w.Write([]byte(tmpl_topic_vars.Topic.ContentHTML)) +w.Write(topic_26) +w.Write([]byte(tmpl_topic_vars.Topic.Content)) +w.Write(topic_27) +w.Write([]byte(tmpl_topic_vars.Topic.UserLink)) +w.Write(topic_28) +w.Write([]byte(tmpl_topic_vars.Topic.CreatedByName)) +w.Write(topic_29) +if tmpl_topic_vars.CurrentUser.Perms.LikeItem { +w.Write(topic_30) +w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) +w.Write(topic_31) +if tmpl_topic_vars.Topic.Liked { w.Write(topic_32) } -if tmpl_topic_vars.CurrentUser.Perms.EditTopic { w.Write(topic_33) -w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) +} +if tmpl_topic_vars.CurrentUser.Perms.EditTopic { w.Write(topic_34) +w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) +w.Write(topic_35) } if tmpl_topic_vars.CurrentUser.Perms.DeleteTopic { -w.Write(topic_35) -w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) w.Write(topic_36) +w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) +w.Write(topic_37) } if tmpl_topic_vars.CurrentUser.Perms.CloseTopic { if tmpl_topic_vars.Topic.IsClosed { -w.Write(topic_37) -w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) w.Write(topic_38) -} else { -w.Write(topic_39) w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) +w.Write(topic_39) +} else { w.Write(topic_40) +w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) +w.Write(topic_41) } } if tmpl_topic_vars.CurrentUser.Perms.PinTopic { if tmpl_topic_vars.Topic.Sticky { -w.Write(topic_41) -w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) w.Write(topic_42) -} else { -w.Write(topic_43) w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) +w.Write(topic_43) +} else { w.Write(topic_44) +w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) +w.Write(topic_45) } } if tmpl_topic_vars.CurrentUser.Perms.ViewIPs { -w.Write(topic_45) -w.Write([]byte(tmpl_topic_vars.Topic.IPAddress)) w.Write(topic_46) -} +w.Write([]byte(tmpl_topic_vars.Topic.IPAddress)) w.Write(topic_47) -w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) +} w.Write(topic_48) -w.Write([]byte(tmpl_topic_vars.CurrentUser.Session)) +w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) w.Write(topic_49) -if tmpl_topic_vars.Topic.LikeCount > 0 { +w.Write([]byte(tmpl_topic_vars.CurrentUser.Session)) w.Write(topic_50) -w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.LikeCount))) +if tmpl_topic_vars.Topic.LikeCount > 0 { w.Write(topic_51) +w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.LikeCount))) +w.Write(topic_52) } if tmpl_topic_vars.Topic.Tag != "" { -w.Write(topic_52) -w.Write([]byte(tmpl_topic_vars.Topic.Tag)) w.Write(topic_53) -} else { +w.Write([]byte(tmpl_topic_vars.Topic.Tag)) w.Write(topic_54) -w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.Level))) +} else { w.Write(topic_55) -} +w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.Level))) w.Write(topic_56) +} +w.Write(topic_57) if len(tmpl_topic_vars.ItemList) != 0 { for _, item := range tmpl_topic_vars.ItemList { if item.ActionType != "" { -w.Write(topic_57) -w.Write([]byte(item.ActionIcon)) w.Write(topic_58) -w.Write([]byte(item.ActionType)) +w.Write([]byte(item.ActionIcon)) w.Write(topic_59) -} else { +w.Write([]byte(item.ActionType)) w.Write(topic_60) -w.Write([]byte(item.ClassName)) +} else { w.Write(topic_61) -if item.Avatar != "" { +w.Write([]byte(item.ClassName)) w.Write(topic_62) -w.Write([]byte(item.Avatar)) +if item.Avatar != "" { w.Write(topic_63) -if item.ContentLines <= 5 { +w.Write([]byte(item.Avatar)) w.Write(topic_64) -} +w.Write([]byte(tmpl_topic_vars.Header.ThemeName)) w.Write(topic_65) -} +if item.ContentLines <= 5 { w.Write(topic_66) +} w.Write(topic_67) -w.Write([]byte(item.ContentHtml)) +} w.Write(topic_68) -w.Write([]byte(item.UserLink)) w.Write(topic_69) -w.Write([]byte(item.CreatedByName)) +w.Write([]byte(item.ContentHtml)) w.Write(topic_70) -if tmpl_topic_vars.CurrentUser.Perms.LikeItem { +w.Write([]byte(item.UserLink)) w.Write(topic_71) -w.Write([]byte(strconv.Itoa(item.ID))) +w.Write([]byte(item.CreatedByName)) w.Write(topic_72) -if item.Liked { +if tmpl_topic_vars.CurrentUser.Perms.LikeItem { w.Write(topic_73) -} -w.Write(topic_74) -} -if tmpl_topic_vars.CurrentUser.Perms.EditReply { -w.Write(topic_75) w.Write([]byte(strconv.Itoa(item.ID))) +w.Write(topic_74) +if item.Liked { +w.Write(topic_75) +} w.Write(topic_76) } -if tmpl_topic_vars.CurrentUser.Perms.DeleteReply { +if tmpl_topic_vars.CurrentUser.Perms.EditReply { w.Write(topic_77) w.Write([]byte(strconv.Itoa(item.ID))) w.Write(topic_78) } -if tmpl_topic_vars.CurrentUser.Perms.ViewIPs { +if tmpl_topic_vars.CurrentUser.Perms.DeleteReply { w.Write(topic_79) -w.Write([]byte(item.IPAddress)) +w.Write([]byte(strconv.Itoa(item.ID))) w.Write(topic_80) } +if tmpl_topic_vars.CurrentUser.Perms.ViewIPs { w.Write(topic_81) -w.Write([]byte(strconv.Itoa(item.ID))) +w.Write([]byte(item.IPAddress)) w.Write(topic_82) -w.Write([]byte(tmpl_topic_vars.CurrentUser.Session)) +} w.Write(topic_83) -if item.LikeCount > 0 { +w.Write([]byte(strconv.Itoa(item.ID))) w.Write(topic_84) -w.Write([]byte(strconv.Itoa(item.LikeCount))) +w.Write([]byte(tmpl_topic_vars.CurrentUser.Session)) w.Write(topic_85) +if item.LikeCount > 0 { +w.Write(topic_86) +w.Write([]byte(strconv.Itoa(item.LikeCount))) +w.Write(topic_87) } if item.Tag != "" { -w.Write(topic_86) -w.Write([]byte(item.Tag)) -w.Write(topic_87) -} else { w.Write(topic_88) -w.Write([]byte(strconv.Itoa(item.Level))) +w.Write([]byte(item.Tag)) w.Write(topic_89) -} +} else { w.Write(topic_90) -} -} -} +w.Write([]byte(strconv.Itoa(item.Level))) w.Write(topic_91) -if tmpl_topic_vars.CurrentUser.Perms.CreateReply { -w.Write(topic_92) -w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) -w.Write(topic_93) } +w.Write(topic_92) +} +} +} +w.Write(topic_93) +if tmpl_topic_vars.CurrentUser.Perms.CreateReply { w.Write(topic_94) +w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) +w.Write(topic_95) +} +w.Write(topic_96) w.Write(footer_0) if len(tmpl_topic_vars.Header.Themes) != 0 { for _, item := range tmpl_topic_vars.Header.Themes { diff --git a/template_topic_alt.go b/template_topic_alt.go index ea0e6895..0aabcabe 100644 --- a/template_topic_alt.go +++ b/template_topic_alt.go @@ -20,33 +20,37 @@ func template_topic_alt(tmpl_topic_alt_vars TopicPage, w http.ResponseWriter) { w.Write(header_0) w.Write([]byte(tmpl_topic_alt_vars.Title)) w.Write(header_1) -w.Write([]byte(tmpl_topic_alt_vars.Header.ThemeName)) +w.Write([]byte(tmpl_topic_alt_vars.Header.Site.Name)) w.Write(header_2) +w.Write([]byte(tmpl_topic_alt_vars.Header.ThemeName)) +w.Write(header_3) if len(tmpl_topic_alt_vars.Header.Stylesheets) != 0 { for _, item := range tmpl_topic_alt_vars.Header.Stylesheets { -w.Write(header_3) -w.Write([]byte(item)) w.Write(header_4) -} -} +w.Write([]byte(item)) w.Write(header_5) +} +} +w.Write(header_6) if len(tmpl_topic_alt_vars.Header.Scripts) != 0 { for _, item := range tmpl_topic_alt_vars.Header.Scripts { -w.Write(header_6) -w.Write([]byte(item)) w.Write(header_7) -} -} +w.Write([]byte(item)) w.Write(header_8) -w.Write([]byte(tmpl_topic_alt_vars.CurrentUser.Session)) -w.Write(header_9) -if !tmpl_topic_alt_vars.CurrentUser.IsSuperMod { -w.Write(header_10) } +} +w.Write(header_9) +w.Write([]byte(tmpl_topic_alt_vars.CurrentUser.Session)) +w.Write(header_10) +w.Write([]byte(tmpl_topic_alt_vars.Header.Site.URL)) w.Write(header_11) +if !tmpl_topic_alt_vars.CurrentUser.IsSuperMod { +w.Write(header_12) +} +w.Write(header_13) w.Write(menu_0) w.Write(menu_1) -w.Write([]byte(tmpl_topic_alt_vars.Header.Site.Name)) +w.Write([]byte(tmpl_topic_alt_vars.Header.Site.ShortName)) w.Write(menu_2) if tmpl_topic_alt_vars.CurrentUser.Loggedin { w.Write(menu_3) @@ -58,16 +62,16 @@ w.Write(menu_5) w.Write(menu_6) } w.Write(menu_7) -w.Write(header_12) -if tmpl_topic_alt_vars.Header.Widgets.RightSidebar != "" { -w.Write(header_13) -} w.Write(header_14) +if tmpl_topic_alt_vars.Header.Widgets.RightSidebar != "" { +w.Write(header_15) +} +w.Write(header_16) if len(tmpl_topic_alt_vars.Header.NoticeList) != 0 { for _, item := range tmpl_topic_alt_vars.Header.NoticeList { -w.Write(header_15) +w.Write(header_17) w.Write([]byte(item)) -w.Write(header_16) +w.Write(header_18) } } if tmpl_topic_alt_vars.Page > 1 { diff --git a/template_topics.go b/template_topics.go index 331b350a..bc3ac107 100644 --- a/template_topics.go +++ b/template_topics.go @@ -20,33 +20,37 @@ func template_topics(tmpl_topics_vars TopicsPage, w http.ResponseWriter) { w.Write(header_0) w.Write([]byte(tmpl_topics_vars.Title)) w.Write(header_1) -w.Write([]byte(tmpl_topics_vars.Header.ThemeName)) +w.Write([]byte(tmpl_topics_vars.Header.Site.Name)) w.Write(header_2) +w.Write([]byte(tmpl_topics_vars.Header.ThemeName)) +w.Write(header_3) if len(tmpl_topics_vars.Header.Stylesheets) != 0 { for _, item := range tmpl_topics_vars.Header.Stylesheets { -w.Write(header_3) -w.Write([]byte(item)) w.Write(header_4) -} -} +w.Write([]byte(item)) w.Write(header_5) +} +} +w.Write(header_6) if len(tmpl_topics_vars.Header.Scripts) != 0 { for _, item := range tmpl_topics_vars.Header.Scripts { -w.Write(header_6) -w.Write([]byte(item)) w.Write(header_7) -} -} +w.Write([]byte(item)) w.Write(header_8) -w.Write([]byte(tmpl_topics_vars.CurrentUser.Session)) -w.Write(header_9) -if !tmpl_topics_vars.CurrentUser.IsSuperMod { -w.Write(header_10) } +} +w.Write(header_9) +w.Write([]byte(tmpl_topics_vars.CurrentUser.Session)) +w.Write(header_10) +w.Write([]byte(tmpl_topics_vars.Header.Site.URL)) w.Write(header_11) +if !tmpl_topics_vars.CurrentUser.IsSuperMod { +w.Write(header_12) +} +w.Write(header_13) w.Write(menu_0) w.Write(menu_1) -w.Write([]byte(tmpl_topics_vars.Header.Site.Name)) +w.Write([]byte(tmpl_topics_vars.Header.Site.ShortName)) w.Write(menu_2) if tmpl_topics_vars.CurrentUser.Loggedin { w.Write(menu_3) @@ -58,92 +62,129 @@ w.Write(menu_5) w.Write(menu_6) } w.Write(menu_7) -w.Write(header_12) -if tmpl_topics_vars.Header.Widgets.RightSidebar != "" { -w.Write(header_13) -} w.Write(header_14) +if tmpl_topics_vars.Header.Widgets.RightSidebar != "" { +w.Write(header_15) +} +w.Write(header_16) if len(tmpl_topics_vars.Header.NoticeList) != 0 { for _, item := range tmpl_topics_vars.Header.NoticeList { -w.Write(header_15) +w.Write(header_17) w.Write([]byte(item)) -w.Write(header_16) +w.Write(header_18) } } w.Write(topics_0) -if len(tmpl_topics_vars.ItemList) != 0 { -for _, item := range tmpl_topics_vars.ItemList { +if tmpl_topics_vars.CurrentUser.ID != 0 { w.Write(topics_1) -if item.Sticky { +} w.Write(topics_2) -} else { -if item.IsClosed { +if tmpl_topics_vars.CurrentUser.ID != 0 { +if len(tmpl_topics_vars.ForumList) != 0 { w.Write(topics_3) -} -} +} else { w.Write(topics_4) -if item.Creator.Avatar != "" { -w.Write(topics_5) -w.Write([]byte(item.Creator.Avatar)) -w.Write(topics_6) } +w.Write(topics_5) +} +w.Write(topics_6) +if tmpl_topics_vars.CurrentUser.ID != 0 { +if len(tmpl_topics_vars.ForumList) != 0 { w.Write(topics_7) -w.Write([]byte(strconv.Itoa(item.PostCount))) +if len(tmpl_topics_vars.ForumList) != 0 { +for _, item := range tmpl_topics_vars.ForumList { w.Write(topics_8) -w.Write([]byte(item.LastReplyAt)) +if item.ID == tmpl_topics_vars.DefaultForum { w.Write(topics_9) -w.Write([]byte(item.Link)) +} w.Write(topics_10) -w.Write([]byte(item.Title)) +w.Write([]byte(strconv.Itoa(item.ID))) w.Write(topics_11) -if item.ForumName != "" { +w.Write([]byte(item.Name)) w.Write(topics_12) -w.Write([]byte(item.ForumLink)) +} +} w.Write(topics_13) -w.Write([]byte(item.ForumName)) +if tmpl_topics_vars.CurrentUser.Perms.UploadFiles { w.Write(topics_14) } w.Write(topics_15) -w.Write([]byte(item.Creator.Link)) -w.Write(topics_16) -w.Write([]byte(item.Creator.Name)) -w.Write(topics_17) -if item.IsClosed { -w.Write(topics_18) } +} +w.Write(topics_16) +if len(tmpl_topics_vars.TopicList) != 0 { +for _, item := range tmpl_topics_vars.TopicList { +w.Write(topics_17) if item.Sticky { +w.Write(topics_18) +} else { +if item.IsClosed { w.Write(topics_19) } +} w.Write(topics_20) -if item.Sticky { +if item.Creator.Avatar != "" { w.Write(topics_21) -} else { -if item.IsClosed { +w.Write([]byte(item.Creator.Avatar)) w.Write(topics_22) } -} w.Write(topics_23) -if item.LastUser.Avatar != "" { +w.Write([]byte(strconv.Itoa(item.PostCount))) w.Write(topics_24) -w.Write([]byte(item.LastUser.Avatar)) -w.Write(topics_25) -} -w.Write(topics_26) -w.Write([]byte(item.LastUser.Link)) -w.Write(topics_27) -w.Write([]byte(item.LastUser.Name)) -w.Write(topics_28) w.Write([]byte(item.LastReplyAt)) +w.Write(topics_25) +w.Write([]byte(item.Link)) +w.Write(topics_26) +w.Write([]byte(item.Title)) +w.Write(topics_27) +if item.ForumName != "" { +w.Write(topics_28) +w.Write([]byte(item.ForumLink)) w.Write(topics_29) +w.Write([]byte(item.ForumName)) +w.Write(topics_30) +} +w.Write(topics_31) +w.Write([]byte(item.Creator.Link)) +w.Write(topics_32) +w.Write([]byte(item.Creator.Name)) +w.Write(topics_33) +if item.IsClosed { +w.Write(topics_34) +} +if item.Sticky { +w.Write(topics_35) +} +w.Write(topics_36) +if item.Sticky { +w.Write(topics_37) +} else { +if item.IsClosed { +w.Write(topics_38) +} +} +w.Write(topics_39) +if item.LastUser.Avatar != "" { +w.Write(topics_40) +w.Write([]byte(item.LastUser.Avatar)) +w.Write(topics_41) +} +w.Write(topics_42) +w.Write([]byte(item.LastUser.Link)) +w.Write(topics_43) +w.Write([]byte(item.LastUser.Name)) +w.Write(topics_44) +w.Write([]byte(item.LastReplyAt)) +w.Write(topics_45) } } else { -w.Write(topics_30) +w.Write(topics_46) if tmpl_topics_vars.CurrentUser.Perms.CreateTopic { -w.Write(topics_31) +w.Write(topics_47) } -w.Write(topics_32) +w.Write(topics_48) } -w.Write(topics_33) +w.Write(topics_49) w.Write(footer_0) if len(tmpl_topics_vars.Header.Themes) != 0 { for _, item := range tmpl_topics_vars.Header.Themes { diff --git a/templates.go b/templates.go index 9a3013aa..ea7a1e5d 100644 --- a/templates.go +++ b/templates.go @@ -876,14 +876,15 @@ func (c *CTemplateSet) compileBoolsub(varname string, varholder string, template fmt.Println("in compileBoolsub") } out, val := c.compileIfVarsub(varname, varholder, templateName, val) + // TODO: What if it's a pointer or an interface? I *think* we've got pointers handled somewhere, but not interfaces which we don't know the types of at compile time switch val.Kind() { - case reflect.Int: + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: out += " > 0" case reflect.Bool: // Do nothing case reflect.String: out += " != \"\"" - case reflect.Int64: - out += " > 0" + case reflect.Slice, reflect.Map: + out = "len(" + out + ") != 0" default: fmt.Println("Variable Name:", varname) fmt.Println("Variable Holder:", varholder) diff --git a/templates/create-topic.html b/templates/create-topic.html index 241b6b54..f3ac245e 100644 --- a/templates/create-topic.html +++ b/templates/create-topic.html @@ -4,25 +4,29 @@

Create Topic

-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + {{if .CurrentUser.Perms.UploadFiles}} + + {{end}} +
+ +
{{template "footer.html" . }} diff --git a/templates/forum.html b/templates/forum.html index 9015d5aa..f7092720 100644 --- a/templates/forum.html +++ b/templates/forum.html @@ -11,11 +11,37 @@ {{if ne .CurrentUser.ID 0}} {{if .CurrentUser.Perms.CreateTopic}} -
+
{{else}}
{{end}}
{{end}} +{{if .CurrentUser.Perms.CreateTopic}} + +{{end}}
{{range .ItemList}}
diff --git a/templates/header.html b/templates/header.html index 5328284b..a6cbbb43 100644 --- a/templates/header.html +++ b/templates/header.html @@ -1,7 +1,7 @@ - {{.Title}} + {{.Title}} | {{.Header.Site.Name}} {{range .Header.Stylesheets}} @@ -10,7 +10,10 @@ {{range .Header.Scripts}} {{end}} - + diff --git a/templates/menu.html b/templates/menu.html index 9b16549a..db263339 100644 --- a/templates/menu.html +++ b/templates/menu.html @@ -2,10 +2,9 @@
-
+

{{.Topic.ContentHTML}}

@@ -55,7 +55,7 @@ {{.ActionType}}
{{else}} -
+
{{/** TODO: We might end up with
s in the inline editor, fix this **/}}

{{.ContentHtml}}

diff --git a/templates/topics.html b/templates/topics.html index 0a0227af..b941e085 100644 --- a/templates/topics.html +++ b/templates/topics.html @@ -2,10 +2,48 @@
-

Topic List

+

All Topics

+ {{if ne .CurrentUser.ID 0}} + {{if .ForumList}} +
+ {{else}}
{{end}} +
+ {{end}}
-
- {{range .ItemList}}
+{{if ne .CurrentUser.ID 0}} + {{if .ForumList}} + + {{end}} +{{end}} +
+ {{range .TopicList}}
{{.PostCount}} replies
{{.LastReplyAt}} diff --git a/themes.go b/themes.go index bb0d11e6..3488d066 100644 --- a/themes.go +++ b/themes.go @@ -139,6 +139,19 @@ func initThemes() error { theme.Active = false // Set this to false, just in case someone explicitly overrode this value in the JSON file + // TODO: Let the theme specify where it's resources are via the JSON file? + // TODO: Let the theme inherit CSS from another theme? + // ? - This might not be too helpful, as it only searches for /public/ and not if /public/ is empty. Still, it might help some people with a slightly less cryptic error + _, err = os.Stat("./themes/" + theme.Name + "/public/") + if err != nil { + if os.IsNotExist(err) { + return errors.New("We couldn't find this theme's resources. E.g. the /public/ folder.") + } else { + log.Print("We weren't able to access this theme's resources due to a permissions issue or some other problem") + return err + } + } + if theme.FullImage != "" { if dev.DebugMode { log.Print("Adding theme image") diff --git a/themes/cosmo-conflux/public/main.css b/themes/cosmo-conflux/public/main.css index 7d1e0a22..570334cd 100644 --- a/themes/cosmo-conflux/public/main.css +++ b/themes/cosmo-conflux/public/main.css @@ -796,7 +796,7 @@ blockquote p { } @media(max-width: 620px) { - .menu_create_topic, .menu_overview, .hide_on_mobile { display: none; } + .menu_overview, .hide_on_mobile { display: none; } } /* This one is specifically for small mobiles.. */ diff --git a/themes/cosmo/public/main.css b/themes/cosmo/public/main.css index c5471386..1da12d9c 100644 --- a/themes/cosmo/public/main.css +++ b/themes/cosmo/public/main.css @@ -807,7 +807,7 @@ blockquote p { } @media(max-width: 620px) { - .menu_create_topic, .menu_overview, .hide_on_mobile { display: none; } + .menu_overview, .hide_on_mobile { display: none; } } /* This one is specifically for small mobiles.. */ diff --git a/themes/cosora/public/panel.css b/themes/cosora/public/panel.css new file mode 100644 index 00000000..e69de29b diff --git a/themes/cosora/theme.json b/themes/cosora/theme.json new file mode 100644 index 00000000..1bbc9d0a --- /dev/null +++ b/themes/cosora/theme.json @@ -0,0 +1,9 @@ +{ + "Name": "cosora", + "FriendlyName": "Cosora", + "Version": "0.0.1", + "Creator": "Azareal", + "URL": "github.com/Azareal/Gosora", + "HideFromThemes":true, + "Tag": "WIP" +} diff --git a/themes/shadow/public/main.css b/themes/shadow/public/main.css index 609a9eb4..7bfb6674 100644 --- a/themes/shadow/public/main.css +++ b/themes/shadow/public/main.css @@ -1,10 +1,21 @@ /* Patch for Edge, until they fix emojis in arial x.x */ @supports (-ms-ime-align:auto) { .user_content { font-family: Segoe UI Emoji, arial; } } +:root { + --main-block-color: rgb(61,61,61); + --main-text-color: white; + --dim-text-color: rgb(205,205,205); + --main-background-color: #222222; + --inner-background-color: #333333; + --input-background-color: #444444; + --input-border-color: #555555; + --input-text-color: #999999; +} + body { font-family: arial; - color: white; - background-color: #222222; + color: var(--main-text-color); + background-color: var(--main-background-color); margin: 0; } p::selection, span::selection, a::selection { @@ -17,15 +28,15 @@ p::selection, span::selection, a::selection { margin-left: auto; margin-right: auto; width: 70%; - background-color: #333333; + background-color: var(--inner-background-color); position: relative; top: -2px; } ul { list-style-type: none; - background-color: rgb(61,61,61); - border-bottom: 1px solid #222222; + background-color: var(--main-block-color); + border-bottom: 1px solid var(--main-background-color); padding-left: 15%; padding-right: 15%; margin: 0; @@ -89,7 +100,6 @@ li { .menu_alerts .alertList { display: none; } - .selectedAlert .alertList { display: block; position: absolute; @@ -99,7 +109,7 @@ li { z-index: 50; right: 15%; font-size: 13px; - background-color: #333333; + background-color: var(--inner-background-color); } .alertItem { @@ -109,19 +119,19 @@ li { height: 40px; background-size: 48px; background-repeat: no-repeat; - background-color: rgb(61,61,61); + background-color: var(--main-block-color); padding-left: 56px; padding-top: 8px; } a { text-decoration: none; - color: white; + color: var(--main-text-color); } .alert { padding-bottom: 12px; - background-color: rgb(61,61,61); + background-color: var(--main-block-color); padding: 12px; display: block; } @@ -147,7 +157,7 @@ a { .rowitem, .formitem { padding-bottom: 12px; - background-color: rgb(61,61,61); + background-color: var(--main-block-color); margin-top: 8px; padding: 12px; } @@ -179,7 +189,7 @@ a { .colline { font-size: 14px; - background-color: rgb(61,61,61); + background-color: var(--main-block-color); margin-top: 5px; padding: 10px; } @@ -218,7 +228,7 @@ a { .user_tag { float: right; - color: rgb(205,205,205); + color: var(--dim-text-color); } .real_username { @@ -234,7 +244,7 @@ a { .mod_button button { border: none; background: none; - color: white; + color: var(--main-text-color); font-size: 12px; padding: 0; } @@ -292,7 +302,7 @@ a { } .level_label, .level { - color: rgb(205,205,205); + color: var(--dim-text-color); float: right; } .level { @@ -310,17 +320,15 @@ a { } textarea { - background-color: #444444; - border-color: #555555; - color: #999999; + background-color: var(--input-background-color); + border-color: var(--input-border-color); + color: var(--input-text-color); width: calc(100% - 15px); min-height: 80px; } - -textarea:focus, input:focus, select:focus { +textarea:focus, input:focus, select:focus, button:focus { outline-color: rgb(95,95,95); } - textarea.large { min-height: 120px; margin-top: 1px; @@ -328,15 +336,10 @@ textarea.large { display: block; } -.topic_reply_form textarea { - width: calc(100% - 5px); - min-height: 80px; -} - .formitem button, .formbutton { - background-color: #444444; - border: 1px solid #555555; - color: #999999; + background-color: var(--input-background-color); + border: 1px solid var(--input-border-color); + color: var(--input-text-color); padding: 7px; padding-bottom: 6px; font-size: 13px; @@ -402,7 +405,7 @@ textarea.large { flex-direction: row; } .pageitem { - background-color: rgb(61,61,61); + background-color: var(--main-block-color); padding: 10px; margin-right: 4px; font-size: 13px; @@ -423,9 +426,9 @@ textarea.large { } .formitem input { - background-color: #444444; - border: 1px solid #555555; - color: #999999; + background-color: var(--input-background-color); + border: 1px solid var(--input-border-color); + color: var(--input-text-color); padding-bottom: 6px; font-size: 13px; @@ -434,9 +437,9 @@ textarea.large { } .formitem select { - background-color: #444444; - border: 1px solid #555555; - color: #999999; + background-color: var(--input-background-color); + border: 1px solid var(--input-border-color); + color: var(--input-text-color); font-size: 13px; padding: 4px; } @@ -458,8 +461,50 @@ input, select, textarea { margin-top: 5px; } +.topic_create_form .topic_board_row .formitem, .topic_create_form .topic_name_row .formitem { + padding-bottom: 5px; +} +.topic_create_form input, .topic_create_form select { + padding: 7px; + font-family: monospace; +} +.topic_create_form select { + padding: 6px; +} +.topic_create_form input { + width: calc(100% - 14px); +} +.topic_create_form textarea, .topic_reply_form textarea { + width: calc(100% - 5px); + min-height: 80px; +} +.topic_create_form textarea { + padding: 7px; + width: calc(100% - 14px); +} + +.topic_button_row .formitem { + display: flex; +} +.topic_create_form .add_file_button { + margin-left: 8px; +} +.topic_create_form .close_form { + margin-left: auto; +} +.topic_create_form .upload_file_dock { + display: flex; +} +.topic_create_form .uploadItem { + display: inline-block; + margin-left: 8px; + background-size: 25px 30px; + background-repeat: no-repeat; + padding-left: 30px; +} + .footer { - background-color: rgb(61,61,61); + background-color: var(--main-block-color); margin-top: 5px; padding: 10px; font-size: 14px; @@ -469,9 +514,9 @@ input, select, textarea { height: 25px; } .footer select { - background-color: #444444; - border: 1px solid #555555; - color: #999999; + background-color: var(--input-background-color); + border: 1px solid var(--input-border-color); + color: var(--input-text-color); font-size: 13px; padding: 4px; } @@ -505,7 +550,7 @@ input, select, textarea { height: 30.4px; padding-left: 5px; width: 100%; - background-color: rgb(61,61,61); + background-color: var(--main-block-color); padding-top: 11px; } .opt a { @@ -545,9 +590,9 @@ input, select, textarea { .topic_name_input { width: 100%; margin-right: 10px; - background-color: #444444; - border: 1px solid #555555; - color: #999999; + background-color: var(--input-background-color); + border: 1px solid var(--input-border-color); + color: var(--input-text-color); padding-bottom: 6px; font-size: 13px; padding: 5px; @@ -561,6 +606,13 @@ input, select, textarea { top: -5px; } +.postImage { + max-width: 100%; + max-height: 200px;/*300px;*/ + background-color: rgb(71,71,71); + padding: 10px; +} + /* Profiles */ #profile_left_lane { width: 220px; @@ -608,9 +660,9 @@ input, select, textarea { } .ip_search_block input { - background-color: #444444; - border: 1px solid #555555; - color: #999999; + background-color: var(--input-background-color); + border: 1px solid var(--input-border-color); + color: var(--input-text-color); margin-top: -3px; margin-bottom: -3px; padding: 4px; @@ -640,7 +692,7 @@ input, select, textarea { padding-top: 10px; padding-bottom: 10px; font-size: 13px; - background-color: rgb(61,61,61); + background-color: var(--main-block-color); } #panel_dashboard_right .colstack_head .rowitem { @@ -808,7 +860,7 @@ input, select, textarea { } @media(max-width: 470px) { - .menu_create_topic, .like_count_label, .like_count { + .like_count_label, .like_count { display: none; } .post_item { diff --git a/themes/shadow/theme.json b/themes/shadow/theme.json index ce6ad87f..fb5ea263 100644 --- a/themes/shadow/theme.json +++ b/themes/shadow/theme.json @@ -4,6 +4,5 @@ "Version": "0.0.1", "Creator": "Azareal", "FullImage": "shadow.png", - "URL": "github.com/Azareal/Gosora", - "Tag": "WIP" + "URL": "github.com/Azareal/Gosora" } diff --git a/themes/tempra-conflux/public/main.css b/themes/tempra-conflux/public/main.css index 12c30d9e..88fed7ec 100644 --- a/themes/tempra-conflux/public/main.css +++ b/themes/tempra-conflux/public/main.css @@ -4,6 +4,11 @@ -webkit-box-sizing: border-box; } +/* TODO: Run a find and replacer in Gosora to support browsers without CSS Variable support */ +:root { + --main-border-color: hsl(0,0%,80%); +} + body { font-family: arial; padding-bottom: 8px; @@ -23,11 +28,11 @@ ul { padding-right: 0px; height: 36px; list-style-type: none; - border: 1px solid hsl(0,0%,80%); + border: 1px solid var(--main-border-color); background: hsl(0, 0%, 97%); margin-bottom: 12px; margin-top: 0px; - border-bottom: 1.5px inset hsl(0,0%,80%); + border-bottom: 1.5px inset var(--main-border-color); margin-left: -8px; margin-right: -8px; } @@ -48,14 +53,14 @@ li a { } .menu_left { float: left; - border-right: 1px solid hsl(0,0%,80%); - border-bottom: 1.5px inset hsl(0,0%,80%); + border-right: 1px solid var(--main-border-color); + border-bottom: 1.5px inset var(--main-border-color); padding-right: 10px; background: hsl(0, 0%, 98%); } .menu_right { float: right; - border-left: 1px solid hsl(0,0%,80%); + border-left: 1px solid var(--main-border-color); padding-right: 10px; } @@ -109,10 +114,10 @@ li a { line-height: 16px; width: 300px; right: calc(5% + 7px); - border-top: 1px solid hsl(0,0%,80%); - border-left: 1px solid hsl(0,0%,80%); - border-right: 1px solid hsl(0,0%,80%); - border-bottom: 1px solid hsl(0,0%,80%); + border-top: 1px solid var(--main-border-color); + border-left: 1px solid var(--main-border-color); + border-right: 1px solid var(--main-border-color); + border-bottom: 1px solid var(--main-border-color); margin-bottom: 10px; } .alertItem { @@ -150,7 +155,7 @@ li a { margin-left: auto; margin-right: auto; background: hsl(0, 0%, 98%); - border: 1px solid hsl(0,0%,80%); + border: 1px solid var(--main-border-color); border-top: none; } @@ -161,14 +166,14 @@ li a { /* Explict declaring each border direction to fix a bug in Chrome where an override to .rowhead was also applying to .rowblock in some cases */ .rowblock { - border: 1px solid hsl(0,0%,80%); + border: 1px solid var(--main-border-color); width: 100%; padding: 0px; padding-top: 0px; - border-top: 1px solid hsl(0,0%,80%); - border-left: 1px solid hsl(0,0%,80%); - border-right: 1px solid hsl(0,0%,80%); - border-bottom: 1.5px inset hsl(0,0%,80%); + border-top: 1px solid var(--main-border-color); + border-left: 1px solid var(--main-border-color); + border-right: 1px solid var(--main-border-color); + border-bottom: 1.5px inset var(--main-border-color); } .rowblock:empty { display: none; @@ -211,7 +216,7 @@ li a { width: calc(70% - 13px); } .colstack_item { - border: 1px solid hsl(0,0%,80%); + border: 1px solid var(--main-border-color); padding: 0px; padding-top: 0px; width: 100%; @@ -230,7 +235,7 @@ li a { margin-top: 2px; } .grid_item { - border: 1px solid hsl(0,0%,80%); + border: 1px solid var(--main-border-color); word-wrap: break-word; background-color: white; width: 100%; @@ -291,6 +296,34 @@ li a { } .opthead { display: none; } +.rowitem.has_opt { + float: left; + width: calc(100% - 50px); + border-right: 1px solid #ccc; + border-bottom: none; +} +.opt { + float: left; + font-size: 32px; + height: 100%; + background-color: hsl(0, 0%, 99%); + width: 50px; + text-align: center; +} +.create_topic_opt a.create_topic_link:before { + content: '🖊︎'; +} +.create_topic_opt, .create_topic_opt a { + color: rgb(120,120,120); + text-decoration: none; +} +.locked_opt { + color: rgb(80,80,80); +} +.locked_opt:before { + content: '🔒︎'; +} + .datarow { padding-top: 10px; padding-bottom: 10px; @@ -310,7 +343,7 @@ li a { clear: both; } .formrow:not(:last-child) { - border-bottom: 1px dotted hsl(0,0%,80%); + border-bottom: 1px dotted var(--main-border-color); } .formitem { @@ -321,7 +354,7 @@ li a { font-weight: normal; } .formitem:not(:last-child) { - border-right: 1px dotted hsl(0,0%,80%); + border-right: 1px dotted var(--main-border-color); } .formitem.invisible_border { border: none; @@ -346,18 +379,18 @@ li a { padding-bottom: 12px;/*16px;*/ /*padding-left: 15px;*/ } + +.formbutton, button { + background: white; + border: 1px solid #8e8e8e; +} .formbutton { padding: 7px; display: block; margin-left: auto; margin-right: auto; font-size: 15px; - border-color: hsl(0,0%,80%); -} - -button { - background: white; - border: 1px solid #8e8e8e; + border-color: var(--main-border-color); } /* Topics */ @@ -386,6 +419,39 @@ button { } } +.postImage { + max-width: 100%; + max-height: 200px; + background-color: white; + padding: 10px; +} + +.topic_create_form .topic_button_row .formitem { + display: flex; +} +.topic_create_form .formbutton:first-child { + margin-left: 0px; + margin-right: 5px; +} +.topic_create_form .formbutton:not(:first-child) { + margin-left: 0px; + margin-right: 5px; +} +.topic_create_form .formbutton:last-child { + margin-left: auto; +} +.topic_create_form .upload_file_dock { + display: flex; +} +.topic_create_form .uploadItem { + display: inline-block; + margin-left: 8px; + margin-right: 8px; + background-size: 25px 35px; + background-repeat: no-repeat; + padding-left: 30px; +} + .topic_status_sticky { display: none; } @@ -467,7 +533,7 @@ button { color: #505050; /* 80,80,80 */ background-color: #FFFFFF; border-style: solid; - border-color: hsl(0,0%,80%); + border-color: var(--main-border-color); border-width: 1px; font-size: 15px; } @@ -499,7 +565,7 @@ button.username { display: block; padding: 5px; margin-bottom: 10px; - border: 1px solid hsl(0,0%,80%); + border: 1px solid var(--main-border-color); background-color: white; } .alert_success { @@ -580,10 +646,10 @@ button.username { } #profile_left_lane { - border: 1px solid hsl(0,0%,80%); + border: 1px solid var(--main-border-color); width: 220px; margin-bottom: 10px; - border-bottom: 1.5px inset hsl(0,0%,80%); + border-bottom: 1.5px inset var(--main-border-color); } #profile_left_lane .avatarRow { overflow: hidden; @@ -612,7 +678,7 @@ button.username { margin-top: 20px; } #profile_right_lane .topic_reply_form { - border-bottom: 1.5px inset hsl(0,0%,80%); + border-bottom: 1.5px inset var(--main-border-color); } .simple { @@ -652,7 +718,7 @@ button.username { position: sticky; top: 4px; /*box-shadow: 0 1px 2px rgba(0,0,0,.1);*/ - border-bottom: 1.5px inset hsl(0,0%,80%); + border-bottom: 1.5px inset var(--main-border-color); } .userinfo .avatar_item { background-repeat: no-repeat, repeat-y; @@ -672,10 +738,12 @@ button.username { margin-bottom: 0; margin-right: 3px; /*box-shadow: 0 1px 2px rgba(0,0,0,.1);*/ - border-bottom: 1.5px inset hsl(0,0%,80%); + border-bottom: 1.5px inset var(--main-border-color); } -.action_item .userinfo { display: none; } +.action_item .userinfo { + display: none; +} .action_item .content_container { min-height: auto; padding: 15px; @@ -689,7 +757,7 @@ button.username { border-width: 1px; background-color: #FFFFFF; border-style: solid; - border-color: hsl(0,0%,80%); + border-color: var(--main-border-color); padding: 0px; padding-left: 5px; padding-right: 5px; @@ -720,7 +788,7 @@ button.username { border-bottom: none; } .topic_reply_form { - border-top: 1px solid hsl(0,0%,80%); + border-top: 1px solid var(--main-border-color); } .post_container .post_item { background-color: #eaeaea; @@ -739,7 +807,7 @@ button.username { } .footer { - border: 1px solid hsl(0,0%,80%); + border: 1px solid var(--main-border-color); margin-top: 12px; clear: both; height: 40px; @@ -747,7 +815,7 @@ button.username { padding-left: 10px; padding-right: 10px; background-color: white; - border-bottom: 1.5px inset hsl(0,0%,80%); + border-bottom: 1.5px inset var(--main-border-color); } .footer select { padding: 2px; @@ -767,7 +835,7 @@ button.username { margin-top: -5px; } .pageitem { - border: 1px solid hsl(0,0%,80%); + border: 1px solid var(--main-border-color); background-color: white; padding: 5px; margin-right: 5px; diff --git a/themes/tempra-conflux/public/media.partial.css b/themes/tempra-conflux/public/media.partial.css index e8326007..0669e01b 100644 --- a/themes/tempra-conflux/public/media.partial.css +++ b/themes/tempra-conflux/public/media.partial.css @@ -12,7 +12,13 @@ } } -@media (max-width: 950px) { +@media(max-width: 950px) { + .sidebar { + display: none; + } +} + +@media (max-width: 700px) { li { height: 29px; font-size: 15px; @@ -20,11 +26,20 @@ padding-top: 6px; padding-bottom: 6px; } + li, li a { + font-size: 15px; + } ul { height: 30px; margin-top: 8px; + margin-left: -1px; + margin-right: -1px; + border-bottom: 1px solid hsl(0,0%,80%); + } + .menu_left, .menu_right { + padding-right: 9px; + border-bottom: 1px solid hsl(0,0%,80%); } - .menu_left, .menu_right { padding-right: 9px; } .menu_alerts { padding-left: 7px; padding-right: 7px; @@ -39,9 +54,16 @@ height: 100% !important; overflow-x: hidden; } - .container { width: auto; } - .sidebar { display: none; } - .selectedAlert .alertList { top: 37px; right: 4px; } + .container { + width: auto; + margin-left: 5px; + margin-right: 5px; + margin-top: 5px; + } + .selectedAlert .alertList { + top: 37px; + right: 4px; + } } @media (max-width: 680px) { @@ -54,7 +76,7 @@ li a { font-size: 14px; } ul { height: 26px; } .menu_left, .menu_right { padding-right: 7px; } - .menu_create_topic, .hide_on_mobile { display: none; } + .hide_on_mobile { display: none; } .menu_alerts { padding-left: 4px; diff --git a/themes/tempra-conflux/tempra-conflux.png b/themes/tempra-conflux/tempra-conflux.png index a01e09b5..8c7bf018 100644 Binary files a/themes/tempra-conflux/tempra-conflux.png and b/themes/tempra-conflux/tempra-conflux.png differ diff --git a/themes/tempra-cursive/public/main.css b/themes/tempra-cursive/public/main.css index e751f1d4..f5ab3848 100644 --- a/themes/tempra-cursive/public/main.css +++ b/themes/tempra-cursive/public/main.css @@ -588,7 +588,6 @@ button.username { ul { height: 26px; } .menu_left { padding-right: 5px; padding-top: 1px; } .menu_right { padding-right: 5px; } - .menu_create_topic { display: none;} .menu_alerts { padding-left: 4px; @@ -596,8 +595,13 @@ button.username { font-size: 16px; padding-top: 1px; } - .menu_alerts .alert_counter { top: -23px; left: 8px; } - .selectedAlert .alertList { top: 33px; } + .menu_alerts .alert_counter { + top: -23px; + left: 8px; + } + .selectedAlert .alertList { + top: 33px; + } .hide_on_mobile { display: none; } .prev_button, .next_button { top: auto; bottom: 5px; } diff --git a/themes/tempra-simple/public/main.css b/themes/tempra-simple/public/main.css index 9ea12e1f..9d0f00ac 100644 --- a/themes/tempra-simple/public/main.css +++ b/themes/tempra-simple/public/main.css @@ -17,7 +17,7 @@ ul { padding-right: 0px; height: 36px; list-style-type: none; - border: 1px solid #ccc; + border: 1px solid hsl(0, 0%, 80%); background-color: rgb(252,252,252); margin-bottom: 12px; } @@ -27,7 +27,7 @@ li { padding-top: 8px; padding-bottom: 8px; background: white; - border-bottom: 1px solid #ccc; + border-bottom: 1px solid hsl(0, 0%, 80%); } li:hover { background: rgb(252,252,252); } li a { @@ -38,12 +38,12 @@ li a { } .menu_left { float: left; - border-right: 1px solid #ccc; + border-right: 1px solid hsl(0, 0%, 80%); padding-right: 10px; } .menu_right { float: right; - border-left: 1px solid #ccc; + border-left: 1px solid hsl(0, 0%, 80%); padding-right: 10px; } .menu_overview { @@ -105,10 +105,10 @@ li a { line-height: 16px; width: 300px; right: calc(5% + 7px); - border-top: 1px solid #ccc; - border-left: 1px solid #ccc; - border-right: 1px solid #ccc; - border-bottom: 1px solid #ccc; + border-top: 1px solid hsl(0, 0%, 80%); + border-left: 1px solid hsl(0, 0%, 80%); + border-right: 1px solid hsl(0, 0%, 80%); + border-bottom: 1px solid hsl(0, 0%, 80%); margin-bottom: 10px; } .alertItem { @@ -150,7 +150,7 @@ li a { } .rowblock { - border: 1px solid #ccc; + border: 1px solid hsl(0, 0%, 80%); width: 100%; padding: 0px; padding-top: 0px; @@ -159,14 +159,14 @@ li a { display: none; } .rowmenu { - border: 1px solid #ccc; + border: 1px solid hsl(0, 0%, 80%); } .rowsmall { font-size: 12px; } /*.colblock_left { - border: 1px solid #ccc; + border: 1px solid hsl(0, 0%, 80%); padding: 0px; padding-top: 0px; width: 30%; @@ -174,7 +174,7 @@ li a { margin-right: 8px; } .colblock_right { - border: 1px solid #ccc; + border: 1px solid hsl(0, 0%, 80%); padding: 0px; padding-top: 0px; width: 65%; @@ -195,7 +195,7 @@ li a { width: calc(70% - 15px); } .colstack_item { - border: 1px solid #ccc; + border: 1px solid hsl(0, 0%, 80%); padding: 0px; padding-top: 0px; width: 100%; @@ -215,7 +215,7 @@ li a { margin-top: 2px; } .grid_item { - border: 1px solid #ccc; + border: 1px solid hsl(0, 0%, 80%); word-wrap: break-word; background-color: white; width: 100%; @@ -260,7 +260,7 @@ li a { background-color: white; } /*.rowitem:not(.passive) { font-size: 17px; }*/ -.rowitem:not(:last-child) { border-bottom: 1px dotted #ccc; } +.rowitem:not(:last-child) { border-bottom: 1px dotted hsl(0, 0%, 80%); } .rowitem a { text-decoration: none; color: black; @@ -272,7 +272,7 @@ li a { .rowitem.has_opt { float: left; width: calc(100% - 50px); - border-right: 1px solid #ccc; + border-right: 1px solid hsl(0, 0%, 80%); border-bottom: none; } .opt { @@ -283,8 +283,8 @@ li a { width: 50px; text-align: center; } -.create_topic_opt a:before { - content '🖊︎'; +.create_topic_opt a.create_topic_link:before { + content: '🖊︎'; } .create_topic_opt, .create_topic_opt a { color: rgb(120,120,120); @@ -321,7 +321,7 @@ li a { display: table; } .formrow:after { clear: both; } -.formrow:not(:last-child) { border-bottom: 1px dotted #ccc; } +.formrow:not(:last-child) { border-bottom: 1px dotted hsl(0, 0%, 80%); } .formitem { float: left; @@ -330,8 +330,12 @@ li a { /*font-size: 17px;*/ font-weight: normal; } -.formitem:not(:last-child) { border-right: 1px dotted #ccc; } -.formitem.invisible_border { border: none; } +.formitem:not(:last-child) { + border-right: 1px dotted hsl(0, 0%, 80%); +} +.formitem.invisible_border { + border: none; +} /* Mostly for textareas */ .formitem:only-child { width: 100%; } @@ -344,24 +348,26 @@ li a { margin: 0 auto; float: none; } -.formitem:not(:only-child) input, .formitem:not(:only-child) select { padding: 3px;/*5px;*/ } +.formitem:not(:only-child) input, .formitem:not(:only-child) select { + padding: 3px;/*5px;*/ +} .formitem:not(:only-child).formlabel { padding-top: 15px;/*18px;*/ padding-bottom: 12px;/*16px;*/ /*padding-left: 15px;*/ } + +.formbutton, button { + background: white; + border: 1px solid #8e8e8e; +} .formbutton { padding: 7px; display: block; margin-left: auto; margin-right: auto; font-size: 15px; - border-color: #ccc; -} - -button { - background: white; - border: 1px solid #8e8e8e; + border-color: hsl(0, 0%, 80%); } /* Topics */ @@ -390,6 +396,39 @@ button { } } +.postImage { + max-width: 100%; + max-height: 200px; + background-color: white; + padding: 10px; +} + +.topic_create_form .topic_button_row .formitem { + display: flex; +} +.topic_create_form .formbutton:first-child { + margin-left: 0px; + margin-right: 5px; +} +.topic_create_form .formbutton:not(:first-child) { + margin-left: 0px; + margin-right: 5px; +} +.topic_create_form .formbutton:last-child { + margin-left: auto; +} +.topic_create_form .upload_file_dock { + display: flex; +} +.topic_create_form .uploadItem { + display: inline-block; + margin-left: 8px; + margin-right: 8px; + background-size: 25px 35px; + background-repeat: no-repeat; + padding-left: 30px; +} + .username, .panel_tag { text-transform: none; margin-left: 0px; @@ -400,7 +439,7 @@ button { color: #505050; /* 80,80,80 */ background-color: #FFFFFF; border-style: solid; - border-color: #ccc; + border-color: hsl(0, 0%, 80%); border-width: 1px; font-size: 15px; } @@ -553,7 +592,7 @@ button.username { } .postQuote { - border: 1px solid #ccc; + border: 1px solid hsl(0, 0%, 80%); background: white; padding: 5px; margin: 0px; @@ -578,7 +617,7 @@ button.username { display: block; padding: 5px; margin-bottom: 10px; - border: 1px solid #ccc; + border: 1px solid hsl(0, 0%, 80%); } .alert_success { display: block; @@ -616,8 +655,12 @@ button.username { text-decoration: none; color: #505050; } -.prev_button { left: 14px; } -.next_button { right: 14px; } +.prev_button { + left: 14px; +} +.next_button { + right: 14px; +} .head_tag_upshift { float: right; position: relative; @@ -625,7 +668,7 @@ button.username { } .footer { - border: 1px solid #ccc; + border: 1px solid hsl(0, 0%, 80%); margin-top: 12px; clear: both; height: 40px; @@ -697,7 +740,7 @@ button.username { padding: 5px; margin-right: 5px; padding-bottom: 4px; - border: 1px solid #ccc; + border: 1px solid hsl(0, 0%, 80%); } .pageitem a { color: black; diff --git a/themes/tempra-simple/public/media.partial.css b/themes/tempra-simple/public/media.partial.css index e978bb65..3b6509c6 100644 --- a/themes/tempra-simple/public/media.partial.css +++ b/themes/tempra-simple/public/media.partial.css @@ -52,7 +52,6 @@ li a { font-size: 14px; } ul { height: 26px; } .menu_left, .menu_right { padding-right: 7px; } - .menu_create_topic { display: none; } .menu_alerts { padding-left: 4px; diff --git a/topic.go b/topic.go index 260f170c..4b1c387d 100644 --- a/topic.go +++ b/topic.go @@ -146,7 +146,7 @@ func (topic *Topic) RemoveLike(uid int) error { func (topic *Topic) Update(name string, content string) error { content = preparseMessage(content) - parsed_content := parseMessage(html.EscapeString(content)) + parsed_content := parseMessage(html.EscapeString(content), topic.ParentID, "forums") _, err := editTopicStmt.Exec(name, content, parsed_content, topic.ID) tcache, ok := topics.(TopicCache) @@ -170,6 +170,7 @@ func (topic *Topic) CreateActionReply(action string, ipaddress string, user User return err } +// Copy gives you a non-pointer concurrency safe copy of the topic func (topic *Topic) Copy() Topic { return *topic } @@ -188,7 +189,7 @@ func getTopicUser(tid int) (TopicUser, error) { // We might be better off just passing seperate topic and user structs to the caller? return copyTopicToTopicUser(topic, user), nil - } else if ucache.GetLength() < ucache.GetCapacity() { + } else if ucache.Length() < ucache.GetCapacity() { topic, err = topics.Get(tid) if err != nil { return TopicUser{ID: tid}, err diff --git a/topic_store.go b/topic_store.go index 999a4960..ae176d43 100644 --- a/topic_store.go +++ b/topic_store.go @@ -8,7 +8,9 @@ package main import ( "database/sql" + "errors" "log" + "strings" "sync" "sync/atomic" @@ -20,17 +22,20 @@ import ( // TODO: Add some sort of update method // ? - Should we add stick, lock, unstick, and unlock methods? These might be better on the Topics not the TopicStore var topics TopicStore +var ErrNoTitle = errors.New("This message is missing a title") +var ErrNoBody = errors.New("This message is missing a body") type TopicStore interface { Get(id int) (*Topic, error) BypassGet(id int) (*Topic, error) Delete(id int) error Exists(id int) bool + Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) AddLastTopic(item *Topic, fid int) error // unimplemented // TODO: Implement these two methods - //GetReplies() ([]*Reply, error) - //GetRepliesRange(lower int, higher int) ([]*Reply, error) - GetGlobalCount() int + //Replies(tid int) ([]*Reply, error) + //RepliesRange(tid int, lower int, higher int) ([]*Reply, error) + GlobalCount() int } type TopicCache interface { @@ -43,7 +48,7 @@ type TopicCache interface { CacheRemoveUnsafe(id int) error Flush() Reload(id int) error - GetLength() int + Length() int SetCapacity(capacity int) GetCapacity() int } @@ -176,6 +181,34 @@ func (mts *MemoryTopicStore) Exists(id int) bool { return mts.exists.QueryRow(id).Scan(&id) == nil } +func (mts *MemoryTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) { + topicName = strings.TrimSpace(topicName) + if topicName == "" { + return 0, ErrNoBody + } + + content = strings.TrimSpace(content) + parsedContent := parseMessage(content, fid, "forums") + if strings.TrimSpace(parsedContent) == "" { + return 0, ErrNoBody + } + + wcount := wordCount(content) + // TODO: Move this statement into the topic store + res, err := createTopicStmt.Exec(fid, topicName, content, parsedContent, uid, ipaddress, wcount, uid) + if err != nil { + return 0, err + } + + lastID, err := res.LastInsertId() + if err != nil { + return 0, err + } + + err = fstore.AddTopic(int(lastID), uid, fid) + return int(lastID), err +} + func (mts *MemoryTopicStore) CacheSet(item *Topic) error { mts.Lock() _, ok := mts.items[item.ID] @@ -241,7 +274,9 @@ func (mts *MemoryTopicStore) Flush() { mts.Unlock() } -func (mts *MemoryTopicStore) GetLength() int { +// ! Is this concurrent? +// Length returns the number of topics in the memory cache +func (mts *MemoryTopicStore) Length() int { return int(mts.length) } @@ -253,8 +288,8 @@ func (mts *MemoryTopicStore) GetCapacity() int { return mts.capacity } -// Return the total number of topics on these forums -func (mts *MemoryTopicStore) GetGlobalCount() int { +// GlobalCount returns the total number of topics on these forums +func (mts *MemoryTopicStore) GlobalCount() int { var tcount int err := mts.topicCount.QueryRow().Scan(&tcount) if err != nil { @@ -314,6 +349,34 @@ func (sts *SQLTopicStore) Exists(id int) bool { return sts.exists.QueryRow(id).Scan(&id) == nil } +func (sts *SQLTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) { + topicName = strings.TrimSpace(topicName) + if topicName == "" { + return 0, ErrNoBody + } + + content = strings.TrimSpace(content) + parsedContent := parseMessage(content, fid, "forums") + if strings.TrimSpace(parsedContent) == "" { + return 0, ErrNoBody + } + + wcount := wordCount(content) + // TODO: Move this statement into the topic store + res, err := createTopicStmt.Exec(fid, topicName, content, parsedContent, uid, ipaddress, wcount, uid) + if err != nil { + return 0, err + } + + lastID, err := res.LastInsertId() + if err != nil { + return 0, err + } + + err = fstore.AddTopic(int(lastID), uid, fid) + return int(lastID), err +} + // TODO: Use a transaction here func (sts *SQLTopicStore) Delete(id int) error { topic, err := sts.Get(id) @@ -347,8 +410,8 @@ func (sts *SQLTopicStore) AddLastTopic(item *Topic, fid int) error { return nil } -// Return the total number of topics on these forums -func (sts *SQLTopicStore) GetGlobalCount() int { +// GlobalCount returns the total number of topics on these forums +func (sts *SQLTopicStore) GlobalCount() int { var tcount int err := sts.topicCount.QueryRow().Scan(&tcount) if err != nil { diff --git a/user.go b/user.go index d131e1ef..d1cafca6 100644 --- a/user.go +++ b/user.go @@ -121,6 +121,24 @@ func (user *User) Activate() (err error) { return err } +func (user *User) ChangeName(username string) (err error) { + _, err = setUsernameStmt.Exec(username, user.ID) + ucache, ok := users.(UserCache) + if ok { + ucache.CacheRemove(user.ID) + } + return err +} + +func (user *User) ChangeAvatar(avatar string) (err error) { + _, err = setAvatarStmt.Exec(avatar, user.ID) + ucache, ok := users.(UserCache) + if ok { + ucache.CacheRemove(user.ID) + } + return err +} + func (user *User) increasePostStats(wcount int, topic bool) error { var mod int baseScore := 1 @@ -201,6 +219,7 @@ func (user *User) decreasePostStats(wcount int, topic bool) error { return err } +// Copy gives you a non-pointer concurrency safe copy of the user func (user *User) Copy() User { return *user } diff --git a/user_store.go b/user_store.go index 5db8edeb..1f66b872 100644 --- a/user_store.go +++ b/user_store.go @@ -25,7 +25,7 @@ type UserStore interface { BulkGetMap(ids []int) (map[int]*User, error) BypassGet(id int) (*User, error) Create(username string, password string, email string, group int, active int) (int, error) - GetGlobalCount() int + GlobalCount() int } type UserCache interface { @@ -38,7 +38,7 @@ type UserCache interface { CacheRemoveUnsafe(id int) error Flush() Reload(id int) error - GetLength() int + Length() int SetCapacity(capacity int) GetCapacity() int } @@ -376,7 +376,9 @@ func (mus *MemoryUserStore) Flush() { mus.Unlock() } -func (mus *MemoryUserStore) GetLength() int { +// ! Is this concurrent? +// Length returns the number of users in the memory cache +func (mus *MemoryUserStore) Length() int { return int(mus.length) } @@ -388,8 +390,8 @@ func (mus *MemoryUserStore) GetCapacity() int { return mus.capacity } -// Return the total number of users registered on the forums -func (mus *MemoryUserStore) GetGlobalCount() int { +// GlobalCount returns the total number of users registered on the forums +func (mus *MemoryUserStore) GlobalCount() int { var ucount int err := mus.userCount.QueryRow().Scan(&ucount) if err != nil { @@ -554,8 +556,8 @@ func (mus *SQLUserStore) Create(username string, password string, email string, return int(lastID), err } -// Return the total number of users registered on the forums -func (mus *SQLUserStore) GetGlobalCount() int { +// GlobalCount returns the total number of users registered on the forums +func (mus *SQLUserStore) GlobalCount() int { var ucount int err := mus.userCount.QueryRow().Scan(&ucount) if err != nil { diff --git a/utils.go b/utils.go index 2ddba612..8cd599cb 100644 --- a/utils.go +++ b/utils.go @@ -57,9 +57,8 @@ func relativeTime(in string) (string, error) { if in == "" { return "", nil } - layout := "2006-01-02 15:04:05" - t, err := time.Parse(layout, in) - //t, err := time.ParseInLocation(layout, in, timeLocation) + + t, err := time.Parse("2006-01-02 15:04:05", in) if err != nil { return "", err } @@ -103,6 +102,8 @@ func relativeTime(in string) (string, error) { // TODO: Write a test for this func convertByteUnit(bytes float64) (float64, string) { switch { + case bytes >= float64(petabyte): + return bytes / float64(petabyte), "PB" case bytes >= float64(terabyte): return bytes / float64(terabyte), "TB" case bytes >= float64(gigabyte): @@ -119,6 +120,8 @@ func convertByteUnit(bytes float64) (float64, string) { // TODO: Write a test for this func convertByteInUnit(bytes float64, unit string) (count float64) { switch unit { + case "PB": + count = bytes / float64(petabyte) case "TB": count = bytes / float64(terabyte) case "GB": @@ -141,7 +144,7 @@ func convertByteInUnit(bytes float64, unit string) (count float64) { func convertUnit(num int) (int, string) { switch { case num >= 1000000000000: - return 0, "∞" + return num / 1000000000000, "T" case num >= 1000000000: return num / 1000000000, "B" case num >= 1000000: @@ -156,8 +159,10 @@ func convertUnit(num int) (int, string) { // TODO: Write a test for this func convertFriendlyUnit(num int) (int, string) { switch { + case num >= 1000000000000000: + return 0, " quadrillion" case num >= 1000000000000: - return 0, " zillion" + return 0, " trillion" case num >= 1000000000: return num / 1000000000, " billion" case num >= 1000000: @@ -347,7 +352,7 @@ func wordCount(input string) (count int) { func getLevel(score int) (level int) { var base float64 = 25 var current, prev float64 - expFactor := 2.8 + var expFactor = 2.8 for i := 1; ; i++ { _, bit := math.Modf(float64(i) / 10) @@ -390,7 +395,7 @@ func getLevelScore(getLevel int) (score int) { func getLevels(maxLevel int) []float64 { var base float64 = 25 var current, prev float64 // = 0 - expFactor := 2.8 + var expFactor = 2.8 var out []float64 out = append(out, 0)