From ca80d0dd6f62e6ecee5c91947dade2fb190031c0 Mon Sep 17 00:00:00 2001 From: Azareal Date: Sun, 27 May 2018 19:18:29 +1000 Subject: [PATCH] Began work on the Nox Theme. Removed the Tempra Cursive Theme. You can now do bulk moderation actions with Shadow. Added: Argon2 as a dependency. The EmailStore. The ReportStore. The Copy method to *Setting. The AddColumn method to the query builder and adapters. The textarea setting type. More logging to better debug issues. The GetOffset method to the UserStore. Removed: Sortable from Code Climate's Analysis. MemberCheck and memberCheck as they're obsolete now. The obsolete url_tags setting. The BcryptGeneratePasswordNoSalt function. Some redundant fields from some of the page structs. Revamped: The Control Panel Setting List and Editor. Refactored: The password hashing logic to make it more amenable to multiple hashing algorithms. The email portion of the Account Manager. The Control Panel User List. The report system. simplePanelUserCheck and simpleUserCheck to remove the duplicated logic as the two do the exact same thing. Fixed: Missing slugs in the profile links in the User Manager. A few template initialisers potentially reducing the number of odd template edge cases. Some problems with the footer. Custom selection colour not applying to images on Shadow. The avatars of the bottom row of the topic list on Conflux leaking out. Other: Moved the startTime variable into package common and exported it. Moved the password hashing logic from user.go to auth.go Split common/themes.go into common/theme.go and common/theme_list.go Replaced the SettingLabels phrase category with the more generic SettingPhrases category. Moved a load of routes, including panel ones into the routes and panel packages. Hid the notifications link from the Account Menu. Moved more inline CSS into the CSS files and made things a little more flexible here and there. Continued work on PgSQL, still a ways away. Guests now have a default avatar like everyone else. Tweaked some of the font sizes on Cosora to make the text look a little nicer. Partially implemented the theme dock override logic. Partially implemented a "symlink" like feature for theme directories. ... And a bunch of other things I might have missed. You will need to run this update script / patcher for this commit. Warning: This is an "unstable commit", therefore some things may be a little less stable than I'd like. For instance, the Shadow Theme is a little broken in this commit. --- .codeclimate.yml | 8 +- README.md | 10 +- common/auth.go | 114 +- common/common.go | 5 +- common/counters/requests.go | 3 +- common/email_store.go | 52 + common/group_store.go | 1 + common/pages.go | 53 +- common/phrases.go | 10 +- common/report_store.go | 53 + common/routes_common.go | 20 +- common/settings.go | 6 + common/site.go | 6 + common/template_init.go | 18 +- common/theme.go | 239 +++ common/{themes.go => theme_list.go} | 363 +--- common/user.go | 37 +- common/user_store.go | 26 + common/utils.go | 2 +- dev-update-linux | 3 + dev-update.bat | 7 + docs/templates.md | 7 + experimental/new-update.bat | 7 + gen_mssql.go | 54 - gen_mysql.go | 48 - gen_pgsql.go | 40 +- gen_router.go | 273 +-- gen_tables.go | 16 +- general_test.go | 2 +- install-linux | 3 + install.bat | 7 + install/install/utils.go | 25 +- langs/english.json | 9 +- main.go | 39 +- member_routes.go | 217 --- misc_test.go | 2 +- panel_routes.go | 1493 +---------------- patcher/patches.go | 212 +++ public/global.js | 3 +- query_gen/lib/builder.go | 4 + query_gen/lib/mssql.go | 109 +- query_gen/lib/mysql.go | 82 +- query_gen/lib/pgsql.go | 50 +- query_gen/lib/querygen.go | 3 + query_gen/main.go | 14 - query_gen/tables.go | 16 + router_gen/main.go | 1 + router_gen/routes.go | 68 +- routes/account.go | 85 + routes/common.go | 3 + routes/forum.go | 3 +- routes/panel/analytics.go | 746 ++++++++ routes/panel/backups.go | 54 + routes/panel/common.go | 31 + routes/panel/debug.go | 42 + routes/panel/filler.txt | 1 - routes/panel/forums.go | 417 +++++ routes/panel/logs.go | 155 ++ routes/panel/settings.go | 118 ++ routes/profile.go | 2 + routes/reports.go | 92 + routes/topic.go | 15 +- schema/mssql/inserts.sql | 1 - schema/mssql/query_pages.sql | 9 + schema/mysql/inserts.sql | 1 - schema/mysql/query_pages.sql | 9 + schema/pgsql/inserts.sql | 79 +- schema/pgsql/query_pages.sql | 9 + schema/schema.json | 2 +- templates/account_menu.html | 2 +- templates/footer.html | 9 +- templates/forum.html | 11 +- templates/forums.html | 5 +- templates/header.html | 28 +- templates/panel_setting.html | 22 +- templates/panel_settings.html | 6 +- templates/panel_users.html | 3 +- templates/topics.html | 25 +- themes/cosora/public/main.css | 66 +- themes/cosora/public/panel.css | 18 + themes/cosora/public/profile.css | 0 themes/nox/public/main.css | 320 ++++ themes/nox/public/profile.css | 0 themes/shadow/public/main.css | 105 +- themes/shadow/public/profile.css | 0 themes/tempra-conflux/public/main.css | 24 +- themes/tempra-conflux/public/profile.css | 0 themes/tempra-cursive/DEVELOPERS.md | 4 - themes/tempra-cursive/public/main.css | 754 --------- themes/tempra-cursive/public/panel.css | 88 - .../tempra-cursive/public/post-avatar-bg.jpg | Bin 539 -> 0 bytes themes/tempra-cursive/tempra-cursive.png | Bin 245735 -> 0 bytes themes/tempra-cursive/theme.json | 20 - themes/tempra-simple/public/main.css | 19 +- themes/tempra-simple/public/profile.css | 0 update-deps-linux | 3 + update-deps.bat | 7 + 97 files changed, 3748 insertions(+), 3505 deletions(-) create mode 100644 common/email_store.go create mode 100644 common/report_store.go create mode 100644 common/theme.go rename common/{themes.go => theme_list.go} (55%) create mode 100644 docs/templates.md delete mode 100644 member_routes.go create mode 100644 routes/common.go create mode 100644 routes/panel/analytics.go create mode 100644 routes/panel/backups.go create mode 100644 routes/panel/common.go create mode 100644 routes/panel/debug.go delete mode 100644 routes/panel/filler.txt create mode 100644 routes/panel/forums.go create mode 100644 routes/panel/logs.go create mode 100644 routes/panel/settings.go create mode 100644 routes/reports.go create mode 100644 schema/mssql/query_pages.sql create mode 100644 schema/mysql/query_pages.sql create mode 100644 schema/pgsql/query_pages.sql create mode 100644 themes/cosora/public/profile.css create mode 100644 themes/nox/public/profile.css create mode 100644 themes/shadow/public/profile.css create mode 100644 themes/tempra-conflux/public/profile.css delete mode 100644 themes/tempra-cursive/DEVELOPERS.md delete mode 100644 themes/tempra-cursive/public/main.css delete mode 100644 themes/tempra-cursive/public/panel.css delete mode 100644 themes/tempra-cursive/public/post-avatar-bg.jpg delete mode 100644 themes/tempra-cursive/tempra-cursive.png delete mode 100644 themes/tempra-cursive/theme.json create mode 100644 themes/tempra-simple/public/profile.css 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 @@
{{lang "account_menu_username"}}
{{lang "account_menu_password"}}
{{lang "account_menu_email"}}
-
{{lang "account_menu_notifications"}}
+ {{/** 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 @@ + + + +
+
-
- +
+
diff --git a/templates/forum.html b/templates/forum.html index eabfd5c4..4df6f88b 100644 --- a/templates/forum.html +++ b/templates/forum.html @@ -88,10 +88,17 @@ {{if .IsClosed}} | 🔒︎{{end}} {{if .Sticky}} | 📍︎{{end}} - + {{/** TODO: Phase this out of Cosora and remove it **/}} +
{{.PostCount}}
{{.LikeCount}} - +
+ +
+
+ {{.PostCount}}
+ {{.LikeCount}} +
{{.LastUser.Name}}'s Avatar diff --git a/templates/forums.html b/templates/forums.html index f09138ff..f9fde264 100644 --- a/templates/forums.html +++ b/templates/forums.html @@ -12,14 +12,13 @@ {{if .Desc}}
{{.Desc}} {{else}} -
{{lang "forums_no_description"}} +
{{lang "forums_no_description"}} {{end}} - {{if .LastReplyer.Avatar}}{{.LastReplyer.Name}}'s Avatar{{end}} - {{if .LastTopic.Title}}{{.LastTopic.Title}}{{else}}{{lang "forums_none"}}{{end}} + {{if .LastTopic.Title}}{{.LastTopic.Title}}{{else}}{{lang "forums_none"}}{{end}} {{if .LastTopicTime}}
{{.LastTopicTime}}{{end}}
diff --git a/templates/header.html b/templates/header.html index ee3f2b80..e569caa9 100644 --- a/templates/header.html +++ b/templates/header.html @@ -22,12 +22,13 @@ {{if not .CurrentUser.IsSuperMod}}{{end}}
+
{{dock "leftOfNav" .Header }}
-
{{dock "rightOfNav" .Header }}
-
-
{{range .Header.NoticeList}} - {{template "notice.html" . }}{{end}} +
+{{/** TODO: Make this a separate template and load it via the theme docks, here for now so we can rapidly prototype the Nox theme **/}} +{{if eq .Header.Theme.Name "nox"}} +
+ +
+ {{.CurrentUser.Name}} + 21 new alerts +
+
+{{end}}
+ +
+
+
+
+
{{range .Header.NoticeList}} + {{template "notice.html" . }}{{end}} +
\ No newline at end of file diff --git a/templates/panel_setting.html b/templates/panel_setting.html index b9240055..14c430b1 100644 --- a/templates/panel_setting.html +++ b/templates/panel_setting.html @@ -4,15 +4,11 @@ {{template "panel-menu.html" . }}
-

