diff --git a/.codeclimate.yml b/.codeclimate.yml index 4cc0ec54..8dee5bca 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -8,10 +8,4 @@ exclude_patterns: - "public/jquery-3.1.1.min.js" - "public/EQCSS.min.js" - "public/EQCSS.js" -- "template_list.go" -- "template_forum.go" -- "template_forums.go" -- "template_topic.go" -- "template_topic_alt.go" -- "template_topics.go" -- "template_profile.go" \ No newline at end of file +- "public/Sortable-1.4.0/*" \ No newline at end of file diff --git a/README.md b/README.md index e12bb192..a146c564 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ go get -u github.com/go-sql-driver/mysql go get -u golang.org/x/crypto/bcrypt +go get -u golang.org/x/crypto/argon2 + go get -u github.com/StackExchange/wmi go get -u github.com/Azareal/gopsutil @@ -189,8 +191,6 @@ We're looking for ways to clean-up the plugin system so that all of them (except ![Tempra Simple Mobile](https://github.com/Azareal/Gosora/blob/master/images/tempra-simple-mobile-375px.png) -![Tempra Cursive Theme](https://github.com/Azareal/Gosora/blob/master/images/tempra-cursive.png) - ![Tempra Conflux Theme](https://github.com/Azareal/Gosora/blob/master/images/tempra-conflux.png) ![Tempra Conflux Mobile](https://github.com/Azareal/Gosora/blob/master/images/tempra-conflux-mobile-320px.png) @@ -207,7 +207,7 @@ More images in the /images/ folder. Beware though, some of them are *really* out * github.com/go-sql-driver/mysql For interfacing with MariaDB. -* golang.org/x/crypto/bcrypt For hashing passwords. +* golang.org/x/crypto/bcrypt and go get -u golang.org/x/crypto/argon2 For hashing passwords. * github.com/Azareal/gopsutil For pulling information on CPU and memory usage. I've temporarily forked this, as we were having stability issues with the latest build. @@ -229,6 +229,8 @@ More images in the /images/ folder. Beware though, some of them are *really* out * github.com/fsnotify/fsnotify A library for watching events on the file system. +* More items to come here, our dependencies are going through a lot of changes, and I'll be documenting those soon ;) + # Bundled Plugins There are several plugins which are bundled with the software by default. These cover various common tasks which aren't common enough to clutter the core with or which have competing implementation methods (E.g. plugin_markdown vs plugin_bbcode for post mark-up). @@ -239,7 +241,7 @@ There are several plugins which are bundled with the software by default. These * Markdown - An extremely simple plugin for converting Markdown into HTML. -* Social Groups - A WIP plugin which lets users create their own little discussion areas which they can administrate / moderate on their own. +* Social Groups - An extremely unstable WIP plugin which lets users create their own little discussion areas which they can administrate / moderate on their own. # Developers diff --git a/common/auth.go b/common/auth.go index 63b70a34..7e97d6bb 100644 --- a/common/auth.go +++ b/common/auth.go @@ -1,30 +1,58 @@ /* * * Gosora Authentication Interface -* Copyright Azareal 2017 - 2018 +* Copyright Azareal 2017 - 2019 * */ package common -import "errors" -import "strconv" -import "net/http" -import "database/sql" +import ( + "database/sql" + "errors" + "net/http" + "strconv" + "strings" -import "golang.org/x/crypto/bcrypt" -import "../query_gen/lib" + "../query_gen/lib" + //"golang.org/x/crypto/argon2" + "golang.org/x/crypto/bcrypt" +) var Auth AuthInt +const SaltLength int = 32 +const SessionLength int = 80 + // ErrMismatchedHashAndPassword is thrown whenever a hash doesn't match it's unhashed password var ErrMismatchedHashAndPassword = bcrypt.ErrMismatchedHashAndPassword // nolint +var ErrHashNotExist = errors.New("We don't recognise that hashing algorithm") +var ErrTooFewHashParams = errors.New("You haven't provided enough hash parameters") + // ErrPasswordTooLong is silly, but we don't want bcrypt to bork on us var ErrPasswordTooLong = errors.New("The password you selected is too long") var ErrWrongPassword = errors.New("That's not the correct password.") var ErrSecretError = errors.New("There was a glitch in the system. Please contact your local administrator.") var ErrNoUserByName = errors.New("We couldn't find an account with that username.") +var DefaultHashAlgo = "bcrypt" // Override this in the configuration file, not here + +//func(realPassword string, password string, salt string) (err error) +var CheckPasswordFuncs = map[string]func(string, string, string) error{ + "bcrypt": BcryptCheckPassword, + //"argon2": Argon2CheckPassword, +} + +//func(password string) (hashedPassword string, salt string, err error) +var GeneratePasswordFuncs = map[string]func(string) (string, string, error){ + "bcrypt": BcryptGeneratePassword, + //"argon2": Argon2GeneratePassword, +} + +var HashPrefixes = map[string]string{ + "$2a$": "bcrypt", + //"argon2$": "argon2", +} // AuthInt is the main authentication interface. type AuthInt interface { @@ -176,3 +204,75 @@ func (auth *DefaultAuth) CreateSession(uid int) (session string, err error) { } return session, nil } + +func CheckPassword(realPassword string, password string, salt string) (err error) { + blasted := strings.Split(realPassword, "$") + prefix := blasted[0] + if len(blasted) > 1 { + prefix += blasted[1] + } + algo, ok := HashPrefixes[prefix] + if !ok { + return ErrHashNotExist + } + checker := CheckPasswordFuncs[algo] + return checker(realPassword, password, salt) +} + +func GeneratePassword(password string) (hash string, salt string, err error) { + gen, ok := GeneratePasswordFuncs[DefaultHashAlgo] + if !ok { + return "", "", ErrHashNotExist + } + return gen(password) +} + +func BcryptCheckPassword(realPassword string, password string, salt string) (err error) { + return bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password+salt)) +} + +// Note: The salt is in the hash, therefore the salt parameter is blank +func BcryptGeneratePassword(password string) (hash string, salt string, err error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", "", err + } + return string(hashedPassword), salt, nil +} + +/*const ( + argon2Time uint32 = 3 + argon2Memory uint32 = 32 * 1024 + argon2Threads uint8 = 4 + argon2KeyLen uint32 = 32 +) + +func Argon2CheckPassword(realPassword string, password string, salt string) (err error) { + split := strings.Split(realPassword, "$") + // TODO: Better validation + if len(split) < 5 { + return ErrTooFewHashParams + } + realKey, _ := base64.StdEncoding.DecodeString(split[len(split)-1]) + time, _ := strconv.Atoi(split[1]) + memory, _ := strconv.Atoi(split[2]) + threads, _ := strconv.Atoi(split[3]) + keyLen, _ := strconv.Atoi(split[4]) + key := argon2.Key([]byte(password), []byte(salt), uint32(time), uint32(memory), uint8(threads), uint32(keyLen)) + if subtle.ConstantTimeCompare(realKey, key) != 1 { + return ErrMismatchedHashAndPassword + } + return nil +} + +func Argon2GeneratePassword(password string) (hash string, salt string, err error) { + sbytes := make([]byte, SaltLength) + _, err = rand.Read(sbytes) + if err != nil { + return "", "", err + } + key := argon2.Key([]byte(password), sbytes, argon2Time, argon2Memory, argon2Threads, argon2KeyLen) + hash = base64.StdEncoding.EncodeToString(key) + return fmt.Sprintf("argon2$%d%d%d%d%s%s", argon2Time, argon2Memory, argon2Threads, argon2KeyLen, salt, hash), string(sbytes), nil +} +*/ diff --git a/common/common.go b/common/common.go index f19bd51a..39f6bcc4 100644 --- a/common/common.go +++ b/common/common.go @@ -3,6 +3,7 @@ package common import ( "database/sql" "log" + "time" "../query_gen/lib" ) @@ -19,9 +20,7 @@ const Gigabyte int = Megabyte * 1024 const Terabyte int = Gigabyte * 1024 const Petabyte int = Terabyte * 1024 -const SaltLength int = 32 -const SessionLength int = 80 - +var StartTime time.Time var TmplPtrMap = make(map[string]interface{}) // ErrNoRows is an alias of sql.ErrNoRows, just in case we end up with non-database/sql datastores diff --git a/common/counters/requests.go b/common/counters/requests.go index 13c2921d..a9e6557b 100644 --- a/common/counters/requests.go +++ b/common/counters/requests.go @@ -19,8 +19,7 @@ type DefaultViewCounter struct { insert *sql.Stmt } -func NewGlobalViewCounter() (*DefaultViewCounter, error) { - acc := qgen.Builder.Accumulator() +func NewGlobalViewCounter(acc *qgen.Accumulator) (*DefaultViewCounter, error) { counter := &DefaultViewCounter{ currentBucket: 0, insert: acc.Insert("viewchunks").Columns("count, createdAt").Fields("?,UTC_TIMESTAMP()").Prepare(), diff --git a/common/email_store.go b/common/email_store.go new file mode 100644 index 00000000..907ca205 --- /dev/null +++ b/common/email_store.go @@ -0,0 +1,52 @@ +package common + +import "database/sql" +import "../query_gen/lib" + +var Emails EmailStore + +type EmailStore interface { + GetEmailsByUser(user *User) (emails []Email, err error) + VerifyEmail(email string) error +} + +type DefaultEmailStore struct { + getEmailsByUser *sql.Stmt + verifyEmail *sql.Stmt +} + +func NewDefaultEmailStore(acc *qgen.Accumulator) (*DefaultEmailStore, error) { + return &DefaultEmailStore{ + getEmailsByUser: acc.Select("emails").Columns("email, validated, token").Where("uid = ?").Prepare(), + + // Need to fix this: Empty string isn't working, it gets set to 1 instead x.x -- Has this been fixed? + verifyEmail: acc.Update("emails").Set("validated = 1, token = ''").Where("email = ?").Prepare(), + }, acc.FirstError() +} + +func (store *DefaultEmailStore) GetEmailsByUser(user *User) (emails []Email, err error) { + email := Email{UserID: user.ID} + rows, err := store.getEmailsByUser.Query(user.ID) + if err != nil { + return emails, err + } + defer rows.Close() + + for rows.Next() { + err := rows.Scan(&email.Email, &email.Validated, &email.Token) + if err != nil { + return emails, err + } + + if email.Email == user.Email { + email.Primary = true + } + emails = append(emails, email) + } + return emails, rows.Err() +} + +func (store *DefaultEmailStore) VerifyEmail(email string) error { + _, err := store.verifyEmail.Exec(email) + return err +} diff --git a/common/group_store.go b/common/group_store.go index 58226cb9..0f342894 100644 --- a/common/group_store.go +++ b/common/group_store.go @@ -314,6 +314,7 @@ func (mgs *MemoryGroupStore) GetRange(lower int, higher int) (groups []*Group, e return mgs.GetAll() } + // TODO: Simplify these four conditionals into two if lower == 0 { if higher < 0 { return nil, errors.New("higher may not be lower than 0") diff --git a/common/pages.go b/common/pages.go index a86cce6c..f370ee28 100644 --- a/common/pages.go +++ b/common/pages.go @@ -74,14 +74,12 @@ type Paginator struct { } type TopicPage struct { - Title string - CurrentUser User - Header *Header - ItemList []ReplyUser - Topic TopicUser - Poll Poll - Page int - LastPage int + *Header + ItemList []ReplyUser + Topic TopicUser + Poll Poll + Page int + LastPage int } type TopicListPage struct { @@ -93,11 +91,9 @@ type TopicListPage struct { } type ForumPage struct { - Title string - CurrentUser User - Header *Header - ItemList []*TopicsRow - Forum *Forum + *Header + ItemList []*TopicsRow + Forum *Forum Paginator } @@ -132,6 +128,14 @@ type IPSearchPage struct { IP string } +type EmailListPage struct { + Title string + CurrentUser User + Header *Header + ItemList []Email + Something interface{} +} + type PanelStats struct { Users int Groups int @@ -169,6 +173,19 @@ type PanelDashboardPage struct { GridItems []GridElement } +type PanelSetting struct { + *Setting + FriendlyName string +} + +type PanelSettingPage struct { + *Header + Stats PanelStats + Zone string + ItemList []OptionLabel + Setting *PanelSetting +} + type PanelTimeGraph struct { Series []int64 // The counts on the left Labels []int64 // unixtimes for the bottom, gets converted into 1:00, 2:00, etc. with JS @@ -282,12 +299,10 @@ type PanelMenuItemPage struct { } type PanelUserPage struct { - Title string - CurrentUser User - Header *Header - Stats PanelStats - Zone string - ItemList []User + *Header + Stats PanelStats + Zone string + ItemList []*User Paginator } diff --git a/common/phrases.go b/common/phrases.go index 27aafb38..3fc9165c 100644 --- a/common/phrases.go +++ b/common/phrases.go @@ -39,7 +39,7 @@ type LanguagePack struct { Levels LevelPhrases GlobalPerms map[string]string LocalPerms map[string]string - SettingLabels map[string]string + SettingPhrases map[string]string PermPresets map[string]string Accounts map[string]string // TODO: Apply these phrases in the software proper UserAgents map[string]string @@ -148,16 +148,16 @@ func GetLocalPermPhrase(name string) string { return res } -func GetSettingLabel(name string) string { - res, ok := currentLangPack.Load().(*LanguagePack).SettingLabels[name] +func GetSettingPhrase(name string) string { + res, ok := currentLangPack.Load().(*LanguagePack).SettingPhrases[name] if !ok { return getPhrasePlaceholder("settings", name) } return res } -func GetAllSettingLabels() map[string]string { - return currentLangPack.Load().(*LanguagePack).SettingLabels +func GetAllSettingPhrases() map[string]string { + return currentLangPack.Load().(*LanguagePack).SettingPhrases } func GetAllPermPresets() map[string]string { diff --git a/common/report_store.go b/common/report_store.go new file mode 100644 index 00000000..1e478d2f --- /dev/null +++ b/common/report_store.go @@ -0,0 +1,53 @@ +package common + +import ( + "database/sql" + "errors" + "strconv" + + "../query_gen/lib" +) + +var Reports ReportStore +var ErrAlreadyReported = errors.New("This item has already been reported") + +// The report system mostly wraps around the topic system for simplicty +type ReportStore interface { + Create(title string, content string, user *User, itemType string, itemID int) (int, error) +} + +type DefaultReportStore struct { + create *sql.Stmt + exists *sql.Stmt +} + +func NewDefaultReportStore(acc *qgen.Accumulator) (*DefaultReportStore, error) { + return &DefaultReportStore{ + create: acc.Insert("topics").Columns("title, content, parsed_content, ipaddress, createdAt, lastReplyAt, createdBy, lastReplyBy, data, parentID, css_class").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,1,'report'").Prepare(), + exists: acc.Count("topics").Where("data = ? AND data != '' AND parentID = 1").Prepare(), + }, acc.FirstError() +} + +// ! There's a data race in this. If two users report one item at the exact same time, then both reports will go through +func (store *DefaultReportStore) Create(title string, content string, user *User, itemType string, itemID int) (int, error) { + var count int + err := store.exists.QueryRow(itemType + "_" + strconv.Itoa(itemID)).Scan(&count) + if err != nil && err != sql.ErrNoRows { + return 0, err + } + if count != 0 { + return 0, ErrAlreadyReported + } + + res, err := store.create.Exec(title, content, ParseMessage(content, 0, ""), user.LastIP, user.ID, user.ID, itemType+"_"+strconv.Itoa(itemID)) + if err != nil { + return 0, err + } + + lastID, err := res.LastInsertId() + if err != nil { + return 0, err + } + + return int(lastID), Forums.AddTopic(int(lastID), user.ID, 1) +} diff --git a/common/routes_common.go b/common/routes_common.go index 923cc9df..e65942bd 100644 --- a/common/routes_common.go +++ b/common/routes_common.go @@ -17,7 +17,6 @@ var PanelUserCheck func(http.ResponseWriter, *http.Request, *User) (*Header, Pan var SimplePanelUserCheck func(http.ResponseWriter, *http.Request, *User) (*HeaderLite, RouteError) = simplePanelUserCheck var SimpleForumUserCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (headerLite *HeaderLite, err RouteError) = simpleForumUserCheck var ForumUserCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (header *Header, err RouteError) = forumUserCheck -var MemberCheck func(w http.ResponseWriter, r *http.Request, user *User) (header *Header, err RouteError) = memberCheck var SimpleUserCheck func(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, err RouteError) = simpleUserCheck var UserCheck func(w http.ResponseWriter, r *http.Request, user *User) (header *Header, err RouteError) = userCheck @@ -166,28 +165,15 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header } func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) { - return &HeaderLite{ - Site: Site, - Settings: SettingBox.Load().(SettingMap), - }, nil -} - -// TODO: Add this to the member routes -func memberCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Header, rerr RouteError) { - header, rerr = UserCheck(w, r, user) - if !user.Loggedin { - return header, NoPermissions(w, r, *user) - } - return header, rerr + return simpleUserCheck(w, r, user) } // SimpleUserCheck is back from the grave, yay :D func simpleUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) { - headerLite = &HeaderLite{ + return &HeaderLite{ Site: Site, Settings: SettingBox.Load().(SettingMap), - } - return headerLite, nil + }, nil } // TODO: Add the ability for admins to restrict certain themes to certain groups? diff --git a/common/settings.go b/common/settings.go index 75643e60..6df747e7 100644 --- a/common/settings.go +++ b/common/settings.go @@ -54,6 +54,12 @@ func init() { }) } +func (setting *Setting) Copy() (out *Setting) { + out = &Setting{Name: ""} + *out = *setting + return out +} + func LoadSettings() error { var sBox = SettingMap(make(map[string]interface{})) settings, err := sBox.BypassGetAll() diff --git a/common/site.go b/common/site.go index 22b237c5..5d51282a 100644 --- a/common/site.go +++ b/common/site.go @@ -51,6 +51,7 @@ type dbConfig struct { type config struct { SslPrivkey string SslFullchain string + HashAlgo string // Defaults to bcrypt, and in the future, possibly something stronger MaxRequestSize int CacheTopicUser int @@ -103,6 +104,11 @@ func ProcessConfig() error { if Config.MaxUsernameLength == 0 { Config.MaxUsernameLength = 100 } + GuestUser.Avatar = BuildAvatar(0, "") + + if Config.HashAlgo != "" { + // TODO: Set the alternate hash algo, e.g. argon2 + } // We need this in here rather than verifyConfig as switchToTestDB() currently overwrites the values it verifies if DbConfig.TestDbname == DbConfig.Dbname { diff --git a/common/template_init.go b/common/template_init.go index c40bebc8..a48ca4a9 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -155,6 +155,14 @@ func CompileTemplates() error { }, } + var header2 = &Header{Site: Site} + *header2 = *header + header2.CurrentUser = user2 + + var header3 = &Header{Site: Site} + *header3 = *header + header3.CurrentUser = user3 + log.Print("Compiling the templates") var now = time.Now() @@ -167,7 +175,8 @@ func CompileTemplates() error { replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, RelativeTime(now), 0, 0, "", "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""}) var varList = make(map[string]tmpl.VarItem) - tpage := TopicPage{"Title", user, header, replyList, topic, poll, 1, 1} + header.Title = "Topic Name" + tpage := TopicPage{header, replyList, topic, poll, 1, 1} topicIDTmpl, err := c.Compile("topic.html", "templates/", "common.TopicPage", tpage, varList) if err != nil { return err @@ -203,17 +212,16 @@ func CompileTemplates() error { var topicsList []*TopicsRow topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, "Date", time.Now(), "Date", user3.ID, 1, "", "127.0.0.1", 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}) - header.Title = "Topic List" + header2.Title = "Topic List" topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, Paginator{[]int{1}, 1, 1}} topicListTmpl, err := c.Compile("topics.html", "templates/", "common.TopicListPage", topicListPage, varList) if err != nil { return err } - //var topicList []TopicUser - //topicList = append(topicList,TopicUser{1,"topic-title","Topic Title","The topic content.",1,false,false,"Date","Date",1,"","127.0.0.1",0,1,"classname","","admin-fred","Admin Fred",config.DefaultGroup,"",0,"","","","",58,false}) forumItem := BlankForum(1, "general-forum.1", "General Forum", "Where the general stuff happens", true, "all", 0, "", 0) - forumPage := ForumPage{"General Forum", user, header, topicsList, forumItem, Paginator{[]int{1}, 1, 1}} + header.Title = "General Forum" + forumPage := ForumPage{header, topicsList, forumItem, Paginator{[]int{1}, 1, 1}} forumTmpl, err := c.Compile("forum.html", "templates/", "common.ForumPage", forumPage, varList) if err != nil { return err diff --git a/common/theme.go b/common/theme.go new file mode 100644 index 00000000..6d3f662d --- /dev/null +++ b/common/theme.go @@ -0,0 +1,239 @@ +/* Copyright Azareal 2016 - 2019 */ +package common + +import ( + //"fmt" + "bytes" + "errors" + "io" + "io/ioutil" + "log" + "mime" + "net/http" + "os" + "path/filepath" + "strings" + "text/template" +) + +type Theme struct { + Path string // Redirect this file to another folder + + Name string + FriendlyName string + Version string + Creator string + FullImage string + MobileFriendly bool + Disabled bool + HideFromThemes bool + BgAvatars bool // For profiles, at the moment + ForkOf string + Tag string + URL string + Docks []string // Allowed Values: leftSidebar, rightSidebar, footer + Settings map[string]ThemeSetting + Templates []TemplateMapping + TemplatesMap map[string]string + TmplPtr map[string]interface{} + Resources []ThemeResource + ResourceTemplates *template.Template + + // Dock intercepters + // TODO: Implement this + MapTmplToDock map[string]ThemeMapTmplToDock // map[dockName]data + RunOnDock func(string) string //(dock string) (sbody string) + + // This variable should only be set and unset by the system, not the theme meta file + Active bool +} + +type ThemeSetting struct { + FriendlyName string + Options []string +} + +type TemplateMapping struct { + Name string + Source string + //When string +} + +type ThemeResource struct { + Name string + Location string + Loggedin bool // Only serve this resource to logged in users +} + +type ThemeMapTmplToDock struct { + //Name string + File string +} + +// TODO: It might be unsafe to call the template parsing functions with fsnotify, do something more concurrent +func (theme *Theme) LoadStaticFiles() error { + theme.ResourceTemplates = template.New("") + template.Must(theme.ResourceTemplates.ParseGlob("./themes/" + theme.Name + "/public/*.css")) + + // It should be safe for us to load the files for all the themes in memory, as-long as the admin hasn't setup a ridiculous number of themes + return theme.AddThemeStaticFiles() +} + +func (theme *Theme) AddThemeStaticFiles() error { + phraseMap := GetTmplPhrases() + // TODO: Use a function instead of a closure to make this more testable? What about a function call inside the closure to take the theme variable into account? + return filepath.Walk("./themes/"+theme.Name+"/public", func(path string, f os.FileInfo, err error) error { + DebugLog("Attempting to add static file '" + path + "' for default theme '" + theme.Name + "'") + if err != nil { + return err + } + if f.IsDir() { + return nil + } + + path = strings.Replace(path, "\\", "/", -1) + data, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + var ext = filepath.Ext(path) + if ext == ".css" && len(data) != 0 { + var b bytes.Buffer + var pieces = strings.Split(path, "/") + var filename = pieces[len(pieces)-1] + err = theme.ResourceTemplates.ExecuteTemplate(&b, filename, CSSData{Phrases: phraseMap}) + if err != nil { + return err + } + data = b.Bytes() + } + + path = strings.TrimPrefix(path, "themes/"+theme.Name+"/public") + gzipData := compressBytesGzip(data) + StaticFiles.Set("/static/"+theme.Name+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)}) + + DebugLog("Added the '/" + theme.Name + path + "' static file for theme " + theme.Name + ".") + return nil + }) +} + +func (theme *Theme) MapTemplates() { + if theme.Templates != nil { + for _, themeTmpl := range theme.Templates { + if themeTmpl.Name == "" { + LogError(errors.New("Invalid destination template name")) + } + if themeTmpl.Source == "" { + LogError(errors.New("Invalid source template name")) + } + + // `go generate` is one possibility for letting plugins inject custom page structs, but it would simply add another step of compilation. It might be simpler than the current build process from the perspective of the administrator? + + destTmplPtr, ok := TmplPtrMap[themeTmpl.Name] + if !ok { + return + } + sourceTmplPtr, ok := TmplPtrMap[themeTmpl.Source] + if !ok { + LogError(errors.New("The source template doesn't exist!")) + } + + switch dTmplPtr := destTmplPtr.(type) { + case *func(TopicPage, io.Writer) error: + switch sTmplPtr := sourceTmplPtr.(type) { + case *func(TopicPage, io.Writer) error: + //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr + overridenTemplates[themeTmpl.Name] = true + *dTmplPtr = *sTmplPtr + default: + LogError(errors.New("The source and destination templates are incompatible")) + } + case *func(TopicListPage, io.Writer) error: + switch sTmplPtr := sourceTmplPtr.(type) { + case *func(TopicListPage, io.Writer) error: + //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr + overridenTemplates[themeTmpl.Name] = true + *dTmplPtr = *sTmplPtr + default: + LogError(errors.New("The source and destination templates are incompatible")) + } + case *func(ForumPage, io.Writer) error: + switch sTmplPtr := sourceTmplPtr.(type) { + case *func(ForumPage, io.Writer) error: + //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr + overridenTemplates[themeTmpl.Name] = true + *dTmplPtr = *sTmplPtr + default: + LogError(errors.New("The source and destination templates are incompatible")) + } + case *func(ForumsPage, io.Writer) error: + switch sTmplPtr := sourceTmplPtr.(type) { + case *func(ForumsPage, io.Writer) error: + //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr + overridenTemplates[themeTmpl.Name] = true + *dTmplPtr = *sTmplPtr + default: + LogError(errors.New("The source and destination templates are incompatible")) + } + case *func(ProfilePage, io.Writer) error: + switch sTmplPtr := sourceTmplPtr.(type) { + case *func(ProfilePage, io.Writer) error: + //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr + overridenTemplates[themeTmpl.Name] = true + *dTmplPtr = *sTmplPtr + default: + LogError(errors.New("The source and destination templates are incompatible")) + } + case *func(CreateTopicPage, io.Writer) error: + switch sTmplPtr := sourceTmplPtr.(type) { + case *func(CreateTopicPage, io.Writer) error: + //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr + overridenTemplates[themeTmpl.Name] = true + *dTmplPtr = *sTmplPtr + default: + LogError(errors.New("The source and destination templates are incompatible")) + } + case *func(IPSearchPage, io.Writer) error: + switch sTmplPtr := sourceTmplPtr.(type) { + case *func(IPSearchPage, io.Writer) error: + //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr + overridenTemplates[themeTmpl.Name] = true + *dTmplPtr = *sTmplPtr + default: + LogError(errors.New("The source and destination templates are incompatible")) + } + case *func(Page, io.Writer) error: + switch sTmplPtr := sourceTmplPtr.(type) { + case *func(Page, io.Writer) error: + //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr + overridenTemplates[themeTmpl.Name] = true + *dTmplPtr = *sTmplPtr + default: + LogError(errors.New("The source and destination templates are incompatible")) + } + default: + log.Print("themeTmpl.Name: ", themeTmpl.Name) + log.Print("themeTmpl.Source: ", themeTmpl.Source) + LogError(errors.New("Unknown destination template type!")) + } + } + } +} + +func (theme Theme) HasDock(name string) bool { + for _, dock := range theme.Docks { + if dock == name { + return true + } + } + return false +} + +func (theme Theme) BuildDock(dock string) (sbody string) { + runOnDock := theme.RunOnDock + if runOnDock != nil { + return runOnDock(dock) + } + return "" +} diff --git a/common/themes.go b/common/theme_list.go similarity index 55% rename from common/themes.go rename to common/theme_list.go index 0076356c..067c7d33 100644 --- a/common/themes.go +++ b/common/theme_list.go @@ -1,79 +1,30 @@ -/* Copyright Azareal 2016 - 2018 */ package common import ( - //"fmt" - "bytes" "database/sql" "encoding/json" "errors" "io" "io/ioutil" "log" - "mime" "net/http" "os" - "path/filepath" "reflect" - "strings" "sync" "sync/atomic" - "text/template" "../query_gen/lib" ) type ThemeList map[string]*Theme -var Themes ThemeList = make(map[string]*Theme) +var Themes ThemeList = make(map[string]*Theme) // ? Refactor this into a store? var DefaultThemeBox atomic.Value var ChangeDefaultThemeMutex sync.Mutex // TODO: Use this when the default theme doesn't exist var fallbackTheme = "cosora" -var overridenTemplates = make(map[string]bool) - -type Theme struct { - Name string - FriendlyName string - Version string - Creator string - FullImage string - MobileFriendly bool - Disabled bool - HideFromThemes bool - BgAvatars bool // For profiles, at the moment - ForkOf string - Tag string - URL string - Docks []string // Allowed Values: leftSidebar, rightSidebar, footer - Settings map[string]ThemeSetting - Templates []TemplateMapping - TemplatesMap map[string]string - TmplPtr map[string]interface{} - Resources []ThemeResource - ResourceTemplates *template.Template - - // This variable should only be set and unset by the system, not the theme meta file - Active bool -} - -type ThemeSetting struct { - FriendlyName string - Options []string -} - -type TemplateMapping struct { - Name string - Source string - //When string -} - -type ThemeResource struct { - Name string - Location string - Loggedin bool // Only serve this resource to logged in users -} +var overridenTemplates = make(map[string]bool) // ? What is this used for? type ThemeStmts struct { getThemes *sql.Stmt @@ -91,6 +42,89 @@ func init() { }) } +func NewThemeList() (themes ThemeList, err error) { + themes = make(map[string]*Theme) + + themeFiles, err := ioutil.ReadDir("./themes") + if err != nil { + return themes, err + } + + for _, themeFile := range themeFiles { + if !themeFile.IsDir() { + continue + } + + themeName := themeFile.Name() + log.Printf("Adding theme '%s'", themeName) + themePath := "./themes/" + themeName + themeFile, err := ioutil.ReadFile(themePath + "/theme.json") + if err != nil { + return themes, err + } + + var theme = &Theme{Name: ""} + err = json.Unmarshal(themeFile, theme) + if err != nil { + return themes, err + } + + // TODO: Implement the static file part of this and fsnotify + if theme.Path != "" { + log.Print("Resolving redirect to " + theme.Path) + themeFile, err := ioutil.ReadFile(theme.Path + "/theme.json") + if err != nil { + return themes, err + } + theme = &Theme{Name: "", Path: theme.Path} + err = json.Unmarshal(themeFile, theme) + if err != nil { + return themes, err + } + } else { + theme.Path = themePath + } + + 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 + log.Print(theme.Path + "/public/") + _, err = os.Stat(theme.Path + "/public/") + if err != nil { + if os.IsNotExist(err) { + return themes, 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 themes, err + } + } + + if theme.FullImage != "" { + DebugLog("Adding theme image") + err = StaticFiles.Add(theme.Path+"/"+theme.FullImage, themePath) + if err != nil { + return themes, err + } + } + + theme.TemplatesMap = make(map[string]string) + theme.TmplPtr = make(map[string]interface{}) + if theme.Templates != nil { + for _, themeTmpl := range theme.Templates { + theme.TemplatesMap[themeTmpl.Name] = themeTmpl.Source + theme.TmplPtr[themeTmpl.Name] = TmplPtrMap["o_"+themeTmpl.Source] + } + } + + // TODO: Bind the built template, or an interpreted one for any dock overrides this theme has + + themes[theme.Name] = theme + } + return themes, nil +} + // TODO: Make the initThemes and LoadThemes functions less confusing // ? - Delete themes which no longer exist in the themes folder from the database? func (themes ThemeList) LoadActiveStatus() error { @@ -141,221 +175,8 @@ func (themes ThemeList) LoadStaticFiles() error { return nil } -func InitThemes() error { - themeFiles, err := ioutil.ReadDir("./themes") - if err != nil { - return err - } - - for _, themeFile := range themeFiles { - if !themeFile.IsDir() { - continue - } - - themeName := themeFile.Name() - log.Printf("Adding theme '%s'", themeName) - themeFile, err := ioutil.ReadFile("./themes/" + themeName + "/theme.json") - if err != nil { - return err - } - - var theme = &Theme{Name: ""} - err = json.Unmarshal(themeFile, theme) - if err != nil { - return err - } - - 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 != "" { - DebugLog("Adding theme image") - err = StaticFiles.Add("./themes/"+themeName+"/"+theme.FullImage, "./themes/"+themeName) - if err != nil { - return err - } - } - - theme.TemplatesMap = make(map[string]string) - theme.TmplPtr = make(map[string]interface{}) - if theme.Templates != nil { - for _, themeTmpl := range theme.Templates { - theme.TemplatesMap[themeTmpl.Name] = themeTmpl.Source - theme.TmplPtr[themeTmpl.Name] = TmplPtrMap["o_"+themeTmpl.Source] - } - } - - Themes[theme.Name] = theme - } - return nil -} - -// TODO: It might be unsafe to call the template parsing functions with fsnotify, do something more concurrent -func (theme *Theme) LoadStaticFiles() error { - theme.ResourceTemplates = template.New("") - template.Must(theme.ResourceTemplates.ParseGlob("./themes/" + theme.Name + "/public/*.css")) - - // It should be safe for us to load the files for all the themes in memory, as-long as the admin hasn't setup a ridiculous number of themes - return theme.AddThemeStaticFiles() -} - -func (theme *Theme) AddThemeStaticFiles() error { - phraseMap := GetTmplPhrases() - // TODO: Use a function instead of a closure to make this more testable? What about a function call inside the closure to take the theme variable into account? - return filepath.Walk("./themes/"+theme.Name+"/public", func(path string, f os.FileInfo, err error) error { - DebugLog("Attempting to add static file '" + path + "' for default theme '" + theme.Name + "'") - if err != nil { - return err - } - if f.IsDir() { - return nil - } - - path = strings.Replace(path, "\\", "/", -1) - data, err := ioutil.ReadFile(path) - if err != nil { - return err - } - - var ext = filepath.Ext(path) - if ext == ".css" && len(data) != 0 { - var b bytes.Buffer - var pieces = strings.Split(path, "/") - var filename = pieces[len(pieces)-1] - err = theme.ResourceTemplates.ExecuteTemplate(&b, filename, CSSData{Phrases: phraseMap}) - if err != nil { - return err - } - data = b.Bytes() - } - - path = strings.TrimPrefix(path, "themes/"+theme.Name+"/public") - gzipData := compressBytesGzip(data) - StaticFiles.Set("/static/"+theme.Name+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)}) - - DebugLog("Added the '/" + theme.Name + path + "' static file for theme " + theme.Name + ".") - return nil - }) -} - -func (theme *Theme) MapTemplates() { - if theme.Templates != nil { - for _, themeTmpl := range theme.Templates { - if themeTmpl.Name == "" { - LogError(errors.New("Invalid destination template name")) - } - if themeTmpl.Source == "" { - LogError(errors.New("Invalid source template name")) - } - - // `go generate` is one possibility for letting plugins inject custom page structs, but it would simply add another step of compilation. It might be simpler than the current build process from the perspective of the administrator? - - destTmplPtr, ok := TmplPtrMap[themeTmpl.Name] - if !ok { - return - } - sourceTmplPtr, ok := TmplPtrMap[themeTmpl.Source] - if !ok { - LogError(errors.New("The source template doesn't exist!")) - } - - switch dTmplPtr := destTmplPtr.(type) { - case *func(TopicPage, io.Writer) error: - switch sTmplPtr := sourceTmplPtr.(type) { - case *func(TopicPage, io.Writer) error: - //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr - overridenTemplates[themeTmpl.Name] = true - *dTmplPtr = *sTmplPtr - default: - LogError(errors.New("The source and destination templates are incompatible")) - } - case *func(TopicListPage, io.Writer) error: - switch sTmplPtr := sourceTmplPtr.(type) { - case *func(TopicListPage, io.Writer) error: - //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr - overridenTemplates[themeTmpl.Name] = true - *dTmplPtr = *sTmplPtr - default: - LogError(errors.New("The source and destination templates are incompatible")) - } - case *func(ForumPage, io.Writer) error: - switch sTmplPtr := sourceTmplPtr.(type) { - case *func(ForumPage, io.Writer) error: - //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr - overridenTemplates[themeTmpl.Name] = true - *dTmplPtr = *sTmplPtr - default: - LogError(errors.New("The source and destination templates are incompatible")) - } - case *func(ForumsPage, io.Writer) error: - switch sTmplPtr := sourceTmplPtr.(type) { - case *func(ForumsPage, io.Writer) error: - //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr - overridenTemplates[themeTmpl.Name] = true - *dTmplPtr = *sTmplPtr - default: - LogError(errors.New("The source and destination templates are incompatible")) - } - case *func(ProfilePage, io.Writer) error: - switch sTmplPtr := sourceTmplPtr.(type) { - case *func(ProfilePage, io.Writer) error: - //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr - overridenTemplates[themeTmpl.Name] = true - *dTmplPtr = *sTmplPtr - default: - LogError(errors.New("The source and destination templates are incompatible")) - } - case *func(CreateTopicPage, io.Writer) error: - switch sTmplPtr := sourceTmplPtr.(type) { - case *func(CreateTopicPage, io.Writer) error: - //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr - overridenTemplates[themeTmpl.Name] = true - *dTmplPtr = *sTmplPtr - default: - LogError(errors.New("The source and destination templates are incompatible")) - } - case *func(IPSearchPage, io.Writer) error: - switch sTmplPtr := sourceTmplPtr.(type) { - case *func(IPSearchPage, io.Writer) error: - //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr - overridenTemplates[themeTmpl.Name] = true - *dTmplPtr = *sTmplPtr - default: - LogError(errors.New("The source and destination templates are incompatible")) - } - case *func(Page, io.Writer) error: - switch sTmplPtr := sourceTmplPtr.(type) { - case *func(Page, io.Writer) error: - //overridenTemplates[themeTmpl.Name] = d_tmpl_ptr - overridenTemplates[themeTmpl.Name] = true - *dTmplPtr = *sTmplPtr - default: - LogError(errors.New("The source and destination templates are incompatible")) - } - default: - log.Print("themeTmpl.Name: ", themeTmpl.Name) - log.Print("themeTmpl.Source: ", themeTmpl.Source) - LogError(errors.New("Unknown destination template type!")) - } - } - } -} - func ResetTemplateOverrides() { log.Print("Resetting the template overrides") - for name := range overridenTemplates { log.Print("Resetting '" + name + "' template override") @@ -542,17 +363,3 @@ func GetDefaultThemeName() string { func SetDefaultThemeName(name string) { DefaultThemeBox.Store(name) } - -func (theme Theme) HasDock(name string) bool { - for _, dock := range theme.Docks { - if dock == name { - return true - } - } - return false -} - -// TODO: Implement this -func (theme Theme) BuildDock(dock string) (sbody string) { - return "" -} diff --git a/common/user.go b/common/user.go index 0b8b0492..0112a1a7 100644 --- a/common/user.go +++ b/common/user.go @@ -14,20 +14,14 @@ import ( "time" "../query_gen/lib" - "golang.org/x/crypto/bcrypt" ) // TODO: Replace any literals with this var BanGroup = 4 +// TODO: Use something else as the guest avatar, maybe a question mark of some sort? // GuestUser is an instance of user which holds guest data to avoid having to initialise a guest every time -var GuestUser = User{ID: 0, Link: "#", Group: 6, Perms: GuestPerms} - -//func(real_password string, password string, salt string) (err error) -var CheckPassword = BcryptCheckPassword - -//func(password string) (hashed_password string, salt string, err error) -var GeneratePassword = BcryptGeneratePassword +var GuestUser = User{ID: 0, Name: "Guest", Link: "#", Group: 6, Perms: GuestPerms} // BuildAvatar is done in site.go to make sure it's done after init var ErrNoTempGroup = errors.New("We couldn't find a temporary group for this user") type User struct { @@ -369,33 +363,6 @@ func BuildAvatar(uid int, avatar string) string { return strings.Replace(Config.Noavatar, "{id}", strconv.Itoa(uid), 1) } -func BcryptCheckPassword(realPassword string, password string, salt string) (err error) { - return bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password+salt)) -} - -// Investigate. Do we need the extra salt? -func BcryptGeneratePassword(password string) (hashedPassword string, salt string, err error) { - salt, err = GenerateSafeString(SaltLength) - if err != nil { - return "", "", err - } - - password = password + salt - hashedPassword, err = BcryptGeneratePasswordNoSalt(password) - if err != nil { - return "", "", err - } - return hashedPassword, salt, nil -} - -func BcryptGeneratePasswordNoSalt(password string) (hash string, err error) { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return "", err - } - return string(hashedPassword), nil -} - // TODO: Move this to *User func SetPassword(uid int, password string) error { hashedPassword, salt, err := GeneratePassword(password) diff --git a/common/user_store.go b/common/user_store.go index 2ed8dba1..6387d077 100644 --- a/common/user_store.go +++ b/common/user_store.go @@ -20,6 +20,7 @@ type UserStore interface { DirtyGet(id int) *User Get(id int) (*User, error) Exists(id int) bool + GetOffset(offset int, perPage int) (users []*User, err error) //BulkGet(ids []int) ([]*User, error) BulkGetMap(ids []int) (map[int]*User, error) BypassGet(id int) (*User, error) @@ -35,6 +36,7 @@ type DefaultUserStore struct { cache UserCache get *sql.Stmt + getOffset *sql.Stmt exists *sql.Stmt register *sql.Stmt usernameExists *sql.Stmt @@ -51,6 +53,7 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) { return &DefaultUserStore{ cache: cache, get: acc.SimpleSelect("users", "name, group, active, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, liked, last_ip, temp_group", "uid = ?", "", ""), + getOffset: acc.Select("users").Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, liked, last_ip, temp_group").Orderby("uid ASC").Limit("?,?").Prepare(), exists: acc.SimpleSelect("users", "uid", "uid = ?", "", ""), register: acc.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()"), // TODO: Implement user_count on users_groups here usernameExists: acc.SimpleSelect("users", "name", "name = ?", "", ""), @@ -92,6 +95,29 @@ func (mus *DefaultUserStore) Get(id int) (*User, error) { return user, err } +// TODO: Optimise this, so we don't wind up hitting the database every-time for small gaps +// TODO: Make this a little more consistent with DefaultGroupStore's GetRange method +func (store *DefaultUserStore) GetOffset(offset int, perPage int) (users []*User, err error) { + rows, err := store.getOffset.Query(offset, perPage) + if err != nil { + return users, err + } + defer rows.Close() + + for rows.Next() { + user := &User{Loggedin: true} + err := rows.Scan(&user.ID, &user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Liked, &user.LastIP, &user.TempGroup) + if err != nil { + return nil, err + } + + user.Init() + store.cache.Set(user) + users = append(users, user) + } + return users, rows.Err() +} + // TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts? // TODO: ID of 0 should always error? func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) { diff --git a/common/utils.go b/common/utils.go index 50fd7ab6..db94a894 100644 --- a/common/utils.go +++ b/common/utils.go @@ -48,7 +48,7 @@ func GenerateSafeString(length int) (string, error) { if err != nil { return "", err } - return base64.URLEncoding.EncodeToString(rb), nil + return base64.StdEncoding.EncodeToString(rb), nil } // TODO: Write a test for this diff --git a/dev-update-linux b/dev-update-linux index dce7df0e..a2f3f5ed 100644 --- a/dev-update-linux +++ b/dev-update-linux @@ -10,6 +10,9 @@ go get -u github.com/denisenkom/go-mssqldb echo "Updating bcrypt" go get -u golang.org/x/crypto/bcrypt +echo "Updating Argon2" +go get -u golang.org/x/crypto/argon2 + echo "Updating gopsutil" go get -u github.com/Azareal/gopsutil diff --git a/dev-update.bat b/dev-update.bat index 7b666ecb..12b3ad15 100644 --- a/dev-update.bat +++ b/dev-update.bat @@ -29,6 +29,13 @@ if %errorlevel% neq 0 ( exit /b %errorlevel% ) +echo Updating the Argon2 library +go get -u golang.org/x/crypto/argon2 +if %errorlevel% neq 0 ( + pause + exit /b %errorlevel% +) + echo Updating /x/sys/windows (dependency for gopsutil) go get -u golang.org/x/sys/windows if %errorlevel% neq 0 ( diff --git a/docs/templates.md b/docs/templates.md new file mode 100644 index 00000000..70400ccb --- /dev/null +++ b/docs/templates.md @@ -0,0 +1,7 @@ +# Templates + +Gosora uses a subset of [Go Templates](https://golang.org/pkg/text/template/) which are run on both the server side and client side with custom transpiler to wring out the most performance. Some more obscure features may not be available, although I am adding them in here and there. + +The base templates are stored in `/templates/` and you can shadow them by placing modified duplicates in `/templates/overrides/`. The default themes all share the same set of templates present there. + +More to come soon. \ No newline at end of file diff --git a/experimental/new-update.bat b/experimental/new-update.bat index e5e75577..833cd0aa 100644 --- a/experimental/new-update.bat +++ b/experimental/new-update.bat @@ -29,6 +29,13 @@ if %errorlevel% neq 0 ( exit /b %errorlevel% ) +echo Updating the Argon2 library +go get -u golang.org/x/crypto/argon2 +if %errorlevel% neq 0 ( + pause + exit /b %errorlevel% +) + echo Updating /x/sys/windows (dependency for gopsutil) go get -u golang.org/x/sys/windows if %errorlevel% neq 0 ( diff --git a/gen_mssql.go b/gen_mssql.go index 3d9996c1..76a2c386 100644 --- a/gen_mssql.go +++ b/gen_mssql.go @@ -10,14 +10,10 @@ import "./common" // nolint type Stmts struct { isPluginActive *sql.Stmt - getUsersOffset *sql.Stmt isThemeDefault *sql.Stmt - getEmailsByUser *sql.Stmt - getTopicBasic *sql.Stmt forumEntryExists *sql.Stmt groupEntryExists *sql.Stmt getForumTopics *sql.Stmt - createReport *sql.Stmt addForumPermsToForum *sql.Stmt addPlugin *sql.Stmt addTheme *sql.Stmt @@ -29,13 +25,11 @@ type Stmts struct { updateGroupPerms *sql.Stmt updateGroup *sql.Stmt updateEmail *sql.Stmt - verifyEmail *sql.Stmt setTempGroup *sql.Stmt updateWordFilter *sql.Stmt bumpSync *sql.Stmt deleteActivityStreamMatch *sql.Stmt deleteWordFilter *sql.Stmt - reportExists *sql.Stmt getActivityFeedByWatcher *sql.Stmt getActivityCountByWatcher *sql.Stmt @@ -59,14 +53,6 @@ func _gen_mssql() (err error) { return err } - common.DebugLog("Preparing getUsersOffset statement.") - stmts.getUsersOffset, err = db.Prepare("SELECT [uid],[name],[group],[active],[is_super_admin],[avatar] FROM [users] ORDER BY uid ASC OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY") - if err != nil { - log.Print("Error in getUsersOffset statement.") - log.Print("Bad Query: ","SELECT [uid],[name],[group],[active],[is_super_admin],[avatar] FROM [users] ORDER BY uid ASC OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY") - return err - } - common.DebugLog("Preparing isThemeDefault statement.") stmts.isThemeDefault, err = db.Prepare("SELECT [default] FROM [themes] WHERE [uname] = ?1") if err != nil { @@ -75,22 +61,6 @@ func _gen_mssql() (err error) { return err } - common.DebugLog("Preparing getEmailsByUser statement.") - stmts.getEmailsByUser, err = db.Prepare("SELECT [email],[validated],[token] FROM [emails] WHERE [uid] = ?1") - if err != nil { - log.Print("Error in getEmailsByUser statement.") - log.Print("Bad Query: ","SELECT [email],[validated],[token] FROM [emails] WHERE [uid] = ?1") - return err - } - - common.DebugLog("Preparing getTopicBasic statement.") - stmts.getTopicBasic, err = db.Prepare("SELECT [title],[content] FROM [topics] WHERE [tid] = ?1") - if err != nil { - log.Print("Error in getTopicBasic statement.") - log.Print("Bad Query: ","SELECT [title],[content] FROM [topics] WHERE [tid] = ?1") - return err - } - common.DebugLog("Preparing forumEntryExists statement.") stmts.forumEntryExists, err = db.Prepare("SELECT [fid] FROM [forums] WHERE [name] = '' ORDER BY fid ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY") if err != nil { @@ -115,14 +85,6 @@ func _gen_mssql() (err error) { return err } - common.DebugLog("Preparing createReport statement.") - stmts.createReport, err = db.Prepare("INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[createdBy],[lastReplyBy],[data],[parentID],[css_class]) VALUES (?,?,?,GETUTCDATE(),GETUTCDATE(),?,?,?,1,'report')") - if err != nil { - log.Print("Error in createReport statement.") - log.Print("Bad Query: ","INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[createdBy],[lastReplyBy],[data],[parentID],[css_class]) VALUES (?,?,?,GETUTCDATE(),GETUTCDATE(),?,?,?,1,'report')") - return err - } - common.DebugLog("Preparing addForumPermsToForum statement.") stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) VALUES (?,?,?,?)") if err != nil { @@ -211,14 +173,6 @@ func _gen_mssql() (err error) { return err } - common.DebugLog("Preparing verifyEmail statement.") - stmts.verifyEmail, err = db.Prepare("UPDATE [emails] SET [validated] = 1,[token] = '' WHERE [email] = ?") - if err != nil { - log.Print("Error in verifyEmail statement.") - log.Print("Bad Query: ","UPDATE [emails] SET [validated] = 1,[token] = '' WHERE [email] = ?") - return err - } - common.DebugLog("Preparing setTempGroup statement.") stmts.setTempGroup, err = db.Prepare("UPDATE [users] SET [temp_group] = ? WHERE [uid] = ?") if err != nil { @@ -258,14 +212,6 @@ func _gen_mssql() (err error) { log.Print("Bad Query: ","DELETE FROM [word_filters] WHERE [wfid] = ?") return err } - - common.DebugLog("Preparing reportExists statement.") - stmts.reportExists, err = db.Prepare("SELECT COUNT(*) AS [count] FROM [topics] WHERE [data] = ? AND [data] != '' AND [parentID] = 1") - if err != nil { - log.Print("Error in reportExists statement.") - log.Print("Bad Query: ","SELECT COUNT(*) AS [count] FROM [topics] WHERE [data] = ? AND [data] != '' AND [parentID] = 1") - return err - } return nil } diff --git a/gen_mysql.go b/gen_mysql.go index 6c298656..79fdf503 100644 --- a/gen_mysql.go +++ b/gen_mysql.go @@ -12,14 +12,10 @@ import "./common" // nolint type Stmts struct { isPluginActive *sql.Stmt - getUsersOffset *sql.Stmt isThemeDefault *sql.Stmt - getEmailsByUser *sql.Stmt - getTopicBasic *sql.Stmt forumEntryExists *sql.Stmt groupEntryExists *sql.Stmt getForumTopics *sql.Stmt - createReport *sql.Stmt addForumPermsToForum *sql.Stmt addPlugin *sql.Stmt addTheme *sql.Stmt @@ -31,13 +27,11 @@ type Stmts struct { updateGroupPerms *sql.Stmt updateGroup *sql.Stmt updateEmail *sql.Stmt - verifyEmail *sql.Stmt setTempGroup *sql.Stmt updateWordFilter *sql.Stmt bumpSync *sql.Stmt deleteActivityStreamMatch *sql.Stmt deleteWordFilter *sql.Stmt - reportExists *sql.Stmt getActivityFeedByWatcher *sql.Stmt getActivityCountByWatcher *sql.Stmt @@ -60,13 +54,6 @@ func _gen_mysql() (err error) { return err } - common.DebugLog("Preparing getUsersOffset statement.") - stmts.getUsersOffset, err = db.Prepare("SELECT `uid`,`name`,`group`,`active`,`is_super_admin`,`avatar` FROM `users` ORDER BY `uid` ASC LIMIT ?,?") - if err != nil { - log.Print("Error in getUsersOffset statement.") - return err - } - common.DebugLog("Preparing isThemeDefault statement.") stmts.isThemeDefault, err = db.Prepare("SELECT `default` FROM `themes` WHERE `uname` = ?") if err != nil { @@ -74,20 +61,6 @@ func _gen_mysql() (err error) { return err } - common.DebugLog("Preparing getEmailsByUser statement.") - stmts.getEmailsByUser, err = db.Prepare("SELECT `email`,`validated`,`token` FROM `emails` WHERE `uid` = ?") - if err != nil { - log.Print("Error in getEmailsByUser statement.") - return err - } - - common.DebugLog("Preparing getTopicBasic statement.") - stmts.getTopicBasic, err = db.Prepare("SELECT `title`,`content` FROM `topics` WHERE `tid` = ?") - if err != nil { - log.Print("Error in getTopicBasic statement.") - return err - } - common.DebugLog("Preparing forumEntryExists statement.") stmts.forumEntryExists, err = db.Prepare("SELECT `fid` FROM `forums` WHERE `name` = '' ORDER BY `fid` ASC LIMIT 0,1") if err != nil { @@ -109,13 +82,6 @@ func _gen_mysql() (err error) { return err } - common.DebugLog("Preparing createReport statement.") - stmts.createReport, err = db.Prepare("INSERT INTO `topics`(`title`,`content`,`parsed_content`,`createdAt`,`lastReplyAt`,`createdBy`,`lastReplyBy`,`data`,`parentID`,`css_class`) VALUES (?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,1,'report')") - if err != nil { - log.Print("Error in createReport statement.") - return err - } - common.DebugLog("Preparing addForumPermsToForum statement.") stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) VALUES (?,?,?,?)") if err != nil { @@ -193,13 +159,6 @@ func _gen_mysql() (err error) { return err } - common.DebugLog("Preparing verifyEmail statement.") - stmts.verifyEmail, err = db.Prepare("UPDATE `emails` SET `validated` = 1,`token` = '' WHERE `email` = ?") - if err != nil { - log.Print("Error in verifyEmail statement.") - return err - } - common.DebugLog("Preparing setTempGroup statement.") stmts.setTempGroup, err = db.Prepare("UPDATE `users` SET `temp_group` = ? WHERE `uid` = ?") if err != nil { @@ -234,13 +193,6 @@ func _gen_mysql() (err error) { log.Print("Error in deleteWordFilter statement.") return err } - - common.DebugLog("Preparing reportExists statement.") - stmts.reportExists, err = db.Prepare("SELECT COUNT(*) AS `count` FROM `topics` WHERE `data` = ? AND `data` != '' AND `parentID` = 1") - if err != nil { - log.Print("Error in reportExists statement.") - return err - } return nil } diff --git a/gen_pgsql.go b/gen_pgsql.go index 3f09bc61..84a4f641 100644 --- a/gen_pgsql.go +++ b/gen_pgsql.go @@ -9,6 +9,10 @@ import "./common" // nolint type Stmts struct { + addForumPermsToForum *sql.Stmt + addPlugin *sql.Stmt + addTheme *sql.Stmt + createWordFilter *sql.Stmt updatePlugin *sql.Stmt updatePluginInstall *sql.Stmt updateTheme *sql.Stmt @@ -16,7 +20,6 @@ type Stmts struct { updateGroupPerms *sql.Stmt updateGroup *sql.Stmt updateEmail *sql.Stmt - verifyEmail *sql.Stmt setTempGroup *sql.Stmt updateWordFilter *sql.Stmt bumpSync *sql.Stmt @@ -35,6 +38,34 @@ type Stmts struct { func _gen_pgsql() (err error) { common.DebugLog("Building the generated statements") + common.DebugLog("Preparing addForumPermsToForum statement.") + stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO "forums_permissions"("gid","fid","preset","permissions") VALUES (?,?,?,?)") + if err != nil { + log.Print("Error in addForumPermsToForum statement.") + return err + } + + common.DebugLog("Preparing addPlugin statement.") + stmts.addPlugin, err = db.Prepare("INSERT INTO "plugins"("uname","active","installed") VALUES (?,?,?)") + if err != nil { + log.Print("Error in addPlugin statement.") + return err + } + + common.DebugLog("Preparing addTheme statement.") + stmts.addTheme, err = db.Prepare("INSERT INTO "themes"("uname","default") VALUES (?,?)") + if err != nil { + log.Print("Error in addTheme statement.") + return err + } + + common.DebugLog("Preparing createWordFilter statement.") + stmts.createWordFilter, err = db.Prepare("INSERT INTO "word_filters"("find","replacement") VALUES (?,?)") + if err != nil { + log.Print("Error in createWordFilter statement.") + return err + } + common.DebugLog("Preparing updatePlugin statement.") stmts.updatePlugin, err = db.Prepare("UPDATE `plugins` SET `active` = ? WHERE `uname` = ?") if err != nil { @@ -84,13 +115,6 @@ func _gen_pgsql() (err error) { return err } - common.DebugLog("Preparing verifyEmail statement.") - stmts.verifyEmail, err = db.Prepare("UPDATE `emails` SET `validated` = 1,`token` = '' WHERE `email` = ?") - if err != nil { - log.Print("Error in verifyEmail statement.") - return err - } - common.DebugLog("Preparing setTempGroup statement.") stmts.setTempGroup, err = db.Prepare("UPDATE `users` SET `temp_group` = ? WHERE `uid` = ?") if err != nil { diff --git a/gen_router.go b/gen_router.go index b73e0216..37dcffee 100644 --- a/gen_router.go +++ b/gen_router.go @@ -14,6 +14,7 @@ import ( "./common" "./common/counters" "./routes" + "./routes/panel" ) var ErrNoRoute = errors.New("That route doesn't exist.") @@ -27,21 +28,21 @@ var RouteMap = map[string]interface{}{ "routes.ChangeTheme": routes.ChangeTheme, "routes.ShowAttachment": routes.ShowAttachment, "common.RouteWebsockets": common.RouteWebsockets, - "routeReportSubmit": routeReportSubmit, + "routes.ReportSubmit": routes.ReportSubmit, "routes.CreateTopic": routes.CreateTopic, "routes.TopicList": routes.TopicList, - "routePanelForums": routePanelForums, - "routePanelForumsCreateSubmit": routePanelForumsCreateSubmit, - "routePanelForumsDelete": routePanelForumsDelete, - "routePanelForumsDeleteSubmit": routePanelForumsDeleteSubmit, - "routePanelForumsEdit": routePanelForumsEdit, - "routePanelForumsEditSubmit": routePanelForumsEditSubmit, - "routePanelForumsEditPermsSubmit": routePanelForumsEditPermsSubmit, - "routePanelForumsEditPermsAdvance": routePanelForumsEditPermsAdvance, - "routePanelForumsEditPermsAdvanceSubmit": routePanelForumsEditPermsAdvanceSubmit, - "routePanelSettings": routePanelSettings, - "routePanelSettingEdit": routePanelSettingEdit, - "routePanelSettingEditSubmit": routePanelSettingEditSubmit, + "panel.Forums": panel.Forums, + "panel.ForumsCreateSubmit": panel.ForumsCreateSubmit, + "panel.ForumsDelete": panel.ForumsDelete, + "panel.ForumsDeleteSubmit": panel.ForumsDeleteSubmit, + "panel.ForumsEdit": panel.ForumsEdit, + "panel.ForumsEditSubmit": panel.ForumsEditSubmit, + "panel.ForumsEditPermsSubmit": panel.ForumsEditPermsSubmit, + "panel.ForumsEditPermsAdvance": panel.ForumsEditPermsAdvance, + "panel.ForumsEditPermsAdvanceSubmit": panel.ForumsEditPermsAdvanceSubmit, + "panel.Settings": panel.Settings, + "panel.SettingEdit": panel.SettingEdit, + "panel.SettingEditSubmit": panel.SettingEditSubmit, "routePanelWordFilters": routePanelWordFilters, "routePanelWordFiltersCreateSubmit": routePanelWordFiltersCreateSubmit, "routePanelWordFiltersEdit": routePanelWordFiltersEdit, @@ -63,31 +64,31 @@ var RouteMap = map[string]interface{}{ "routePanelUsers": routePanelUsers, "routePanelUsersEdit": routePanelUsersEdit, "routePanelUsersEditSubmit": routePanelUsersEditSubmit, - "routePanelAnalyticsViews": routePanelAnalyticsViews, - "routePanelAnalyticsRoutes": routePanelAnalyticsRoutes, - "routePanelAnalyticsAgents": routePanelAnalyticsAgents, - "routePanelAnalyticsSystems": routePanelAnalyticsSystems, - "routePanelAnalyticsLanguages": routePanelAnalyticsLanguages, - "routePanelAnalyticsReferrers": routePanelAnalyticsReferrers, - "routePanelAnalyticsRouteViews": routePanelAnalyticsRouteViews, - "routePanelAnalyticsAgentViews": routePanelAnalyticsAgentViews, - "routePanelAnalyticsForumViews": routePanelAnalyticsForumViews, - "routePanelAnalyticsSystemViews": routePanelAnalyticsSystemViews, - "routePanelAnalyticsLanguageViews": routePanelAnalyticsLanguageViews, - "routePanelAnalyticsReferrerViews": routePanelAnalyticsReferrerViews, - "routePanelAnalyticsPosts": routePanelAnalyticsPosts, - "routePanelAnalyticsTopics": routePanelAnalyticsTopics, - "routePanelAnalyticsForums": routePanelAnalyticsForums, + "panel.AnalyticsViews": panel.AnalyticsViews, + "panel.AnalyticsRoutes": panel.AnalyticsRoutes, + "panel.AnalyticsAgents": panel.AnalyticsAgents, + "panel.AnalyticsSystems": panel.AnalyticsSystems, + "panel.AnalyticsLanguages": panel.AnalyticsLanguages, + "panel.AnalyticsReferrers": panel.AnalyticsReferrers, + "panel.AnalyticsRouteViews": panel.AnalyticsRouteViews, + "panel.AnalyticsAgentViews": panel.AnalyticsAgentViews, + "panel.AnalyticsForumViews": panel.AnalyticsForumViews, + "panel.AnalyticsSystemViews": panel.AnalyticsSystemViews, + "panel.AnalyticsLanguageViews": panel.AnalyticsLanguageViews, + "panel.AnalyticsReferrerViews": panel.AnalyticsReferrerViews, + "panel.AnalyticsPosts": panel.AnalyticsPosts, + "panel.AnalyticsTopics": panel.AnalyticsTopics, + "panel.AnalyticsForums": panel.AnalyticsForums, "routePanelGroups": routePanelGroups, "routePanelGroupsEdit": routePanelGroupsEdit, "routePanelGroupsEditPerms": routePanelGroupsEditPerms, "routePanelGroupsEditSubmit": routePanelGroupsEditSubmit, "routePanelGroupsEditPermsSubmit": routePanelGroupsEditPermsSubmit, "routePanelGroupsCreateSubmit": routePanelGroupsCreateSubmit, - "routePanelBackups": routePanelBackups, - "routePanelLogsRegs": routePanelLogsRegs, - "routePanelLogsMod": routePanelLogsMod, - "routePanelDebug": routePanelDebug, + "panel.Backups": panel.Backups, + "panel.LogsRegs": panel.LogsRegs, + "panel.LogsMod": panel.LogsMod, + "panel.Debug": panel.Debug, "routePanelDashboard": routePanelDashboard, "routes.AccountEditCritical": routes.AccountEditCritical, "routes.AccountEditCriticalSubmit": routes.AccountEditCriticalSubmit, @@ -95,8 +96,8 @@ var RouteMap = map[string]interface{}{ "routes.AccountEditAvatarSubmit": routes.AccountEditAvatarSubmit, "routes.AccountEditUsername": routes.AccountEditUsername, "routes.AccountEditUsernameSubmit": routes.AccountEditUsernameSubmit, - "routeAccountEditEmail": routeAccountEditEmail, - "routeAccountEditEmailTokenSubmit": routeAccountEditEmailTokenSubmit, + "routes.AccountEditEmail": routes.AccountEditEmail, + "routes.AccountEditEmailTokenSubmit": routes.AccountEditEmailTokenSubmit, "routes.ViewProfile": routes.ViewProfile, "routes.BanUserSubmit": routes.BanUserSubmit, "routes.UnbanUser": routes.UnbanUser, @@ -144,21 +145,21 @@ var routeMapEnum = map[string]int{ "routes.ChangeTheme": 5, "routes.ShowAttachment": 6, "common.RouteWebsockets": 7, - "routeReportSubmit": 8, + "routes.ReportSubmit": 8, "routes.CreateTopic": 9, "routes.TopicList": 10, - "routePanelForums": 11, - "routePanelForumsCreateSubmit": 12, - "routePanelForumsDelete": 13, - "routePanelForumsDeleteSubmit": 14, - "routePanelForumsEdit": 15, - "routePanelForumsEditSubmit": 16, - "routePanelForumsEditPermsSubmit": 17, - "routePanelForumsEditPermsAdvance": 18, - "routePanelForumsEditPermsAdvanceSubmit": 19, - "routePanelSettings": 20, - "routePanelSettingEdit": 21, - "routePanelSettingEditSubmit": 22, + "panel.Forums": 11, + "panel.ForumsCreateSubmit": 12, + "panel.ForumsDelete": 13, + "panel.ForumsDeleteSubmit": 14, + "panel.ForumsEdit": 15, + "panel.ForumsEditSubmit": 16, + "panel.ForumsEditPermsSubmit": 17, + "panel.ForumsEditPermsAdvance": 18, + "panel.ForumsEditPermsAdvanceSubmit": 19, + "panel.Settings": 20, + "panel.SettingEdit": 21, + "panel.SettingEditSubmit": 22, "routePanelWordFilters": 23, "routePanelWordFiltersCreateSubmit": 24, "routePanelWordFiltersEdit": 25, @@ -180,31 +181,31 @@ var routeMapEnum = map[string]int{ "routePanelUsers": 41, "routePanelUsersEdit": 42, "routePanelUsersEditSubmit": 43, - "routePanelAnalyticsViews": 44, - "routePanelAnalyticsRoutes": 45, - "routePanelAnalyticsAgents": 46, - "routePanelAnalyticsSystems": 47, - "routePanelAnalyticsLanguages": 48, - "routePanelAnalyticsReferrers": 49, - "routePanelAnalyticsRouteViews": 50, - "routePanelAnalyticsAgentViews": 51, - "routePanelAnalyticsForumViews": 52, - "routePanelAnalyticsSystemViews": 53, - "routePanelAnalyticsLanguageViews": 54, - "routePanelAnalyticsReferrerViews": 55, - "routePanelAnalyticsPosts": 56, - "routePanelAnalyticsTopics": 57, - "routePanelAnalyticsForums": 58, + "panel.AnalyticsViews": 44, + "panel.AnalyticsRoutes": 45, + "panel.AnalyticsAgents": 46, + "panel.AnalyticsSystems": 47, + "panel.AnalyticsLanguages": 48, + "panel.AnalyticsReferrers": 49, + "panel.AnalyticsRouteViews": 50, + "panel.AnalyticsAgentViews": 51, + "panel.AnalyticsForumViews": 52, + "panel.AnalyticsSystemViews": 53, + "panel.AnalyticsLanguageViews": 54, + "panel.AnalyticsReferrerViews": 55, + "panel.AnalyticsPosts": 56, + "panel.AnalyticsTopics": 57, + "panel.AnalyticsForums": 58, "routePanelGroups": 59, "routePanelGroupsEdit": 60, "routePanelGroupsEditPerms": 61, "routePanelGroupsEditSubmit": 62, "routePanelGroupsEditPermsSubmit": 63, "routePanelGroupsCreateSubmit": 64, - "routePanelBackups": 65, - "routePanelLogsRegs": 66, - "routePanelLogsMod": 67, - "routePanelDebug": 68, + "panel.Backups": 65, + "panel.LogsRegs": 66, + "panel.LogsMod": 67, + "panel.Debug": 68, "routePanelDashboard": 69, "routes.AccountEditCritical": 70, "routes.AccountEditCriticalSubmit": 71, @@ -212,8 +213,8 @@ var routeMapEnum = map[string]int{ "routes.AccountEditAvatarSubmit": 73, "routes.AccountEditUsername": 74, "routes.AccountEditUsernameSubmit": 75, - "routeAccountEditEmail": 76, - "routeAccountEditEmailTokenSubmit": 77, + "routes.AccountEditEmail": 76, + "routes.AccountEditEmailTokenSubmit": 77, "routes.ViewProfile": 78, "routes.BanUserSubmit": 79, "routes.UnbanUser": 80, @@ -259,21 +260,21 @@ var reverseRouteMapEnum = map[int]string{ 5: "routes.ChangeTheme", 6: "routes.ShowAttachment", 7: "common.RouteWebsockets", - 8: "routeReportSubmit", + 8: "routes.ReportSubmit", 9: "routes.CreateTopic", 10: "routes.TopicList", - 11: "routePanelForums", - 12: "routePanelForumsCreateSubmit", - 13: "routePanelForumsDelete", - 14: "routePanelForumsDeleteSubmit", - 15: "routePanelForumsEdit", - 16: "routePanelForumsEditSubmit", - 17: "routePanelForumsEditPermsSubmit", - 18: "routePanelForumsEditPermsAdvance", - 19: "routePanelForumsEditPermsAdvanceSubmit", - 20: "routePanelSettings", - 21: "routePanelSettingEdit", - 22: "routePanelSettingEditSubmit", + 11: "panel.Forums", + 12: "panel.ForumsCreateSubmit", + 13: "panel.ForumsDelete", + 14: "panel.ForumsDeleteSubmit", + 15: "panel.ForumsEdit", + 16: "panel.ForumsEditSubmit", + 17: "panel.ForumsEditPermsSubmit", + 18: "panel.ForumsEditPermsAdvance", + 19: "panel.ForumsEditPermsAdvanceSubmit", + 20: "panel.Settings", + 21: "panel.SettingEdit", + 22: "panel.SettingEditSubmit", 23: "routePanelWordFilters", 24: "routePanelWordFiltersCreateSubmit", 25: "routePanelWordFiltersEdit", @@ -295,31 +296,31 @@ var reverseRouteMapEnum = map[int]string{ 41: "routePanelUsers", 42: "routePanelUsersEdit", 43: "routePanelUsersEditSubmit", - 44: "routePanelAnalyticsViews", - 45: "routePanelAnalyticsRoutes", - 46: "routePanelAnalyticsAgents", - 47: "routePanelAnalyticsSystems", - 48: "routePanelAnalyticsLanguages", - 49: "routePanelAnalyticsReferrers", - 50: "routePanelAnalyticsRouteViews", - 51: "routePanelAnalyticsAgentViews", - 52: "routePanelAnalyticsForumViews", - 53: "routePanelAnalyticsSystemViews", - 54: "routePanelAnalyticsLanguageViews", - 55: "routePanelAnalyticsReferrerViews", - 56: "routePanelAnalyticsPosts", - 57: "routePanelAnalyticsTopics", - 58: "routePanelAnalyticsForums", + 44: "panel.AnalyticsViews", + 45: "panel.AnalyticsRoutes", + 46: "panel.AnalyticsAgents", + 47: "panel.AnalyticsSystems", + 48: "panel.AnalyticsLanguages", + 49: "panel.AnalyticsReferrers", + 50: "panel.AnalyticsRouteViews", + 51: "panel.AnalyticsAgentViews", + 52: "panel.AnalyticsForumViews", + 53: "panel.AnalyticsSystemViews", + 54: "panel.AnalyticsLanguageViews", + 55: "panel.AnalyticsReferrerViews", + 56: "panel.AnalyticsPosts", + 57: "panel.AnalyticsTopics", + 58: "panel.AnalyticsForums", 59: "routePanelGroups", 60: "routePanelGroupsEdit", 61: "routePanelGroupsEditPerms", 62: "routePanelGroupsEditSubmit", 63: "routePanelGroupsEditPermsSubmit", 64: "routePanelGroupsCreateSubmit", - 65: "routePanelBackups", - 66: "routePanelLogsRegs", - 67: "routePanelLogsMod", - 68: "routePanelDebug", + 65: "panel.Backups", + 66: "panel.LogsRegs", + 67: "panel.LogsMod", + 68: "panel.Debug", 69: "routePanelDashboard", 70: "routes.AccountEditCritical", 71: "routes.AccountEditCriticalSubmit", @@ -327,8 +328,8 @@ var reverseRouteMapEnum = map[int]string{ 73: "routes.AccountEditAvatarSubmit", 74: "routes.AccountEditUsername", 75: "routes.AccountEditUsernameSubmit", - 76: "routeAccountEditEmail", - 77: "routeAccountEditEmailTokenSubmit", + 76: "routes.AccountEditEmail", + 77: "routes.AccountEditEmailTokenSubmit", 78: "routes.ViewProfile", 79: "routes.BanUserSubmit", 80: "routes.UnbanUser", @@ -908,7 +909,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(8) - err = routeReportSubmit(w,req,user,extraData) + err = routes.ReportSubmit(w,req,user,extraData) } if err != nil { router.handleError(err,w,req,user) @@ -941,7 +942,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch(req.URL.Path) { case "/panel/forums/": counters.RouteViewCounter.Bump(11) - err = routePanelForums(w,req,user) + err = panel.Forums(w,req,user) case "/panel/forums/create/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -950,7 +951,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(12) - err = routePanelForumsCreateSubmit(w,req,user) + err = panel.ForumsCreateSubmit(w,req,user) case "/panel/forums/delete/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -959,7 +960,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(13) - err = routePanelForumsDelete(w,req,user,extraData) + err = panel.ForumsDelete(w,req,user,extraData) case "/panel/forums/delete/submit/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -968,10 +969,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(14) - err = routePanelForumsDeleteSubmit(w,req,user,extraData) + err = panel.ForumsDeleteSubmit(w,req,user,extraData) case "/panel/forums/edit/": counters.RouteViewCounter.Bump(15) - err = routePanelForumsEdit(w,req,user,extraData) + err = panel.ForumsEdit(w,req,user,extraData) case "/panel/forums/edit/submit/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -980,7 +981,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(16) - err = routePanelForumsEditSubmit(w,req,user,extraData) + err = panel.ForumsEditSubmit(w,req,user,extraData) case "/panel/forums/edit/perms/submit/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -989,10 +990,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(17) - err = routePanelForumsEditPermsSubmit(w,req,user,extraData) + err = panel.ForumsEditPermsSubmit(w,req,user,extraData) case "/panel/forums/edit/perms/": counters.RouteViewCounter.Bump(18) - err = routePanelForumsEditPermsAdvance(w,req,user,extraData) + err = panel.ForumsEditPermsAdvance(w,req,user,extraData) case "/panel/forums/edit/perms/adv/submit/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -1001,13 +1002,13 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(19) - err = routePanelForumsEditPermsAdvanceSubmit(w,req,user,extraData) + err = panel.ForumsEditPermsAdvanceSubmit(w,req,user,extraData) case "/panel/settings/": counters.RouteViewCounter.Bump(20) - err = routePanelSettings(w,req,user) + err = panel.Settings(w,req,user) case "/panel/settings/edit/": counters.RouteViewCounter.Bump(21) - err = routePanelSettingEdit(w,req,user,extraData) + err = panel.SettingEdit(w,req,user,extraData) case "/panel/settings/edit/submit/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -1016,7 +1017,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(22) - err = routePanelSettingEditSubmit(w,req,user,extraData) + err = panel.SettingEditSubmit(w,req,user,extraData) case "/panel/settings/word-filters/": counters.RouteViewCounter.Bump(23) err = routePanelWordFilters(w,req,user) @@ -1160,7 +1161,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(44) - err = routePanelAnalyticsViews(w,req,user) + err = panel.AnalyticsViews(w,req,user) case "/panel/analytics/routes/": err = common.ParseForm(w,req,user) if err != nil { @@ -1169,7 +1170,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(45) - err = routePanelAnalyticsRoutes(w,req,user) + err = panel.AnalyticsRoutes(w,req,user) case "/panel/analytics/agents/": err = common.ParseForm(w,req,user) if err != nil { @@ -1178,7 +1179,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(46) - err = routePanelAnalyticsAgents(w,req,user) + err = panel.AnalyticsAgents(w,req,user) case "/panel/analytics/systems/": err = common.ParseForm(w,req,user) if err != nil { @@ -1187,7 +1188,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(47) - err = routePanelAnalyticsSystems(w,req,user) + err = panel.AnalyticsSystems(w,req,user) case "/panel/analytics/langs/": err = common.ParseForm(w,req,user) if err != nil { @@ -1196,7 +1197,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(48) - err = routePanelAnalyticsLanguages(w,req,user) + err = panel.AnalyticsLanguages(w,req,user) case "/panel/analytics/referrers/": err = common.ParseForm(w,req,user) if err != nil { @@ -1205,25 +1206,25 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(49) - err = routePanelAnalyticsReferrers(w,req,user) + err = panel.AnalyticsReferrers(w,req,user) case "/panel/analytics/route/": counters.RouteViewCounter.Bump(50) - err = routePanelAnalyticsRouteViews(w,req,user,extraData) + err = panel.AnalyticsRouteViews(w,req,user,extraData) case "/panel/analytics/agent/": counters.RouteViewCounter.Bump(51) - err = routePanelAnalyticsAgentViews(w,req,user,extraData) + err = panel.AnalyticsAgentViews(w,req,user,extraData) case "/panel/analytics/forum/": counters.RouteViewCounter.Bump(52) - err = routePanelAnalyticsForumViews(w,req,user,extraData) + err = panel.AnalyticsForumViews(w,req,user,extraData) case "/panel/analytics/system/": counters.RouteViewCounter.Bump(53) - err = routePanelAnalyticsSystemViews(w,req,user,extraData) + err = panel.AnalyticsSystemViews(w,req,user,extraData) case "/panel/analytics/lang/": counters.RouteViewCounter.Bump(54) - err = routePanelAnalyticsLanguageViews(w,req,user,extraData) + err = panel.AnalyticsLanguageViews(w,req,user,extraData) case "/panel/analytics/referrer/": counters.RouteViewCounter.Bump(55) - err = routePanelAnalyticsReferrerViews(w,req,user,extraData) + err = panel.AnalyticsReferrerViews(w,req,user,extraData) case "/panel/analytics/posts/": err = common.ParseForm(w,req,user) if err != nil { @@ -1232,7 +1233,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(56) - err = routePanelAnalyticsPosts(w,req,user) + err = panel.AnalyticsPosts(w,req,user) case "/panel/analytics/topics/": err = common.ParseForm(w,req,user) if err != nil { @@ -1241,7 +1242,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(57) - err = routePanelAnalyticsTopics(w,req,user) + err = panel.AnalyticsTopics(w,req,user) case "/panel/analytics/forums/": err = common.ParseForm(w,req,user) if err != nil { @@ -1250,7 +1251,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(58) - err = routePanelAnalyticsForums(w,req,user) + err = panel.AnalyticsForums(w,req,user) case "/panel/groups/": counters.RouteViewCounter.Bump(59) err = routePanelGroups(w,req,user) @@ -1295,13 +1296,13 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(65) - err = routePanelBackups(w,req,user,extraData) + err = panel.Backups(w,req,user,extraData) case "/panel/logs/regs/": counters.RouteViewCounter.Bump(66) - err = routePanelLogsRegs(w,req,user) + err = panel.LogsRegs(w,req,user) case "/panel/logs/mod/": counters.RouteViewCounter.Bump(67) - err = routePanelLogsMod(w,req,user) + err = panel.LogsMod(w,req,user) case "/panel/debug/": err = common.AdminOnly(w,req,user) if err != nil { @@ -1310,7 +1311,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(68) - err = routePanelDebug(w,req,user) + err = panel.Debug(w,req,user) default: counters.RouteViewCounter.Bump(69) err = routePanelDashboard(w,req,user) @@ -1405,7 +1406,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(76) - err = routeAccountEditEmail(w,req,user) + err = routes.AccountEditEmail(w,req,user) case "/user/edit/token/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -1420,7 +1421,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } counters.RouteViewCounter.Bump(77) - err = routeAccountEditEmailTokenSubmit(w,req,user,extraData) + err = routes.AccountEditEmailTokenSubmit(w,req,user,extraData) default: req.URL.Path += extraData counters.RouteViewCounter.Bump(78) diff --git a/gen_tables.go b/gen_tables.go index 55c40060..8067327b 100644 --- a/gen_tables.go +++ b/gen_tables.go @@ -2,18 +2,20 @@ package main var dbTablePrimaryKeys = map[string]string{ + "topics":"tid", + "attachments":"attachID", + "menus":"mid", "users_groups":"gid", "users_groups_scheduler":"uid", + "registration_logs":"rlid", + "word_filters":"wfid", + "menu_items":"miid", + "polls":"pollID", "users_replies":"rid", - "topics":"tid", + "activity_stream":"asid", + "pages":"pid", "replies":"rid", "revisions":"reviseID", - "activity_stream":"asid", - "word_filters":"wfid", - "menus":"mid", "users":"uid", - "menu_items":"miid", "forums":"fid", - "attachments":"attachID", - "polls":"pollID", } diff --git a/general_test.go b/general_test.go index def7c284..f1b457f5 100644 --- a/general_test.go +++ b/general_test.go @@ -53,7 +53,7 @@ func gloinit() (err error) { if err != nil { return err } - err = common.InitThemes() + common.Themes, err = common.NewThemeList() if err != nil { return err } diff --git a/install-linux b/install-linux index f0fb001e..d1541cf7 100644 --- a/install-linux +++ b/install-linux @@ -10,6 +10,9 @@ go get -u github.com/denisenkom/go-mssqldb echo "Installing bcrypt" go get -u golang.org/x/crypto/bcrypt +echo "Installing Argon2" +go get -u golang.org/x/crypto/argon2 + echo "Installing gopsutil" go get -u github.com/Azareal/gopsutil diff --git a/install.bat b/install.bat index 34c22631..55a961a2 100644 --- a/install.bat +++ b/install.bat @@ -29,6 +29,13 @@ if %errorlevel% neq 0 ( exit /b %errorlevel% ) +echo Installing the Argon2 library +go get -u golang.org/x/crypto/argon2 +if %errorlevel% neq 0 ( + pause + exit /b %errorlevel% +) + echo Installing /x/sys/windows (dependency for gopsutil) go get -u golang.org/x/sys/windows if %errorlevel% neq 0 ( diff --git a/install/install/utils.go b/install/install/utils.go index 25253ead..16c33284 100644 --- a/install/install/utils.go +++ b/install/install/utils.go @@ -13,28 +13,15 @@ func GenerateSafeString(length int) (string, error) { if err != nil { return "", err } - return base64.URLEncoding.EncodeToString(rb), nil + return base64.StdEncoding.EncodeToString(rb), nil } -// Generate a bcrypt hash from a password and a salt -func BcryptGeneratePassword(password string) (hashedPassword string, salt string, err error) { - salt, err = GenerateSafeString(saltLength) - if err != nil { - return "", "", err - } - - password = password + salt - hashedPassword, err = bcryptGeneratePasswordNoSalt(password) - if err != nil { - return "", "", err - } - return hashedPassword, salt, nil -} - -func bcryptGeneratePasswordNoSalt(password string) (hash string, err error) { +// Generate a bcrypt hash +// Note: The salt is in the hash, therefore the salt value is blank +func bcryptGeneratePassword(password string) (hash string, salt string, err error) { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { - return "", err + return "", "", err } - return string(hashedPassword), nil + return string(hashedPassword), salt, nil } diff --git a/langs/english.json b/langs/english.json index 241b6394..0d3f2a45 100644 --- a/langs/english.json +++ b/langs/english.json @@ -44,8 +44,12 @@ "MoveTopic": "Can move topics in or out" }, - "SettingLabels": { - "activation_type": "Activate All,Email Activation,Admin Approval" + "SettingPhrases": { + "activation_type":"Activation Type", + "activation_type_label": "Activate All,Email Activation,Admin Approval", + "bigpost_min_words":"Big Post Minimum Words", + "megapost_min_words":"Mega Post Minimum Words", + "meta_desc":"Meta Description" }, "PermPresets": { @@ -263,6 +267,7 @@ "topics_click_topics_to_select":"Click the topics to select them", "topics_new_topic":"New Topic", "forum_locked":"Locked", + "topics_moderate":"Moderate", "topics_replies_suffix":" replies", "forums_topics_suffix":" topics", "topics_gap_likes_suffix":" likes", diff --git a/main.go b/main.go index 863f4f8c..f588f3ec 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,6 @@ import ( var version = common.Version{Major: 0, Minor: 1, Patch: 0, Tag: "dev"} var router *GenRouter -var startTime time.Time var logWriter = io.MultiWriter(os.Stderr) // TODO: Wrap the globals in here so we can pass pointers to them to subpackages @@ -107,6 +106,14 @@ func afterDBInit() (err error) { } log.Print("Initialising the stores") + common.Reports, err = common.NewDefaultReportStore(acc) + if err != nil { + return err + } + common.Emails, err = common.NewDefaultEmailStore(acc) + if err != nil { + return err + } common.RegLogs, err = common.NewRegLogStore(acc) if err != nil { return err @@ -140,7 +147,8 @@ func afterDBInit() (err error) { return err } - counters.GlobalViewCounter, err = counters.NewGlobalViewCounter() + log.Print("Initialising the view counters") + counters.GlobalViewCounter, err = counters.NewGlobalViewCounter(acc) if err != nil { return err } @@ -195,18 +203,6 @@ func main() { return } }()*/ - - // WIP: Mango Test - /*res, err := ioutil.ReadFile("./templates/topic.html") - if err != nil { - log.Fatal(err) - } - tagIndices, err := mangoParse(string(res)) - if err != nil { - log.Fatal(err) - } - log.Printf("tagIndices: %+v\n", tagIndices) - log.Fatal("")*/ config.Config() // TODO: Have a file for each run with the time/date the server started as the file name? @@ -217,18 +213,9 @@ func main() { } logWriter = io.MultiWriter(os.Stderr, f) log.SetOutput(logWriter) - - //if profiling { - // f, err := os.Create("startup_cpu.prof") - // if err != nil { - // log.Fatal(err) - // } - // pprof.StartCPUProfile(f) - //} - log.Print("Running Gosora v" + version.String()) fmt.Println("") - startTime = time.Now() + common.StartTime = time.Now() log.Print("Processing configuration data") err = common.ProcessConfig() @@ -236,7 +223,7 @@ func main() { log.Fatal(err) } - err = common.InitThemes() + common.Themes, err = common.NewThemeList() if err != nil { log.Fatal(err) } @@ -271,6 +258,7 @@ func main() { log.Fatal(err) } + log.Print("Initialising the file watcher") watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) @@ -337,6 +325,7 @@ func main() { } } + log.Print("Initialising the task system") var runTasks = func(tasks []func() error) { for _, task := range tasks { if task() != nil { diff --git a/member_routes.go b/member_routes.go deleted file mode 100644 index 175c01d1..00000000 --- a/member_routes.go +++ /dev/null @@ -1,217 +0,0 @@ -package main - -import ( - "net/http" - "strconv" - - "./common" - "./common/counters" -) - -func routeReportSubmit(w http.ResponseWriter, r *http.Request, user common.User, sitemID string) common.RouteError { - itemID, err := strconv.Atoi(sitemID) - if err != nil { - return common.LocalError("Bad ID", w, r, user) - } - itemType := r.FormValue("type") - - var fid = 1 - var title, content string - if itemType == "reply" { - reply, err := common.Rstore.Get(itemID) - if err == ErrNoRows { - return common.LocalError("We were unable to find the reported post", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - - topic, err := common.Topics.Get(reply.ParentID) - if err == ErrNoRows { - return common.LocalError("We weren't able to find the topic the reported post is supposed to be in", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - - title = "Reply: " + topic.Title - content = reply.Content + "\n\nOriginal Post: #rid-" + strconv.Itoa(itemID) - } else if itemType == "user-reply" { - userReply, err := common.Prstore.Get(itemID) - if err == ErrNoRows { - return common.LocalError("We weren't able to find the reported post", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - - profileOwner, err := common.Users.Get(userReply.ParentID) - if err == ErrNoRows { - return common.LocalError("We weren't able to find the profile the reported post is supposed to be on", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - title = "Profile: " + profileOwner.Name - content = userReply.Content + "\n\nOriginal Post: @" + strconv.Itoa(userReply.ParentID) - } else if itemType == "topic" { - err = stmts.getTopicBasic.QueryRow(itemID).Scan(&title, &content) - if err == ErrNoRows { - return common.NotFound(w, r, nil) - } else if err != nil { - return common.InternalError(err, w, r) - } - title = "Topic: " + title - content = content + "\n\nOriginal Post: #tid-" + strconv.Itoa(itemID) - } else { - _, hasHook := common.RunVhookNeedHook("report_preassign", &itemID, &itemType) - if hasHook { - return nil - } - - // Don't try to guess the type - return common.LocalError("Unknown type", w, r, user) - } - - var count int - err = stmts.reportExists.QueryRow(itemType + "_" + strconv.Itoa(itemID)).Scan(&count) - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - if count != 0 { - return common.LocalError("Someone has already reported this!", w, r, user) - } - - // TODO: Repost attachments in the reports forum, so that the mods can see them - // ? - Can we do this via the TopicStore? Should we do a ReportStore? - res, err := stmts.createReport.Exec(title, content, common.ParseMessage(content, 0, ""), user.ID, user.ID, itemType+"_"+strconv.Itoa(itemID)) - if err != nil { - return common.InternalError(err, w, r) - } - - lastID, err := res.LastInsertId() - if err != nil { - return common.InternalError(err, w, r) - } - - err = common.Forums.AddTopic(int(lastID), user.ID, fid) - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - counters.PostCounter.Bump() - - http.Redirect(w, r, "/topic/"+strconv.FormatInt(lastID, 10), http.StatusSeeOther) - return nil -} - -func routeAccountEditEmail(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, ferr := common.UserCheck(w, r, &user) - if ferr != nil { - return ferr - } - - email := common.Email{UserID: user.ID} - var emailList []interface{} - rows, err := stmts.getEmailsByUser.Query(user.ID) - if err != nil { - return common.InternalError(err, w, r) - } - defer rows.Close() - - for rows.Next() { - err := rows.Scan(&email.Email, &email.Validated, &email.Token) - if err != nil { - return common.InternalError(err, w, r) - } - - if email.Email == user.Email { - email.Primary = true - } - emailList = append(emailList, email) - } - err = rows.Err() - if err != nil { - return common.InternalError(err, w, r) - } - - // Was this site migrated from another forum software? Most of them don't have multiple emails for a single user. - // This also applies when the admin switches site.EnableEmails on after having it off for a while. - if len(emailList) == 0 { - email.Email = user.Email - email.Validated = false - email.Primary = true - emailList = append(emailList, email) - } - - if !common.Site.EnableEmails { - headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("account_mail_disabled")) - } - if r.FormValue("verified") == "1" { - headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("account_mail_verify_success")) - } - - pi := common.Page{"Email Manager", user, headerVars, emailList, nil} - if common.RunPreRenderHook("pre_render_account_own_edit_email", w, r, &user, &pi) { - return nil - } - err = common.Templates.ExecuteTemplate(w, "account_own_edit_email.html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - return nil -} - -// TODO: Do a session check on this? -func routeAccountEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, user common.User, token string) common.RouteError { - headerVars, ferr := common.UserCheck(w, r, &user) - if ferr != nil { - return ferr - } - - email := common.Email{UserID: user.ID} - targetEmail := common.Email{UserID: user.ID} - var emailList []interface{} - rows, err := stmts.getEmailsByUser.Query(user.ID) - if err != nil { - return common.InternalError(err, w, r) - } - defer rows.Close() - - for rows.Next() { - err := rows.Scan(&email.Email, &email.Validated, &email.Token) - if err != nil { - return common.InternalError(err, w, r) - } - - if email.Email == user.Email { - email.Primary = true - } - if email.Token == token { - targetEmail = email - } - emailList = append(emailList, email) - } - err = rows.Err() - if err != nil { - return common.InternalError(err, w, r) - } - - if len(emailList) == 0 { - return common.LocalError("A verification email was never sent for you!", w, r, user) - } - if targetEmail.Token == "" { - return common.LocalError("That's not a valid token!", w, r, user) - } - - _, err = stmts.verifyEmail.Exec(user.Email) - if err != nil { - return common.InternalError(err, w, r) - } - - // If Email Activation is on, then activate the account while we're here - if headerVars.Settings["activation_type"] == 2 { - err = user.Activate() - if err != nil { - return common.InternalError(err, w, r) - } - } - http.Redirect(w, r, "/user/edit/email/?verified=1", http.StatusSeeOther) - - return nil -} diff --git a/misc_test.go b/misc_test.go index 8b42907b..a4fd7a28 100644 --- a/misc_test.go +++ b/misc_test.go @@ -866,7 +866,7 @@ func TestAuth(t *testing.T) { realPassword = "Madame Cassandra's Mystic Orb" t.Logf("Set realPassword to '%s'", realPassword) t.Log("Hashing the real password") - hashedPassword, err = common.BcryptGeneratePasswordNoSalt(realPassword) + hashedPassword, err = common.BcryptGeneratePassword(realPassword) if err != nil { t.Error(err) } diff --git a/panel_routes.go b/panel_routes.go index 77575f94..72440b66 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -6,19 +6,12 @@ import ( "errors" "fmt" "html" - "html/template" - "io/ioutil" "log" "net/http" - "os" - "path/filepath" - "runtime" "strconv" "strings" - "time" "./common" - "./query_gen/lib" "github.com/Azareal/gopsutil/mem" ) @@ -172,1242 +165,6 @@ func routePanelDashboard(w http.ResponseWriter, r *http.Request, user common.Use return panelRenderTemplate("panel_dashboard", w, r, user, &pi) } -func routePanelForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.ManageForums { - return common.NoPermissions(w, r, user) - } - - // TODO: Paginate this? - var forumList []interface{} - forums, err := common.Forums.GetAll() - if err != nil { - return common.InternalError(err, w, r) - } - - // ? - Should we generate something similar to the forumView? It might be a little overkill for a page which is rarely loaded in comparison to /forums/ - for _, forum := range forums { - if forum.Name != "" && forum.ParentID == 0 { - fadmin := common.ForumAdmin{forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, forum.TopicCount, common.PresetToLang(forum.Preset)} - if fadmin.Preset == "" { - fadmin.Preset = "custom" - } - forumList = append(forumList, fadmin) - } - } - - if r.FormValue("created") == "1" { - headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("panel_forum_created")) - } else if r.FormValue("deleted") == "1" { - headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("panel_forum_deleted")) - } else if r.FormValue("updated") == "1" { - headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("panel_forum_updated")) - } - - pi := common.PanelPage{common.GetTitlePhrase("panel_forums"), user, headerVars, stats, "forums", forumList, nil} - return panelRenderTemplate("panel_forums", w, r, user, &pi) -} - -func routePanelForumsCreateSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - _, ferr := common.SimplePanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.ManageForums { - return common.NoPermissions(w, r, user) - } - - fname := r.PostFormValue("forum-name") - fdesc := r.PostFormValue("forum-desc") - fpreset := common.StripInvalidPreset(r.PostFormValue("forum-preset")) - factive := r.PostFormValue("forum-active") - active := (factive == "on" || factive == "1") - - _, err := common.Forums.Create(fname, fdesc, active, fpreset) - if err != nil { - return common.InternalError(err, w, r) - } - - http.Redirect(w, r, "/panel/forums/?created=1", http.StatusSeeOther) - return nil -} - -// TODO: Revamp this -func routePanelForumsDelete(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.ManageForums { - return common.NoPermissions(w, r, user) - } - - fid, err := strconv.Atoi(sfid) - if err != nil { - return common.LocalError("The provided Forum ID is not a valid number.", w, r, user) - } - - forum, err := common.Forums.Get(fid) - if err == ErrNoRows { - return common.LocalError("The forum you're trying to delete doesn't exist.", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - - // TODO: Make this a phrase - confirmMsg := "Are you sure you want to delete the '" + forum.Name + "' forum?" - yousure := common.AreYouSure{"/panel/forums/delete/submit/" + strconv.Itoa(fid), confirmMsg} - - pi := common.PanelPage{common.GetTitlePhrase("panel_delete_forum"), user, headerVars, stats, "forums", tList, yousure} - if common.RunPreRenderHook("pre_render_panel_delete_forum", w, r, &user, &pi) { - return nil - } - err = common.Templates.ExecuteTemplate(w, "are_you_sure.html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - return nil -} - -func routePanelForumsDeleteSubmit(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { - _, ferr := common.SimplePanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.ManageForums { - return common.NoPermissions(w, r, user) - } - - fid, err := strconv.Atoi(sfid) - if err != nil { - return common.LocalError("The provided Forum ID is not a valid number.", w, r, user) - } - - err = common.Forums.Delete(fid) - if err == ErrNoRows { - return common.LocalError("The forum you're trying to delete doesn't exist.", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - - http.Redirect(w, r, "/panel/forums/?deleted=1", http.StatusSeeOther) - return nil -} - -func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.ManageForums { - return common.NoPermissions(w, r, user) - } - - fid, err := strconv.Atoi(sfid) - if err != nil { - return common.LocalError("The provided Forum ID is not a valid number.", w, r, user) - } - - forum, err := common.Forums.Get(fid) - if err == ErrNoRows { - return common.LocalError("The forum you're trying to edit doesn't exist.", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - - if forum.Preset == "" { - forum.Preset = "custom" - } - - glist, err := common.Groups.GetAll() - if err != nil { - return common.InternalError(err, w, r) - } - - var gplist []common.GroupForumPermPreset - for gid, group := range glist { - if gid == 0 { - continue - } - forumPerms, err := common.FPStore.Get(fid, group.ID) - if err == ErrNoRows { - forumPerms = common.BlankForumPerms() - } else if err != nil { - return common.InternalError(err, w, r) - } - gplist = append(gplist, common.GroupForumPermPreset{group, common.ForumPermsToGroupForumPreset(forumPerms)}) - } - - if r.FormValue("updated") == "1" { - headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("panel_forum_updated")) - } - - pi := common.PanelEditForumPage{common.GetTitlePhrase("panel_edit_forum"), user, headerVars, stats, "forums", forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, gplist} - if common.RunPreRenderHook("pre_render_panel_edit_forum", w, r, &user, &pi) { - return nil - } - err = common.Templates.ExecuteTemplate(w, "panel-forum-edit.html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - - return nil -} - -func routePanelForumsEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { - _, ferr := common.SimplePanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.ManageForums { - return common.NoPermissions(w, r, user) - } - isJs := (r.PostFormValue("js") == "1") - - fid, err := strconv.Atoi(sfid) - if err != nil { - return common.LocalErrorJSQ("The provided Forum ID is not a valid number.", w, r, user, isJs) - } - - forum, err := common.Forums.Get(fid) - if err == ErrNoRows { - return common.LocalErrorJSQ("The forum you're trying to edit doesn't exist.", w, r, user, isJs) - } else if err != nil { - return common.InternalErrorJSQ(err, w, r, isJs) - } - - forumName := r.PostFormValue("forum_name") - forumDesc := r.PostFormValue("forum_desc") - forumPreset := common.StripInvalidPreset(r.PostFormValue("forum_preset")) - forumActive := r.PostFormValue("forum_active") - - var active = false - if forumActive == "" { - active = forum.Active - } else if forumActive == "1" || forumActive == "Show" { - active = true - } - - err = forum.Update(forumName, forumDesc, active, forumPreset) - if err != nil { - return common.InternalErrorJSQ(err, w, r, isJs) - } - // ? Should we redirect to the forum editor instead? - return panelSuccessRedirect("/panel/forums/", w, r, isJs) -} - -func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { - _, ferr := common.SimplePanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.ManageForums { - return common.NoPermissions(w, r, user) - } - isJs := (r.PostFormValue("js") == "1") - - fid, err := strconv.Atoi(sfid) - if err != nil { - return common.LocalErrorJSQ("The provided Forum ID is not a valid number.", w, r, user, isJs) - } - - gid, err := strconv.Atoi(r.PostFormValue("gid")) - if err != nil { - return common.LocalErrorJSQ("Invalid Group ID", w, r, user, isJs) - } - - forum, err := common.Forums.Get(fid) - if err == ErrNoRows { - return common.LocalErrorJSQ("This forum doesn't exist", w, r, user, isJs) - } else if err != nil { - return common.InternalErrorJSQ(err, w, r, isJs) - } - - permPreset := common.StripInvalidGroupForumPreset(r.PostFormValue("perm_preset")) - err = forum.SetPreset(permPreset, gid) - if err != nil { - return common.LocalErrorJSQ(err.Error(), w, r, user, isJs) - } - - return panelSuccessRedirect("/panel/forums/edit/"+strconv.Itoa(fid)+"?updated=1", w, r, isJs) -} - -// A helper function for the Advanced portion of the Forum Perms Editor -func panelForumPermsExtractDash(paramList string) (fid int, gid int, err error) { - params := strings.Split(paramList, "-") - if len(params) != 2 { - return fid, gid, errors.New("Parameter count mismatch") - } - - fid, err = strconv.Atoi(params[0]) - if err != nil { - return fid, gid, errors.New("The provided Forum ID is not a valid number.") - } - - gid, err = strconv.Atoi(params[1]) - if err != nil { - err = errors.New("The provided Group ID is not a valid number.") - } - - return fid, gid, err -} - -func routePanelForumsEditPermsAdvance(w http.ResponseWriter, r *http.Request, user common.User, paramList string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.ManageForums { - return common.NoPermissions(w, r, user) - } - - fid, gid, err := panelForumPermsExtractDash(paramList) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - - forum, err := common.Forums.Get(fid) - if err == ErrNoRows { - return common.LocalError("The forum you're trying to edit doesn't exist.", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - - if forum.Preset == "" { - forum.Preset = "custom" - } - - forumPerms, err := common.FPStore.Get(fid, gid) - if err == ErrNoRows { - forumPerms = common.BlankForumPerms() - } else if err != nil { - return common.InternalError(err, w, r) - } - - var formattedPermList []common.NameLangToggle - - // TODO: Load the phrases in bulk for efficiency? - // TODO: Reduce the amount of code duplication between this and the group editor. Also, can we grind this down into one line or use a code generator to stay current more easily? - var addNameLangToggle = func(permStr string, perm bool) { - formattedPermList = append(formattedPermList, common.NameLangToggle{permStr, common.GetLocalPermPhrase(permStr), perm}) - } - addNameLangToggle("ViewTopic", forumPerms.ViewTopic) - addNameLangToggle("LikeItem", forumPerms.LikeItem) - addNameLangToggle("CreateTopic", forumPerms.CreateTopic) - //<-- - addNameLangToggle("EditTopic", forumPerms.EditTopic) - addNameLangToggle("DeleteTopic", forumPerms.DeleteTopic) - addNameLangToggle("CreateReply", forumPerms.CreateReply) - addNameLangToggle("EditReply", forumPerms.EditReply) - addNameLangToggle("DeleteReply", forumPerms.DeleteReply) - addNameLangToggle("PinTopic", forumPerms.PinTopic) - addNameLangToggle("CloseTopic", forumPerms.CloseTopic) - addNameLangToggle("MoveTopic", forumPerms.MoveTopic) - - if r.FormValue("updated") == "1" { - headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("panel_forums_perms_updated")) - } - - pi := common.PanelEditForumGroupPage{common.GetTitlePhrase("panel_edit_forum"), user, headerVars, stats, "forums", forum.ID, gid, forum.Name, forum.Desc, forum.Active, forum.Preset, formattedPermList} - if common.RunPreRenderHook("pre_render_panel_edit_forum", w, r, &user, &pi) { - return nil - } - err = common.Templates.ExecuteTemplate(w, "panel-forum-edit-perms.html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - - return nil -} - -func routePanelForumsEditPermsAdvanceSubmit(w http.ResponseWriter, r *http.Request, user common.User, paramList string) common.RouteError { - _, ferr := common.SimplePanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.ManageForums { - return common.NoPermissions(w, r, user) - } - isJs := (r.PostFormValue("js") == "1") - - fid, gid, err := panelForumPermsExtractDash(paramList) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - - forum, err := common.Forums.Get(fid) - if err == ErrNoRows { - return common.LocalError("The forum you're trying to edit doesn't exist.", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - - forumPerms, err := common.FPStore.GetCopy(fid, gid) - if err == ErrNoRows { - forumPerms = *common.BlankForumPerms() - } else if err != nil { - return common.InternalError(err, w, r) - } - - var extractPerm = func(name string) bool { - pvalue := r.PostFormValue("forum-perm-" + name) - return (pvalue == "1") - } - - // TODO: Generate this code? - forumPerms.ViewTopic = extractPerm("ViewTopic") - forumPerms.LikeItem = extractPerm("LikeItem") - forumPerms.CreateTopic = extractPerm("CreateTopic") - forumPerms.EditTopic = extractPerm("EditTopic") - forumPerms.DeleteTopic = extractPerm("DeleteTopic") - forumPerms.CreateReply = extractPerm("CreateReply") - forumPerms.EditReply = extractPerm("EditReply") - forumPerms.DeleteReply = extractPerm("DeleteReply") - forumPerms.PinTopic = extractPerm("PinTopic") - forumPerms.CloseTopic = extractPerm("CloseTopic") - forumPerms.MoveTopic = extractPerm("MoveTopic") - - err = forum.SetPerms(&forumPerms, "custom", gid) - if err != nil { - return common.LocalErrorJSQ(err.Error(), w, r, user, isJs) - } - - return panelSuccessRedirect("/panel/forums/edit/perms/"+strconv.Itoa(fid)+"-"+strconv.Itoa(gid)+"?updated=1", w, r, isJs) -} - -type AnalyticsTimeRange struct { - Quantity int - Unit string - Slices int - SliceWidth int - Range string -} - -func panelAnalyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err error) { - timeRange.Quantity = 6 - timeRange.Unit = "hour" - timeRange.Slices = 12 - timeRange.SliceWidth = 60 * 30 - timeRange.Range = "six-hours" - - switch rawTimeRange { - case "one-month": - timeRange.Quantity = 30 - timeRange.Unit = "day" - timeRange.Slices = 30 - timeRange.SliceWidth = 60 * 60 * 24 - timeRange.Range = "one-month" - case "one-week": - timeRange.Quantity = 7 - timeRange.Unit = "day" - timeRange.Slices = 14 - timeRange.SliceWidth = 60 * 60 * 12 - timeRange.Range = "one-week" - case "two-days": // Two days is experimental - timeRange.Quantity = 2 - timeRange.Unit = "day" - timeRange.Slices = 24 - timeRange.SliceWidth = 60 * 60 * 2 - timeRange.Range = "two-days" - case "one-day": - timeRange.Quantity = 1 - timeRange.Unit = "day" - timeRange.Slices = 24 - timeRange.SliceWidth = 60 * 60 - timeRange.Range = "one-day" - case "twelve-hours": - timeRange.Quantity = 12 - timeRange.Slices = 24 - timeRange.Range = "twelve-hours" - case "six-hours", "": - timeRange.Range = "six-hours" - default: - return timeRange, errors.New("Unknown time range") - } - return timeRange, nil -} - -func panelAnalyticsTimeRangeToLabelList(timeRange AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) { - viewMap = make(map[int64]int64) - var currentTime = time.Now().Unix() - for i := 1; i <= timeRange.Slices; i++ { - var label = currentTime - int64(i*timeRange.SliceWidth) - revLabelList = append(revLabelList, label) - viewMap[label] = 0 - } - for _, value := range revLabelList { - labelList = append(labelList, value) - } - return revLabelList, labelList, viewMap -} - -func panelAnalyticsRowsToViewMap(rows *sql.Rows, labelList []int64, viewMap map[int64]int64) (map[int64]int64, error) { - defer rows.Close() - for rows.Next() { - var count int64 - var createdAt time.Time - err := rows.Scan(&count, &createdAt) - if err != nil { - return viewMap, err - } - - var unixCreatedAt = createdAt.Unix() - // TODO: Bulk log this - if common.Dev.SuperDebug { - log.Print("count: ", count) - log.Print("createdAt: ", createdAt) - log.Print("unixCreatedAt: ", unixCreatedAt) - } - - for _, value := range labelList { - if unixCreatedAt > value { - viewMap[value] += count - break - } - } - } - return viewMap, rows.Err() -} - -func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.AddSheet("chartist/chartist.min.css") - headerVars.AddScript("chartist/chartist.min.js") - headerVars.AddScript("analytics.js") - - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) - - common.DebugLog("in routePanelAnalyticsViews") - acc := qgen.Builder.Accumulator() - rows, err := acc.Select("viewchunks").Columns("count, createdAt").Where("route = ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - viewMap, err = panelAnalyticsRowsToViewMap(rows, labelList, viewMap) - if err != nil { - return common.InternalError(err, w, r) - } - - var viewList []int64 - var viewItems []common.PanelAnalyticsItem - for _, value := range revLabelList { - viewList = append(viewList, viewMap[value]) - viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) - } - graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} - common.DebugLogf("graph: %+v\n", graph) - - pi := common.PanelAnalyticsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", graph, viewItems, timeRange.Range} - return panelRenderTemplate("panel_analytics_views", w, r, user, &pi) -} - -func routePanelAnalyticsRouteViews(w http.ResponseWriter, r *http.Request, user common.User, route string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.AddSheet("chartist/chartist.min.css") - headerVars.AddScript("chartist/chartist.min.js") - headerVars.AddScript("analytics.js") - - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) - - common.DebugLog("in routePanelAnalyticsRouteViews") - acc := qgen.Builder.Accumulator() - // TODO: Validate the route is valid - rows, err := acc.Select("viewchunks").Columns("count, createdAt").Where("route = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(route) - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - viewMap, err = panelAnalyticsRowsToViewMap(rows, labelList, viewMap) - if err != nil { - return common.InternalError(err, w, r) - } - - var viewList []int64 - var viewItems []common.PanelAnalyticsItem - for _, value := range revLabelList { - viewList = append(viewList, viewMap[value]) - viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) - } - graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} - common.DebugLogf("graph: %+v\n", graph) - - pi := common.PanelAnalyticsRoutePage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", html.EscapeString(route), graph, viewItems, timeRange.Range} - return panelRenderTemplate("panel_analytics_route_views", w, r, user, &pi) -} - -func routePanelAnalyticsAgentViews(w http.ResponseWriter, r *http.Request, user common.User, agent string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.AddSheet("chartist/chartist.min.css") - headerVars.AddScript("chartist/chartist.min.js") - headerVars.AddScript("analytics.js") - - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) - - // ? Only allow valid agents? The problem with this is that agents wind up getting renamed and it would take a migration to get them all up to snuff - agent = html.EscapeString(agent) - - common.DebugLog("in routePanelAnalyticsAgentViews") - acc := qgen.Builder.Accumulator() - // TODO: Verify the agent is valid - rows, err := acc.Select("viewchunks_agents").Columns("count, createdAt").Where("browser = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(agent) - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - viewMap, err = panelAnalyticsRowsToViewMap(rows, labelList, viewMap) - if err != nil { - return common.InternalError(err, w, r) - } - - var viewList []int64 - for _, value := range revLabelList { - viewList = append(viewList, viewMap[value]) - } - graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} - common.DebugLogf("graph: %+v\n", graph) - - friendlyAgent, ok := common.GetUserAgentPhrase(agent) - if !ok { - friendlyAgent = agent - } - - pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", agent, friendlyAgent, graph, timeRange.Range} - return panelRenderTemplate("panel_analytics_agent_views", w, r, user, &pi) -} - -func routePanelAnalyticsForumViews(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.AddSheet("chartist/chartist.min.css") - headerVars.AddScript("chartist/chartist.min.js") - headerVars.AddScript("analytics.js") - - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) - - fid, err := strconv.Atoi(sfid) - if err != nil { - return common.LocalError("Invalid integer", w, r, user) - } - - common.DebugLog("in routePanelAnalyticsForumViews") - acc := qgen.Builder.Accumulator() - // TODO: Verify the agent is valid - rows, err := acc.Select("viewchunks_forums").Columns("count, createdAt").Where("forum = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(fid) - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - viewMap, err = panelAnalyticsRowsToViewMap(rows, labelList, viewMap) - if err != nil { - return common.InternalError(err, w, r) - } - - var viewList []int64 - for _, value := range revLabelList { - viewList = append(viewList, viewMap[value]) - } - graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} - common.DebugLogf("graph: %+v\n", graph) - - forum, err := common.Forums.Get(fid) - if err != nil { - return common.InternalError(err, w, r) - } - - pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", sfid, forum.Name, graph, timeRange.Range} - return panelRenderTemplate("panel_analytics_forum_views", w, r, user, &pi) -} - -func routePanelAnalyticsSystemViews(w http.ResponseWriter, r *http.Request, user common.User, system string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.AddSheet("chartist/chartist.min.css") - headerVars.AddScript("chartist/chartist.min.js") - headerVars.AddScript("analytics.js") - - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) - system = html.EscapeString(system) - - common.DebugLog("in routePanelAnalyticsSystemViews") - acc := qgen.Builder.Accumulator() - // TODO: Verify the OS name is valid - rows, err := acc.Select("viewchunks_systems").Columns("count, createdAt").Where("system = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(system) - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - viewMap, err = panelAnalyticsRowsToViewMap(rows, labelList, viewMap) - if err != nil { - return common.InternalError(err, w, r) - } - - var viewList []int64 - for _, value := range revLabelList { - viewList = append(viewList, viewMap[value]) - } - graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} - common.DebugLogf("graph: %+v\n", graph) - - friendlySystem, ok := common.GetOSPhrase(system) - if !ok { - friendlySystem = system - } - - pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", system, friendlySystem, graph, timeRange.Range} - return panelRenderTemplate("panel_analytics_system_views", w, r, user, &pi) -} - -func routePanelAnalyticsLanguageViews(w http.ResponseWriter, r *http.Request, user common.User, lang string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.AddSheet("chartist/chartist.min.css") - headerVars.AddScript("chartist/chartist.min.js") - headerVars.AddScript("analytics.js") - - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) - lang = html.EscapeString(lang) - - common.DebugLog("in routePanelAnalyticsLanguageViews") - acc := qgen.Builder.Accumulator() - // TODO: Verify the language code is valid - rows, err := acc.Select("viewchunks_langs").Columns("count, createdAt").Where("lang = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(lang) - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - viewMap, err = panelAnalyticsRowsToViewMap(rows, labelList, viewMap) - if err != nil { - return common.InternalError(err, w, r) - } - - var viewList []int64 - for _, value := range revLabelList { - viewList = append(viewList, viewMap[value]) - } - graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} - common.DebugLogf("graph: %+v\n", graph) - - friendlyLang, ok := common.GetHumanLangPhrase(lang) - if !ok { - friendlyLang = lang - } - - pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", lang, friendlyLang, graph, timeRange.Range} - return panelRenderTemplate("panel_analytics_lang_views", w, r, user, &pi) -} - -func routePanelAnalyticsReferrerViews(w http.ResponseWriter, r *http.Request, user common.User, domain string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.AddSheet("chartist/chartist.min.css") - headerVars.AddScript("chartist/chartist.min.js") - headerVars.AddScript("analytics.js") - - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) - - common.DebugLog("in routePanelAnalyticsReferrerViews") - acc := qgen.Builder.Accumulator() - // TODO: Verify the agent is valid - rows, err := acc.Select("viewchunks_referrers").Columns("count, createdAt").Where("domain = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(domain) - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - viewMap, err = panelAnalyticsRowsToViewMap(rows, labelList, viewMap) - if err != nil { - return common.InternalError(err, w, r) - } - - var viewList []int64 - for _, value := range revLabelList { - viewList = append(viewList, viewMap[value]) - } - graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} - common.DebugLogf("graph: %+v\n", graph) - - pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", html.EscapeString(domain), "", graph, timeRange.Range} - return panelRenderTemplate("panel_analytics_referrer_views", w, r, user, &pi) -} - -func routePanelAnalyticsTopics(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.AddSheet("chartist/chartist.min.css") - headerVars.AddScript("chartist/chartist.min.js") - headerVars.AddScript("analytics.js") - - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) - - common.DebugLog("in routePanelAnalyticsTopics") - acc := qgen.Builder.Accumulator() - rows, err := acc.Select("topicchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - viewMap, err = panelAnalyticsRowsToViewMap(rows, labelList, viewMap) - if err != nil { - return common.InternalError(err, w, r) - } - - var viewList []int64 - var viewItems []common.PanelAnalyticsItem - for _, value := range revLabelList { - viewList = append(viewList, viewMap[value]) - viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) - } - graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} - common.DebugLogf("graph: %+v\n", graph) - - pi := common.PanelAnalyticsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", graph, viewItems, timeRange.Range} - return panelRenderTemplate("panel_analytics_topics", w, r, user, &pi) -} - -func routePanelAnalyticsPosts(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.AddSheet("chartist/chartist.min.css") - headerVars.AddScript("chartist/chartist.min.js") - headerVars.AddScript("analytics.js") - - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange) - - common.DebugLog("in routePanelAnalyticsPosts") - acc := qgen.Builder.Accumulator() - rows, err := acc.Select("postchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - viewMap, err = panelAnalyticsRowsToViewMap(rows, labelList, viewMap) - if err != nil { - return common.InternalError(err, w, r) - } - - var viewList []int64 - var viewItems []common.PanelAnalyticsItem - for _, value := range revLabelList { - viewList = append(viewList, viewMap[value]) - viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) - } - graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} - common.DebugLogf("graph: %+v\n", graph) - - pi := common.PanelAnalyticsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", graph, viewItems, timeRange.Range} - return panelRenderTemplate("panel_analytics_posts", w, r, user, &pi) -} - -func panelAnalyticsRowsToNameMap(rows *sql.Rows) (map[string]int, error) { - nameMap := make(map[string]int) - defer rows.Close() - for rows.Next() { - var count int - var name string - err := rows.Scan(&count, &name) - if err != nil { - return nameMap, err - } - - // TODO: Bulk log this - if common.Dev.SuperDebug { - log.Print("count: ", count) - log.Print("name: ", name) - } - nameMap[name] += count - } - return nameMap, rows.Err() -} - -func routePanelAnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - - acc := qgen.Builder.Accumulator() - rows, err := acc.Select("viewchunks_forums").Columns("count, forum").Where("forum != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - forumMap, err := panelAnalyticsRowsToNameMap(rows) - if err != nil { - return common.InternalError(err, w, r) - } - - // TODO: Sort this slice - var forumItems []common.PanelAnalyticsAgentsItem - for sfid, count := range forumMap { - fid, err := strconv.Atoi(sfid) - if err != nil { - return common.InternalError(err, w, r) - } - forum, err := common.Forums.Get(fid) - if err != nil { - return common.InternalError(err, w, r) - } - forumItems = append(forumItems, common.PanelAnalyticsAgentsItem{ - Agent: sfid, - FriendlyAgent: forum.Name, - Count: count, - }) - } - - pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", forumItems, timeRange.Range} - return panelRenderTemplate("panel_analytics_forums", w, r, user, &pi) -} - -func routePanelAnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - - acc := qgen.Builder.Accumulator() - rows, err := acc.Select("viewchunks").Columns("count, route").Where("route != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - routeMap, err := panelAnalyticsRowsToNameMap(rows) - if err != nil { - return common.InternalError(err, w, r) - } - - // TODO: Sort this slice - var routeItems []common.PanelAnalyticsRoutesItem - for route, count := range routeMap { - routeItems = append(routeItems, common.PanelAnalyticsRoutesItem{ - Route: route, - Count: count, - }) - } - - pi := common.PanelAnalyticsRoutesPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", routeItems, timeRange.Range} - return panelRenderTemplate("panel_analytics_routes", w, r, user, &pi) -} - -func routePanelAnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - - acc := qgen.Builder.Accumulator() - rows, err := acc.Select("viewchunks_agents").Columns("count, browser").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - agentMap, err := panelAnalyticsRowsToNameMap(rows) - if err != nil { - return common.InternalError(err, w, r) - } - - // TODO: Sort this slice - var agentItems []common.PanelAnalyticsAgentsItem - for agent, count := range agentMap { - aAgent, ok := common.GetUserAgentPhrase(agent) - if !ok { - aAgent = agent - } - agentItems = append(agentItems, common.PanelAnalyticsAgentsItem{ - Agent: agent, - FriendlyAgent: aAgent, - Count: count, - }) - } - - pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", agentItems, timeRange.Range} - return panelRenderTemplate("panel_analytics_agents", w, r, user, &pi) -} - -func routePanelAnalyticsSystems(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - - acc := qgen.Builder.Accumulator() - rows, err := acc.Select("viewchunks_systems").Columns("count, system").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - osMap, err := panelAnalyticsRowsToNameMap(rows) - if err != nil { - return common.InternalError(err, w, r) - } - - // TODO: Sort this slice - var systemItems []common.PanelAnalyticsAgentsItem - for system, count := range osMap { - sSystem, ok := common.GetOSPhrase(system) - if !ok { - sSystem = system - } - systemItems = append(systemItems, common.PanelAnalyticsAgentsItem{ - Agent: system, - FriendlyAgent: sSystem, - Count: count, - }) - } - - pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", systemItems, timeRange.Range} - return panelRenderTemplate("panel_analytics_systems", w, r, user, &pi) -} - -func routePanelAnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - - acc := qgen.Builder.Accumulator() - rows, err := acc.Select("viewchunks_langs").Columns("count, lang").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - langMap, err := panelAnalyticsRowsToNameMap(rows) - if err != nil { - return common.InternalError(err, w, r) - } - - // TODO: Can we de-duplicate these analytics functions further? - // TODO: Sort this slice - var langItems []common.PanelAnalyticsAgentsItem - for lang, count := range langMap { - lLang, ok := common.GetHumanLangPhrase(lang) - if !ok { - lLang = lang - } - langItems = append(langItems, common.PanelAnalyticsAgentsItem{ - Agent: lang, - FriendlyAgent: lLang, - Count: count, - }) - } - - pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", langItems, timeRange.Range} - return panelRenderTemplate("panel_analytics_langs", w, r, user, &pi) -} - -func routePanelAnalyticsReferrers(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - - acc := qgen.Builder.Accumulator() - rows, err := acc.Select("viewchunks_referrers").Columns("count, domain").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - - refMap, err := panelAnalyticsRowsToNameMap(rows) - if err != nil { - return common.InternalError(err, w, r) - } - - // TODO: Sort this slice - var refItems []common.PanelAnalyticsAgentsItem - for domain, count := range refMap { - refItems = append(refItems, common.PanelAnalyticsAgentsItem{ - Agent: html.EscapeString(domain), - Count: count, - }) - } - - pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", refItems, timeRange.Range} - return panelRenderTemplate("panel_analytics_referrers", w, r, user, &pi) -} - -func routePanelSettings(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.EditSettings { - return common.NoPermissions(w, r, user) - } - var settingList = make(map[string]interface{}) - - settings, err := headerVars.Settings.BypassGetAll() - if err != nil { - return common.InternalError(err, w, r) - } - - // nolint need the type so people viewing this file understand what it returns without visiting phrases.go - var settingLabels map[string]string = common.GetAllSettingLabels() - for _, setting := range settings { - if setting.Type == "list" { - llist := settingLabels[setting.Name] - labels := strings.Split(llist, ",") - conv, err := strconv.Atoi(setting.Content) - if err != nil { - return common.LocalError("The setting '"+setting.Name+"' can't be converted to an integer", w, r, user) - } - setting.Content = labels[conv-1] - } else if setting.Type == "bool" { - if setting.Content == "1" { - setting.Content = "Yes" - } else { - setting.Content = "No" - } - } - settingList[setting.Name] = setting.Content - } - - pi := common.PanelPage{common.GetTitlePhrase("panel_settings"), user, headerVars, stats, "settings", tList, settingList} - return panelRenderTemplate("panel_settings", w, r, user, &pi) -} - -func routePanelSettingEdit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.EditSettings { - return common.NoPermissions(w, r, user) - } - - setting, err := headerVars.Settings.BypassGet(sname) - if err == ErrNoRows { - return common.LocalError("The setting you want to edit doesn't exist.", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - - var itemList []interface{} - if setting.Type == "list" { - llist := common.GetSettingLabel(setting.Name) - conv, err := strconv.Atoi(setting.Content) - if err != nil { - return common.LocalError("The value of this setting couldn't be converted to an integer", w, r, user) - } - - for index, label := range strings.Split(llist, ",") { - itemList = append(itemList, common.OptionLabel{ - Label: label, - Value: index + 1, - Selected: conv == (index + 1), - }) - } - } - - pi := common.PanelPage{common.GetTitlePhrase("panel_edit_setting"), user, headerVars, stats, "settings", itemList, setting} - return panelRenderTemplate("panel_setting", w, r, user, &pi) -} - -func routePanelSettingEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError { - headerLite, ferr := common.SimplePanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if !user.Perms.EditSettings { - return common.NoPermissions(w, r, user) - } - - scontent := r.PostFormValue("setting-value") - err := headerLite.Settings.Update(sname, scontent) - if err != nil { - if common.SafeSettingError(err) { - return common.LocalError(err.Error(), w, r, user) - } - return common.InternalError(err, w, r) - } - - http.Redirect(w, r, "/panel/settings/", http.StatusSeeOther) - return nil -} - func routePanelWordFilters(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) if ferr != nil { @@ -1714,47 +471,23 @@ func routePanelPluginsInstall(w http.ResponseWriter, r *http.Request, user commo } func routePanelUsers(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + header, stats, ferr := common.PanelUserCheck(w, r, &user) if ferr != nil { return ferr } + header.Title = common.GetTitlePhrase("panel_users") page, _ := strconv.Atoi(r.FormValue("page")) perPage := 10 offset, page, lastPage := common.PageOffset(stats.Users, page, perPage) - var userList []common.User - // TODO: Move this into the common.UserStore - rows, err := stmts.getUsersOffset.Query(offset, perPage) - if err != nil { - return common.InternalError(err, w, r) - } - defer rows.Close() - - // TODO: Add a common.UserStore method for iterating over global users and global user offsets - for rows.Next() { - puser := &common.User{ID: 0} - err := rows.Scan(&puser.ID, &puser.Name, &puser.Group, &puser.Active, &puser.IsSuperAdmin, &puser.Avatar) - if err != nil { - return common.InternalError(err, w, r) - } - - puser.InitPerms() - puser.Avatar = common.BuildAvatar(puser.ID, puser.Avatar) - if common.Groups.DirtyGet(puser.Group).Tag != "" { - puser.Tag = common.Groups.DirtyGet(puser.Group).Tag - } else { - puser.Tag = "" - } - userList = append(userList, *puser) - } - err = rows.Err() + users, err := common.Users.GetOffset(offset, perPage) if err != nil { return common.InternalError(err, w, r) } pageList := common.Paginate(stats.Users, perPage, 5) - pi := common.PanelUserPage{common.GetTitlePhrase("panel_users"), user, headerVars, stats, "users", userList, common.Paginator{pageList, page, lastPage}} + pi := common.PanelUserPage{header, stats, "users", users, common.Paginator{pageList, page, lastPage}} return panelRenderTemplate("panel_users", w, r, user, &pi) } @@ -2657,221 +1390,3 @@ func routePanelThemesMenuItemOrderSubmit(w http.ResponseWriter, r *http.Request, return panelSuccessRedirect("/panel/themes/menus/edit/"+strconv.Itoa(mid), w, r, isJs) } - -func routePanelBackups(w http.ResponseWriter, r *http.Request, user common.User, backupURL string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - - if backupURL != "" { - // We don't want them trying to break out of this directory, it shouldn't hurt since it's a super admin, but it's always good to practice good security hygiene, especially if this is one of many instances on a managed server not controlled by the superadmin/s - backupURL = common.Stripslashes(backupURL) - - var ext = filepath.Ext("./backups/" + backupURL) - if ext == ".sql" { - info, err := os.Stat("./backups/" + backupURL) - if err != nil { - return common.NotFound(w, r, headerVars) - } - // TODO: Change the served filename to gosora_backup_%timestamp%.sql, the time the file was generated, not when it was modified aka what the name of it should be - w.Header().Set("Content-Disposition", "attachment; filename=gosora_backup.sql") - w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) - // TODO: Fix the problem where non-existent files aren't greeted with custom 404s on ServeFile()'s side - http.ServeFile(w, r, "./backups/"+backupURL) - return nil - } - return common.NotFound(w, r, headerVars) - } - - var backupList []common.BackupItem - backupFiles, err := ioutil.ReadDir("./backups") - if err != nil { - return common.InternalError(err, w, r) - } - for _, backupFile := range backupFiles { - var ext = filepath.Ext(backupFile.Name()) - if ext != ".sql" { - continue - } - backupList = append(backupList, common.BackupItem{backupFile.Name(), backupFile.ModTime()}) - } - - pi := common.PanelBackupPage{common.GetTitlePhrase("panel_backups"), user, headerVars, stats, "backups", backupList} - return panelRenderTemplate("panel_backups", w, r, user, &pi) -} - -func routePanelLogsRegs(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - - logCount := common.RegLogs.GlobalCount() - page, _ := strconv.Atoi(r.FormValue("page")) - perPage := 10 - offset, page, lastPage := common.PageOffset(logCount, page, perPage) - - logs, err := common.RegLogs.GetOffset(offset, perPage) - if err != nil { - return common.InternalError(err, w, r) - } - var llist = make([]common.PageRegLogItem, len(logs)) - for index, log := range logs { - llist[index] = common.PageRegLogItem{log, strings.Replace(strings.TrimSuffix(log.FailureReason,"|"), "|", " | ", -1)} - } - - pageList := common.Paginate(logCount, perPage, 5) - pi := common.PanelRegLogsPage{common.GetTitlePhrase("panel_registration_logs"), user, headerVars, stats, "logs", llist, common.Paginator{pageList, page, lastPage}} - return panelRenderTemplate("panel_reglogs", w, r, user, &pi) -} - -// TODO: Log errors when something really screwy is going on? -func handleUnknownUser(user *common.User, err error) *common.User { - if err != nil { - return &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)} - } - return user -} -func handleUnknownTopic(topic *common.Topic, err error) *common.Topic { - if err != nil { - return &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)} - } - return topic -} - -// TODO: Move the log building logic into /common/ and it's own abstraction -func topicElementTypeAction(action string, elementType string, elementID int, actor *common.User, topic *common.Topic) (out string) { - if action == "delete" { - return fmt.Sprintf("Topic #%d was deleted by %s", elementID, actor.Link, actor.Name) - } - switch action { - case "lock": - out = "%s was locked by %s" - case "unlock": - out = "%s was reopened by %s" - case "stick": - out = "%s was pinned by %s" - case "unstick": - out = "%s was unpinned by %s" - case "move": - out = "%s was moved by %s" // TODO: Add where it was moved to, we'll have to change the source data for that, most likely? Investigate that and try to work this in - default: - return fmt.Sprintf("Unknown action '%s' on elementType '%s' by %s", action, elementType, actor.Link, actor.Name) - } - return fmt.Sprintf(out, topic.Link, topic.Title, actor.Link, actor.Name) -} - -func modlogsElementType(action string, elementType string, elementID int, actor *common.User) (out string) { - switch elementType { - case "topic": - topic := handleUnknownTopic(common.Topics.Get(elementID)) - out = topicElementTypeAction(action, elementType, elementID, actor, topic) - case "user": - targetUser := handleUnknownUser(common.Users.Get(elementID)) - switch action { - case "ban": - out = "%s was banned by %s" - case "unban": - out = "%s was unbanned by %s" - case "activate": - out = "%s was activated by %s" - } - out = fmt.Sprintf(out, targetUser.Link, targetUser.Name, actor.Link, actor.Name) - case "reply": - if action == "delete" { - topic := handleUnknownTopic(common.TopicByReplyID(elementID)) - out = fmt.Sprintf("A reply in %s was deleted by %s", topic.Link, topic.Title, actor.Link, actor.Name) - } - } - - if out == "" { - out = fmt.Sprintf("Unknown action '%s' on elementType '%s' by %s", action, elementType, actor.Link, actor.Name) - } - return out -} - -func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - - logCount := common.ModLogs.GlobalCount() - page, _ := strconv.Atoi(r.FormValue("page")) - perPage := 10 - offset, page, lastPage := common.PageOffset(logCount, page, perPage) - - logs, err := common.ModLogs.GetOffset(offset, perPage) - if err != nil { - return common.InternalError(err, w, r) - } - var llist = make([]common.PageLogItem, len(logs)) - for index, log := range logs { - actor := handleUnknownUser(common.Users.Get(log.ActorID)) - action := modlogsElementType(log.Action, log.ElementType, log.ElementID, actor) - llist[index] = common.PageLogItem{Action: template.HTML(action), IPAddress: log.IPAddress, DoneAt: log.DoneAt} - } - - pageList := common.Paginate(logCount, perPage, 5) - pi := common.PanelLogsPage{common.GetTitlePhrase("panel_mod_logs"), user, headerVars, stats, "logs", llist, common.Paginator{pageList, page, lastPage}} - return panelRenderTemplate("panel_modlogs", w, r, user, &pi) -} - -func routePanelLogsAdmin(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - - logCount := common.ModLogs.GlobalCount() - page, _ := strconv.Atoi(r.FormValue("page")) - perPage := 10 - offset, page, lastPage := common.PageOffset(logCount, page, perPage) - - logs, err := common.AdminLogs.GetOffset(offset, perPage) - if err != nil { - return common.InternalError(err, w, r) - } - var llist = make([]common.PageLogItem, len(logs)) - for index, log := range logs { - actor := handleUnknownUser(common.Users.Get(log.ActorID)) - action := modlogsElementType(log.Action, log.ElementType, log.ElementID, actor) - llist[index] = common.PageLogItem{Action: template.HTML(action), IPAddress: log.IPAddress, DoneAt: log.DoneAt} - } - - pageList := common.Paginate(logCount, perPage, 5) - pi := common.PanelLogsPage{common.GetTitlePhrase("panel_admin_logs"), user, headerVars, stats, "logs", llist, common.Paginator{pageList, page, lastPage}} - return panelRenderTemplate("panel_adminlogs", w, r, user, &pi) -} - -func routePanelDebug(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - - goVersion := runtime.Version() - dbVersion := qgen.Builder.DbVersion() - var uptime string - upDuration := time.Since(startTime) - hours := int(upDuration.Hours()) - minutes := int(upDuration.Minutes()) - if hours > 24 { - days := hours / 24 - hours -= days * 24 - uptime += strconv.Itoa(days) + "d" - uptime += strconv.Itoa(hours) + "h" - } else if hours >= 1 { - uptime += strconv.Itoa(hours) + "h" - } - uptime += strconv.Itoa(minutes) + "m" - - dbStats := db.Stats() - openConnCount := dbStats.OpenConnections - // Disk I/O? - // TODO: Fetch the adapter from Builder rather than getting it from a global? - - pi := common.PanelDebugPage{common.GetTitlePhrase("panel_debug"), user, headerVars, stats, "debug", goVersion, dbVersion, uptime, openConnCount, dbAdapter} - return panelRenderTemplate("panel_debug", w, r, user, &pi) -} diff --git a/patcher/patches.go b/patcher/patches.go index e90419fc..d686a7e0 100644 --- a/patcher/patches.go +++ b/patcher/patches.go @@ -12,6 +12,7 @@ func init() { addPatch(1, patch1) addPatch(2, patch2) addPatch(3, patch3) + addPatch(4, patch4) } func patch0(scanner *bufio.Scanner) (err error) { @@ -235,3 +236,214 @@ func patch3(scanner *bufio.Scanner) error { return nil } + +func patch4(scanner *bufio.Scanner) error { + // ! Don't reuse this function blindly, it doesn't escape apostrophes + var replaceTextWhere = func(replaceThis string, withThis string) error { + return execStmt(qgen.Builder.SimpleUpdate("viewchunks", "route = '"+withThis+"'", "route = '"+replaceThis+"'")) + } + + err := replaceTextWhere("routeReportSubmit", "routes.ReportSubmit") + if err != nil { + return err + } + + err = replaceTextWhere("routeAccountEditEmail", "routes.AccountEditEmail") + if err != nil { + return err + } + + err = replaceTextWhere("routeAccountEditEmailTokenSubmit", "routes.AccountEditEmailTokenSubmit") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelLogsRegs", "panel.LogsRegs") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelLogsMod", "panel.LogsMod") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelLogsAdmin", "panel.LogsAdmin") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelDebug", "panel.Debug") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsViews", "panel.AnalyticsViews") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsRouteViews", "panel.AnalyticsRouteViews") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsRouteViews", "panel.AnalyticsRouteViews") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsAgentViews", "panel.AnalyticsAgentViews") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsForumViews", "panel.AnalyticsForumViews") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsSystemViews", "panel.AnalyticsSystemViews") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsLanguageViews", "panel.AnalyticsLanguageViews") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsReferrerViews", "panel.AnalyticsReferrerViews") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsTopics", "panel.AnalyticsTopics") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsPosts", "panel.AnalyticsPosts") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsForums", "panel.AnalyticsForums") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsRoutes", "panel.AnalyticsRoutes") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsAgents", "panel.AnalyticsAgents") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsSystems", "panel.AnalyticsSystems") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsLanguages", "panel.AnalyticsLanguages") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelAnalyticsReferrers", "panel.AnalyticsReferrers") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelSettings", "panel.Settings") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelSettingEdit", "panel.SettingEdit") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelSettingEditSubmit", "panel.SettingEditSubmit") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelForums", "panel.Forums") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelForumsCreateSubmit", "panel.ForumsCreateSubmit") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelForumsDelete", "panel.ForumsDelete") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelForumsDeleteSubmit", "panel.ForumsDeleteSubmit") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelForumsEdit", "panel.ForumsEdit") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelForumsEditSubmit", "panel.ForumsEditSubmit") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelForumsEditPermsSubmit", "panel.ForumsEditPermsSubmit") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelForumsEditPermsAdvance", "panel.ForumsEditPermsAdvance") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelForumsEditPermsAdvanceSubmit", "panel.ForumsEditPermsAdvanceSubmit") + if err != nil { + return err + } + + err = replaceTextWhere("routePanelBackups", "panel.Backups") + if err != nil { + return err + } + + err = execStmt(qgen.Builder.SimpleDelete("settings", "name='url_tags'")) + if err != nil { + return err + } + + err = execStmt(qgen.Builder.CreateTable("pages", "utf8mb4", "utf8mb4_general_ci", + []qgen.DBTableColumn{ + qgen.DBTableColumn{"pid", "int", 0, false, true, ""}, + qgen.DBTableColumn{"name", "varchar", 200, false, false, ""}, + qgen.DBTableColumn{"title", "varchar", 200, false, false, ""}, + qgen.DBTableColumn{"body", "text", 0, false, false, ""}, + qgen.DBTableColumn{"allowedGroups", "text", 0, false, false, ""}, + qgen.DBTableColumn{"menuID", "int", 0, false, false, "-1"}, + }, + []qgen.DBTableKey{ + qgen.DBTableKey{"pid", "primary"}, + }, + )) + if err != nil { + return err + } + + return nil +} diff --git a/public/global.js b/public/global.js index d354f862..57d4b90b 100644 --- a/public/global.js +++ b/public/global.js @@ -547,9 +547,10 @@ $(document).ready(function(){ uploadFiles.addEventListener("change", uploadFileHandler, false); } - $(".moderate_link").click(function(event) { + $(".moderate_link").click((event) => { event.preventDefault(); $(".pre_opt").removeClass("auto_hide"); + $(".moderate_link").addClass("moderate_open"); $(".topic_row").each(function(){ $(this).click(function(){ selectedTopics.push(parseInt($(this).attr("data-tid"),10)); diff --git a/query_gen/lib/builder.go b/query_gen/lib/builder.go index b8790908..f3511025 100644 --- a/query_gen/lib/builder.go +++ b/query_gen/lib/builder.go @@ -104,6 +104,10 @@ func (build *builder) CreateTable(table string, charset string, collation string return build.prepare(build.adapter.CreateTable("_builder", table, charset, collation, columns, keys)) } +func (build *builder) AddColumn(table string, column DBTableColumn) (stmt *sql.Stmt, err error) { + return build.prepare(build.adapter.AddColumn("_builder", table, column)) +} + func (build *builder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) { return build.prepare(build.adapter.SimpleInsert("_builder", table, columns, fields)) } diff --git a/query_gen/lib/mssql.go b/query_gen/lib/mssql.go index 7951dd0d..1c689602 100644 --- a/query_gen/lib/mssql.go +++ b/query_gen/lib/mssql.go @@ -71,53 +71,7 @@ func (adapter *MssqlAdapter) CreateTable(name string, table string, charset stri var querystr = "CREATE TABLE [" + table + "] (" for _, column := range columns { - var max bool - var createdAt bool - switch column.Type { - case "createdAt": - column.Type = "datetime" - createdAt = true - case "varchar": - column.Type = "nvarchar" - case "text": - column.Type = "nvarchar" - max = true - case "json": - column.Type = "nvarchar" - max = true - case "boolean": - column.Type = "bit" - } - - var size string - if column.Size > 0 { - size = " (" + strconv.Itoa(column.Size) + ")" - } - if max { - size = " (MAX)" - } - - var end string - if column.Default != "" { - end = " DEFAULT " - if createdAt { - end += "GETUTCDATE()" // TODO: Use GETUTCDATE() in updates instead of the neutral format - } else if adapter.stringyType(column.Type) && column.Default != "''" { - end += "'" + column.Default + "'" - } else { - end += column.Default - } - } - - if !column.Null { - end += " not null" - } - - // ! Not exactly the meaning of auto increment... - if column.AutoIncrement { - end += " IDENTITY" - } - + column, size, end := adapter.parseColumn(column) querystr += "\n\t[" + column.Name + "] " + column.Type + size + end + "," } @@ -140,6 +94,67 @@ func (adapter *MssqlAdapter) CreateTable(name string, table string, charset stri return querystr, nil } +func (adapter *MssqlAdapter) parseColumn(column DBTableColumn) (col DBTableColumn, size string, end string) { + var max, createdAt bool + switch column.Type { + case "createdAt": + column.Type = "datetime" + createdAt = true + case "varchar": + column.Type = "nvarchar" + case "text": + column.Type = "nvarchar" + max = true + case "json": + column.Type = "nvarchar" + max = true + case "boolean": + column.Type = "bit" + } + + if column.Size > 0 { + size = " (" + strconv.Itoa(column.Size) + ")" + } + if max { + size = " (MAX)" + } + + if column.Default != "" { + end = " DEFAULT " + if createdAt { + end += "GETUTCDATE()" // TODO: Use GETUTCDATE() in updates instead of the neutral format + } else if adapter.stringyType(column.Type) && column.Default != "''" { + end += "'" + column.Default + "'" + } else { + end += column.Default + } + } + if !column.Null { + end += " not null" + } + + // ! Not exactly the meaning of auto increment... + if column.AutoIncrement { + end += " IDENTITY" + } + return column, size, end +} + +// TODO: Test this, not sure if some things work +func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + + column, size, end := adapter.parseColumn(column) + querystr := "ALTER TABLE [" + table + "] ADD [" + column.Name + "] " + column.Type + size + end + ";" + adapter.pushStatement(name, "add-column", querystr) + return querystr, nil +} + func (adapter *MssqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { if name == "" { return "", errors.New("You need a name for this statement") diff --git a/query_gen/lib/mysql.go b/query_gen/lib/mysql.go index 2d05b469..1cd43d33 100644 --- a/query_gen/lib/mysql.go +++ b/query_gen/lib/mysql.go @@ -86,39 +86,7 @@ func (adapter *MysqlAdapter) CreateTable(name string, table string, charset stri var querystr = "CREATE TABLE `" + table + "` (" for _, column := range columns { - // Make it easier to support Cassandra in the future - if column.Type == "createdAt" { - column.Type = "datetime" - } else if column.Type == "json" { - column.Type = "text" - } - - var size string - if column.Size > 0 { - size = "(" + strconv.Itoa(column.Size) + ")" - } - - var end string - // TODO: Exclude the other variants of text like mediumtext and longtext too - if column.Default != "" && column.Type != "text" { - end = " DEFAULT " - if adapter.stringyType(column.Type) && column.Default != "''" { - end += "'" + column.Default + "'" - } else { - end += column.Default - } - } - - if column.Null { - end += " null" - } else { - end += " not null" - } - - if column.AutoIncrement { - end += " AUTO_INCREMENT" - } - + column, size, end := adapter.parseColumn(column) querystr += "\n\t`" + column.Name + "` " + column.Type + size + end + "," } @@ -148,6 +116,54 @@ func (adapter *MysqlAdapter) CreateTable(name string, table string, charset stri return querystr + ";", nil } +func (adapter *MysqlAdapter) parseColumn(column DBTableColumn) (col DBTableColumn, size string, end string) { + // Make it easier to support Cassandra in the future + if column.Type == "createdAt" { + column.Type = "datetime" + } else if column.Type == "json" { + column.Type = "text" + } + if column.Size > 0 { + size = "(" + strconv.Itoa(column.Size) + ")" + } + + // TODO: Exclude the other variants of text like mediumtext and longtext too + if column.Default != "" && column.Type != "text" { + end = " DEFAULT " + if adapter.stringyType(column.Type) && column.Default != "''" { + end += "'" + column.Default + "'" + } else { + end += column.Default + } + } + + if column.Null { + end += " null" + } else { + end += " not null" + } + if column.AutoIncrement { + end += " AUTO_INCREMENT" + } + return column, size, end +} + +// TODO: Support AFTER column +// TODO: Test to make sure everything works here +func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + + column, size, end := adapter.parseColumn(column) + querystr := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end + ";" + adapter.pushStatement(name, "add-column", querystr) + return querystr, nil +} + func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { if name == "" { return "", errors.New("You need a name for this statement") diff --git a/query_gen/lib/pgsql.go b/query_gen/lib/pgsql.go index e0ff5fc1..7afe90a3 100644 --- a/query_gen/lib/pgsql.go +++ b/query_gen/lib/pgsql.go @@ -119,7 +119,7 @@ func (adapter *PgsqlAdapter) CreateTable(name string, table string, charset stri } // TODO: Implement this -func (adapter *PgsqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { +func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { if name == "" { return "", errors.New("You need a name for this statement") } @@ -129,6 +129,54 @@ func (adapter *PgsqlAdapter) SimpleInsert(name string, table string, columns str return "", nil } +// TODO: Test this +// ! We need to get the last ID out of this somehow, maybe add returning to every query? Might require some sort of wrapper over the sql statements +func (adapter *PgsqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + + var querystr = "INSERT INTO \"" + table + "\"(" + if columns != "" { + querystr += adapter.buildColumns(columns) + ") VALUES (" + for _, field := range processFields(fields) { + nameLen := len(field.Name) + if field.Name[0] == '"' && field.Name[nameLen-1] == '"' && nameLen >= 3 { + field.Name = "'" + field.Name[1:nameLen-1] + "'" + } + if field.Name[0] == '\'' && field.Name[nameLen-1] == '\'' && nameLen >= 3 { + field.Name = "'" + strings.Replace(field.Name[1:nameLen-1], "'", "''", -1) + "'" + } + querystr += field.Name + "," + } + querystr = querystr[0 : len(querystr)-1] + } else { + querystr += ") VALUES (" + } + querystr += ")" + + adapter.pushStatement(name, "insert", querystr) + return querystr, nil +} + +func (adapter *PgsqlAdapter) buildColumns(columns string) (querystr string) { + if columns == "" { + return "" + } + // Escape the column names, just in case we've used a reserved keyword + for _, column := range processColumns(columns) { + if column.Type == "function" { + querystr += column.Left + "," + } else { + querystr += "\"" + column.Left + "\"," + } + } + return querystr[0 : len(querystr)-1] +} + // TODO: Implement this func (adapter *PgsqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) { if name == "" { diff --git a/query_gen/lib/querygen.go b/query_gen/lib/querygen.go index 31aa9ebc..a0dcda3b 100644 --- a/query_gen/lib/querygen.go +++ b/query_gen/lib/querygen.go @@ -106,6 +106,9 @@ type Adapter interface { DropTable(name string, table string) (string, error) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) + // TODO: Some way to add indices and keys + // TODO: Test this + AddColumn(name string, table string, column DBTableColumn) (string, error) SimpleInsert(name string, table string, columns string, fields string) (string, error) SimpleUpdate(name string, table string, set string, where string) (string, error) SimpleDelete(name string, table string, where string) (string, error) diff --git a/query_gen/main.go b/query_gen/main.go index e8386b48..e0505a25 100644 --- a/query_gen/main.go +++ b/query_gen/main.go @@ -111,8 +111,6 @@ func writeStatements(adapter qgen.Adapter) error { func seedTables(adapter qgen.Adapter) error { qgen.Install.SimpleInsert("sync", "last_update", "UTC_TIMESTAMP()") - - qgen.Install.SimpleInsert("settings", "name, content, type", "'url_tags','1','bool'") qgen.Install.SimpleInsert("settings", "name, content, type, constraints", "'activation_type','1','list','1-3'") qgen.Install.SimpleInsert("settings", "name, content, type", "'bigpost_min_words','250','int'") qgen.Install.SimpleInsert("settings", "name, content, type", "'megapost_min_words','1000','int'") @@ -261,14 +259,8 @@ func writeSelects(adapter qgen.Adapter) error { //build.Select("isPluginInstalled").Table("plugins").Columns("installed").Where("uname = ?").Parse() - build.Select("getUsersOffset").Table("users").Columns("uid, name, group, active, is_super_admin, avatar").Orderby("uid ASC").Limit("?,?").Parse() - build.Select("isThemeDefault").Table("themes").Columns("default").Where("uname = ?").Parse() - build.Select("getEmailsByUser").Table("emails").Columns("email, validated, token").Where("uid = ?").Parse() - - build.Select("getTopicBasic").Table("topics").Columns("title, content").Where("tid = ?").Parse() // TODO: Comment this out and see if anything breaks - build.Select("forumEntryExists").Table("forums").Columns("fid").Where("name = ''").Orderby("fid ASC").Limit("0,1").Parse() build.Select("groupEntryExists").Table("users_groups").Columns("gid").Where("name = ''").Orderby("gid ASC").Limit("0,1").Parse() @@ -289,8 +281,6 @@ func writeInnerJoins(adapter qgen.Adapter) (err error) { func writeInserts(adapter qgen.Adapter) error { build := adapter.Builder() - build.Insert("createReport").Table("topics").Columns("title, content, parsed_content, createdAt, lastReplyAt, createdBy, lastReplyBy, data, parentID, css_class").Fields("?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,1,'report'").Parse() - build.Insert("addForumPermsToForum").Table("forums_permissions").Columns("gid,fid,preset,permissions").Fields("?,?,?,?").Parse() build.Insert("addPlugin").Table("plugins").Columns("uname, active, installed").Fields("?,?,?").Parse() @@ -319,8 +309,6 @@ func writeUpdates(adapter qgen.Adapter) error { build.Update("updateEmail").Table("emails").Set("email = ?, uid = ?, validated = ?, token = ?").Where("email = ?").Parse() - build.Update("verifyEmail").Table("emails").Set("validated = 1, token = ''").Where("email = ?").Parse() // Need to fix this: Empty string isn't working, it gets set to 1 instead x.x -- Has this been fixed? - build.Update("setTempGroup").Table("users").Set("temp_group = ?").Where("uid = ?").Parse() build.Update("updateWordFilter").Table("word_filters").Set("find = ?, replacement = ?").Where("wfid = ?").Parse() @@ -344,8 +332,6 @@ func writeDeletes(adapter qgen.Adapter) error { } func writeSimpleCounts(adapter qgen.Adapter) error { - adapter.SimpleCount("reportExists", "topics", "data = ? AND data != '' AND parentID = 1", "") - return nil } diff --git a/query_gen/tables.go b/query_gen/tables.go index fa6cd78d..320431a8 100644 --- a/query_gen/tables.go +++ b/query_gen/tables.go @@ -412,6 +412,22 @@ func createTables(adapter qgen.Adapter) error { }, ) + qgen.Install.CreateTable("pages", "utf8mb4", "utf8mb4_general_ci", + []qgen.DBTableColumn{ + qgen.DBTableColumn{"pid", "int", 0, false, true, ""}, + //qgen.DBTableColumn{"path", "varchar", 200, false, false, ""}, + qgen.DBTableColumn{"name", "varchar", 200, false, false, ""}, + qgen.DBTableColumn{"title", "varchar", 200, false, false, ""}, + qgen.DBTableColumn{"body", "text", 0, false, false, ""}, + // TODO: Make this a table? + qgen.DBTableColumn{"allowedGroups", "text", 0, false, false, ""}, + qgen.DBTableColumn{"menuID", "int", 0, false, false, "-1"}, // simple sidebar menu + }, + []qgen.DBTableKey{ + qgen.DBTableKey{"pid", "primary"}, + }, + ) + qgen.Install.CreateTable("registration_logs", "", "", []qgen.DBTableColumn{ qgen.DBTableColumn{"rlid", "int", 0, false, true, ""}, diff --git a/router_gen/main.go b/router_gen/main.go index 6d0bd3aa..723e0c03 100644 --- a/router_gen/main.go +++ b/router_gen/main.go @@ -232,6 +232,7 @@ import ( "./common" "./common/counters" "./routes" + "./routes/panel" ) var ErrNoRoute = errors.New("That route doesn't exist.") diff --git a/router_gen/routes.go b/router_gen/routes.go index 8bc6ad63..aed77310 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -14,7 +14,7 @@ func routes() { // TODO: Reduce the number of Befores. With a new method, perhaps? reportGroup := newRouteGroup("/report/", - Action("routeReportSubmit", "/report/submit/", "extraData"), + Action("routes.ReportSubmit", "/report/submit/", "extraData"), ).Before("NoBanned") addRouteGroup(reportGroup) @@ -46,8 +46,8 @@ func buildUserRoutes() { UploadAction("routes.AccountEditAvatarSubmit", "/user/edit/avatar/submit/").MaxSizeVar("int(common.Config.MaxRequestSize)"), MemberView("routes.AccountEditUsername", "/user/edit/username/"), Action("routes.AccountEditUsernameSubmit", "/user/edit/username/submit/"), // TODO: Full test this - MemberView("routeAccountEditEmail", "/user/edit/email/"), - Action("routeAccountEditEmailTokenSubmit", "/user/edit/token/", "extraData"), + MemberView("routes.AccountEditEmail", "/user/edit/email/"), + Action("routes.AccountEditEmailTokenSubmit", "/user/edit/token/", "extraData"), ) addRouteGroup(userGroup) @@ -131,19 +131,19 @@ func buildPanelRoutes() { panelGroup := newRouteGroup("/panel/").Before("SuperModOnly") panelGroup.Routes( View("routePanelDashboard", "/panel/"), - View("routePanelForums", "/panel/forums/"), - Action("routePanelForumsCreateSubmit", "/panel/forums/create/"), - Action("routePanelForumsDelete", "/panel/forums/delete/", "extraData"), - Action("routePanelForumsDeleteSubmit", "/panel/forums/delete/submit/", "extraData"), - View("routePanelForumsEdit", "/panel/forums/edit/", "extraData"), - Action("routePanelForumsEditSubmit", "/panel/forums/edit/submit/", "extraData"), - Action("routePanelForumsEditPermsSubmit", "/panel/forums/edit/perms/submit/", "extraData"), - View("routePanelForumsEditPermsAdvance", "/panel/forums/edit/perms/", "extraData"), - Action("routePanelForumsEditPermsAdvanceSubmit", "/panel/forums/edit/perms/adv/submit/", "extraData"), + View("panel.Forums", "/panel/forums/"), + Action("panel.ForumsCreateSubmit", "/panel/forums/create/"), + Action("panel.ForumsDelete", "/panel/forums/delete/", "extraData"), + Action("panel.ForumsDeleteSubmit", "/panel/forums/delete/submit/", "extraData"), + View("panel.ForumsEdit", "/panel/forums/edit/", "extraData"), + Action("panel.ForumsEditSubmit", "/panel/forums/edit/submit/", "extraData"), + Action("panel.ForumsEditPermsSubmit", "/panel/forums/edit/perms/submit/", "extraData"), + View("panel.ForumsEditPermsAdvance", "/panel/forums/edit/perms/", "extraData"), + Action("panel.ForumsEditPermsAdvanceSubmit", "/panel/forums/edit/perms/adv/submit/", "extraData"), - View("routePanelSettings", "/panel/settings/"), - View("routePanelSettingEdit", "/panel/settings/edit/", "extraData"), - Action("routePanelSettingEditSubmit", "/panel/settings/edit/submit/", "extraData"), + View("panel.Settings", "/panel/settings/"), + View("panel.SettingEdit", "/panel/settings/edit/", "extraData"), + Action("panel.SettingEditSubmit", "/panel/settings/edit/submit/", "extraData"), View("routePanelWordFilters", "/panel/settings/word-filters/"), Action("routePanelWordFiltersCreateSubmit", "/panel/settings/word-filters/create/"), @@ -170,21 +170,21 @@ func buildPanelRoutes() { View("routePanelUsersEdit", "/panel/users/edit/", "extraData"), Action("routePanelUsersEditSubmit", "/panel/users/edit/submit/", "extraData"), - View("routePanelAnalyticsViews", "/panel/analytics/views/").Before("ParseForm"), - View("routePanelAnalyticsRoutes", "/panel/analytics/routes/").Before("ParseForm"), - View("routePanelAnalyticsAgents", "/panel/analytics/agents/").Before("ParseForm"), - View("routePanelAnalyticsSystems", "/panel/analytics/systems/").Before("ParseForm"), - View("routePanelAnalyticsLanguages", "/panel/analytics/langs/").Before("ParseForm"), - View("routePanelAnalyticsReferrers", "/panel/analytics/referrers/").Before("ParseForm"), - View("routePanelAnalyticsRouteViews", "/panel/analytics/route/", "extraData"), - View("routePanelAnalyticsAgentViews", "/panel/analytics/agent/", "extraData"), - View("routePanelAnalyticsForumViews", "/panel/analytics/forum/", "extraData"), - View("routePanelAnalyticsSystemViews", "/panel/analytics/system/", "extraData"), - View("routePanelAnalyticsLanguageViews", "/panel/analytics/lang/", "extraData"), - View("routePanelAnalyticsReferrerViews", "/panel/analytics/referrer/", "extraData"), - View("routePanelAnalyticsPosts", "/panel/analytics/posts/").Before("ParseForm"), - View("routePanelAnalyticsTopics", "/panel/analytics/topics/").Before("ParseForm"), - View("routePanelAnalyticsForums", "/panel/analytics/forums/").Before("ParseForm"), + View("panel.AnalyticsViews", "/panel/analytics/views/").Before("ParseForm"), + View("panel.AnalyticsRoutes", "/panel/analytics/routes/").Before("ParseForm"), + View("panel.AnalyticsAgents", "/panel/analytics/agents/").Before("ParseForm"), + View("panel.AnalyticsSystems", "/panel/analytics/systems/").Before("ParseForm"), + View("panel.AnalyticsLanguages", "/panel/analytics/langs/").Before("ParseForm"), + View("panel.AnalyticsReferrers", "/panel/analytics/referrers/").Before("ParseForm"), + View("panel.AnalyticsRouteViews", "/panel/analytics/route/", "extraData"), + View("panel.AnalyticsAgentViews", "/panel/analytics/agent/", "extraData"), + View("panel.AnalyticsForumViews", "/panel/analytics/forum/", "extraData"), + View("panel.AnalyticsSystemViews", "/panel/analytics/system/", "extraData"), + View("panel.AnalyticsLanguageViews", "/panel/analytics/lang/", "extraData"), + View("panel.AnalyticsReferrerViews", "/panel/analytics/referrer/", "extraData"), + View("panel.AnalyticsPosts", "/panel/analytics/posts/").Before("ParseForm"), + View("panel.AnalyticsTopics", "/panel/analytics/topics/").Before("ParseForm"), + View("panel.AnalyticsForums", "/panel/analytics/forums/").Before("ParseForm"), View("routePanelGroups", "/panel/groups/"), View("routePanelGroupsEdit", "/panel/groups/edit/", "extraData"), @@ -193,10 +193,10 @@ func buildPanelRoutes() { Action("routePanelGroupsEditPermsSubmit", "/panel/groups/edit/perms/submit/", "extraData"), Action("routePanelGroupsCreateSubmit", "/panel/groups/create/"), - View("routePanelBackups", "/panel/backups/", "extraData").Before("SuperAdminOnly"), // TODO: Test - View("routePanelLogsRegs", "/panel/logs/regs/"), - View("routePanelLogsMod", "/panel/logs/mod/"), - View("routePanelDebug", "/panel/debug/").Before("AdminOnly"), + View("panel.Backups", "/panel/backups/", "extraData").Before("SuperAdminOnly"), // TODO: Tests for this + View("panel.LogsRegs", "/panel/logs/regs/"), + View("panel.LogsMod", "/panel/logs/mod/"), + View("panel.Debug", "/panel/debug/").Before("AdminOnly"), ) addRouteGroup(panelGroup) } diff --git a/routes/account.go b/routes/account.go index 9127014c..4b8ed52f 100644 --- a/routes/account.go +++ b/routes/account.go @@ -402,3 +402,88 @@ func AccountEditUsernameSubmit(w http.ResponseWriter, r *http.Request, user comm http.Redirect(w, r, "/user/edit/username/?updated=1", http.StatusSeeOther) return nil } + +func AccountEditEmail(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, ferr := common.UserCheck(w, r, &user) + if ferr != nil { + return ferr + } + + emails, err := common.Emails.GetEmailsByUser(&user) + if err != nil { + return common.InternalError(err, w, r) + } + + // Was this site migrated from another forum software? Most of them don't have multiple emails for a single user. + // This also applies when the admin switches site.EnableEmails on after having it off for a while. + if len(emails) == 0 { + email := common.Email{UserID: user.ID} + email.Email = user.Email + email.Validated = false + email.Primary = true + emails = append(emails, email) + } + + if !common.Site.EnableEmails { + headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("account_mail_disabled")) + } + if r.FormValue("verified") == "1" { + headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("account_mail_verify_success")) + } + + pi := common.EmailListPage{"Email Manager", user, headerVars, emails, nil} + if common.RunPreRenderHook("pre_render_account_own_edit_email", w, r, &user, &pi) { + return nil + } + err = common.Templates.ExecuteTemplate(w, "account_own_edit_email.html", pi) + if err != nil { + return common.InternalError(err, w, r) + } + return nil +} + +// TODO: Do a session check on this? +func AccountEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, user common.User, token string) common.RouteError { + headerVars, ferr := common.UserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !common.Site.EnableEmails { + http.Redirect(w, r, "/user/edit/email/", http.StatusSeeOther) + return nil + } + + targetEmail := common.Email{UserID: user.ID} + emails, err := common.Emails.GetEmailsByUser(&user) + if err != nil { + return common.InternalError(err, w, r) + } + for _, email := range emails { + if email.Token == token { + targetEmail = email + } + } + + if len(emails) == 0 { + return common.LocalError("A verification email was never sent for you!", w, r, user) + } + if targetEmail.Token == "" { + return common.LocalError("That's not a valid token!", w, r, user) + } + + err = common.Emails.VerifyEmail(user.Email) + if err != nil { + return common.InternalError(err, w, r) + } + + // If Email Activation is on, then activate the account while we're here + if headerVars.Settings["activation_type"] == 2 { + err = user.Activate() + if err != nil { + return common.InternalError(err, w, r) + } + } + http.Redirect(w, r, "/user/edit/email/?verified=1", http.StatusSeeOther) + + return nil +} diff --git a/routes/common.go b/routes/common.go new file mode 100644 index 00000000..5e18e505 --- /dev/null +++ b/routes/common.go @@ -0,0 +1,3 @@ +package routes + +var successJSONBytes = []byte(`{"success":"1"}`) diff --git a/routes/forum.go b/routes/forum.go index 95321323..5777b87e 100644 --- a/routes/forum.go +++ b/routes/forum.go @@ -56,6 +56,7 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, sfid st } else if err != nil { return common.InternalError(err, w, r) } + header.Title = forum.Name // TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete offset, page, lastPage := common.PageOffset(forum.TopicCount, page, common.Config.ItemsPerPage) @@ -112,7 +113,7 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, sfid st } pageList := common.Paginate(forum.TopicCount, common.Config.ItemsPerPage, 5) - pi := common.ForumPage{forum.Name, user, header, topicList, forum, common.Paginator{pageList, page, lastPage}} + pi := common.ForumPage{header, topicList, forum, common.Paginator{pageList, page, lastPage}} if common.RunPreRenderHook("pre_render_forum", w, r, &user, &pi) { return nil } diff --git a/routes/panel/analytics.go b/routes/panel/analytics.go new file mode 100644 index 00000000..1d32d381 --- /dev/null +++ b/routes/panel/analytics.go @@ -0,0 +1,746 @@ +package panel + +import ( + "database/sql" + "errors" + "html" + "log" + "net/http" + "strconv" + "time" + + "../../common" + "../../query_gen/lib" +) + +// TODO: Move this to another file, probably common/pages.go +type AnalyticsTimeRange struct { + Quantity int + Unit string + Slices int + SliceWidth int + Range string +} + +func analyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err error) { + timeRange.Quantity = 6 + timeRange.Unit = "hour" + timeRange.Slices = 12 + timeRange.SliceWidth = 60 * 30 + timeRange.Range = "six-hours" + + switch rawTimeRange { + case "one-month": + timeRange.Quantity = 30 + timeRange.Unit = "day" + timeRange.Slices = 30 + timeRange.SliceWidth = 60 * 60 * 24 + timeRange.Range = "one-month" + case "one-week": + timeRange.Quantity = 7 + timeRange.Unit = "day" + timeRange.Slices = 14 + timeRange.SliceWidth = 60 * 60 * 12 + timeRange.Range = "one-week" + case "two-days": // Two days is experimental + timeRange.Quantity = 2 + timeRange.Unit = "day" + timeRange.Slices = 24 + timeRange.SliceWidth = 60 * 60 * 2 + timeRange.Range = "two-days" + case "one-day": + timeRange.Quantity = 1 + timeRange.Unit = "day" + timeRange.Slices = 24 + timeRange.SliceWidth = 60 * 60 + timeRange.Range = "one-day" + case "twelve-hours": + timeRange.Quantity = 12 + timeRange.Slices = 24 + timeRange.Range = "twelve-hours" + case "six-hours", "": + timeRange.Range = "six-hours" + default: + return timeRange, errors.New("Unknown time range") + } + return timeRange, nil +} + +func analyticsTimeRangeToLabelList(timeRange AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) { + viewMap = make(map[int64]int64) + var currentTime = time.Now().Unix() + for i := 1; i <= timeRange.Slices; i++ { + var label = currentTime - int64(i*timeRange.SliceWidth) + revLabelList = append(revLabelList, label) + viewMap[label] = 0 + } + for _, value := range revLabelList { + labelList = append(labelList, value) + } + return revLabelList, labelList, viewMap +} + +func analyticsRowsToViewMap(rows *sql.Rows, labelList []int64, viewMap map[int64]int64) (map[int64]int64, error) { + defer rows.Close() + for rows.Next() { + var count int64 + var createdAt time.Time + err := rows.Scan(&count, &createdAt) + if err != nil { + return viewMap, err + } + + var unixCreatedAt = createdAt.Unix() + // TODO: Bulk log this + if common.Dev.SuperDebug { + log.Print("count: ", count) + log.Print("createdAt: ", createdAt) + log.Print("unixCreatedAt: ", unixCreatedAt) + } + + for _, value := range labelList { + if unixCreatedAt > value { + viewMap[value] += count + break + } + } + } + return viewMap, rows.Err() +} + +func AnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.AddSheet("chartist/chartist.min.css") + headerVars.AddScript("chartist/chartist.min.js") + headerVars.AddScript("analytics.js") + + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) + + common.DebugLog("in panel.AnalyticsViews") + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("viewchunks").Columns("count, createdAt").Where("route = ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) + if err != nil { + return common.InternalError(err, w, r) + } + + var viewList []int64 + var viewItems []common.PanelAnalyticsItem + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + common.DebugLogf("graph: %+v\n", graph) + + pi := common.PanelAnalyticsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", graph, viewItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_views", w, r, user, &pi) +} + +func AnalyticsRouteViews(w http.ResponseWriter, r *http.Request, user common.User, route string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.AddSheet("chartist/chartist.min.css") + headerVars.AddScript("chartist/chartist.min.js") + headerVars.AddScript("analytics.js") + + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) + + common.DebugLog("in panel.AnalyticsRouteViews") + acc := qgen.Builder.Accumulator() + // TODO: Validate the route is valid + rows, err := acc.Select("viewchunks").Columns("count, createdAt").Where("route = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(route) + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) + if err != nil { + return common.InternalError(err, w, r) + } + + var viewList []int64 + var viewItems []common.PanelAnalyticsItem + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + common.DebugLogf("graph: %+v\n", graph) + + pi := common.PanelAnalyticsRoutePage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", html.EscapeString(route), graph, viewItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_route_views", w, r, user, &pi) +} + +func AnalyticsAgentViews(w http.ResponseWriter, r *http.Request, user common.User, agent string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.AddSheet("chartist/chartist.min.css") + headerVars.AddScript("chartist/chartist.min.js") + headerVars.AddScript("analytics.js") + + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) + + // ? Only allow valid agents? The problem with this is that agents wind up getting renamed and it would take a migration to get them all up to snuff + agent = html.EscapeString(agent) + + common.DebugLog("in panel.AnalyticsAgentViews") + acc := qgen.Builder.Accumulator() + // TODO: Verify the agent is valid + rows, err := acc.Select("viewchunks_agents").Columns("count, createdAt").Where("browser = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(agent) + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) + if err != nil { + return common.InternalError(err, w, r) + } + + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + common.DebugLogf("graph: %+v\n", graph) + + friendlyAgent, ok := common.GetUserAgentPhrase(agent) + if !ok { + friendlyAgent = agent + } + + pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", agent, friendlyAgent, graph, timeRange.Range} + return panelRenderTemplate("panel_analytics_agent_views", w, r, user, &pi) +} + +func AnalyticsForumViews(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.AddSheet("chartist/chartist.min.css") + headerVars.AddScript("chartist/chartist.min.js") + headerVars.AddScript("analytics.js") + + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) + + fid, err := strconv.Atoi(sfid) + if err != nil { + return common.LocalError("Invalid integer", w, r, user) + } + + common.DebugLog("in panel.AnalyticsForumViews") + acc := qgen.Builder.Accumulator() + // TODO: Verify the agent is valid + rows, err := acc.Select("viewchunks_forums").Columns("count, createdAt").Where("forum = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(fid) + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) + if err != nil { + return common.InternalError(err, w, r) + } + + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + common.DebugLogf("graph: %+v\n", graph) + + forum, err := common.Forums.Get(fid) + if err != nil { + return common.InternalError(err, w, r) + } + + pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", sfid, forum.Name, graph, timeRange.Range} + return panelRenderTemplate("panel_analytics_forum_views", w, r, user, &pi) +} + +func AnalyticsSystemViews(w http.ResponseWriter, r *http.Request, user common.User, system string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.AddSheet("chartist/chartist.min.css") + headerVars.AddScript("chartist/chartist.min.js") + headerVars.AddScript("analytics.js") + + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) + system = html.EscapeString(system) + + common.DebugLog("in panel.AnalyticsSystemViews") + acc := qgen.Builder.Accumulator() + // TODO: Verify the OS name is valid + rows, err := acc.Select("viewchunks_systems").Columns("count, createdAt").Where("system = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(system) + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) + if err != nil { + return common.InternalError(err, w, r) + } + + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + common.DebugLogf("graph: %+v\n", graph) + + friendlySystem, ok := common.GetOSPhrase(system) + if !ok { + friendlySystem = system + } + + pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", system, friendlySystem, graph, timeRange.Range} + return panelRenderTemplate("panel_analytics_system_views", w, r, user, &pi) +} + +func AnalyticsLanguageViews(w http.ResponseWriter, r *http.Request, user common.User, lang string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.AddSheet("chartist/chartist.min.css") + headerVars.AddScript("chartist/chartist.min.js") + headerVars.AddScript("analytics.js") + + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) + lang = html.EscapeString(lang) + + common.DebugLog("in panel.AnalyticsLanguageViews") + acc := qgen.Builder.Accumulator() + // TODO: Verify the language code is valid + rows, err := acc.Select("viewchunks_langs").Columns("count, createdAt").Where("lang = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(lang) + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) + if err != nil { + return common.InternalError(err, w, r) + } + + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + common.DebugLogf("graph: %+v\n", graph) + + friendlyLang, ok := common.GetHumanLangPhrase(lang) + if !ok { + friendlyLang = lang + } + + pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", lang, friendlyLang, graph, timeRange.Range} + return panelRenderTemplate("panel_analytics_lang_views", w, r, user, &pi) +} + +func AnalyticsReferrerViews(w http.ResponseWriter, r *http.Request, user common.User, domain string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.AddSheet("chartist/chartist.min.css") + headerVars.AddScript("chartist/chartist.min.js") + headerVars.AddScript("analytics.js") + + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) + + common.DebugLog("in panel.AnalyticsReferrerViews") + acc := qgen.Builder.Accumulator() + // TODO: Verify the agent is valid + rows, err := acc.Select("viewchunks_referrers").Columns("count, createdAt").Where("domain = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(domain) + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) + if err != nil { + return common.InternalError(err, w, r) + } + + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + common.DebugLogf("graph: %+v\n", graph) + + pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", html.EscapeString(domain), "", graph, timeRange.Range} + return panelRenderTemplate("panel_analytics_referrer_views", w, r, user, &pi) +} + +func AnalyticsTopics(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.AddSheet("chartist/chartist.min.css") + headerVars.AddScript("chartist/chartist.min.js") + headerVars.AddScript("analytics.js") + + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) + + common.DebugLog("in panel.AnalyticsTopics") + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("topicchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) + if err != nil { + return common.InternalError(err, w, r) + } + + var viewList []int64 + var viewItems []common.PanelAnalyticsItem + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + common.DebugLogf("graph: %+v\n", graph) + + pi := common.PanelAnalyticsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", graph, viewItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_topics", w, r, user, &pi) +} + +func AnalyticsPosts(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.AddSheet("chartist/chartist.min.css") + headerVars.AddScript("chartist/chartist.min.js") + headerVars.AddScript("analytics.js") + + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) + + common.DebugLog("in panel.AnalyticsPosts") + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("postchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) + if err != nil { + return common.InternalError(err, w, r) + } + + var viewList []int64 + var viewItems []common.PanelAnalyticsItem + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + common.DebugLogf("graph: %+v\n", graph) + + pi := common.PanelAnalyticsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", graph, viewItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_posts", w, r, user, &pi) +} + +func analyticsRowsToNameMap(rows *sql.Rows) (map[string]int, error) { + nameMap := make(map[string]int) + defer rows.Close() + for rows.Next() { + var count int + var name string + err := rows.Scan(&count, &name) + if err != nil { + return nameMap, err + } + + // TODO: Bulk log this + if common.Dev.SuperDebug { + log.Print("count: ", count) + log.Print("name: ", name) + } + nameMap[name] += count + } + return nameMap, rows.Err() +} + +func AnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("viewchunks_forums").Columns("count, forum").Where("forum != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + forumMap, err := analyticsRowsToNameMap(rows) + if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Sort this slice + var forumItems []common.PanelAnalyticsAgentsItem + for sfid, count := range forumMap { + fid, err := strconv.Atoi(sfid) + if err != nil { + return common.InternalError(err, w, r) + } + forum, err := common.Forums.Get(fid) + if err != nil { + return common.InternalError(err, w, r) + } + forumItems = append(forumItems, common.PanelAnalyticsAgentsItem{ + Agent: sfid, + FriendlyAgent: forum.Name, + Count: count, + }) + } + + pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", forumItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_forums", w, r, user, &pi) +} + +func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("viewchunks").Columns("count, route").Where("route != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + routeMap, err := analyticsRowsToNameMap(rows) + if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Sort this slice + var routeItems []common.PanelAnalyticsRoutesItem + for route, count := range routeMap { + routeItems = append(routeItems, common.PanelAnalyticsRoutesItem{ + Route: route, + Count: count, + }) + } + + pi := common.PanelAnalyticsRoutesPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", routeItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_routes", w, r, user, &pi) +} + +func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("viewchunks_agents").Columns("count, browser").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + agentMap, err := analyticsRowsToNameMap(rows) + if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Sort this slice + var agentItems []common.PanelAnalyticsAgentsItem + for agent, count := range agentMap { + aAgent, ok := common.GetUserAgentPhrase(agent) + if !ok { + aAgent = agent + } + agentItems = append(agentItems, common.PanelAnalyticsAgentsItem{ + Agent: agent, + FriendlyAgent: aAgent, + Count: count, + }) + } + + pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", agentItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_agents", w, r, user, &pi) +} + +func AnalyticsSystems(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("viewchunks_systems").Columns("count, system").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + osMap, err := analyticsRowsToNameMap(rows) + if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Sort this slice + var systemItems []common.PanelAnalyticsAgentsItem + for system, count := range osMap { + sSystem, ok := common.GetOSPhrase(system) + if !ok { + sSystem = system + } + systemItems = append(systemItems, common.PanelAnalyticsAgentsItem{ + Agent: system, + FriendlyAgent: sSystem, + Count: count, + }) + } + + pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", systemItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_systems", w, r, user, &pi) +} + +func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("viewchunks_langs").Columns("count, lang").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + langMap, err := analyticsRowsToNameMap(rows) + if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Can we de-duplicate these analytics functions further? + // TODO: Sort this slice + var langItems []common.PanelAnalyticsAgentsItem + for lang, count := range langMap { + lLang, ok := common.GetHumanLangPhrase(lang) + if !ok { + lLang = lang + } + langItems = append(langItems, common.PanelAnalyticsAgentsItem{ + Agent: lang, + FriendlyAgent: lLang, + Count: count, + }) + } + + pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", langItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_langs", w, r, user, &pi) +} + +func AnalyticsReferrers(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("viewchunks_referrers").Columns("count, domain").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + + refMap, err := analyticsRowsToNameMap(rows) + if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Sort this slice + var refItems []common.PanelAnalyticsAgentsItem + for domain, count := range refMap { + refItems = append(refItems, common.PanelAnalyticsAgentsItem{ + Agent: html.EscapeString(domain), + Count: count, + }) + } + + pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", refItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_referrers", w, r, user, &pi) +} diff --git a/routes/panel/backups.go b/routes/panel/backups.go new file mode 100644 index 00000000..7f4614bb --- /dev/null +++ b/routes/panel/backups.go @@ -0,0 +1,54 @@ +package panel + +import ( + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strconv" + + "../../common" +) + +func Backups(w http.ResponseWriter, r *http.Request, user common.User, backupURL string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + + if backupURL != "" { + // We don't want them trying to break out of this directory, it shouldn't hurt since it's a super admin, but it's always good to practice good security hygiene, especially if this is one of many instances on a managed server not controlled by the superadmin/s + backupURL = common.Stripslashes(backupURL) + + var ext = filepath.Ext("./backups/" + backupURL) + if ext == ".sql" { + info, err := os.Stat("./backups/" + backupURL) + if err != nil { + return common.NotFound(w, r, headerVars) + } + // TODO: Change the served filename to gosora_backup_%timestamp%.sql, the time the file was generated, not when it was modified aka what the name of it should be + w.Header().Set("Content-Disposition", "attachment; filename=gosora_backup.sql") + w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) + // TODO: Fix the problem where non-existent files aren't greeted with custom 404s on ServeFile()'s side + http.ServeFile(w, r, "./backups/"+backupURL) + return nil + } + return common.NotFound(w, r, headerVars) + } + + var backupList []common.BackupItem + backupFiles, err := ioutil.ReadDir("./backups") + if err != nil { + return common.InternalError(err, w, r) + } + for _, backupFile := range backupFiles { + var ext = filepath.Ext(backupFile.Name()) + if ext != ".sql" { + continue + } + backupList = append(backupList, common.BackupItem{backupFile.Name(), backupFile.ModTime()}) + } + + pi := common.PanelBackupPage{common.GetTitlePhrase("panel_backups"), user, headerVars, stats, "backups", backupList} + return panelRenderTemplate("panel_backups", w, r, user, &pi) +} diff --git a/routes/panel/common.go b/routes/panel/common.go new file mode 100644 index 00000000..5b3a8a06 --- /dev/null +++ b/routes/panel/common.go @@ -0,0 +1,31 @@ +package panel + +import ( + "net/http" + + "../../common" +) + +// A blank list to fill out that parameter in Page for routes which don't use it +var tList []interface{} +var successJSONBytes = []byte(`{"success":"1"}`) + +// We're trying to reduce the amount of boilerplate in here, so I added these two functions, they might wind up circulating outside this file in the future +func panelSuccessRedirect(dest string, w http.ResponseWriter, r *http.Request, isJs bool) common.RouteError { + if !isJs { + http.Redirect(w, r, dest, http.StatusSeeOther) + } else { + w.Write(successJSONBytes) + } + return nil +} +func panelRenderTemplate(tmplName string, w http.ResponseWriter, r *http.Request, user common.User, pi interface{}) common.RouteError { + if common.RunPreRenderHook("pre_render_"+tmplName, w, r, &user, pi) { + return nil + } + err := common.Templates.ExecuteTemplate(w, tmplName+".html", pi) + if err != nil { + return common.InternalError(err, w, r) + } + return nil +} diff --git a/routes/panel/debug.go b/routes/panel/debug.go new file mode 100644 index 00000000..cc2963fe --- /dev/null +++ b/routes/panel/debug.go @@ -0,0 +1,42 @@ +package panel + +import ( + "net/http" + "runtime" + "strconv" + "time" + + "../../common" + "../../query_gen/lib" +) + +func Debug(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + + goVersion := runtime.Version() + dbVersion := qgen.Builder.DbVersion() + var uptime string + upDuration := time.Since(common.StartTime) + hours := int(upDuration.Hours()) + minutes := int(upDuration.Minutes()) + if hours > 24 { + days := hours / 24 + hours -= days * 24 + uptime += strconv.Itoa(days) + "d" + uptime += strconv.Itoa(hours) + "h" + } else if hours >= 1 { + uptime += strconv.Itoa(hours) + "h" + } + uptime += strconv.Itoa(minutes) + "m" + + dbStats := qgen.Builder.GetConn().Stats() + openConnCount := dbStats.OpenConnections + // Disk I/O? + // TODO: Fetch the adapter from Builder rather than getting it from a global? + + pi := common.PanelDebugPage{common.GetTitlePhrase("panel_debug"), user, headerVars, stats, "debug", goVersion, dbVersion, uptime, openConnCount, qgen.Builder.GetAdapter().GetName()} + return panelRenderTemplate("panel_debug", w, r, user, &pi) +} diff --git a/routes/panel/filler.txt b/routes/panel/filler.txt deleted file mode 100644 index 20e14b1e..00000000 --- a/routes/panel/filler.txt +++ /dev/null @@ -1 +0,0 @@ -This file is here so that Git will include this folder in the repository. \ No newline at end of file diff --git a/routes/panel/forums.go b/routes/panel/forums.go new file mode 100644 index 00000000..cdaae236 --- /dev/null +++ b/routes/panel/forums.go @@ -0,0 +1,417 @@ +package panel + +import ( + "database/sql" + "errors" + "net/http" + "strconv" + "strings" + + "../../common" +) + +func Forums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.ManageForums { + return common.NoPermissions(w, r, user) + } + + // TODO: Paginate this? + var forumList []interface{} + forums, err := common.Forums.GetAll() + if err != nil { + return common.InternalError(err, w, r) + } + + // ? - Should we generate something similar to the forumView? It might be a little overkill for a page which is rarely loaded in comparison to /forums/ + for _, forum := range forums { + if forum.Name != "" && forum.ParentID == 0 { + fadmin := common.ForumAdmin{forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, forum.TopicCount, common.PresetToLang(forum.Preset)} + if fadmin.Preset == "" { + fadmin.Preset = "custom" + } + forumList = append(forumList, fadmin) + } + } + + if r.FormValue("created") == "1" { + headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("panel_forum_created")) + } else if r.FormValue("deleted") == "1" { + headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("panel_forum_deleted")) + } else if r.FormValue("updated") == "1" { + headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("panel_forum_updated")) + } + + pi := common.PanelPage{common.GetTitlePhrase("panel_forums"), user, headerVars, stats, "forums", forumList, nil} + return panelRenderTemplate("panel_forums", w, r, user, &pi) +} + +func ForumsCreateSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + _, ferr := common.SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.ManageForums { + return common.NoPermissions(w, r, user) + } + + fname := r.PostFormValue("forum-name") + fdesc := r.PostFormValue("forum-desc") + fpreset := common.StripInvalidPreset(r.PostFormValue("forum-preset")) + factive := r.PostFormValue("forum-active") + active := (factive == "on" || factive == "1") + + _, err := common.Forums.Create(fname, fdesc, active, fpreset) + if err != nil { + return common.InternalError(err, w, r) + } + + http.Redirect(w, r, "/panel/forums/?created=1", http.StatusSeeOther) + return nil +} + +// TODO: Revamp this +func ForumsDelete(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.ManageForums { + return common.NoPermissions(w, r, user) + } + + fid, err := strconv.Atoi(sfid) + if err != nil { + return common.LocalError("The provided Forum ID is not a valid number.", w, r, user) + } + + forum, err := common.Forums.Get(fid) + if err == sql.ErrNoRows { + return common.LocalError("The forum you're trying to delete doesn't exist.", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Make this a phrase + confirmMsg := "Are you sure you want to delete the '" + forum.Name + "' forum?" + yousure := common.AreYouSure{"/panel/forums/delete/submit/" + strconv.Itoa(fid), confirmMsg} + + pi := common.PanelPage{common.GetTitlePhrase("panel_delete_forum"), user, headerVars, stats, "forums", tList, yousure} + if common.RunPreRenderHook("pre_render_panel_delete_forum", w, r, &user, &pi) { + return nil + } + err = common.Templates.ExecuteTemplate(w, "are_you_sure.html", pi) + if err != nil { + return common.InternalError(err, w, r) + } + return nil +} + +func ForumsDeleteSubmit(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { + _, ferr := common.SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.ManageForums { + return common.NoPermissions(w, r, user) + } + + fid, err := strconv.Atoi(sfid) + if err != nil { + return common.LocalError("The provided Forum ID is not a valid number.", w, r, user) + } + + err = common.Forums.Delete(fid) + if err == sql.ErrNoRows { + return common.LocalError("The forum you're trying to delete doesn't exist.", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + + http.Redirect(w, r, "/panel/forums/?deleted=1", http.StatusSeeOther) + return nil +} + +func ForumsEdit(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.ManageForums { + return common.NoPermissions(w, r, user) + } + + fid, err := strconv.Atoi(sfid) + if err != nil { + return common.LocalError("The provided Forum ID is not a valid number.", w, r, user) + } + + forum, err := common.Forums.Get(fid) + if err == sql.ErrNoRows { + return common.LocalError("The forum you're trying to edit doesn't exist.", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + + if forum.Preset == "" { + forum.Preset = "custom" + } + + glist, err := common.Groups.GetAll() + if err != nil { + return common.InternalError(err, w, r) + } + + var gplist []common.GroupForumPermPreset + for gid, group := range glist { + if gid == 0 { + continue + } + forumPerms, err := common.FPStore.Get(fid, group.ID) + if err == sql.ErrNoRows { + forumPerms = common.BlankForumPerms() + } else if err != nil { + return common.InternalError(err, w, r) + } + gplist = append(gplist, common.GroupForumPermPreset{group, common.ForumPermsToGroupForumPreset(forumPerms)}) + } + + if r.FormValue("updated") == "1" { + headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("panel_forum_updated")) + } + + pi := common.PanelEditForumPage{common.GetTitlePhrase("panel_edit_forum"), user, headerVars, stats, "forums", forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, gplist} + if common.RunPreRenderHook("pre_render_panel_edit_forum", w, r, &user, &pi) { + return nil + } + err = common.Templates.ExecuteTemplate(w, "panel-forum-edit.html", pi) + if err != nil { + return common.InternalError(err, w, r) + } + + return nil +} + +func ForumsEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { + _, ferr := common.SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.ManageForums { + return common.NoPermissions(w, r, user) + } + isJs := (r.PostFormValue("js") == "1") + + fid, err := strconv.Atoi(sfid) + if err != nil { + return common.LocalErrorJSQ("The provided Forum ID is not a valid number.", w, r, user, isJs) + } + + forum, err := common.Forums.Get(fid) + if err == sql.ErrNoRows { + return common.LocalErrorJSQ("The forum you're trying to edit doesn't exist.", w, r, user, isJs) + } else if err != nil { + return common.InternalErrorJSQ(err, w, r, isJs) + } + + forumName := r.PostFormValue("forum_name") + forumDesc := r.PostFormValue("forum_desc") + forumPreset := common.StripInvalidPreset(r.PostFormValue("forum_preset")) + forumActive := r.PostFormValue("forum_active") + + var active = false + if forumActive == "" { + active = forum.Active + } else if forumActive == "1" || forumActive == "Show" { + active = true + } + + err = forum.Update(forumName, forumDesc, active, forumPreset) + if err != nil { + return common.InternalErrorJSQ(err, w, r, isJs) + } + // ? Should we redirect to the forum editor instead? + return panelSuccessRedirect("/panel/forums/", w, r, isJs) +} + +func ForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { + _, ferr := common.SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.ManageForums { + return common.NoPermissions(w, r, user) + } + isJs := (r.PostFormValue("js") == "1") + + fid, err := strconv.Atoi(sfid) + if err != nil { + return common.LocalErrorJSQ("The provided Forum ID is not a valid number.", w, r, user, isJs) + } + + gid, err := strconv.Atoi(r.PostFormValue("gid")) + if err != nil { + return common.LocalErrorJSQ("Invalid Group ID", w, r, user, isJs) + } + + forum, err := common.Forums.Get(fid) + if err == sql.ErrNoRows { + return common.LocalErrorJSQ("This forum doesn't exist", w, r, user, isJs) + } else if err != nil { + return common.InternalErrorJSQ(err, w, r, isJs) + } + + permPreset := common.StripInvalidGroupForumPreset(r.PostFormValue("perm_preset")) + err = forum.SetPreset(permPreset, gid) + if err != nil { + return common.LocalErrorJSQ(err.Error(), w, r, user, isJs) + } + + return panelSuccessRedirect("/panel/forums/edit/"+strconv.Itoa(fid)+"?updated=1", w, r, isJs) +} + +// A helper function for the Advanced portion of the Forum Perms Editor +func forumPermsExtractDash(paramList string) (fid int, gid int, err error) { + params := strings.Split(paramList, "-") + if len(params) != 2 { + return fid, gid, errors.New("Parameter count mismatch") + } + + fid, err = strconv.Atoi(params[0]) + if err != nil { + return fid, gid, errors.New("The provided Forum ID is not a valid number.") + } + + gid, err = strconv.Atoi(params[1]) + if err != nil { + err = errors.New("The provided Group ID is not a valid number.") + } + + return fid, gid, err +} + +func ForumsEditPermsAdvance(w http.ResponseWriter, r *http.Request, user common.User, paramList string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.ManageForums { + return common.NoPermissions(w, r, user) + } + + fid, gid, err := forumPermsExtractDash(paramList) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + forum, err := common.Forums.Get(fid) + if err == sql.ErrNoRows { + return common.LocalError("The forum you're trying to edit doesn't exist.", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + + if forum.Preset == "" { + forum.Preset = "custom" + } + + forumPerms, err := common.FPStore.Get(fid, gid) + if err == sql.ErrNoRows { + forumPerms = common.BlankForumPerms() + } else if err != nil { + return common.InternalError(err, w, r) + } + + var formattedPermList []common.NameLangToggle + + // TODO: Load the phrases in bulk for efficiency? + // TODO: Reduce the amount of code duplication between this and the group editor. Also, can we grind this down into one line or use a code generator to stay current more easily? + var addNameLangToggle = func(permStr string, perm bool) { + formattedPermList = append(formattedPermList, common.NameLangToggle{permStr, common.GetLocalPermPhrase(permStr), perm}) + } + addNameLangToggle("ViewTopic", forumPerms.ViewTopic) + addNameLangToggle("LikeItem", forumPerms.LikeItem) + addNameLangToggle("CreateTopic", forumPerms.CreateTopic) + //<-- + addNameLangToggle("EditTopic", forumPerms.EditTopic) + addNameLangToggle("DeleteTopic", forumPerms.DeleteTopic) + addNameLangToggle("CreateReply", forumPerms.CreateReply) + addNameLangToggle("EditReply", forumPerms.EditReply) + addNameLangToggle("DeleteReply", forumPerms.DeleteReply) + addNameLangToggle("PinTopic", forumPerms.PinTopic) + addNameLangToggle("CloseTopic", forumPerms.CloseTopic) + addNameLangToggle("MoveTopic", forumPerms.MoveTopic) + + if r.FormValue("updated") == "1" { + headerVars.NoticeList = append(headerVars.NoticeList, common.GetNoticePhrase("panel_forums_perms_updated")) + } + + pi := common.PanelEditForumGroupPage{common.GetTitlePhrase("panel_edit_forum"), user, headerVars, stats, "forums", forum.ID, gid, forum.Name, forum.Desc, forum.Active, forum.Preset, formattedPermList} + if common.RunPreRenderHook("pre_render_panel_edit_forum", w, r, &user, &pi) { + return nil + } + err = common.Templates.ExecuteTemplate(w, "panel-forum-edit-perms.html", pi) + if err != nil { + return common.InternalError(err, w, r) + } + + return nil +} + +func ForumsEditPermsAdvanceSubmit(w http.ResponseWriter, r *http.Request, user common.User, paramList string) common.RouteError { + _, ferr := common.SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.ManageForums { + return common.NoPermissions(w, r, user) + } + isJs := (r.PostFormValue("js") == "1") + + fid, gid, err := forumPermsExtractDash(paramList) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + forum, err := common.Forums.Get(fid) + if err == sql.ErrNoRows { + return common.LocalError("The forum you're trying to edit doesn't exist.", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + + forumPerms, err := common.FPStore.GetCopy(fid, gid) + if err == sql.ErrNoRows { + forumPerms = *common.BlankForumPerms() + } else if err != nil { + return common.InternalError(err, w, r) + } + + var extractPerm = func(name string) bool { + pvalue := r.PostFormValue("forum-perm-" + name) + return (pvalue == "1") + } + + // TODO: Generate this code? + forumPerms.ViewTopic = extractPerm("ViewTopic") + forumPerms.LikeItem = extractPerm("LikeItem") + forumPerms.CreateTopic = extractPerm("CreateTopic") + forumPerms.EditTopic = extractPerm("EditTopic") + forumPerms.DeleteTopic = extractPerm("DeleteTopic") + forumPerms.CreateReply = extractPerm("CreateReply") + forumPerms.EditReply = extractPerm("EditReply") + forumPerms.DeleteReply = extractPerm("DeleteReply") + forumPerms.PinTopic = extractPerm("PinTopic") + forumPerms.CloseTopic = extractPerm("CloseTopic") + forumPerms.MoveTopic = extractPerm("MoveTopic") + + err = forum.SetPerms(&forumPerms, "custom", gid) + if err != nil { + return common.LocalErrorJSQ(err.Error(), w, r, user, isJs) + } + + return panelSuccessRedirect("/panel/forums/edit/perms/"+strconv.Itoa(fid)+"-"+strconv.Itoa(gid)+"?updated=1", w, r, isJs) +} diff --git a/routes/panel/logs.go b/routes/panel/logs.go new file mode 100644 index 00000000..3f905aed --- /dev/null +++ b/routes/panel/logs.go @@ -0,0 +1,155 @@ +package panel + +import ( + "fmt" + "html/template" + "net/http" + "strconv" + "strings" + + "../../common" +) + +func LogsRegs(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + + logCount := common.RegLogs.GlobalCount() + page, _ := strconv.Atoi(r.FormValue("page")) + perPage := 10 + offset, page, lastPage := common.PageOffset(logCount, page, perPage) + + logs, err := common.RegLogs.GetOffset(offset, perPage) + if err != nil { + return common.InternalError(err, w, r) + } + var llist = make([]common.PageRegLogItem, len(logs)) + for index, log := range logs { + llist[index] = common.PageRegLogItem{log, strings.Replace(strings.TrimSuffix(log.FailureReason, "|"), "|", " | ", -1)} + } + + pageList := common.Paginate(logCount, perPage, 5) + pi := common.PanelRegLogsPage{common.GetTitlePhrase("panel_registration_logs"), user, headerVars, stats, "logs", llist, common.Paginator{pageList, page, lastPage}} + return panelRenderTemplate("panel_reglogs", w, r, user, &pi) +} + +// TODO: Log errors when something really screwy is going on? +func handleUnknownUser(user *common.User, err error) *common.User { + if err != nil { + return &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)} + } + return user +} +func handleUnknownTopic(topic *common.Topic, err error) *common.Topic { + if err != nil { + return &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)} + } + return topic +} + +// TODO: Move the log building logic into /common/ and it's own abstraction +func topicElementTypeAction(action string, elementType string, elementID int, actor *common.User, topic *common.Topic) (out string) { + if action == "delete" { + return fmt.Sprintf("Topic #%d was deleted by %s", elementID, actor.Link, actor.Name) + } + switch action { + case "lock": + out = "%s was locked by %s" + case "unlock": + out = "%s was reopened by %s" + case "stick": + out = "%s was pinned by %s" + case "unstick": + out = "%s was unpinned by %s" + case "move": + out = "%s was moved by %s" // TODO: Add where it was moved to, we'll have to change the source data for that, most likely? Investigate that and try to work this in + default: + return fmt.Sprintf("Unknown action '%s' on elementType '%s' by %s", action, elementType, actor.Link, actor.Name) + } + return fmt.Sprintf(out, topic.Link, topic.Title, actor.Link, actor.Name) +} + +func modlogsElementType(action string, elementType string, elementID int, actor *common.User) (out string) { + switch elementType { + case "topic": + topic := handleUnknownTopic(common.Topics.Get(elementID)) + out = topicElementTypeAction(action, elementType, elementID, actor, topic) + case "user": + targetUser := handleUnknownUser(common.Users.Get(elementID)) + switch action { + case "ban": + out = "%s was banned by %s" + case "unban": + out = "%s was unbanned by %s" + case "activate": + out = "%s was activated by %s" + } + out = fmt.Sprintf(out, targetUser.Link, targetUser.Name, actor.Link, actor.Name) + case "reply": + if action == "delete" { + topic := handleUnknownTopic(common.TopicByReplyID(elementID)) + out = fmt.Sprintf("A reply in %s was deleted by %s", topic.Link, topic.Title, actor.Link, actor.Name) + } + } + + if out == "" { + out = fmt.Sprintf("Unknown action '%s' on elementType '%s' by %s", action, elementType, actor.Link, actor.Name) + } + return out +} + +func LogsMod(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + + logCount := common.ModLogs.GlobalCount() + page, _ := strconv.Atoi(r.FormValue("page")) + perPage := 10 + offset, page, lastPage := common.PageOffset(logCount, page, perPage) + + logs, err := common.ModLogs.GetOffset(offset, perPage) + if err != nil { + return common.InternalError(err, w, r) + } + var llist = make([]common.PageLogItem, len(logs)) + for index, log := range logs { + actor := handleUnknownUser(common.Users.Get(log.ActorID)) + action := modlogsElementType(log.Action, log.ElementType, log.ElementID, actor) + llist[index] = common.PageLogItem{Action: template.HTML(action), IPAddress: log.IPAddress, DoneAt: log.DoneAt} + } + + pageList := common.Paginate(logCount, perPage, 5) + pi := common.PanelLogsPage{common.GetTitlePhrase("panel_mod_logs"), user, headerVars, stats, "logs", llist, common.Paginator{pageList, page, lastPage}} + return panelRenderTemplate("panel_modlogs", w, r, user, &pi) +} + +func LogsAdmin(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + + logCount := common.ModLogs.GlobalCount() + page, _ := strconv.Atoi(r.FormValue("page")) + perPage := 10 + offset, page, lastPage := common.PageOffset(logCount, page, perPage) + + logs, err := common.AdminLogs.GetOffset(offset, perPage) + if err != nil { + return common.InternalError(err, w, r) + } + var llist = make([]common.PageLogItem, len(logs)) + for index, log := range logs { + actor := handleUnknownUser(common.Users.Get(log.ActorID)) + action := modlogsElementType(log.Action, log.ElementType, log.ElementID, actor) + llist[index] = common.PageLogItem{Action: template.HTML(action), IPAddress: log.IPAddress, DoneAt: log.DoneAt} + } + + pageList := common.Paginate(logCount, perPage, 5) + pi := common.PanelLogsPage{common.GetTitlePhrase("panel_admin_logs"), user, headerVars, stats, "logs", llist, common.Paginator{pageList, page, lastPage}} + return panelRenderTemplate("panel_adminlogs", w, r, user, &pi) +} diff --git a/routes/panel/settings.go b/routes/panel/settings.go new file mode 100644 index 00000000..cf18af67 --- /dev/null +++ b/routes/panel/settings.go @@ -0,0 +1,118 @@ +package panel + +import ( + "database/sql" + "fmt" + "html" + "net/http" + "strconv" + "strings" + + "../../common" +) + +func Settings(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + header, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.EditSettings { + return common.NoPermissions(w, r, user) + } + + settings, err := header.Settings.BypassGetAll() + if err != nil { + return common.InternalError(err, w, r) + } + settingPhrases := common.GetAllSettingPhrases() + + var settingList []*common.PanelSetting + for _, settingPtr := range settings { + setting := settingPtr.Copy() + if setting.Type == "list" { + llist := settingPhrases[setting.Name+"_label"] + labels := strings.Split(llist, ",") + conv, err := strconv.Atoi(setting.Content) + if err != nil { + return common.LocalError("The setting '"+setting.Name+"' can't be converted to an integer", w, r, user) + } + setting.Content = labels[conv-1] + } else if setting.Type == "bool" { + if setting.Content == "1" { + setting.Content = "Yes" + } else { + setting.Content = "No" + } + } else if setting.Type == "html-attribute" { + setting.Type = "textarea" + } + settingList = append(settingList, &common.PanelSetting{setting, common.GetSettingPhrase(setting.Name)}) + } + + pi := common.PanelPage{common.GetTitlePhrase("panel_settings"), user, header, stats, "settings", tList, settingList} + return panelRenderTemplate("panel_settings", w, r, user, &pi) +} + +func SettingEdit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError { + header, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.EditSettings { + return common.NoPermissions(w, r, user) + } + header.Title = common.GetTitlePhrase("panel_edit_setting") + + setting, err := header.Settings.BypassGet(sname) + if err == sql.ErrNoRows { + return common.LocalError("The setting you want to edit doesn't exist.", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + + var itemList []common.OptionLabel + if setting.Type == "list" { + llist := common.GetSettingPhrase(setting.Name + "_label") + conv, err := strconv.Atoi(setting.Content) + if err != nil { + return common.LocalError("The value of this setting couldn't be converted to an integer", w, r, user) + } + fmt.Println("llist: ", llist) + + for index, label := range strings.Split(llist, ",") { + itemList = append(itemList, common.OptionLabel{ + Label: label, + Value: index + 1, + Selected: conv == (index + 1), + }) + } + } else if setting.Type == "html-attribute" { + setting.Type = "textarea" + } + + pSetting := &common.PanelSetting{setting, common.GetSettingPhrase(setting.Name)} + pi := common.PanelSettingPage{header, stats, "settings", itemList, pSetting} + return panelRenderTemplate("panel_setting", w, r, user, &pi) +} + +func SettingEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError { + headerLite, ferr := common.SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.EditSettings { + return common.NoPermissions(w, r, user) + } + + scontent := html.EscapeString(r.PostFormValue("setting-value")) + err := headerLite.Settings.Update(sname, scontent) + if err != nil { + if common.SafeSettingError(err) { + return common.LocalError(err.Error(), w, r, user) + } + return common.InternalError(err, w, r) + } + + http.Redirect(w, r, "/panel/settings/", http.StatusSeeOther) + return nil +} diff --git a/routes/profile.go b/routes/profile.go index 74f9b7ab..04d9f80f 100644 --- a/routes/profile.go +++ b/routes/profile.go @@ -32,6 +32,8 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User) commo if ferr != nil { return ferr } + // TODO: Preload this? + header.AddScript("profile.css") var err error var replyCreatedAt time.Time diff --git a/routes/reports.go b/routes/reports.go new file mode 100644 index 00000000..4a2239fc --- /dev/null +++ b/routes/reports.go @@ -0,0 +1,92 @@ +package routes + +import ( + "database/sql" + "net/http" + "strconv" + + "../common" + "../common/counters" +) + +func ReportSubmit(w http.ResponseWriter, r *http.Request, user common.User, sitemID string) common.RouteError { + _, ferr := common.SimpleUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + isJs := (r.PostFormValue("isJs") == "1") + + itemID, err := strconv.Atoi(sitemID) + if err != nil { + return common.LocalError("Bad ID", w, r, user) + } + itemType := r.FormValue("type") + + var title, content string + if itemType == "reply" { + reply, err := common.Rstore.Get(itemID) + if err == sql.ErrNoRows { + return common.LocalError("We were unable to find the reported post", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + + topic, err := common.Topics.Get(reply.ParentID) + if err == sql.ErrNoRows { + return common.LocalError("We weren't able to find the topic the reported post is supposed to be in", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + + title = "Reply: " + topic.Title + content = reply.Content + "\n\nOriginal Post: #rid-" + strconv.Itoa(itemID) + } else if itemType == "user-reply" { + userReply, err := common.Prstore.Get(itemID) + if err == sql.ErrNoRows { + return common.LocalError("We weren't able to find the reported post", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + + profileOwner, err := common.Users.Get(userReply.ParentID) + if err == sql.ErrNoRows { + return common.LocalError("We weren't able to find the profile the reported post is supposed to be on", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + title = "Profile: " + profileOwner.Name + content = userReply.Content + "\n\nOriginal Post: @" + strconv.Itoa(userReply.ParentID) + } else if itemType == "topic" { + topic, err := common.Topics.Get(itemID) + if err == sql.ErrNoRows { + return common.NotFound(w, r, nil) + } else if err != nil { + return common.InternalError(err, w, r) + } + title = "Topic: " + topic.Title + content = topic.Content + "\n\nOriginal Post: #tid-" + strconv.Itoa(itemID) + } else { + _, hasHook := common.RunVhookNeedHook("report_preassign", &itemID, &itemType) + if hasHook { + return nil + } + + // Don't try to guess the type + return common.LocalError("Unknown type", w, r, user) + } + + // TODO: Repost attachments in the reports forum, so that the mods can see them + _, err = common.Reports.Create(title, content, &user, itemType, itemID) + if err == common.ErrAlreadyReported { + return common.LocalError("Someone has already reported this!", w, r, user) + } + counters.PostCounter.Bump() + + if !isJs { + // TODO: Redirect back to where we came from + http.Redirect(w, r, "/", http.StatusSeeOther) + } else { + _, _ = w.Write(successJSONBytes) + } + return nil +} diff --git a/routes/topic.go b/routes/topic.go index dfe00c47..2fabe805 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -37,8 +37,6 @@ func init() { }) } -var successJSONBytes = []byte(`{"success":"1"}`) - func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit string) common.RouteError { page, _ := strconv.Atoi(r.FormValue("page")) @@ -64,7 +62,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit topic.ClassName = "" //log.Printf("topic: %+v\n", topic) - headerVars, ferr := common.ForumUserCheck(w, r, &user, topic.ParentID) + header, ferr := common.ForumUserCheck(w, r, &user, topic.ParentID) if ferr != nil { return ferr } @@ -72,10 +70,11 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit //log.Printf("user.Perms: %+v\n", user.Perms) return common.NoPermissions(w, r, user) } - headerVars.Zone = "view_topic" + header.Title = topic.Title + header.Zone = "view_topic" // TODO: Only include these on pages with polls - headerVars.AddSheet("chartist/chartist.min.css") - headerVars.AddScript("chartist/chartist.min.js") + header.AddSheet("chartist/chartist.min.css") + header.AddScript("chartist/chartist.min.js") topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums") topic.ContentLines = strings.Count(topic.Content, "\n") @@ -121,7 +120,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit // Calculate the offset offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage) - tpage := common.TopicPage{topic.Title, user, headerVars, []common.ReplyUser{}, topic, poll, page, lastPage} + tpage := common.TopicPage{header, []common.ReplyUser{}, topic, poll, page, lastPage} // Get the replies if we have any... if topic.PostCount > 0 { @@ -227,7 +226,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit if common.RunPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) { return nil } - err = common.RunThemeTemplate(headerVars.Theme.Name, "topic", tpage, w) + err = common.RunThemeTemplate(header.Theme.Name, "topic", tpage, w) if err != nil { return common.InternalError(err, w, r) } diff --git a/schema/mssql/inserts.sql b/schema/mssql/inserts.sql index 48da10a7..97678750 100644 --- a/schema/mssql/inserts.sql +++ b/schema/mssql/inserts.sql @@ -1,5 +1,4 @@ INSERT INTO [sync] ([last_update]) VALUES (GETUTCDATE()); -INSERT INTO [settings] ([name],[content],[type]) VALUES ('url_tags','1','bool'); INSERT INTO [settings] ([name],[content],[type],[constraints]) VALUES ('activation_type','1','list','1-3'); INSERT INTO [settings] ([name],[content],[type]) VALUES ('bigpost_min_words','250','int'); INSERT INTO [settings] ([name],[content],[type]) VALUES ('megapost_min_words','1000','int'); diff --git a/schema/mssql/query_pages.sql b/schema/mssql/query_pages.sql new file mode 100644 index 00000000..c6846225 --- /dev/null +++ b/schema/mssql/query_pages.sql @@ -0,0 +1,9 @@ +CREATE TABLE [pages] ( + [pid] int not null IDENTITY, + [name] nvarchar (200) not null, + [title] nvarchar (200) not null, + [body] nvarchar (MAX) not null, + [allowedGroups] nvarchar (MAX) not null, + [menuID] int DEFAULT -1 not null, + primary key([pid]) +); \ No newline at end of file diff --git a/schema/mysql/inserts.sql b/schema/mysql/inserts.sql index b5f2cae0..a8c2c13d 100644 --- a/schema/mysql/inserts.sql +++ b/schema/mysql/inserts.sql @@ -1,5 +1,4 @@ INSERT INTO `sync`(`last_update`) VALUES (UTC_TIMESTAMP()); -INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('url_tags','1','bool'); INSERT INTO `settings`(`name`,`content`,`type`,`constraints`) VALUES ('activation_type','1','list','1-3'); INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('bigpost_min_words','250','int'); INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('megapost_min_words','1000','int'); diff --git a/schema/mysql/query_pages.sql b/schema/mysql/query_pages.sql new file mode 100644 index 00000000..df069195 --- /dev/null +++ b/schema/mysql/query_pages.sql @@ -0,0 +1,9 @@ +CREATE TABLE `pages` ( + `pid` int not null AUTO_INCREMENT, + `name` varchar(200) not null, + `title` varchar(200) not null, + `body` text not null, + `allowedGroups` text not null, + `menuID` int DEFAULT -1 not null, + primary key(`pid`) +) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; \ No newline at end of file diff --git a/schema/pgsql/inserts.sql b/schema/pgsql/inserts.sql index b5613872..6c3ebd96 100644 --- a/schema/pgsql/inserts.sql +++ b/schema/pgsql/inserts.sql @@ -1,40 +1,39 @@ -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; -; +INSERT INTO "sync"("last_update") VALUES (UTC_TIMESTAMP()); +INSERT INTO "settings"("name","content","type","constraints") VALUES ('activation_type','1','list','1-3'); +INSERT INTO "settings"("name","content","type") VALUES ('bigpost_min_words','250','int'); +INSERT INTO "settings"("name","content","type") VALUES ('megapost_min_words','1000','int'); +INSERT INTO "settings"("name","content","type") VALUES ('meta_desc','','html-attribute'); +INSERT INTO "themes"("uname","default") VALUES ('cosora',1); +INSERT INTO "emails"("email","uid","validated") VALUES ('admin@localhost',1,1); +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,"MoveTopic":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,"MoveTopic":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'); +INSERT INTO "forums"("name","active","desc") VALUES ('Reports',0,'All the reports go here'); +INSERT INTO "forums"("name","lastTopicID","lastReplyerID","desc") VALUES ('General',1,1,'A place for general discussions which don''t fit elsewhere'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (1,1,'{"ViewTopic":true,"CreateReply":true,"CreateTopic":true,"PinTopic":true,"CloseTopic":true}'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (2,1,'{"ViewTopic":true,"CreateReply":true,"CloseTopic":true}'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (3,1,'{}'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (4,1,'{}'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (5,1,'{}'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (6,1,'{}'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (1,2,'{"ViewTopic":true,"CreateReply":true,"CreateTopic":true,"LikeItem":true,"EditTopic":true,"DeleteTopic":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (2,2,'{"ViewTopic":true,"CreateReply":true,"CreateTopic":true,"LikeItem":true,"EditTopic":true,"DeleteTopic":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (3,2,'{"ViewTopic":true,"CreateReply":true,"CreateTopic":true,"LikeItem":true}'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (4,2,'{"ViewTopic":true}'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (5,2,'{"ViewTopic":true}'); +INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (6,2,'{"ViewTopic":true}'); +INSERT INTO "topics"("title","content","parsed_content","createdAt","lastReplyAt","lastReplyBy","createdBy","parentID","ipaddress") VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',UTC_TIMESTAMP(),UTC_TIMESTAMP(),1,1,2,'::1'); +INSERT INTO "replies"("tid","content","parsed_content","createdAt","createdBy","lastUpdated","lastEdit","lastEditBy","ipaddress") VALUES (1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'::1'); +INSERT INTO "menus"() VALUES (); +INSERT INTO "menu_items"("mid","name","htmlID","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0); +INSERT INTO "menu_items"("mid","name","htmlID","cssClass","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1); +INSERT INTO "menu_items"("mid","htmlID","cssClass","position","tmplName","order") VALUES (1,'general_alerts','menu_alerts','right','menu_alerts',2); +INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","order") VALUES (1,'{lang.menu_account}','menu_account','left','/user/edit/critical/','{lang.menu_account_aria}','{lang.menu_account_tooltip}',1,3); +INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","order") VALUES (1,'{lang.menu_profile}','menu_profile','left','{me.Link}','{lang.menu_profile_aria}','{lang.menu_profile_tooltip}',1,4); +INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","staffOnly","order") VALUES (1,'{lang.menu_panel}','menu_panel menu_account','left','/panel/','{lang.menu_panel_aria}','{lang.menu_panel_tooltip}',1,1,5); +INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","order") VALUES (1,'{lang.menu_logout}','menu_logout','left','/accounts/logout/?session={me.Session}','{lang.menu_logout_aria}','{lang.menu_logout_tooltip}',1,6); +INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","guestOnly","order") VALUES (1,'{lang.menu_register}','menu_register','left','/accounts/create/','{lang.menu_register_aria}','{lang.menu_register_tooltip}',1,7); +INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","guestOnly","order") VALUES (1,'{lang.menu_login}','menu_login','left','/accounts/login/','{lang.menu_login_aria}','{lang.menu_login_tooltip}',1,8); diff --git a/schema/pgsql/query_pages.sql b/schema/pgsql/query_pages.sql new file mode 100644 index 00000000..dfbf4cbc --- /dev/null +++ b/schema/pgsql/query_pages.sql @@ -0,0 +1,9 @@ +CREATE TABLE `pages` ( + `pid` serial not null, + `name` varchar (200) not null, + `title` varchar (200) not null, + `body` text not null, + `allowedGroups` text not null, + `menuID` int DEFAULT -1 not null, + primary key(`pid`) +); \ No newline at end of file diff --git a/schema/schema.json b/schema/schema.json index 79cd1f5a..0e619035 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -1,5 +1,5 @@ { - "DBVersion":"4", + "DBVersion":"5", "DynamicFileVersion":"0", "MinGoVersion":"1.10", "MinVersion":"" diff --git a/templates/account_menu.html b/templates/account_menu.html index b440b88a..9b40cc8a 100644 --- a/templates/account_menu.html +++ b/templates/account_menu.html @@ -9,7 +9,7 @@
- + {{/** TODO: Add an alerts page with pagination to go through alerts which either don't fit in the alerts drop-down or which have already been dismissed. Bear in mind though that dismissed alerts older than two weeks might be purged to save space and to speed up the database **/}} diff --git a/templates/footer.html b/templates/footer.html index affbf28f..850a4045 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -1,3 +1,8 @@ + + + +