{{lang "panel_setting_head"}}

+

{{.Setting.FriendlyName}}

-
-
- -
{{.Something.Name}}
-
- {{if eq .Something.Type "list"}} + + {{if eq .Setting.Type "list"}}
@@ -21,19 +17,23 @@
- {{else if eq .Something.Type "bool"}} + {{else if eq .Setting.Type "bool"}}
+ {{else if eq .Setting.Type "textarea"}} +
+
+
{{else}}{{end}}
diff --git a/templates/panel_settings.html b/templates/panel_settings.html index f555511d..5faaa5f3 100644 --- a/templates/panel_settings.html +++ b/templates/panel_settings.html @@ -6,10 +6,10 @@

{{lang "panel_settings_head"}}

- {{range $key, $value := .Something}} + {{range .Something}} {{end}}
diff --git a/templates/panel_users.html b/templates/panel_users.html index cdeaa537..ea5ab8b8 100644 --- a/templates/panel_users.html +++ b/templates/panel_users.html @@ -11,9 +11,8 @@
{{.Name}}'s Avatar {{.Name}} - {{lang "panel_users_profile"}} + {{lang "panel_users_profile"}} {{if (.Tag) and (.IsSuperMod)}}{{.Tag}}{{end}} - {{if .IsBanned}}{{lang "panel_users_unban"}}{{else if not .IsSuperMod}}{{lang "panel_users_ban"}}{{end}} {{if not .Active}}{{lang "panel_users_activate"}}{{end}} diff --git a/templates/topics.html b/templates/topics.html index a78ebaba..0a549b15 100644 --- a/templates/topics.html +++ b/templates/topics.html @@ -113,17 +113,26 @@ {{if .IsClosed}} | 🔒︎{{end}} {{if .Sticky}} | 📍︎{{end}} - + {{/** TODO: Phase this out of Cosora and remove it **/}} +
{{.PostCount}}
{{.LikeCount}} - +
-
- {{.LastUser.Name}}'s Avatar - - {{.LastUser.Name}}
- {{.RelativeLastReplyAt}} -
+
+
+ {{.PostCount}}
+ {{.LikeCount}} +
+
+
+
+ {{.LastUser.Name}}'s Avatar + + {{.LastUser.Name}}
+ {{.RelativeLastReplyAt}} +
+
{{else}}
{{lang "topics_no_topics"}}{{if .CurrentUser.Perms.CreateTopic}} {{lang "topics_start_one"}}{{end}}
{{end}}
diff --git a/themes/cosora/public/main.css b/themes/cosora/public/main.css index 31efef90..424a1c45 100644 --- a/themes/cosora/public/main.css +++ b/themes/cosora/public/main.css @@ -53,6 +53,10 @@ body, #main { padding-right: 0px; padding-bottom: 0px; } +.footBlock { + padding-left: 8px; + padding-right: 8px; +} .container { background-color: var(--element-background-color); } @@ -62,6 +66,7 @@ body, #main { padding-top: 14px; padding-left: 8px; padding-right: 8px; + padding-bottom: 14px; } .sidebar { width: 200px; @@ -386,10 +391,10 @@ h1, h3 { } .topic_list_title_block .pre_opt { border-left: 1px solid var(--element-border-color); - padding-left: 12px; + padding-left: 11px; height: 20px; color: var(--light-text-color); - margin-right: 10px; + margin-right: 9px; } .topic_list_title_block .pre_opt:before { content: "{{index .Phrases "topics_click_topics_to_select"}}"; @@ -413,6 +418,9 @@ h1, h3 { font: normal normal normal 14px/1 FontAwesome; font-size: 18px; } +.mod_opt .moderate_open { + display: none; +} .topic_create_form { display: flex !important; @@ -686,6 +694,9 @@ textarea { border: 1px solid var(--element-border-color); border-bottom: 2px solid var(--element-border-color); } +.topic_middle { + display: none; +} .rowlist .rowitem { background-color: var(--element-background-color); padding: 12px; @@ -816,6 +827,9 @@ textarea { flex: 1 1 0px; border-left: none; } +.topic_right_inside { + display: flex; +} .topic_left img { border-radius: 30px; @@ -824,7 +838,7 @@ textarea { margin-top: 8px; margin-left: 4px; } -.topic_right img { +.topic_right_inside img { border-radius: 30px; height: 42px; width: 42px; @@ -837,7 +851,7 @@ textarea { margin-bottom: 14px; width: 220px; } -.topic_right > span { +.topic_right_inside > span { margin-top: 12px; margin-left: 8px; } @@ -885,6 +899,9 @@ textarea { border-bottom: 2px solid var(--element-border-color); padding: 14px; } +.forum_list .forum_nodesc { + font-style: italic; +} .forum_right { display: flex; } @@ -1325,9 +1342,6 @@ textarea { } /* TODO: Make widget_about's CSS less footer centric */ -.footer { - margin-top: 14px; -} .footerBit, .footer .widget { border-top: 1px solid var(--element-border-color); padding: 12px; @@ -1456,15 +1470,29 @@ textarea { max-width: 1000px; margin-left: auto; margin-right: auto; + } + .footer { + max-width: 1000px; + margin-left: auto; + margin-right: auto; + } + #main { padding-top: 18px; padding-left: 16px; padding-right: 16px; border-left: 1px solid hsl(20,0%,95%); border-right: 1px solid hsl(20,0%,95%); } - #back { + .footer { + padding-left: 8px; + padding-right: 8px; + } + #back, .footer, .footBlock { background-color: hsl(0,0%,95%); } + #back:not(.zone_panel) .footBlock { + display: flex; + } } @media(min-width: 721px) { @@ -1591,12 +1619,17 @@ textarea { font-size: 18px; } main > .rowhead, #main > .rowhead { - margin-left: 0px; - margin-right: 0px; border: none; border-bottom: 2px solid var(--header-border-color); } - + #main { + padding-top: 0px; + } + main > .rowhead, #main > .rowhead, main > .opthead, #main > .opthead { + margin-left: -3px; + margin-right: -3px; + } + .topic_list { display: flex; flex-wrap: wrap; @@ -1622,13 +1655,13 @@ textarea { border-left: 1px solid var(--element-border-color); background-color: hsl(0,0%,95%); } - .topic_right br, .topic_right img { + .topic_right_inside br, .topic_right_inside img { display: none; } .topic_right.topic_sticky { border-bottom: 2px solid var(--element-border-color); } - .topic_right > span { + .topic_right_inside > span { margin-top: 6px; margin-bottom: 6px; } @@ -1729,13 +1762,6 @@ textarea { } } @media(max-width: 520px) { - #main { - padding-top: 0px; - } - main > .rowhead, #main > .rowhead, main > .opthead, #main > .opthead { - margin-left: -3px; - margin-right: -3px; - } .edit_item, .button_container .open_edit, .delete_item, .pin_item, .unpin_item, .lock_item, .unlock_item, .ip_item_button, .report_item:not(.profile_menu_item) { display: none; } diff --git a/themes/cosora/public/panel.css b/themes/cosora/public/panel.css index 35bfb711..d11c35df 100644 --- a/themes/cosora/public/panel.css +++ b/themes/cosora/public/panel.css @@ -5,6 +5,7 @@ border-left: none; border-right: none; padding-left: 0px; + padding-bottom: 0px; } #back { background-color: inherit; @@ -147,6 +148,10 @@ padding-bottom: 4px; margin-bottom: 6px; } +#panel_setting textarea { + width: 100%; + height: 80px; +} #forum_quick_perms .formitem { display: flex; @@ -256,4 +261,17 @@ .colstack_left { margin-top: -14.5px; } +} + +@media(min-width: 1000px) { + .footBlock { + padding-left: 0px; + padding-right: 0px; + } + .footer { + max-width: none; + width: 100%; + margin-left: 0px; + margin-right: 0px; + } } \ No newline at end of file diff --git a/themes/cosora/public/profile.css b/themes/cosora/public/profile.css new file mode 100644 index 00000000..e69de29b diff --git a/themes/nox/public/main.css b/themes/nox/public/main.css index e69de29b..6073ca15 100644 --- a/themes/nox/public/main.css +++ b/themes/nox/public/main.css @@ -0,0 +1,320 @@ +:root { + --darkest-background: #222222; +} + +* { + box-sizing: border-box; +} +body { + margin: 0px; + padding: 0px; + color: #AAAAAA; + font-family: "Segoe UI"; +} +a { + color: white; + text-decoration: none; +} + +nav.nav { + background: var(--darkest-background); + border-bottom: 1px solid #444444; + width: calc(100% - 200px); + float: left; +} +ul { + list-style-type: none; + margin-top: 0px; + margin-bottom: 0px; + clear: both; +} +li { + float: left; + margin-right: 12px; +} +li a { + padding-top: 35px; + padding-bottom: 22px; + font-size: 18px; + display: inline-block; + color: #aaaaaa; +} +#menu_overview { + margin-right: 24px; +} +#menu_overview a { + font-size: 22px; + padding-bottom: 21px; + color: rgb(221,221,221); + padding-top: 31px; +} +.menu_topics a { + border-bottom: 2px solid #777777; + padding-bottom: 21px; + color: #dddddd; +} +.menu_alerts { + display: none; +} +.right_of_nav { + float: left; + width: 200px; + background-color: var(--darkest-background); + border-bottom: 1px solid #444444; + padding-top: 12px; + padding-bottom: 12px; + padding-right: 12px; +} +.user_box { + display: flex; + flex-direction: row; + background-color: #333333; + border: 1px solid #444444; + padding-top: 10px; + padding-bottom: 10px; + padding-left: 10px; +} +.user_box img { + display: block; + width: 36px; + height: 36px; + border-radius: 32px; + margin-right: 8px; +} +.user_box .username { + display: block; + font-size: 16px; + padding-top: 4px; + line-height: 10px; +} +.user_box .alerts { + font-size: 12px; + line-height: 12px; +} +.container { + clear: both; +} +#back { + background: #333333; + padding: 24px; + padding-top: 12px; + clear: both; + display: flex; +} +#main, #main .rowblock { + width: 100%; +} + +.sidebar { + width: 320px; +} +.rowblock:not(.topic_list):not(.rowhead):not(.opthead) .rowitem { + background-color: #444444; + border-color: #555555; + display: flex; + padding: 12px; + margin-left: 12px; +} + +h1, h3 { + -webkit-margin-before: 0; + -webkit-margin-after: 0; + margin-block-start: 0; + margin-block-end: 0; + margin-top: 0px; + margin-bottom: 0px; + font-weight: normal; +} + +.mod_floater, .modal_pane { + display: none; +} + +.rowhead, .opthead { + margin-left: 18px; + margin-bottom: 8px; +} +.rowhead h1, .opthead h1 { + font-size: 23px; +} +.sidebar .rowhead { + margin-top: 4px; + margin-bottom: 8px; +} +.sidebar .rowhead h1 { + font-size: 20px; +} + +.topic_row:not(:last-child) { + margin-bottom: 8px; +} +.topic_row { + background-color: #444444; + border-color: #555555; + display: flex; +} +.topic_left, .topic_right, .topic_middle { + padding: 16px; + padding-bottom: 12px; + display: flex; + width: 33%; +} +.topic_middle { + padding-top: 15px; +} +.topic_left { + margin-right: auto; +} +.topic_left img, .topic_right img { + border-radius: 24px; + height: 38px; + width: 38px; + margin-right: 8px; + margin-top: 1px; +} +.topic_inner_left { + display: flex; + flex-direction: column; +} +.topic_inner_left .parent_forum { + display: none; /* Comment this until we figure out how to make it work */ +} +.topic_right_inside { + display: flex; + margin-left: auto; + width: 180px; +} +.topic_right_inside .lastName, .topic_left .rowtopic { + font-size: 15px !important; + line-height: 22px; +} +.topic_right_inside .lastReplyAt, .topic_left .starter { + font-size: 14px; + line-height: 14px; +} +.topic_right_inside span { + display: flex; + flex-direction: column; +} +.topic_inner_left br, .topic_right_inside br, .topic_inner_right { + display: none; +} +.topic_middle .replyCount:after { + content: "replies"; + margin-left: 3px; +} +.topic_middle .likeCount:after { + content: "likes"; + margin-left: 3px; +} +.topic_middle_inside { + margin-left: auto; + margin-right: auto; + width: 80px; +} +.topic_status_e { + display: none; +} + +.pageset { + display: flex; + margin-top: 8px; +} + +.pageitem { + background-color: #444444; + padding: 6px; + margin-right: 6px; +} + +#prevFloat, #nextFloat { + display: none; +} +.forum_list .rowitem { + margin-bottom: 8px; + display: flex; +} +.forum_list .forum_left { + margin-left: 8px; +} +.forum_list .forum_nodesc { + font-style: italic; +} +.forum_list .forum_right { + display: flex; + margin-left: auto; + margin-right: 8px; + padding-top: 2px; + width: 140px; +} +.forum_list .forum_right img { + margin-right: 10px; + margin-top: 2px; +} +.forum_list .forum_right span { + line-height: 19px; +} +.extra_little_row_avatar { + border-radius: 24px; + height: 36px; + width: 36px; +} + +.footer .widget { + padding: 12px; +} +#poweredByHolder { + display: flex; + padding: 12px; + padding-left: 16px; + padding-right: 16px; +} +#poweredBy { + margin-right: auto; +} +.footer .widget, #poweredByHolder { + background-color: #444444; + border-top: 1px solid #555555; +} + +@media(min-width: 1010px) { + .container { + background-color: #292929; + } + #back { + width: 1000px; + margin-left: auto; + margin-right: auto; + border-left: 1px solid #444444; + border-right: 1px solid #444444; + } + .footBlock { + display: flex; + } + .footer { + margin-left: auto; + margin-right: auto; + width: 1000px; + display: flex; + flex-direction: column; + } + .footer .widget, #poweredByHolder { + border-left: 1px solid #555555; + border-right: 1px solid #555555; + } +} + +@media(min-width: 1330px) { + nav.nav { + width: calc(85% - 200px) + } + ul { + margin-left: 205px; + } + .right_of_nav { + width: calc(15% + 200px); + } + .user_box { + width: 200px; + } +} diff --git a/themes/nox/public/profile.css b/themes/nox/public/profile.css new file mode 100644 index 00000000..e69de29b diff --git a/themes/shadow/public/main.css b/themes/shadow/public/main.css index 54a92fe9..d7552ec1 100644 --- a/themes/shadow/public/main.css +++ b/themes/shadow/public/main.css @@ -21,7 +21,7 @@ body { background-color: var(--main-background-color); margin: 0; } -p::selection, span::selection, a::selection { +*::selection { background-color: hsl(0,0%,75%); color: hsl(0,0%,20%); font-weight: 100; @@ -226,6 +226,9 @@ a { } /* TODO: Add the avatars to the forum list */ +.forum_list .forum_nodesc { + font-style: italic; +} .extra_little_row_avatar { display: none; } @@ -377,7 +380,7 @@ textarea.large { display: block; } -.formitem button, .formbutton { +.formitem button, .formbutton, .mod_floater_submit, .pane_buttons button { background-color: var(--input-background-color); border: 1px solid var(--input-border-color); color: var(--input-text-color); @@ -385,6 +388,15 @@ textarea.large { padding-bottom: 6px; font-size: 13px; } +.mod_floater_submit { + padding: 5px; + padding-bottom: 4px; + margin-left: 2px; +} +.pane_buttons button { + padding: 5px; + padding-bottom: 4px; +} .formrow { flex-direction: row; @@ -666,16 +678,97 @@ input[type=checkbox]:checked + label.poll_option_label .sel { font-size: 11px; } +.topic_list_title_block .pre_opt:before { + content: "{{index .Phrases "topics_click_topics_to_select"}}"; + font-size: 14px; +} .create_topic_opt a:before { content: "{{index .Phrases "topics_new_topic"}}"; + margin-left: 3px; } .locked_opt a:before { content: "{{index .Phrases "forum_locked"}}"; } +.mod_opt a { + margin-left: 4px; +} +.mod_opt a:after { + content: "{{index .Phrases "topics_moderate"}}"; + padding-left: 1px; +} +.create_topic_opt { + order: 1; +} +.mod_opt { + order: 2; +} +.pre_opt { + order: 3; + margin-left: auto; + margin-right: 12px; +} + +@keyframes fadein { + from { opacity: 0; } + to { opacity: 1; } +} +.mod_floater { + position: fixed; + bottom: 15px; + right: 15px; + width: 150px; + height: 65px; + font-size: 14px; + padding: 14px; + z-index: 9999; + animation: fadein 0.8s; + background-color: var(--main-block-color); +} +.mod_floater_head { + margin-bottom: 8px; +} +.modal_pane { + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background-color: var(--main-block-color); + border: 2px solid #333333; + padding-left: 24px; + padding-right: 24px; + z-index: 9999; + animation: fadein 0.8s; +} +.pane_header { + font-size: 15px; +} +.pane_header h3 { + -webkit-margin-before: 0; + -webkit-margin-after: 0; + margin-block-start: 0; + margin-block-end: 0; + margin-top: 10px; + margin-bottom: 10px; + font-weight: normal; +} +.pane_row { + font-size: 14px; + margin-bottom: 1px; +} +.pane_selected { + font-weight: bold; +} +.pane_buttons { + margin-top: 7px; + margin-bottom: 8px; +} .topic_list .topic_row { display: flex; } +.topic_selected .rowitem { + background-color: hsla(0, 0%, 29%, 1); +} /* Temporary hack, so that I don't break the topic lists of the other themes */ .topic_list .topic_inner_right { display: none; @@ -706,10 +799,13 @@ input[type=checkbox]:checked + label.poll_option_label .sel { width: 284px; padding: 0px; } +.topic_right_inside { + display: flex; +} .topic_list .topic_left img, .topic_list .topic_right img { width: 64px; } -.topic_list .topic_inner_left, .topic_right > span { +.topic_list .topic_inner_left, .topic_right_inside > span { margin-left: 8px; margin-top: 12px; } @@ -725,6 +821,9 @@ input[type=checkbox]:checked + label.poll_option_label .sel { .topic_list .starter:before { content: "{{index .Phrases "topics_starter"}}: "; } +.topic_middle { + display: none; +} .topic_name_input { width: 100%; diff --git a/themes/shadow/public/profile.css b/themes/shadow/public/profile.css new file mode 100644 index 00000000..e69de29b diff --git a/themes/tempra-conflux/public/main.css b/themes/tempra-conflux/public/main.css index 1e83d01d..902dd164 100644 --- a/themes/tempra-conflux/public/main.css +++ b/themes/tempra-conflux/public/main.css @@ -158,6 +158,10 @@ li a { #back { padding: 12px; padding-top: 0px; + display: flex; +} +#main { + width: 100%; } /* Explict declaring each border direction to fix a bug in Chrome where an override to .rowhead was also applying to .rowblock in some cases */ @@ -419,6 +423,9 @@ li a { } /* TODO: Add the avatars to the forum list */ +.forum_list .forum_nodesc { + font-style: italic; +} .extra_little_row_avatar { display: none; } @@ -434,6 +441,7 @@ li a { .topic_list .topic_row { display: grid; grid-template-columns: calc(100% - 204px) 204px; + overflow: hidden; } .topic_list .topic_inner_right { display: none; @@ -466,14 +474,20 @@ li a { padding: 0px; height: 58px; } -.topic_left img, .topic_right img { +.topic_right_inside { + display: flex; +} +.topic_left img, .topic_right_inside img { width: 64px; height: auto; } -.topic_left .topic_inner_left, .topic_right > span { +.topic_left .topic_inner_left, .topic_right_inside > span { margin-top: 10px; margin-left: 8px; } +.topic_middle { + display: none; +} .postImage { max-width: 100%; @@ -969,9 +983,13 @@ input[type=checkbox]:checked + label.poll_option_label .sel { content: "{{index .Phrases "topic_report_button_text"}}"; } +.footer { + margin-left: 12px; + margin-right: 12px; + margin-bottom: 12px; +} #poweredByHolder { border: 1px solid var(--main-border-color); - margin-top: 12px; clear: both; height: 40px; padding: 6px; diff --git a/themes/tempra-conflux/public/profile.css b/themes/tempra-conflux/public/profile.css new file mode 100644 index 00000000..e69de29b diff --git a/themes/tempra-cursive/DEVELOPERS.md b/themes/tempra-cursive/DEVELOPERS.md deleted file mode 100644 index 18c93314..00000000 --- a/themes/tempra-cursive/DEVELOPERS.md +++ /dev/null @@ -1,4 +0,0 @@ -# Theme Notes - -/public/post-avatar-bg.jpg is a solid rgb(255,255,255) white. - diff --git a/themes/tempra-cursive/public/main.css b/themes/tempra-cursive/public/main.css deleted file mode 100644 index 6f550c23..00000000 --- a/themes/tempra-cursive/public/main.css +++ /dev/null @@ -1,754 +0,0 @@ -* { - box-sizing: border-box; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; -} - -body { - font-family: cursive; - padding-bottom: 8px; -} - -/* Patch for Edge, until they fix emojis in arial x.x */ -@supports (-ms-ime-align:auto) { .user_content { font-family: Segoe UI Emoji, arial; } } - -ul { - padding-left: 0px; - padding-right: 0px; - height: 36px; - list-style-type: none; - border: 1px solid #ccc; - background-color: white; - margin-bottom: 12px; -} -li { - height: 35px; - padding-left: 10px; - padding-top: 8px; - padding-bottom: 8px; -} -li:hover { background: rgb(250,250,250); } -li a { - text-decoration: none; - color: black; - font-size: 17px; -} -.menu_left { - float: left; - border-right: 1px solid #ccc; - padding-right: 10px; - font-family: cursive; - padding-top: 4px; -} -.menu_right { - float: right; - border-left: 1px solid #ccc; - padding-right: 10px; -} - -#menu_forums a:after { - content: "Forums"; -} -.menu_topics a:after { - content: "Topics"; -} -.menu_account a:after { - content: "Account"; -} -.menu_profile a:after { - content: "Profile"; -} -.menu_panel a:after { - content: "Panel"; -} -.menu_logout a:after { - content: "Logout"; -} -.menu_login a:after { - content: "Login"; -} -.menu_register a:after { - content: "Register"; -} - -.alert_bell:before { - content: '🔔︎'; -} -.menu_bell { - cursor: default; -} -.menu_alerts { - font-size: 20px; - padding-top: 2px; - color: rgb(80,80,80); - z-index: 500; -} -.menu_alerts .alert_counter { - position: relative; - font-family: arial; - font-size: 8px; - top: -25px; - background-color: rgb(190,0,0); - color: white; - width: 14px; - left: 10px; - line-height: 8px; - padding-top: 2.5px; - height: 14px; - text-align: center; - border: white solid 1px; -} -.menu_alerts .alert_counter:empty { - display: none; -} - -.selectedAlert, .selectedAlert:hover { - background: white; - color: black; -} -.menu_alerts .alertList { - display: none; -} - -.selectedAlert .alertList { - position: absolute; - top: 51px; - display: block; - background: white; - font-size: 10px; - line-height: 16px; - width: 300px; - right: calc(5% + 7px); - border: 1px solid #ccc; - margin-bottom: 10px; -} -.alertItem { - padding: 8px; - overflow: hidden; - text-overflow: ellipsis; - padding-top: 15px; - padding-bottom: 16px; -} -.alertItem.withAvatar { - background-size: 60px; - background-repeat: no-repeat; - padding-right: 12px; - padding-left: 68px; - height: 50px; -} -.alertItem.withAvatar:not(:last-child) { - border-bottom: 1px solid rgb(230,230,230); -} -.alertItem.withAvatar .text { - overflow: hidden; - text-overflow: ellipsis; - float: right; - height: 40px; - width: 100%; - white-space: nowrap; -} -.alertItem .text { - font-size: 13px; - font-weight: normal; - margin-left: 5px; -} - -.container { - width: 90%; - padding: 0px; - margin-left: auto; - margin-right: auto; -} - -.rowblock { - border: 1px solid #ccc; - width: 100%; - padding: 0px; - padding-top: 0px; -} -.rowblock:empty { - display: none; -} -.rowsmall { - font-size: 12px; -} - -.bgsub { - display: none; -} -.bgavatars .rowitem { - background-repeat: no-repeat; - background-size: 50px; - padding-left: 58px; -} - -.colstack_left { - float: left; - width: 30%; - margin-right: 8px; -} -.colstack_right { - float: left; - width: 65%; - width: calc(70% - 15px); -} -.colstack_item { - border: 1px solid #ccc; - padding: 0px; - padding-top: 0px; - width: 100%; - margin-bottom: 12px; - overflow: hidden; - word-wrap: break-word; -} -.colstack_head { margin-bottom: 0px; } -.colstack_left:empty, .colstack_right:empty { - display: none; -} - -.colstack_grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - /*grid-gap: 15px;*/ - grid-gap: 12px; - margin-left: 5px; - margin-top: 2px; -} -.grid_item { - border: 1px solid #ccc; - word-wrap: break-word; - background-color: white; - width: 100%; - overflow: hidden; -} -.grid_stat, .grid_istat { - /*padding-top: 15px;*/ - text-align: center; - /*padding-bottom: 15px; - font-size: 20px;*/ - padding-top: 12px; - padding-bottom: 12px; - font-size: 16px; -} -.grid_istat { - /*margin-bottom: 10px;*/ - margin-bottom: 3px; -} - -.stat_green { - background-color: lightgreen; - border-color: lightgreen; -} -.stat_orange { - background-color: #ffe4b3; - border-color: #ffe4b3; -} -.stat_red { - background-color: #ffb2b2; - border-color: #ffb2b2; -} -.stat_disabled { - background-color: lightgray; - border-color: lightgray; -} - -.rowitem { - width: 100%; - padding-left: 8px; - padding-right: 8px; - padding-top: 12px; - padding-bottom: 12px; - background-color: white; - font-family: cursive; -} -.rowitem:not(:last-child) { - border-bottom: 1px dotted #ccc; -} -.rowitem a { - text-decoration: none; - color: black; -} -.rowitem a:hover { color: silver; } -.top_post { margin-bottom: 16px; } -.opthead { display: none; } - -.datarow { - padding-top: 10px; - padding-bottom: 10px; -} - -.formrow { - width: 100%; - background-color: white; -} - -/* Clearfix */ -.formrow:before, .formrow:after { - content: " "; - display: table; -} -.formrow:after { clear: both; } -.formrow:not(:last-child) { border-bottom: 1px dotted #ccc; } - -.formitem { - float: left; - padding: 10px; - min-width: 20%; - /*font-size: 17px;*/ - font-weight: normal; -} -.formitem:not(:last-child) { border-right: 1px dotted #ccc; } -.formitem.invisible_border { border: none; } - -/* Mostly for textareas */ -.formitem:only-child { width: 100%; } -.formitem textarea { - width: 100%; - height: 100px; - outline-color: #8e8e8e; -} -.formitem:has-child() { - margin: 0 auto; - float: none; -} -.formitem:not(:only-child) input, .formitem:not(:only-child) select { - padding: 3px;/*5px;*/ -} -.formitem:not(:only-child).formlabel { - padding-top: 15px;/*18px;*/ - padding-bottom: 12px;/*16px;*/ - /*padding-left: 15px;*/ -} -.formbutton { - padding: 7px; - display: block; - margin-left: auto; - margin-right: auto; - font-size: 15px; - border-color: #ccc; -} - -.dont_have_account { - color: #505050; - font-size: 12px; - font-weight: normal; - float: right; -} - -button, input[type="submit"] { - background: white; - border: 1px solid #8e8e8e; -} - -/* TODO: Add the avatars to the forum list */ -.extra_little_row_avatar { - display: none; -} -.shift_left { - float: left; -} -.shift_right { - float: right; -} - -/* Topics */ - -.topic_list .starter:before { - content: "Starter: "; -} - -.topic_sticky { - background-color: rgb(255,255,234); -} -.topic_closed { - background-color: rgb(248,248,248); -} - -.topic_status { - text-transform: none; - margin-left: 8px; - padding-left: 2px; - padding-right: 2px; - padding-top: 2px; - padding-bottom: 2px; - background-color: #E8E8E8; /* 232,232,232. All three RGB colours being the same seems to create a shade of gray */ - color: #505050; /* 80,80,80 */ - border-radius: 2px; -} -.topic_status:empty { display: none; } - -.username, .panel_tag { - text-transform: none; - margin-left: 0px; - padding-left: 0px; - padding-right: 0px; - padding-top: 2px; - padding-bottom: 2px; - color: #505050; /* 80,80,80 */ - font-size: 15px; - background: none; -} -button.username { - position: relative; - top: -0.25px; -} -.username.level { color: #303030; } -.username.real_username { - color: #404040; - font-size: 16px; - padding-right: 4px; -} -.username.real_username:hover { color: black; } - -.tag-text { - padding-top: 23px; - display: inline-block; -} - -.user_tag { - float: right; - color: #505050; - font-size: 16px; -} - -.post_item { - background-size: 128px; - padding-left: calc(128px + 12px); -} - -.controls { - width: 100%; - display: inline-block; - /*margin-top: 20px;*/ -} - -.controls > .username { - display: inline-block; - padding-bottom: 0px; -} - -.real_username { - margin-right: -8px; -} - -.mod_button > button { - font-family: cursive; - font-size: 12px; - color: #202020; - opacity: 0.7; - border: none; -} -.post_item > .mod_button > button:hover { - opacity: 0.9; -} - -.mod_button:not(:last-child) { - margin-right: 4px; -} - -.like_label:before { - content: "+1"; -} -.like_count_label:before { - content: "likes"; -} -.like_count_label { - color: #202020; - opacity: 0.7; - font-size: 12px; -} -.like_count { - color: #202020; - opacity: 0.7; - padding-left: 1px; - padding-right: 2px; - font-size: 12px; -} -.like_count:before { - content: "|"; - margin-right: 5px; -} -.edit_label:before { content: "Edit"; } -.trash_label:before { content: "Delete"; } -.pin_label:before { content: "Pin"; } -.unpin_label:before { content: "Unpin"; } -.flag_label:before { content: "Flag"; } -.level_label { margin-right: 1px; color: #505050; } -.level_label:before { content: "Level"; opacity:0.85; } - -.controls { - margin-top: 23px; - display: inline-block; - width: 100%; -} - -.action_item { - padding: 14px; - text-align: center; - background-color: rgb(255,245,245); -} - -.postQuote { - border: 1px solid #ccc; - background: white; - padding: 5px; - margin: 0px; - display: inline-block; - width: 100%; - margin-bottom: 8px; -} - -.level { - float: right; - border-left: none; - padding-left: 3px; - padding-right: 5px; - font-family: cursive; - font-size: 15px; - color: #202020; - opacity: 0.7; - border: none; -} - -.mention { - font-weight: bold; -} -.show_on_edit, .auto_hide, .hide_on_big, .show_on_mobile { - display: none; -} - -.alert { - display: block; - padding: 5px; - margin-bottom: 10px; - border: 1px solid #ccc; -} -.alert_success { - display: block; - padding: 5px; - border: 1px solid A2FC00; - margin-bottom: 10px; - background-color: DAF7A6; -} -.alert_error { - display: block; - padding: 5px; - border: 1px solid #FF004B; - margin-bottom: 8px; - background-color: #FEB7CC; -} -.prev_button, .next_button { - position: fixed; - top: 50%; - font-size: 30px; - border-width: 1px; - background-color: #FFFFFF; - border-style: dotted; - border-color: #505050; - padding: 0px; - padding-left: 5px; - padding-right: 5px; - z-index: 100; -} - -.prev_button a, .next_button a { - line-height: 28px; - margin-top: 2px; - margin-bottom: 0px; - display: block; - text-decoration: none; - color: #505050; -} -.prev_button { left: 14px; } -.next_button { right: 14px; } - -.head_tag_upshift { - color: #202020; - opacity: 0.7; - font-size: 12px; -} - -#profile_comments .rowitem { - background-repeat: no-repeat, repeat-y; - background-size: 128px; - padding-left: 136px; -} - -#profile_left_lane .avatarRow { - padding: 0; -} -#profile_left_pane .nameRow .username { - float: right; - font-weight: normal; -} -#profile_left_pane .report_item:after { - content: "Report"; -} - -/* Media Queries */ - -@media(min-width: 881px) { - .shrink_main { - float: left; - width: calc(75% - 12px); - } - .sidebar { - float: left; - width: 25%; - margin-left: 12px; - } -} - -@media (max-width: 880px) { - li { - height: 29px; - font-size: 15px; - padding-left: 9px; - padding-top: 2px; - padding-bottom: 6px; - } - ul { - height: 30px; - margin-top: 8px; - } - .menu_left { padding-right: 9px; padding-top: 2px; } - .menu_right { padding-right: 9px; } - .menu_alerts { - padding-left: 7px; - padding-right: 7px; - font-size: 18px; - } - - body { - padding-left: 4px; - padding-right: 4px; - margin: 0px !important; - width: 100% !important; - height: 100% !important; - overflow-x: hidden; - } - .container { width: auto; } - .sidebar { display: none; } - .selectedAlert .alertList { top: 37px; right: 4px; } -} - -@media (max-width: 810px) { - body { font-family: arial; } -} - -@media (max-width: 700px) { - li { - padding-left: 5px; - padding-top: 3px; - padding-bottom: 2px; - height: 25px; - } - li a { font-size: 14px; } - ul { height: 26px; } - .menu_left { padding-right: 5px; padding-top: 1px; } - .menu_right { padding-right: 5px; } - - .menu_alerts { - padding-left: 4px; - padding-right: 4px; - font-size: 16px; - padding-top: 1px; - } - .menu_alerts .alert_counter { - top: -23px; - left: 8px; - } - .selectedAlert .alertList { - top: 33px; - } - - .hide_on_mobile { - display: none; - } - .prev_button, .next_button { - top: auto; - bottom: 5px; - } - .colstack_grid { - grid-template-columns: none; - grid-gap: 8px; - } - .grid_istat { - margin-bottom: 0px; - } -} - -@media (max-width: 350px) { - .hide_on_micro { display: none !important; } -} - -@media (max-width: 470px) { - #menu_overview, .menu_profile, .hide_on_micro { display: none; } - .selectedAlert .alertList { - width: 135px; - margin-bottom: 5px; - } - .alertItem.withAvatar { - background-size: 36px; - text-align: right; - padding-left: 10px; - height: 46px; - } - .alertItem { - padding: 8px; - } - .alertItem.withAvatar .text { - width: calc(100% - 20px); - height: 30px; - white-space: normal; - } - .alertItem .text { - font-size: 10px; - font-weight: bold; - margin-left: 0px; - } - - .post_container { overflow: visible !important; } - .post_item { - background-position: 0px 2px !important; - background-size: 64px auto !important; - padding-left: 2px !important; - min-height: 96px; - position: relative !important; - } - .post_item > .user_content { - margin-left: 75px !important; - width: 100% !important; - min-height: 45px; - } - .post_item > .mod_button { - float: right !important; - margin-left: 2px !important; - position: relative; - top: -14px; - } - .post_item > .mod_button > button { opacity: 1; } - .post_item > .real_username { - position: absolute; - top: 70px; - float: left; - margin-top: -2px; - padding-top: 3px !important; - margin-right: 2px; - width: 60px; - font-size: 15px; - text-align: center; - } - .post_item > .controls { - margin-top: 0px; - margin-left: 74px; - width: calc(100% - 74px); - } - .container { width: 100% !important; } -} - -@media (max-width: 330px) { - li { padding-left: 6px; } - .menu_left { padding-right: 6px; } - .menu_alerts { border-left: none; } -} diff --git a/themes/tempra-cursive/public/panel.css b/themes/tempra-cursive/public/panel.css deleted file mode 100644 index b9b3fdb1..00000000 --- a/themes/tempra-cursive/public/panel.css +++ /dev/null @@ -1,88 +0,0 @@ -/* Control Panel */ - -.edit_button:before { - content: "Edit"; -} -.delete_button:after { - content: "Delete"; -} - -.tag-mini { - margin-left: 0px; - padding-left: 0px; - padding-right: 0px; - padding-top: 2px; - padding-bottom: 2px; - - font-family: cursive; - font-size: 12px; - color: #202020; - opacity: 0.7; -} - -.panel_floater { - float: right; -} -#panel_groups > .rowitem > .panel_floater { - float: none; -} -#panel_groups > .rowitem > .panel_floater > .panel_right_button { - float: right; -} -#panel_forums > .rowitem > .panel_floater { - float: none; -} -#panel_forums > .rowitem > .panel_floater > .panel_buttons { - float: right; -} -#panel_forums > .rowitem > span > .forum_name { - margin-right: 4px; -} -#panel_forums > .rowitem > .panel_floater > .panel_buttons > .panel_right_button { - color: #505050; - font-size: 14px; -} - -#panel_word_filters .itemSeparator:before { - content: " || "; - padding-left: 2px; - padding-right: 2px; -} - -.panel_rank_tag, .forum_preset, .forum_active { - float: none; - color: #202020; - opacity: 0.7; - font-size: 10px; -} -.panel_rank_tag_admin:before { content: "Admin Group"; } -.panel_rank_tag_mod:before { content: "Mod Group"; } -.panel_rank_tag_banned:before { content: "Banned Group"; } -.panel_rank_tag_guest:before { content: "Guest Group"; } -.panel_rank_tag_member:before { content: "Member Group"; } - -.forum_preset_announce:after { content: "Announcements"; } -.forum_preset_members:after { content: "Member Only"; } -.forum_preset_staff:after { content: "Staff Only"; } -.forum_preset_admins:after { content: "Admin Only"; } -.forum_preset_archive:after { content: "Archive"; } -.forum_preset_all:after { content: "Public"; } -.forum_preset_custom, .forum_preset_ { display: none !important; } -.forum_active_Hide:before { content: "Hidden"; } -.forum_active_Hide + .forum_preset:before { content: " | "; } -.forum_active_Show { display: none !important; } -.forum_active_name { color: #707070; } -.builtin_forum_divider { border-bottom-style: solid; } - -.perm_preset_no_access:before { content: "No Access"; color: maroon; } -.perm_preset_read_only:before { content: "Read Only"; color: green; } -.perm_preset_can_post:before { content: "Can Post"; color: green; } -.perm_preset_can_moderate:before { content: "Can Moderate"; color: darkblue; } -.perm_preset_custom:before { content: "Custom"; color: black; } -.perm_preset_default:before { content: "Default"; } - -@media(max-width: 1300px) { - .theme_row { - background-image: none !important; - } -} diff --git a/themes/tempra-cursive/public/post-avatar-bg.jpg b/themes/tempra-cursive/public/post-avatar-bg.jpg deleted file mode 100644 index 70739f9346b9e5a5c9fd85d7432ee0d3ac957fc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 539 zcmb7r;Ft(yFdV$vj#hL-SxX%>tqpa+bQQHiJ{eUKFsS8lwz(I>%yR$e5 aPl%9nj$@!1D)Ts!F8* diff --git a/themes/tempra-cursive/tempra-cursive.png b/themes/tempra-cursive/tempra-cursive.png deleted file mode 100644 index 4d731aae3beeafe62f31d95a44880165e8d532e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 245735 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 !fR;f{ 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 diff --git a/themes/tempra-cursive/theme.json b/themes/tempra-cursive/theme.json deleted file mode 100644 index 344cf847..00000000 --- a/themes/tempra-cursive/theme.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "Name": "tempra-cursive", - "FriendlyName": "Tempra Cursive", - "Version": "0.1.0-dev", - "Creator": "Azareal", - "FullImage": "tempra-cursive.png", - "ForkOf": "tempra-simple", - "MobileFriendly": true, - "HideFromThemes": true, - "BgAvatars":true, - "URL": "github.com/Azareal/Gosora", - "Docks":["topMenu"], - "Templates": [ - { - "Name": "topic", - "Source": "topic" - } - ], - "Docks":["rightSidebar"] -} diff --git a/themes/tempra-simple/public/main.css b/themes/tempra-simple/public/main.css index 755209f3..c77caa7a 100644 --- a/themes/tempra-simple/public/main.css +++ b/themes/tempra-simple/public/main.css @@ -149,6 +149,12 @@ li a { margin-left: auto; margin-right: auto; } +#back { + display: flex; +} +#main { + width: 100%; +} .rowblock { border: 1px solid hsl(0, 0%, 80%); @@ -420,6 +426,9 @@ input, select { } /* TODO: Add the avatars to the forum list */ +.forum_list .forum_nodesc { + font-style: italic; +} .extra_little_row_avatar { display: none; } @@ -474,14 +483,20 @@ input, select { height: 58px; overflow: hidden; } -.topic_left img, .topic_right img { +.topic_right_inside { + display: flex; +} +.topic_left img, .topic_right_inside img { width: 64px; height: auto; } -.topic_left .topic_inner_left, .topic_right > span { +.topic_left .topic_inner_left, .topic_right_inside > span { margin-top: 10px; margin-left: 8px; } +.topic_middle { + display: none; +} .postImage { max-width: 100%; diff --git a/themes/tempra-simple/public/profile.css b/themes/tempra-simple/public/profile.css new file mode 100644 index 00000000..e69de29b diff --git a/update-deps-linux b/update-deps-linux index 21d1305a..9ebca826 100644 --- a/update-deps-linux +++ b/update-deps-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/update-deps.bat b/update-deps.bat index 510cb126..3575b1d6 100644 --- a/update-deps.bat +++ b/update-deps.bat @@ -26,6 +26,13 @@ if %errorlevel% neq 0 ( exit /b %errorlevel% ) +echo Updating Argon2 +go get -u golang.org/x/crypto/argon2 +if %errorlevel% neq 0 ( + pause + exit /b %errorlevel% +) + echo Updating /x/system/windows (dependency for gopsutil) go get -u golang.org/x/sys/windows if %errorlevel% neq 0 (