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.
This commit is contained in:
Azareal 2018-05-27 19:18:29 +10:00
parent 8ff8ce8e51
commit ca80d0dd6f
97 changed files with 3748 additions and 3505 deletions

View File

@ -8,10 +8,4 @@ exclude_patterns:
- "public/jquery-3.1.1.min.js" - "public/jquery-3.1.1.min.js"
- "public/EQCSS.min.js" - "public/EQCSS.min.js"
- "public/EQCSS.js" - "public/EQCSS.js"
- "template_list.go" - "public/Sortable-1.4.0/*"
- "template_forum.go"
- "template_forums.go"
- "template_topic.go"
- "template_topic_alt.go"
- "template_topics.go"
- "template_profile.go"

View File

@ -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/bcrypt
go get -u golang.org/x/crypto/argon2
go get -u github.com/StackExchange/wmi go get -u github.com/StackExchange/wmi
go get -u github.com/Azareal/gopsutil 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 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 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) ![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. * 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. * 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. * 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 # 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). 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. * 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 # Developers

View File

@ -1,30 +1,58 @@
/* /*
* *
* Gosora Authentication Interface * Gosora Authentication Interface
* Copyright Azareal 2017 - 2018 * Copyright Azareal 2017 - 2019
* *
*/ */
package common package common
import "errors" import (
import "strconv" "database/sql"
import "net/http" "errors"
import "database/sql" "net/http"
"strconv"
"strings"
import "golang.org/x/crypto/bcrypt" "../query_gen/lib"
import "../query_gen/lib" //"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
)
var Auth AuthInt var Auth AuthInt
const SaltLength int = 32
const SessionLength int = 80
// ErrMismatchedHashAndPassword is thrown whenever a hash doesn't match it's unhashed password // ErrMismatchedHashAndPassword is thrown whenever a hash doesn't match it's unhashed password
var ErrMismatchedHashAndPassword = bcrypt.ErrMismatchedHashAndPassword var ErrMismatchedHashAndPassword = bcrypt.ErrMismatchedHashAndPassword
// nolint // 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 // 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 ErrPasswordTooLong = errors.New("The password you selected is too long")
var ErrWrongPassword = errors.New("That's not the correct password.") 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 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 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. // AuthInt is the main authentication interface.
type AuthInt interface { type AuthInt interface {
@ -176,3 +204,75 @@ func (auth *DefaultAuth) CreateSession(uid int) (session string, err error) {
} }
return session, nil 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
}
*/

View File

@ -3,6 +3,7 @@ package common
import ( import (
"database/sql" "database/sql"
"log" "log"
"time"
"../query_gen/lib" "../query_gen/lib"
) )
@ -19,9 +20,7 @@ const Gigabyte int = Megabyte * 1024
const Terabyte int = Gigabyte * 1024 const Terabyte int = Gigabyte * 1024
const Petabyte int = Terabyte * 1024 const Petabyte int = Terabyte * 1024
const SaltLength int = 32 var StartTime time.Time
const SessionLength int = 80
var TmplPtrMap = make(map[string]interface{}) var TmplPtrMap = make(map[string]interface{})
// ErrNoRows is an alias of sql.ErrNoRows, just in case we end up with non-database/sql datastores // ErrNoRows is an alias of sql.ErrNoRows, just in case we end up with non-database/sql datastores

View File

@ -19,8 +19,7 @@ type DefaultViewCounter struct {
insert *sql.Stmt insert *sql.Stmt
} }
func NewGlobalViewCounter() (*DefaultViewCounter, error) { func NewGlobalViewCounter(acc *qgen.Accumulator) (*DefaultViewCounter, error) {
acc := qgen.Builder.Accumulator()
counter := &DefaultViewCounter{ counter := &DefaultViewCounter{
currentBucket: 0, currentBucket: 0,
insert: acc.Insert("viewchunks").Columns("count, createdAt").Fields("?,UTC_TIMESTAMP()").Prepare(), insert: acc.Insert("viewchunks").Columns("count, createdAt").Fields("?,UTC_TIMESTAMP()").Prepare(),

52
common/email_store.go Normal file
View File

@ -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
}

View File

@ -314,6 +314,7 @@ func (mgs *MemoryGroupStore) GetRange(lower int, higher int) (groups []*Group, e
return mgs.GetAll() return mgs.GetAll()
} }
// TODO: Simplify these four conditionals into two
if lower == 0 { if lower == 0 {
if higher < 0 { if higher < 0 {
return nil, errors.New("higher may not be lower than 0") return nil, errors.New("higher may not be lower than 0")

View File

@ -74,9 +74,7 @@ type Paginator struct {
} }
type TopicPage struct { type TopicPage struct {
Title string *Header
CurrentUser User
Header *Header
ItemList []ReplyUser ItemList []ReplyUser
Topic TopicUser Topic TopicUser
Poll Poll Poll Poll
@ -93,9 +91,7 @@ type TopicListPage struct {
} }
type ForumPage struct { type ForumPage struct {
Title string *Header
CurrentUser User
Header *Header
ItemList []*TopicsRow ItemList []*TopicsRow
Forum *Forum Forum *Forum
Paginator Paginator
@ -132,6 +128,14 @@ type IPSearchPage struct {
IP string IP string
} }
type EmailListPage struct {
Title string
CurrentUser User
Header *Header
ItemList []Email
Something interface{}
}
type PanelStats struct { type PanelStats struct {
Users int Users int
Groups int Groups int
@ -169,6 +173,19 @@ type PanelDashboardPage struct {
GridItems []GridElement GridItems []GridElement
} }
type PanelSetting struct {
*Setting
FriendlyName string
}
type PanelSettingPage struct {
*Header
Stats PanelStats
Zone string
ItemList []OptionLabel
Setting *PanelSetting
}
type PanelTimeGraph struct { type PanelTimeGraph struct {
Series []int64 // The counts on the left Series []int64 // The counts on the left
Labels []int64 // unixtimes for the bottom, gets converted into 1:00, 2:00, etc. with JS 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 { type PanelUserPage struct {
Title string *Header
CurrentUser User
Header *Header
Stats PanelStats Stats PanelStats
Zone string Zone string
ItemList []User ItemList []*User
Paginator Paginator
} }

View File

@ -39,7 +39,7 @@ type LanguagePack struct {
Levels LevelPhrases Levels LevelPhrases
GlobalPerms map[string]string GlobalPerms map[string]string
LocalPerms map[string]string LocalPerms map[string]string
SettingLabels map[string]string SettingPhrases map[string]string
PermPresets map[string]string PermPresets map[string]string
Accounts map[string]string // TODO: Apply these phrases in the software proper Accounts map[string]string // TODO: Apply these phrases in the software proper
UserAgents map[string]string UserAgents map[string]string
@ -148,16 +148,16 @@ func GetLocalPermPhrase(name string) string {
return res return res
} }
func GetSettingLabel(name string) string { func GetSettingPhrase(name string) string {
res, ok := currentLangPack.Load().(*LanguagePack).SettingLabels[name] res, ok := currentLangPack.Load().(*LanguagePack).SettingPhrases[name]
if !ok { if !ok {
return getPhrasePlaceholder("settings", name) return getPhrasePlaceholder("settings", name)
} }
return res return res
} }
func GetAllSettingLabels() map[string]string { func GetAllSettingPhrases() map[string]string {
return currentLangPack.Load().(*LanguagePack).SettingLabels return currentLangPack.Load().(*LanguagePack).SettingPhrases
} }
func GetAllPermPresets() map[string]string { func GetAllPermPresets() map[string]string {

53
common/report_store.go Normal file
View File

@ -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)
}

View File

@ -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 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 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 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 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 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) { func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) {
return &HeaderLite{ return simpleUserCheck(w, r, user)
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
} }
// SimpleUserCheck is back from the grave, yay :D // SimpleUserCheck is back from the grave, yay :D
func simpleUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) { func simpleUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) {
headerLite = &HeaderLite{ return &HeaderLite{
Site: Site, Site: Site,
Settings: SettingBox.Load().(SettingMap), Settings: SettingBox.Load().(SettingMap),
} }, nil
return headerLite, nil
} }
// TODO: Add the ability for admins to restrict certain themes to certain groups? // TODO: Add the ability for admins to restrict certain themes to certain groups?

View File

@ -54,6 +54,12 @@ func init() {
}) })
} }
func (setting *Setting) Copy() (out *Setting) {
out = &Setting{Name: ""}
*out = *setting
return out
}
func LoadSettings() error { func LoadSettings() error {
var sBox = SettingMap(make(map[string]interface{})) var sBox = SettingMap(make(map[string]interface{}))
settings, err := sBox.BypassGetAll() settings, err := sBox.BypassGetAll()

View File

@ -51,6 +51,7 @@ type dbConfig struct {
type config struct { type config struct {
SslPrivkey string SslPrivkey string
SslFullchain string SslFullchain string
HashAlgo string // Defaults to bcrypt, and in the future, possibly something stronger
MaxRequestSize int MaxRequestSize int
CacheTopicUser int CacheTopicUser int
@ -103,6 +104,11 @@ func ProcessConfig() error {
if Config.MaxUsernameLength == 0 { if Config.MaxUsernameLength == 0 {
Config.MaxUsernameLength = 100 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 // We need this in here rather than verifyConfig as switchToTestDB() currently overwrites the values it verifies
if DbConfig.TestDbname == DbConfig.Dbname { if DbConfig.TestDbname == DbConfig.Dbname {

View File

@ -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") log.Print("Compiling the templates")
var now = time.Now() 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, "", ""}) 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) 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) topicIDTmpl, err := c.Compile("topic.html", "templates/", "common.TopicPage", tpage, varList)
if err != nil { if err != nil {
return err return err
@ -203,17 +212,16 @@ func CompileTemplates() error {
var topicsList []*TopicsRow 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"}) 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}} topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, Paginator{[]int{1}, 1, 1}}
topicListTmpl, err := c.Compile("topics.html", "templates/", "common.TopicListPage", topicListPage, varList) topicListTmpl, err := c.Compile("topics.html", "templates/", "common.TopicListPage", topicListPage, varList)
if err != nil { if err != nil {
return err 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) 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) forumTmpl, err := c.Compile("forum.html", "templates/", "common.ForumPage", forumPage, varList)
if err != nil { if err != nil {
return err return err

239
common/theme.go Normal file
View File

@ -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 ""
}

View File

@ -1,79 +1,30 @@
/* Copyright Azareal 2016 - 2018 */
package common package common
import ( import (
//"fmt"
"bytes"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"mime"
"net/http" "net/http"
"os" "os"
"path/filepath"
"reflect" "reflect"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"text/template"
"../query_gen/lib" "../query_gen/lib"
) )
type ThemeList map[string]*Theme 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 DefaultThemeBox atomic.Value
var ChangeDefaultThemeMutex sync.Mutex var ChangeDefaultThemeMutex sync.Mutex
// TODO: Use this when the default theme doesn't exist // TODO: Use this when the default theme doesn't exist
var fallbackTheme = "cosora" var fallbackTheme = "cosora"
var overridenTemplates = make(map[string]bool) var overridenTemplates = make(map[string]bool) // ? What is this used for?
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
}
type ThemeStmts struct { type ThemeStmts struct {
getThemes *sql.Stmt 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 // TODO: Make the initThemes and LoadThemes functions less confusing
// ? - Delete themes which no longer exist in the themes folder from the database? // ? - Delete themes which no longer exist in the themes folder from the database?
func (themes ThemeList) LoadActiveStatus() error { func (themes ThemeList) LoadActiveStatus() error {
@ -141,221 +175,8 @@ func (themes ThemeList) LoadStaticFiles() error {
return nil 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() { func ResetTemplateOverrides() {
log.Print("Resetting the template overrides") log.Print("Resetting the template overrides")
for name := range overridenTemplates { for name := range overridenTemplates {
log.Print("Resetting '" + name + "' template override") log.Print("Resetting '" + name + "' template override")
@ -542,17 +363,3 @@ func GetDefaultThemeName() string {
func SetDefaultThemeName(name string) { func SetDefaultThemeName(name string) {
DefaultThemeBox.Store(name) 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 ""
}

View File

@ -14,20 +14,14 @@ import (
"time" "time"
"../query_gen/lib" "../query_gen/lib"
"golang.org/x/crypto/bcrypt"
) )
// TODO: Replace any literals with this // TODO: Replace any literals with this
var BanGroup = 4 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 // 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} 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
//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 ErrNoTempGroup = errors.New("We couldn't find a temporary group for this user") var ErrNoTempGroup = errors.New("We couldn't find a temporary group for this user")
type User struct { type User struct {
@ -369,33 +363,6 @@ func BuildAvatar(uid int, avatar string) string {
return strings.Replace(Config.Noavatar, "{id}", strconv.Itoa(uid), 1) 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 // TODO: Move this to *User
func SetPassword(uid int, password string) error { func SetPassword(uid int, password string) error {
hashedPassword, salt, err := GeneratePassword(password) hashedPassword, salt, err := GeneratePassword(password)

View File

@ -20,6 +20,7 @@ type UserStore interface {
DirtyGet(id int) *User DirtyGet(id int) *User
Get(id int) (*User, error) Get(id int) (*User, error)
Exists(id int) bool Exists(id int) bool
GetOffset(offset int, perPage int) (users []*User, err error)
//BulkGet(ids []int) ([]*User, error) //BulkGet(ids []int) ([]*User, error)
BulkGetMap(ids []int) (map[int]*User, error) BulkGetMap(ids []int) (map[int]*User, error)
BypassGet(id int) (*User, error) BypassGet(id int) (*User, error)
@ -35,6 +36,7 @@ type DefaultUserStore struct {
cache UserCache cache UserCache
get *sql.Stmt get *sql.Stmt
getOffset *sql.Stmt
exists *sql.Stmt exists *sql.Stmt
register *sql.Stmt register *sql.Stmt
usernameExists *sql.Stmt usernameExists *sql.Stmt
@ -51,6 +53,7 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) {
return &DefaultUserStore{ return &DefaultUserStore{
cache: cache, 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 = ?", "", ""), 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 = ?", "", ""), 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 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 = ?", "", ""), usernameExists: acc.SimpleSelect("users", "name", "name = ?", "", ""),
@ -92,6 +95,29 @@ func (mus *DefaultUserStore) Get(id int) (*User, error) {
return user, err 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: 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? // TODO: ID of 0 should always error?
func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) { func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) {

View File

@ -48,7 +48,7 @@ func GenerateSafeString(length int) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return base64.URLEncoding.EncodeToString(rb), nil return base64.StdEncoding.EncodeToString(rb), nil
} }
// TODO: Write a test for this // TODO: Write a test for this

View File

@ -10,6 +10,9 @@ go get -u github.com/denisenkom/go-mssqldb
echo "Updating bcrypt" echo "Updating bcrypt"
go get -u golang.org/x/crypto/bcrypt go get -u golang.org/x/crypto/bcrypt
echo "Updating Argon2"
go get -u golang.org/x/crypto/argon2
echo "Updating gopsutil" echo "Updating gopsutil"
go get -u github.com/Azareal/gopsutil go get -u github.com/Azareal/gopsutil

View File

@ -29,6 +29,13 @@ if %errorlevel% neq 0 (
exit /b %errorlevel% 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) echo Updating /x/sys/windows (dependency for gopsutil)
go get -u golang.org/x/sys/windows go get -u golang.org/x/sys/windows
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (

7
docs/templates.md Normal file
View File

@ -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.

View File

@ -29,6 +29,13 @@ if %errorlevel% neq 0 (
exit /b %errorlevel% 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) echo Updating /x/sys/windows (dependency for gopsutil)
go get -u golang.org/x/sys/windows go get -u golang.org/x/sys/windows
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (

View File

@ -10,14 +10,10 @@ import "./common"
// nolint // nolint
type Stmts struct { type Stmts struct {
isPluginActive *sql.Stmt isPluginActive *sql.Stmt
getUsersOffset *sql.Stmt
isThemeDefault *sql.Stmt isThemeDefault *sql.Stmt
getEmailsByUser *sql.Stmt
getTopicBasic *sql.Stmt
forumEntryExists *sql.Stmt forumEntryExists *sql.Stmt
groupEntryExists *sql.Stmt groupEntryExists *sql.Stmt
getForumTopics *sql.Stmt getForumTopics *sql.Stmt
createReport *sql.Stmt
addForumPermsToForum *sql.Stmt addForumPermsToForum *sql.Stmt
addPlugin *sql.Stmt addPlugin *sql.Stmt
addTheme *sql.Stmt addTheme *sql.Stmt
@ -29,13 +25,11 @@ type Stmts struct {
updateGroupPerms *sql.Stmt updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt updateGroup *sql.Stmt
updateEmail *sql.Stmt updateEmail *sql.Stmt
verifyEmail *sql.Stmt
setTempGroup *sql.Stmt setTempGroup *sql.Stmt
updateWordFilter *sql.Stmt updateWordFilter *sql.Stmt
bumpSync *sql.Stmt bumpSync *sql.Stmt
deleteActivityStreamMatch *sql.Stmt deleteActivityStreamMatch *sql.Stmt
deleteWordFilter *sql.Stmt deleteWordFilter *sql.Stmt
reportExists *sql.Stmt
getActivityFeedByWatcher *sql.Stmt getActivityFeedByWatcher *sql.Stmt
getActivityCountByWatcher *sql.Stmt getActivityCountByWatcher *sql.Stmt
@ -59,14 +53,6 @@ func _gen_mssql() (err error) {
return err 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.") common.DebugLog("Preparing isThemeDefault statement.")
stmts.isThemeDefault, err = db.Prepare("SELECT [default] FROM [themes] WHERE [uname] = ?1") stmts.isThemeDefault, err = db.Prepare("SELECT [default] FROM [themes] WHERE [uname] = ?1")
if err != nil { if err != nil {
@ -75,22 +61,6 @@ func _gen_mssql() (err error) {
return err 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.") 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") 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 { if err != nil {
@ -115,14 +85,6 @@ func _gen_mssql() (err error) {
return err 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.") common.DebugLog("Preparing addForumPermsToForum statement.")
stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) VALUES (?,?,?,?)") stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) VALUES (?,?,?,?)")
if err != nil { if err != nil {
@ -211,14 +173,6 @@ func _gen_mssql() (err error) {
return err 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.") common.DebugLog("Preparing setTempGroup statement.")
stmts.setTempGroup, err = db.Prepare("UPDATE [users] SET [temp_group] = ? WHERE [uid] = ?") stmts.setTempGroup, err = db.Prepare("UPDATE [users] SET [temp_group] = ? WHERE [uid] = ?")
if err != nil { if err != nil {
@ -259,13 +213,5 @@ func _gen_mssql() (err error) {
return err 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 return nil
} }

View File

@ -12,14 +12,10 @@ import "./common"
// nolint // nolint
type Stmts struct { type Stmts struct {
isPluginActive *sql.Stmt isPluginActive *sql.Stmt
getUsersOffset *sql.Stmt
isThemeDefault *sql.Stmt isThemeDefault *sql.Stmt
getEmailsByUser *sql.Stmt
getTopicBasic *sql.Stmt
forumEntryExists *sql.Stmt forumEntryExists *sql.Stmt
groupEntryExists *sql.Stmt groupEntryExists *sql.Stmt
getForumTopics *sql.Stmt getForumTopics *sql.Stmt
createReport *sql.Stmt
addForumPermsToForum *sql.Stmt addForumPermsToForum *sql.Stmt
addPlugin *sql.Stmt addPlugin *sql.Stmt
addTheme *sql.Stmt addTheme *sql.Stmt
@ -31,13 +27,11 @@ type Stmts struct {
updateGroupPerms *sql.Stmt updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt updateGroup *sql.Stmt
updateEmail *sql.Stmt updateEmail *sql.Stmt
verifyEmail *sql.Stmt
setTempGroup *sql.Stmt setTempGroup *sql.Stmt
updateWordFilter *sql.Stmt updateWordFilter *sql.Stmt
bumpSync *sql.Stmt bumpSync *sql.Stmt
deleteActivityStreamMatch *sql.Stmt deleteActivityStreamMatch *sql.Stmt
deleteWordFilter *sql.Stmt deleteWordFilter *sql.Stmt
reportExists *sql.Stmt
getActivityFeedByWatcher *sql.Stmt getActivityFeedByWatcher *sql.Stmt
getActivityCountByWatcher *sql.Stmt getActivityCountByWatcher *sql.Stmt
@ -60,13 +54,6 @@ func _gen_mysql() (err error) {
return err 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.") common.DebugLog("Preparing isThemeDefault statement.")
stmts.isThemeDefault, err = db.Prepare("SELECT `default` FROM `themes` WHERE `uname` = ?") stmts.isThemeDefault, err = db.Prepare("SELECT `default` FROM `themes` WHERE `uname` = ?")
if err != nil { if err != nil {
@ -74,20 +61,6 @@ func _gen_mysql() (err error) {
return err 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.") common.DebugLog("Preparing forumEntryExists statement.")
stmts.forumEntryExists, err = db.Prepare("SELECT `fid` FROM `forums` WHERE `name` = '' ORDER BY `fid` ASC LIMIT 0,1") stmts.forumEntryExists, err = db.Prepare("SELECT `fid` FROM `forums` WHERE `name` = '' ORDER BY `fid` ASC LIMIT 0,1")
if err != nil { if err != nil {
@ -109,13 +82,6 @@ func _gen_mysql() (err error) {
return err 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.") common.DebugLog("Preparing addForumPermsToForum statement.")
stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) VALUES (?,?,?,?)") stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) VALUES (?,?,?,?)")
if err != nil { if err != nil {
@ -193,13 +159,6 @@ func _gen_mysql() (err error) {
return err 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.") common.DebugLog("Preparing setTempGroup statement.")
stmts.setTempGroup, err = db.Prepare("UPDATE `users` SET `temp_group` = ? WHERE `uid` = ?") stmts.setTempGroup, err = db.Prepare("UPDATE `users` SET `temp_group` = ? WHERE `uid` = ?")
if err != nil { if err != nil {
@ -235,12 +194,5 @@ func _gen_mysql() (err error) {
return err 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 return nil
} }

View File

@ -9,6 +9,10 @@ import "./common"
// nolint // nolint
type Stmts struct { type Stmts struct {
addForumPermsToForum *sql.Stmt
addPlugin *sql.Stmt
addTheme *sql.Stmt
createWordFilter *sql.Stmt
updatePlugin *sql.Stmt updatePlugin *sql.Stmt
updatePluginInstall *sql.Stmt updatePluginInstall *sql.Stmt
updateTheme *sql.Stmt updateTheme *sql.Stmt
@ -16,7 +20,6 @@ type Stmts struct {
updateGroupPerms *sql.Stmt updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt updateGroup *sql.Stmt
updateEmail *sql.Stmt updateEmail *sql.Stmt
verifyEmail *sql.Stmt
setTempGroup *sql.Stmt setTempGroup *sql.Stmt
updateWordFilter *sql.Stmt updateWordFilter *sql.Stmt
bumpSync *sql.Stmt bumpSync *sql.Stmt
@ -35,6 +38,34 @@ type Stmts struct {
func _gen_pgsql() (err error) { func _gen_pgsql() (err error) {
common.DebugLog("Building the generated statements") 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.") common.DebugLog("Preparing updatePlugin statement.")
stmts.updatePlugin, err = db.Prepare("UPDATE `plugins` SET `active` = ? WHERE `uname` = ?") stmts.updatePlugin, err = db.Prepare("UPDATE `plugins` SET `active` = ? WHERE `uname` = ?")
if err != nil { if err != nil {
@ -84,13 +115,6 @@ func _gen_pgsql() (err error) {
return err 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.") common.DebugLog("Preparing setTempGroup statement.")
stmts.setTempGroup, err = db.Prepare("UPDATE `users` SET `temp_group` = ? WHERE `uid` = ?") stmts.setTempGroup, err = db.Prepare("UPDATE `users` SET `temp_group` = ? WHERE `uid` = ?")
if err != nil { if err != nil {

View File

@ -14,6 +14,7 @@ import (
"./common" "./common"
"./common/counters" "./common/counters"
"./routes" "./routes"
"./routes/panel"
) )
var ErrNoRoute = errors.New("That route doesn't exist.") var ErrNoRoute = errors.New("That route doesn't exist.")
@ -27,21 +28,21 @@ var RouteMap = map[string]interface{}{
"routes.ChangeTheme": routes.ChangeTheme, "routes.ChangeTheme": routes.ChangeTheme,
"routes.ShowAttachment": routes.ShowAttachment, "routes.ShowAttachment": routes.ShowAttachment,
"common.RouteWebsockets": common.RouteWebsockets, "common.RouteWebsockets": common.RouteWebsockets,
"routeReportSubmit": routeReportSubmit, "routes.ReportSubmit": routes.ReportSubmit,
"routes.CreateTopic": routes.CreateTopic, "routes.CreateTopic": routes.CreateTopic,
"routes.TopicList": routes.TopicList, "routes.TopicList": routes.TopicList,
"routePanelForums": routePanelForums, "panel.Forums": panel.Forums,
"routePanelForumsCreateSubmit": routePanelForumsCreateSubmit, "panel.ForumsCreateSubmit": panel.ForumsCreateSubmit,
"routePanelForumsDelete": routePanelForumsDelete, "panel.ForumsDelete": panel.ForumsDelete,
"routePanelForumsDeleteSubmit": routePanelForumsDeleteSubmit, "panel.ForumsDeleteSubmit": panel.ForumsDeleteSubmit,
"routePanelForumsEdit": routePanelForumsEdit, "panel.ForumsEdit": panel.ForumsEdit,
"routePanelForumsEditSubmit": routePanelForumsEditSubmit, "panel.ForumsEditSubmit": panel.ForumsEditSubmit,
"routePanelForumsEditPermsSubmit": routePanelForumsEditPermsSubmit, "panel.ForumsEditPermsSubmit": panel.ForumsEditPermsSubmit,
"routePanelForumsEditPermsAdvance": routePanelForumsEditPermsAdvance, "panel.ForumsEditPermsAdvance": panel.ForumsEditPermsAdvance,
"routePanelForumsEditPermsAdvanceSubmit": routePanelForumsEditPermsAdvanceSubmit, "panel.ForumsEditPermsAdvanceSubmit": panel.ForumsEditPermsAdvanceSubmit,
"routePanelSettings": routePanelSettings, "panel.Settings": panel.Settings,
"routePanelSettingEdit": routePanelSettingEdit, "panel.SettingEdit": panel.SettingEdit,
"routePanelSettingEditSubmit": routePanelSettingEditSubmit, "panel.SettingEditSubmit": panel.SettingEditSubmit,
"routePanelWordFilters": routePanelWordFilters, "routePanelWordFilters": routePanelWordFilters,
"routePanelWordFiltersCreateSubmit": routePanelWordFiltersCreateSubmit, "routePanelWordFiltersCreateSubmit": routePanelWordFiltersCreateSubmit,
"routePanelWordFiltersEdit": routePanelWordFiltersEdit, "routePanelWordFiltersEdit": routePanelWordFiltersEdit,
@ -63,31 +64,31 @@ var RouteMap = map[string]interface{}{
"routePanelUsers": routePanelUsers, "routePanelUsers": routePanelUsers,
"routePanelUsersEdit": routePanelUsersEdit, "routePanelUsersEdit": routePanelUsersEdit,
"routePanelUsersEditSubmit": routePanelUsersEditSubmit, "routePanelUsersEditSubmit": routePanelUsersEditSubmit,
"routePanelAnalyticsViews": routePanelAnalyticsViews, "panel.AnalyticsViews": panel.AnalyticsViews,
"routePanelAnalyticsRoutes": routePanelAnalyticsRoutes, "panel.AnalyticsRoutes": panel.AnalyticsRoutes,
"routePanelAnalyticsAgents": routePanelAnalyticsAgents, "panel.AnalyticsAgents": panel.AnalyticsAgents,
"routePanelAnalyticsSystems": routePanelAnalyticsSystems, "panel.AnalyticsSystems": panel.AnalyticsSystems,
"routePanelAnalyticsLanguages": routePanelAnalyticsLanguages, "panel.AnalyticsLanguages": panel.AnalyticsLanguages,
"routePanelAnalyticsReferrers": routePanelAnalyticsReferrers, "panel.AnalyticsReferrers": panel.AnalyticsReferrers,
"routePanelAnalyticsRouteViews": routePanelAnalyticsRouteViews, "panel.AnalyticsRouteViews": panel.AnalyticsRouteViews,
"routePanelAnalyticsAgentViews": routePanelAnalyticsAgentViews, "panel.AnalyticsAgentViews": panel.AnalyticsAgentViews,
"routePanelAnalyticsForumViews": routePanelAnalyticsForumViews, "panel.AnalyticsForumViews": panel.AnalyticsForumViews,
"routePanelAnalyticsSystemViews": routePanelAnalyticsSystemViews, "panel.AnalyticsSystemViews": panel.AnalyticsSystemViews,
"routePanelAnalyticsLanguageViews": routePanelAnalyticsLanguageViews, "panel.AnalyticsLanguageViews": panel.AnalyticsLanguageViews,
"routePanelAnalyticsReferrerViews": routePanelAnalyticsReferrerViews, "panel.AnalyticsReferrerViews": panel.AnalyticsReferrerViews,
"routePanelAnalyticsPosts": routePanelAnalyticsPosts, "panel.AnalyticsPosts": panel.AnalyticsPosts,
"routePanelAnalyticsTopics": routePanelAnalyticsTopics, "panel.AnalyticsTopics": panel.AnalyticsTopics,
"routePanelAnalyticsForums": routePanelAnalyticsForums, "panel.AnalyticsForums": panel.AnalyticsForums,
"routePanelGroups": routePanelGroups, "routePanelGroups": routePanelGroups,
"routePanelGroupsEdit": routePanelGroupsEdit, "routePanelGroupsEdit": routePanelGroupsEdit,
"routePanelGroupsEditPerms": routePanelGroupsEditPerms, "routePanelGroupsEditPerms": routePanelGroupsEditPerms,
"routePanelGroupsEditSubmit": routePanelGroupsEditSubmit, "routePanelGroupsEditSubmit": routePanelGroupsEditSubmit,
"routePanelGroupsEditPermsSubmit": routePanelGroupsEditPermsSubmit, "routePanelGroupsEditPermsSubmit": routePanelGroupsEditPermsSubmit,
"routePanelGroupsCreateSubmit": routePanelGroupsCreateSubmit, "routePanelGroupsCreateSubmit": routePanelGroupsCreateSubmit,
"routePanelBackups": routePanelBackups, "panel.Backups": panel.Backups,
"routePanelLogsRegs": routePanelLogsRegs, "panel.LogsRegs": panel.LogsRegs,
"routePanelLogsMod": routePanelLogsMod, "panel.LogsMod": panel.LogsMod,
"routePanelDebug": routePanelDebug, "panel.Debug": panel.Debug,
"routePanelDashboard": routePanelDashboard, "routePanelDashboard": routePanelDashboard,
"routes.AccountEditCritical": routes.AccountEditCritical, "routes.AccountEditCritical": routes.AccountEditCritical,
"routes.AccountEditCriticalSubmit": routes.AccountEditCriticalSubmit, "routes.AccountEditCriticalSubmit": routes.AccountEditCriticalSubmit,
@ -95,8 +96,8 @@ var RouteMap = map[string]interface{}{
"routes.AccountEditAvatarSubmit": routes.AccountEditAvatarSubmit, "routes.AccountEditAvatarSubmit": routes.AccountEditAvatarSubmit,
"routes.AccountEditUsername": routes.AccountEditUsername, "routes.AccountEditUsername": routes.AccountEditUsername,
"routes.AccountEditUsernameSubmit": routes.AccountEditUsernameSubmit, "routes.AccountEditUsernameSubmit": routes.AccountEditUsernameSubmit,
"routeAccountEditEmail": routeAccountEditEmail, "routes.AccountEditEmail": routes.AccountEditEmail,
"routeAccountEditEmailTokenSubmit": routeAccountEditEmailTokenSubmit, "routes.AccountEditEmailTokenSubmit": routes.AccountEditEmailTokenSubmit,
"routes.ViewProfile": routes.ViewProfile, "routes.ViewProfile": routes.ViewProfile,
"routes.BanUserSubmit": routes.BanUserSubmit, "routes.BanUserSubmit": routes.BanUserSubmit,
"routes.UnbanUser": routes.UnbanUser, "routes.UnbanUser": routes.UnbanUser,
@ -144,21 +145,21 @@ var routeMapEnum = map[string]int{
"routes.ChangeTheme": 5, "routes.ChangeTheme": 5,
"routes.ShowAttachment": 6, "routes.ShowAttachment": 6,
"common.RouteWebsockets": 7, "common.RouteWebsockets": 7,
"routeReportSubmit": 8, "routes.ReportSubmit": 8,
"routes.CreateTopic": 9, "routes.CreateTopic": 9,
"routes.TopicList": 10, "routes.TopicList": 10,
"routePanelForums": 11, "panel.Forums": 11,
"routePanelForumsCreateSubmit": 12, "panel.ForumsCreateSubmit": 12,
"routePanelForumsDelete": 13, "panel.ForumsDelete": 13,
"routePanelForumsDeleteSubmit": 14, "panel.ForumsDeleteSubmit": 14,
"routePanelForumsEdit": 15, "panel.ForumsEdit": 15,
"routePanelForumsEditSubmit": 16, "panel.ForumsEditSubmit": 16,
"routePanelForumsEditPermsSubmit": 17, "panel.ForumsEditPermsSubmit": 17,
"routePanelForumsEditPermsAdvance": 18, "panel.ForumsEditPermsAdvance": 18,
"routePanelForumsEditPermsAdvanceSubmit": 19, "panel.ForumsEditPermsAdvanceSubmit": 19,
"routePanelSettings": 20, "panel.Settings": 20,
"routePanelSettingEdit": 21, "panel.SettingEdit": 21,
"routePanelSettingEditSubmit": 22, "panel.SettingEditSubmit": 22,
"routePanelWordFilters": 23, "routePanelWordFilters": 23,
"routePanelWordFiltersCreateSubmit": 24, "routePanelWordFiltersCreateSubmit": 24,
"routePanelWordFiltersEdit": 25, "routePanelWordFiltersEdit": 25,
@ -180,31 +181,31 @@ var routeMapEnum = map[string]int{
"routePanelUsers": 41, "routePanelUsers": 41,
"routePanelUsersEdit": 42, "routePanelUsersEdit": 42,
"routePanelUsersEditSubmit": 43, "routePanelUsersEditSubmit": 43,
"routePanelAnalyticsViews": 44, "panel.AnalyticsViews": 44,
"routePanelAnalyticsRoutes": 45, "panel.AnalyticsRoutes": 45,
"routePanelAnalyticsAgents": 46, "panel.AnalyticsAgents": 46,
"routePanelAnalyticsSystems": 47, "panel.AnalyticsSystems": 47,
"routePanelAnalyticsLanguages": 48, "panel.AnalyticsLanguages": 48,
"routePanelAnalyticsReferrers": 49, "panel.AnalyticsReferrers": 49,
"routePanelAnalyticsRouteViews": 50, "panel.AnalyticsRouteViews": 50,
"routePanelAnalyticsAgentViews": 51, "panel.AnalyticsAgentViews": 51,
"routePanelAnalyticsForumViews": 52, "panel.AnalyticsForumViews": 52,
"routePanelAnalyticsSystemViews": 53, "panel.AnalyticsSystemViews": 53,
"routePanelAnalyticsLanguageViews": 54, "panel.AnalyticsLanguageViews": 54,
"routePanelAnalyticsReferrerViews": 55, "panel.AnalyticsReferrerViews": 55,
"routePanelAnalyticsPosts": 56, "panel.AnalyticsPosts": 56,
"routePanelAnalyticsTopics": 57, "panel.AnalyticsTopics": 57,
"routePanelAnalyticsForums": 58, "panel.AnalyticsForums": 58,
"routePanelGroups": 59, "routePanelGroups": 59,
"routePanelGroupsEdit": 60, "routePanelGroupsEdit": 60,
"routePanelGroupsEditPerms": 61, "routePanelGroupsEditPerms": 61,
"routePanelGroupsEditSubmit": 62, "routePanelGroupsEditSubmit": 62,
"routePanelGroupsEditPermsSubmit": 63, "routePanelGroupsEditPermsSubmit": 63,
"routePanelGroupsCreateSubmit": 64, "routePanelGroupsCreateSubmit": 64,
"routePanelBackups": 65, "panel.Backups": 65,
"routePanelLogsRegs": 66, "panel.LogsRegs": 66,
"routePanelLogsMod": 67, "panel.LogsMod": 67,
"routePanelDebug": 68, "panel.Debug": 68,
"routePanelDashboard": 69, "routePanelDashboard": 69,
"routes.AccountEditCritical": 70, "routes.AccountEditCritical": 70,
"routes.AccountEditCriticalSubmit": 71, "routes.AccountEditCriticalSubmit": 71,
@ -212,8 +213,8 @@ var routeMapEnum = map[string]int{
"routes.AccountEditAvatarSubmit": 73, "routes.AccountEditAvatarSubmit": 73,
"routes.AccountEditUsername": 74, "routes.AccountEditUsername": 74,
"routes.AccountEditUsernameSubmit": 75, "routes.AccountEditUsernameSubmit": 75,
"routeAccountEditEmail": 76, "routes.AccountEditEmail": 76,
"routeAccountEditEmailTokenSubmit": 77, "routes.AccountEditEmailTokenSubmit": 77,
"routes.ViewProfile": 78, "routes.ViewProfile": 78,
"routes.BanUserSubmit": 79, "routes.BanUserSubmit": 79,
"routes.UnbanUser": 80, "routes.UnbanUser": 80,
@ -259,21 +260,21 @@ var reverseRouteMapEnum = map[int]string{
5: "routes.ChangeTheme", 5: "routes.ChangeTheme",
6: "routes.ShowAttachment", 6: "routes.ShowAttachment",
7: "common.RouteWebsockets", 7: "common.RouteWebsockets",
8: "routeReportSubmit", 8: "routes.ReportSubmit",
9: "routes.CreateTopic", 9: "routes.CreateTopic",
10: "routes.TopicList", 10: "routes.TopicList",
11: "routePanelForums", 11: "panel.Forums",
12: "routePanelForumsCreateSubmit", 12: "panel.ForumsCreateSubmit",
13: "routePanelForumsDelete", 13: "panel.ForumsDelete",
14: "routePanelForumsDeleteSubmit", 14: "panel.ForumsDeleteSubmit",
15: "routePanelForumsEdit", 15: "panel.ForumsEdit",
16: "routePanelForumsEditSubmit", 16: "panel.ForumsEditSubmit",
17: "routePanelForumsEditPermsSubmit", 17: "panel.ForumsEditPermsSubmit",
18: "routePanelForumsEditPermsAdvance", 18: "panel.ForumsEditPermsAdvance",
19: "routePanelForumsEditPermsAdvanceSubmit", 19: "panel.ForumsEditPermsAdvanceSubmit",
20: "routePanelSettings", 20: "panel.Settings",
21: "routePanelSettingEdit", 21: "panel.SettingEdit",
22: "routePanelSettingEditSubmit", 22: "panel.SettingEditSubmit",
23: "routePanelWordFilters", 23: "routePanelWordFilters",
24: "routePanelWordFiltersCreateSubmit", 24: "routePanelWordFiltersCreateSubmit",
25: "routePanelWordFiltersEdit", 25: "routePanelWordFiltersEdit",
@ -295,31 +296,31 @@ var reverseRouteMapEnum = map[int]string{
41: "routePanelUsers", 41: "routePanelUsers",
42: "routePanelUsersEdit", 42: "routePanelUsersEdit",
43: "routePanelUsersEditSubmit", 43: "routePanelUsersEditSubmit",
44: "routePanelAnalyticsViews", 44: "panel.AnalyticsViews",
45: "routePanelAnalyticsRoutes", 45: "panel.AnalyticsRoutes",
46: "routePanelAnalyticsAgents", 46: "panel.AnalyticsAgents",
47: "routePanelAnalyticsSystems", 47: "panel.AnalyticsSystems",
48: "routePanelAnalyticsLanguages", 48: "panel.AnalyticsLanguages",
49: "routePanelAnalyticsReferrers", 49: "panel.AnalyticsReferrers",
50: "routePanelAnalyticsRouteViews", 50: "panel.AnalyticsRouteViews",
51: "routePanelAnalyticsAgentViews", 51: "panel.AnalyticsAgentViews",
52: "routePanelAnalyticsForumViews", 52: "panel.AnalyticsForumViews",
53: "routePanelAnalyticsSystemViews", 53: "panel.AnalyticsSystemViews",
54: "routePanelAnalyticsLanguageViews", 54: "panel.AnalyticsLanguageViews",
55: "routePanelAnalyticsReferrerViews", 55: "panel.AnalyticsReferrerViews",
56: "routePanelAnalyticsPosts", 56: "panel.AnalyticsPosts",
57: "routePanelAnalyticsTopics", 57: "panel.AnalyticsTopics",
58: "routePanelAnalyticsForums", 58: "panel.AnalyticsForums",
59: "routePanelGroups", 59: "routePanelGroups",
60: "routePanelGroupsEdit", 60: "routePanelGroupsEdit",
61: "routePanelGroupsEditPerms", 61: "routePanelGroupsEditPerms",
62: "routePanelGroupsEditSubmit", 62: "routePanelGroupsEditSubmit",
63: "routePanelGroupsEditPermsSubmit", 63: "routePanelGroupsEditPermsSubmit",
64: "routePanelGroupsCreateSubmit", 64: "routePanelGroupsCreateSubmit",
65: "routePanelBackups", 65: "panel.Backups",
66: "routePanelLogsRegs", 66: "panel.LogsRegs",
67: "routePanelLogsMod", 67: "panel.LogsMod",
68: "routePanelDebug", 68: "panel.Debug",
69: "routePanelDashboard", 69: "routePanelDashboard",
70: "routes.AccountEditCritical", 70: "routes.AccountEditCritical",
71: "routes.AccountEditCriticalSubmit", 71: "routes.AccountEditCriticalSubmit",
@ -327,8 +328,8 @@ var reverseRouteMapEnum = map[int]string{
73: "routes.AccountEditAvatarSubmit", 73: "routes.AccountEditAvatarSubmit",
74: "routes.AccountEditUsername", 74: "routes.AccountEditUsername",
75: "routes.AccountEditUsernameSubmit", 75: "routes.AccountEditUsernameSubmit",
76: "routeAccountEditEmail", 76: "routes.AccountEditEmail",
77: "routeAccountEditEmailTokenSubmit", 77: "routes.AccountEditEmailTokenSubmit",
78: "routes.ViewProfile", 78: "routes.ViewProfile",
79: "routes.BanUserSubmit", 79: "routes.BanUserSubmit",
80: "routes.UnbanUser", 80: "routes.UnbanUser",
@ -908,7 +909,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(8) counters.RouteViewCounter.Bump(8)
err = routeReportSubmit(w,req,user,extraData) err = routes.ReportSubmit(w,req,user,extraData)
} }
if err != nil { if err != nil {
router.handleError(err,w,req,user) router.handleError(err,w,req,user)
@ -941,7 +942,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch(req.URL.Path) { switch(req.URL.Path) {
case "/panel/forums/": case "/panel/forums/":
counters.RouteViewCounter.Bump(11) counters.RouteViewCounter.Bump(11)
err = routePanelForums(w,req,user) err = panel.Forums(w,req,user)
case "/panel/forums/create/": case "/panel/forums/create/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
if err != nil { if err != nil {
@ -950,7 +951,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(12) counters.RouteViewCounter.Bump(12)
err = routePanelForumsCreateSubmit(w,req,user) err = panel.ForumsCreateSubmit(w,req,user)
case "/panel/forums/delete/": case "/panel/forums/delete/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
if err != nil { if err != nil {
@ -959,7 +960,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(13) counters.RouteViewCounter.Bump(13)
err = routePanelForumsDelete(w,req,user,extraData) err = panel.ForumsDelete(w,req,user,extraData)
case "/panel/forums/delete/submit/": case "/panel/forums/delete/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
if err != nil { if err != nil {
@ -968,10 +969,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(14) counters.RouteViewCounter.Bump(14)
err = routePanelForumsDeleteSubmit(w,req,user,extraData) err = panel.ForumsDeleteSubmit(w,req,user,extraData)
case "/panel/forums/edit/": case "/panel/forums/edit/":
counters.RouteViewCounter.Bump(15) counters.RouteViewCounter.Bump(15)
err = routePanelForumsEdit(w,req,user,extraData) err = panel.ForumsEdit(w,req,user,extraData)
case "/panel/forums/edit/submit/": case "/panel/forums/edit/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
if err != nil { if err != nil {
@ -980,7 +981,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(16) counters.RouteViewCounter.Bump(16)
err = routePanelForumsEditSubmit(w,req,user,extraData) err = panel.ForumsEditSubmit(w,req,user,extraData)
case "/panel/forums/edit/perms/submit/": case "/panel/forums/edit/perms/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
if err != nil { if err != nil {
@ -989,10 +990,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(17) counters.RouteViewCounter.Bump(17)
err = routePanelForumsEditPermsSubmit(w,req,user,extraData) err = panel.ForumsEditPermsSubmit(w,req,user,extraData)
case "/panel/forums/edit/perms/": case "/panel/forums/edit/perms/":
counters.RouteViewCounter.Bump(18) counters.RouteViewCounter.Bump(18)
err = routePanelForumsEditPermsAdvance(w,req,user,extraData) err = panel.ForumsEditPermsAdvance(w,req,user,extraData)
case "/panel/forums/edit/perms/adv/submit/": case "/panel/forums/edit/perms/adv/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
if err != nil { if err != nil {
@ -1001,13 +1002,13 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(19) counters.RouteViewCounter.Bump(19)
err = routePanelForumsEditPermsAdvanceSubmit(w,req,user,extraData) err = panel.ForumsEditPermsAdvanceSubmit(w,req,user,extraData)
case "/panel/settings/": case "/panel/settings/":
counters.RouteViewCounter.Bump(20) counters.RouteViewCounter.Bump(20)
err = routePanelSettings(w,req,user) err = panel.Settings(w,req,user)
case "/panel/settings/edit/": case "/panel/settings/edit/":
counters.RouteViewCounter.Bump(21) counters.RouteViewCounter.Bump(21)
err = routePanelSettingEdit(w,req,user,extraData) err = panel.SettingEdit(w,req,user,extraData)
case "/panel/settings/edit/submit/": case "/panel/settings/edit/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
if err != nil { if err != nil {
@ -1016,7 +1017,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(22) counters.RouteViewCounter.Bump(22)
err = routePanelSettingEditSubmit(w,req,user,extraData) err = panel.SettingEditSubmit(w,req,user,extraData)
case "/panel/settings/word-filters/": case "/panel/settings/word-filters/":
counters.RouteViewCounter.Bump(23) counters.RouteViewCounter.Bump(23)
err = routePanelWordFilters(w,req,user) err = routePanelWordFilters(w,req,user)
@ -1160,7 +1161,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(44) counters.RouteViewCounter.Bump(44)
err = routePanelAnalyticsViews(w,req,user) err = panel.AnalyticsViews(w,req,user)
case "/panel/analytics/routes/": case "/panel/analytics/routes/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
if err != nil { if err != nil {
@ -1169,7 +1170,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(45) counters.RouteViewCounter.Bump(45)
err = routePanelAnalyticsRoutes(w,req,user) err = panel.AnalyticsRoutes(w,req,user)
case "/panel/analytics/agents/": case "/panel/analytics/agents/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
if err != nil { if err != nil {
@ -1178,7 +1179,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(46) counters.RouteViewCounter.Bump(46)
err = routePanelAnalyticsAgents(w,req,user) err = panel.AnalyticsAgents(w,req,user)
case "/panel/analytics/systems/": case "/panel/analytics/systems/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
if err != nil { if err != nil {
@ -1187,7 +1188,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(47) counters.RouteViewCounter.Bump(47)
err = routePanelAnalyticsSystems(w,req,user) err = panel.AnalyticsSystems(w,req,user)
case "/panel/analytics/langs/": case "/panel/analytics/langs/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
if err != nil { if err != nil {
@ -1196,7 +1197,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(48) counters.RouteViewCounter.Bump(48)
err = routePanelAnalyticsLanguages(w,req,user) err = panel.AnalyticsLanguages(w,req,user)
case "/panel/analytics/referrers/": case "/panel/analytics/referrers/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
if err != nil { if err != nil {
@ -1205,25 +1206,25 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(49) counters.RouteViewCounter.Bump(49)
err = routePanelAnalyticsReferrers(w,req,user) err = panel.AnalyticsReferrers(w,req,user)
case "/panel/analytics/route/": case "/panel/analytics/route/":
counters.RouteViewCounter.Bump(50) counters.RouteViewCounter.Bump(50)
err = routePanelAnalyticsRouteViews(w,req,user,extraData) err = panel.AnalyticsRouteViews(w,req,user,extraData)
case "/panel/analytics/agent/": case "/panel/analytics/agent/":
counters.RouteViewCounter.Bump(51) counters.RouteViewCounter.Bump(51)
err = routePanelAnalyticsAgentViews(w,req,user,extraData) err = panel.AnalyticsAgentViews(w,req,user,extraData)
case "/panel/analytics/forum/": case "/panel/analytics/forum/":
counters.RouteViewCounter.Bump(52) counters.RouteViewCounter.Bump(52)
err = routePanelAnalyticsForumViews(w,req,user,extraData) err = panel.AnalyticsForumViews(w,req,user,extraData)
case "/panel/analytics/system/": case "/panel/analytics/system/":
counters.RouteViewCounter.Bump(53) counters.RouteViewCounter.Bump(53)
err = routePanelAnalyticsSystemViews(w,req,user,extraData) err = panel.AnalyticsSystemViews(w,req,user,extraData)
case "/panel/analytics/lang/": case "/panel/analytics/lang/":
counters.RouteViewCounter.Bump(54) counters.RouteViewCounter.Bump(54)
err = routePanelAnalyticsLanguageViews(w,req,user,extraData) err = panel.AnalyticsLanguageViews(w,req,user,extraData)
case "/panel/analytics/referrer/": case "/panel/analytics/referrer/":
counters.RouteViewCounter.Bump(55) counters.RouteViewCounter.Bump(55)
err = routePanelAnalyticsReferrerViews(w,req,user,extraData) err = panel.AnalyticsReferrerViews(w,req,user,extraData)
case "/panel/analytics/posts/": case "/panel/analytics/posts/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
if err != nil { if err != nil {
@ -1232,7 +1233,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(56) counters.RouteViewCounter.Bump(56)
err = routePanelAnalyticsPosts(w,req,user) err = panel.AnalyticsPosts(w,req,user)
case "/panel/analytics/topics/": case "/panel/analytics/topics/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
if err != nil { if err != nil {
@ -1241,7 +1242,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(57) counters.RouteViewCounter.Bump(57)
err = routePanelAnalyticsTopics(w,req,user) err = panel.AnalyticsTopics(w,req,user)
case "/panel/analytics/forums/": case "/panel/analytics/forums/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
if err != nil { if err != nil {
@ -1250,7 +1251,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(58) counters.RouteViewCounter.Bump(58)
err = routePanelAnalyticsForums(w,req,user) err = panel.AnalyticsForums(w,req,user)
case "/panel/groups/": case "/panel/groups/":
counters.RouteViewCounter.Bump(59) counters.RouteViewCounter.Bump(59)
err = routePanelGroups(w,req,user) err = routePanelGroups(w,req,user)
@ -1295,13 +1296,13 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(65) counters.RouteViewCounter.Bump(65)
err = routePanelBackups(w,req,user,extraData) err = panel.Backups(w,req,user,extraData)
case "/panel/logs/regs/": case "/panel/logs/regs/":
counters.RouteViewCounter.Bump(66) counters.RouteViewCounter.Bump(66)
err = routePanelLogsRegs(w,req,user) err = panel.LogsRegs(w,req,user)
case "/panel/logs/mod/": case "/panel/logs/mod/":
counters.RouteViewCounter.Bump(67) counters.RouteViewCounter.Bump(67)
err = routePanelLogsMod(w,req,user) err = panel.LogsMod(w,req,user)
case "/panel/debug/": case "/panel/debug/":
err = common.AdminOnly(w,req,user) err = common.AdminOnly(w,req,user)
if err != nil { if err != nil {
@ -1310,7 +1311,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(68) counters.RouteViewCounter.Bump(68)
err = routePanelDebug(w,req,user) err = panel.Debug(w,req,user)
default: default:
counters.RouteViewCounter.Bump(69) counters.RouteViewCounter.Bump(69)
err = routePanelDashboard(w,req,user) err = routePanelDashboard(w,req,user)
@ -1405,7 +1406,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(76) counters.RouteViewCounter.Bump(76)
err = routeAccountEditEmail(w,req,user) err = routes.AccountEditEmail(w,req,user)
case "/user/edit/token/": case "/user/edit/token/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
if err != nil { if err != nil {
@ -1420,7 +1421,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(77) counters.RouteViewCounter.Bump(77)
err = routeAccountEditEmailTokenSubmit(w,req,user,extraData) err = routes.AccountEditEmailTokenSubmit(w,req,user,extraData)
default: default:
req.URL.Path += extraData req.URL.Path += extraData
counters.RouteViewCounter.Bump(78) counters.RouteViewCounter.Bump(78)

View File

@ -2,18 +2,20 @@
package main package main
var dbTablePrimaryKeys = map[string]string{ var dbTablePrimaryKeys = map[string]string{
"topics":"tid",
"attachments":"attachID",
"menus":"mid",
"users_groups":"gid", "users_groups":"gid",
"users_groups_scheduler":"uid", "users_groups_scheduler":"uid",
"registration_logs":"rlid",
"word_filters":"wfid",
"menu_items":"miid",
"polls":"pollID",
"users_replies":"rid", "users_replies":"rid",
"topics":"tid", "activity_stream":"asid",
"pages":"pid",
"replies":"rid", "replies":"rid",
"revisions":"reviseID", "revisions":"reviseID",
"activity_stream":"asid",
"word_filters":"wfid",
"menus":"mid",
"users":"uid", "users":"uid",
"menu_items":"miid",
"forums":"fid", "forums":"fid",
"attachments":"attachID",
"polls":"pollID",
} }

View File

@ -53,7 +53,7 @@ func gloinit() (err error) {
if err != nil { if err != nil {
return err return err
} }
err = common.InitThemes() common.Themes, err = common.NewThemeList()
if err != nil { if err != nil {
return err return err
} }

View File

@ -10,6 +10,9 @@ go get -u github.com/denisenkom/go-mssqldb
echo "Installing bcrypt" echo "Installing bcrypt"
go get -u golang.org/x/crypto/bcrypt go get -u golang.org/x/crypto/bcrypt
echo "Installing Argon2"
go get -u golang.org/x/crypto/argon2
echo "Installing gopsutil" echo "Installing gopsutil"
go get -u github.com/Azareal/gopsutil go get -u github.com/Azareal/gopsutil

View File

@ -29,6 +29,13 @@ if %errorlevel% neq 0 (
exit /b %errorlevel% 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) echo Installing /x/sys/windows (dependency for gopsutil)
go get -u golang.org/x/sys/windows go get -u golang.org/x/sys/windows
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (

View File

@ -13,28 +13,15 @@ func GenerateSafeString(length int) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return base64.URLEncoding.EncodeToString(rb), nil return base64.StdEncoding.EncodeToString(rb), nil
} }
// Generate a bcrypt hash from a password and a salt // Generate a bcrypt hash
func BcryptGeneratePassword(password string) (hashedPassword string, salt string, err error) { // Note: The salt is in the hash, therefore the salt value is blank
salt, err = GenerateSafeString(saltLength) func bcryptGeneratePassword(password string) (hash string, salt string, err error) {
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) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
return "", err return "", "", err
} }
return string(hashedPassword), nil return string(hashedPassword), salt, nil
} }

View File

@ -44,8 +44,12 @@
"MoveTopic": "Can move topics in or out" "MoveTopic": "Can move topics in or out"
}, },
"SettingLabels": { "SettingPhrases": {
"activation_type": "Activate All,Email Activation,Admin Approval" "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": { "PermPresets": {
@ -263,6 +267,7 @@
"topics_click_topics_to_select":"Click the topics to select them", "topics_click_topics_to_select":"Click the topics to select them",
"topics_new_topic":"New Topic", "topics_new_topic":"New Topic",
"forum_locked":"Locked", "forum_locked":"Locked",
"topics_moderate":"Moderate",
"topics_replies_suffix":" replies", "topics_replies_suffix":" replies",
"forums_topics_suffix":" topics", "forums_topics_suffix":" topics",
"topics_gap_likes_suffix":" likes", "topics_gap_likes_suffix":" likes",

39
main.go
View File

@ -29,7 +29,6 @@ import (
var version = common.Version{Major: 0, Minor: 1, Patch: 0, Tag: "dev"} var version = common.Version{Major: 0, Minor: 1, Patch: 0, Tag: "dev"}
var router *GenRouter var router *GenRouter
var startTime time.Time
var logWriter = io.MultiWriter(os.Stderr) var logWriter = io.MultiWriter(os.Stderr)
// TODO: Wrap the globals in here so we can pass pointers to them to subpackages // 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") 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) common.RegLogs, err = common.NewRegLogStore(acc)
if err != nil { if err != nil {
return err return err
@ -140,7 +147,8 @@ func afterDBInit() (err error) {
return err return err
} }
counters.GlobalViewCounter, err = counters.NewGlobalViewCounter() log.Print("Initialising the view counters")
counters.GlobalViewCounter, err = counters.NewGlobalViewCounter(acc)
if err != nil { if err != nil {
return err return err
} }
@ -195,18 +203,6 @@ func main() {
return 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() config.Config()
// TODO: Have a file for each run with the time/date the server started as the file name? // 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) logWriter = io.MultiWriter(os.Stderr, f)
log.SetOutput(logWriter) 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()) log.Print("Running Gosora v" + version.String())
fmt.Println("") fmt.Println("")
startTime = time.Now() common.StartTime = time.Now()
log.Print("Processing configuration data") log.Print("Processing configuration data")
err = common.ProcessConfig() err = common.ProcessConfig()
@ -236,7 +223,7 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
err = common.InitThemes() common.Themes, err = common.NewThemeList()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -271,6 +258,7 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
log.Print("Initialising the file watcher")
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -337,6 +325,7 @@ func main() {
} }
} }
log.Print("Initialising the task system")
var runTasks = func(tasks []func() error) { var runTasks = func(tasks []func() error) {
for _, task := range tasks { for _, task := range tasks {
if task() != nil { if task() != nil {

View File

@ -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
}

View File

@ -866,7 +866,7 @@ func TestAuth(t *testing.T) {
realPassword = "Madame Cassandra's Mystic Orb" realPassword = "Madame Cassandra's Mystic Orb"
t.Logf("Set realPassword to '%s'", realPassword) t.Logf("Set realPassword to '%s'", realPassword)
t.Log("Hashing the real password") t.Log("Hashing the real password")
hashedPassword, err = common.BcryptGeneratePasswordNoSalt(realPassword) hashedPassword, err = common.BcryptGeneratePassword(realPassword)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ func init() {
addPatch(1, patch1) addPatch(1, patch1)
addPatch(2, patch2) addPatch(2, patch2)
addPatch(3, patch3) addPatch(3, patch3)
addPatch(4, patch4)
} }
func patch0(scanner *bufio.Scanner) (err error) { func patch0(scanner *bufio.Scanner) (err error) {
@ -235,3 +236,214 @@ func patch3(scanner *bufio.Scanner) error {
return nil 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
}

View File

@ -547,9 +547,10 @@ $(document).ready(function(){
uploadFiles.addEventListener("change", uploadFileHandler, false); uploadFiles.addEventListener("change", uploadFileHandler, false);
} }
$(".moderate_link").click(function(event) { $(".moderate_link").click((event) => {
event.preventDefault(); event.preventDefault();
$(".pre_opt").removeClass("auto_hide"); $(".pre_opt").removeClass("auto_hide");
$(".moderate_link").addClass("moderate_open");
$(".topic_row").each(function(){ $(".topic_row").each(function(){
$(this).click(function(){ $(this).click(function(){
selectedTopics.push(parseInt($(this).attr("data-tid"),10)); selectedTopics.push(parseInt($(this).attr("data-tid"),10));

View File

@ -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)) 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) { 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)) return build.prepare(build.adapter.SimpleInsert("_builder", table, columns, fields))
} }

View File

@ -71,53 +71,7 @@ func (adapter *MssqlAdapter) CreateTable(name string, table string, charset stri
var querystr = "CREATE TABLE [" + table + "] (" var querystr = "CREATE TABLE [" + table + "] ("
for _, column := range columns { for _, column := range columns {
var max bool column, size, end := adapter.parseColumn(column)
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"
}
querystr += "\n\t[" + column.Name + "] " + column.Type + size + end + "," 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 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) { func (adapter *MssqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
if name == "" { if name == "" {
return "", errors.New("You need a name for this statement") return "", errors.New("You need a name for this statement")

View File

@ -86,39 +86,7 @@ func (adapter *MysqlAdapter) CreateTable(name string, table string, charset stri
var querystr = "CREATE TABLE `" + table + "` (" var querystr = "CREATE TABLE `" + table + "` ("
for _, column := range columns { for _, column := range columns {
// Make it easier to support Cassandra in the future column, size, end := adapter.parseColumn(column)
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"
}
querystr += "\n\t`" + column.Name + "` " + column.Type + size + end + "," 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 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) { func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
if name == "" { if name == "" {
return "", errors.New("You need a name for this statement") return "", errors.New("You need a name for this statement")

View File

@ -119,7 +119,7 @@ func (adapter *PgsqlAdapter) CreateTable(name string, table string, charset stri
} }
// TODO: Implement this // 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 == "" { if name == "" {
return "", errors.New("You need a name for this statement") 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 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 // TODO: Implement this
func (adapter *PgsqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) { func (adapter *PgsqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) {
if name == "" { if name == "" {

View File

@ -106,6 +106,9 @@ type Adapter interface {
DropTable(name string, table string) (string, error) DropTable(name string, table string) (string, error)
CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (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) SimpleInsert(name string, table string, columns string, fields string) (string, error)
SimpleUpdate(name string, table string, set string, where string) (string, error) SimpleUpdate(name string, table string, set string, where string) (string, error)
SimpleDelete(name string, table string, where string) (string, error) SimpleDelete(name string, table string, where string) (string, error)

View File

@ -111,8 +111,6 @@ func writeStatements(adapter qgen.Adapter) error {
func seedTables(adapter qgen.Adapter) error { func seedTables(adapter qgen.Adapter) error {
qgen.Install.SimpleInsert("sync", "last_update", "UTC_TIMESTAMP()") 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, 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", "'bigpost_min_words','250','int'")
qgen.Install.SimpleInsert("settings", "name, content, type", "'megapost_min_words','1000','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("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("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("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() 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 { func writeInserts(adapter qgen.Adapter) error {
build := adapter.Builder() 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("addForumPermsToForum").Table("forums_permissions").Columns("gid,fid,preset,permissions").Fields("?,?,?,?").Parse()
build.Insert("addPlugin").Table("plugins").Columns("uname, active, installed").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("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("setTempGroup").Table("users").Set("temp_group = ?").Where("uid = ?").Parse()
build.Update("updateWordFilter").Table("word_filters").Set("find = ?, replacement = ?").Where("wfid = ?").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 { func writeSimpleCounts(adapter qgen.Adapter) error {
adapter.SimpleCount("reportExists", "topics", "data = ? AND data != '' AND parentID = 1", "")
return nil return nil
} }

View File

@ -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.Install.CreateTable("registration_logs", "", "",
[]qgen.DBTableColumn{ []qgen.DBTableColumn{
qgen.DBTableColumn{"rlid", "int", 0, false, true, ""}, qgen.DBTableColumn{"rlid", "int", 0, false, true, ""},

View File

@ -232,6 +232,7 @@ import (
"./common" "./common"
"./common/counters" "./common/counters"
"./routes" "./routes"
"./routes/panel"
) )
var ErrNoRoute = errors.New("That route doesn't exist.") var ErrNoRoute = errors.New("That route doesn't exist.")

View File

@ -14,7 +14,7 @@ func routes() {
// TODO: Reduce the number of Befores. With a new method, perhaps? // TODO: Reduce the number of Befores. With a new method, perhaps?
reportGroup := newRouteGroup("/report/", reportGroup := newRouteGroup("/report/",
Action("routeReportSubmit", "/report/submit/", "extraData"), Action("routes.ReportSubmit", "/report/submit/", "extraData"),
).Before("NoBanned") ).Before("NoBanned")
addRouteGroup(reportGroup) addRouteGroup(reportGroup)
@ -46,8 +46,8 @@ func buildUserRoutes() {
UploadAction("routes.AccountEditAvatarSubmit", "/user/edit/avatar/submit/").MaxSizeVar("int(common.Config.MaxRequestSize)"), UploadAction("routes.AccountEditAvatarSubmit", "/user/edit/avatar/submit/").MaxSizeVar("int(common.Config.MaxRequestSize)"),
MemberView("routes.AccountEditUsername", "/user/edit/username/"), MemberView("routes.AccountEditUsername", "/user/edit/username/"),
Action("routes.AccountEditUsernameSubmit", "/user/edit/username/submit/"), // TODO: Full test this Action("routes.AccountEditUsernameSubmit", "/user/edit/username/submit/"), // TODO: Full test this
MemberView("routeAccountEditEmail", "/user/edit/email/"), MemberView("routes.AccountEditEmail", "/user/edit/email/"),
Action("routeAccountEditEmailTokenSubmit", "/user/edit/token/", "extraData"), Action("routes.AccountEditEmailTokenSubmit", "/user/edit/token/", "extraData"),
) )
addRouteGroup(userGroup) addRouteGroup(userGroup)
@ -131,19 +131,19 @@ func buildPanelRoutes() {
panelGroup := newRouteGroup("/panel/").Before("SuperModOnly") panelGroup := newRouteGroup("/panel/").Before("SuperModOnly")
panelGroup.Routes( panelGroup.Routes(
View("routePanelDashboard", "/panel/"), View("routePanelDashboard", "/panel/"),
View("routePanelForums", "/panel/forums/"), View("panel.Forums", "/panel/forums/"),
Action("routePanelForumsCreateSubmit", "/panel/forums/create/"), Action("panel.ForumsCreateSubmit", "/panel/forums/create/"),
Action("routePanelForumsDelete", "/panel/forums/delete/", "extraData"), Action("panel.ForumsDelete", "/panel/forums/delete/", "extraData"),
Action("routePanelForumsDeleteSubmit", "/panel/forums/delete/submit/", "extraData"), Action("panel.ForumsDeleteSubmit", "/panel/forums/delete/submit/", "extraData"),
View("routePanelForumsEdit", "/panel/forums/edit/", "extraData"), View("panel.ForumsEdit", "/panel/forums/edit/", "extraData"),
Action("routePanelForumsEditSubmit", "/panel/forums/edit/submit/", "extraData"), Action("panel.ForumsEditSubmit", "/panel/forums/edit/submit/", "extraData"),
Action("routePanelForumsEditPermsSubmit", "/panel/forums/edit/perms/submit/", "extraData"), Action("panel.ForumsEditPermsSubmit", "/panel/forums/edit/perms/submit/", "extraData"),
View("routePanelForumsEditPermsAdvance", "/panel/forums/edit/perms/", "extraData"), View("panel.ForumsEditPermsAdvance", "/panel/forums/edit/perms/", "extraData"),
Action("routePanelForumsEditPermsAdvanceSubmit", "/panel/forums/edit/perms/adv/submit/", "extraData"), Action("panel.ForumsEditPermsAdvanceSubmit", "/panel/forums/edit/perms/adv/submit/", "extraData"),
View("routePanelSettings", "/panel/settings/"), View("panel.Settings", "/panel/settings/"),
View("routePanelSettingEdit", "/panel/settings/edit/", "extraData"), View("panel.SettingEdit", "/panel/settings/edit/", "extraData"),
Action("routePanelSettingEditSubmit", "/panel/settings/edit/submit/", "extraData"), Action("panel.SettingEditSubmit", "/panel/settings/edit/submit/", "extraData"),
View("routePanelWordFilters", "/panel/settings/word-filters/"), View("routePanelWordFilters", "/panel/settings/word-filters/"),
Action("routePanelWordFiltersCreateSubmit", "/panel/settings/word-filters/create/"), Action("routePanelWordFiltersCreateSubmit", "/panel/settings/word-filters/create/"),
@ -170,21 +170,21 @@ func buildPanelRoutes() {
View("routePanelUsersEdit", "/panel/users/edit/", "extraData"), View("routePanelUsersEdit", "/panel/users/edit/", "extraData"),
Action("routePanelUsersEditSubmit", "/panel/users/edit/submit/", "extraData"), Action("routePanelUsersEditSubmit", "/panel/users/edit/submit/", "extraData"),
View("routePanelAnalyticsViews", "/panel/analytics/views/").Before("ParseForm"), View("panel.AnalyticsViews", "/panel/analytics/views/").Before("ParseForm"),
View("routePanelAnalyticsRoutes", "/panel/analytics/routes/").Before("ParseForm"), View("panel.AnalyticsRoutes", "/panel/analytics/routes/").Before("ParseForm"),
View("routePanelAnalyticsAgents", "/panel/analytics/agents/").Before("ParseForm"), View("panel.AnalyticsAgents", "/panel/analytics/agents/").Before("ParseForm"),
View("routePanelAnalyticsSystems", "/panel/analytics/systems/").Before("ParseForm"), View("panel.AnalyticsSystems", "/panel/analytics/systems/").Before("ParseForm"),
View("routePanelAnalyticsLanguages", "/panel/analytics/langs/").Before("ParseForm"), View("panel.AnalyticsLanguages", "/panel/analytics/langs/").Before("ParseForm"),
View("routePanelAnalyticsReferrers", "/panel/analytics/referrers/").Before("ParseForm"), View("panel.AnalyticsReferrers", "/panel/analytics/referrers/").Before("ParseForm"),
View("routePanelAnalyticsRouteViews", "/panel/analytics/route/", "extraData"), View("panel.AnalyticsRouteViews", "/panel/analytics/route/", "extraData"),
View("routePanelAnalyticsAgentViews", "/panel/analytics/agent/", "extraData"), View("panel.AnalyticsAgentViews", "/panel/analytics/agent/", "extraData"),
View("routePanelAnalyticsForumViews", "/panel/analytics/forum/", "extraData"), View("panel.AnalyticsForumViews", "/panel/analytics/forum/", "extraData"),
View("routePanelAnalyticsSystemViews", "/panel/analytics/system/", "extraData"), View("panel.AnalyticsSystemViews", "/panel/analytics/system/", "extraData"),
View("routePanelAnalyticsLanguageViews", "/panel/analytics/lang/", "extraData"), View("panel.AnalyticsLanguageViews", "/panel/analytics/lang/", "extraData"),
View("routePanelAnalyticsReferrerViews", "/panel/analytics/referrer/", "extraData"), View("panel.AnalyticsReferrerViews", "/panel/analytics/referrer/", "extraData"),
View("routePanelAnalyticsPosts", "/panel/analytics/posts/").Before("ParseForm"), View("panel.AnalyticsPosts", "/panel/analytics/posts/").Before("ParseForm"),
View("routePanelAnalyticsTopics", "/panel/analytics/topics/").Before("ParseForm"), View("panel.AnalyticsTopics", "/panel/analytics/topics/").Before("ParseForm"),
View("routePanelAnalyticsForums", "/panel/analytics/forums/").Before("ParseForm"), View("panel.AnalyticsForums", "/panel/analytics/forums/").Before("ParseForm"),
View("routePanelGroups", "/panel/groups/"), View("routePanelGroups", "/panel/groups/"),
View("routePanelGroupsEdit", "/panel/groups/edit/", "extraData"), View("routePanelGroupsEdit", "/panel/groups/edit/", "extraData"),
@ -193,10 +193,10 @@ func buildPanelRoutes() {
Action("routePanelGroupsEditPermsSubmit", "/panel/groups/edit/perms/submit/", "extraData"), Action("routePanelGroupsEditPermsSubmit", "/panel/groups/edit/perms/submit/", "extraData"),
Action("routePanelGroupsCreateSubmit", "/panel/groups/create/"), Action("routePanelGroupsCreateSubmit", "/panel/groups/create/"),
View("routePanelBackups", "/panel/backups/", "extraData").Before("SuperAdminOnly"), // TODO: Test View("panel.Backups", "/panel/backups/", "extraData").Before("SuperAdminOnly"), // TODO: Tests for this
View("routePanelLogsRegs", "/panel/logs/regs/"), View("panel.LogsRegs", "/panel/logs/regs/"),
View("routePanelLogsMod", "/panel/logs/mod/"), View("panel.LogsMod", "/panel/logs/mod/"),
View("routePanelDebug", "/panel/debug/").Before("AdminOnly"), View("panel.Debug", "/panel/debug/").Before("AdminOnly"),
) )
addRouteGroup(panelGroup) addRouteGroup(panelGroup)
} }

View File

@ -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) http.Redirect(w, r, "/user/edit/username/?updated=1", http.StatusSeeOther)
return nil 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
}

3
routes/common.go Normal file
View File

@ -0,0 +1,3 @@
package routes
var successJSONBytes = []byte(`{"success":"1"}`)

View File

@ -56,6 +56,7 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, sfid st
} else if err != nil { } else if err != nil {
return common.InternalError(err, w, r) 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 // 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) 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) 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) { if common.RunPreRenderHook("pre_render_forum", w, r, &user, &pi) {
return nil return nil
} }

746
routes/panel/analytics.go Normal file
View File

@ -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)
}

54
routes/panel/backups.go Normal file
View File

@ -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)
}

31
routes/panel/common.go Normal file
View File

@ -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
}

42
routes/panel/debug.go Normal file
View File

@ -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)
}

View File

@ -1 +0,0 @@
This file is here so that Git will include this folder in the repository.

417
routes/panel/forums.go Normal file
View File

@ -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)
}

155
routes/panel/logs.go Normal file
View File

@ -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 <a href='%s'>%s</a>", elementID, actor.Link, actor.Name)
}
switch action {
case "lock":
out = "<a href='%s'>%s</a> was locked by <a href='%s'>%s</a>"
case "unlock":
out = "<a href='%s'>%s</a> was reopened by <a href='%s'>%s</a>"
case "stick":
out = "<a href='%s'>%s</a> was pinned by <a href='%s'>%s</a>"
case "unstick":
out = "<a href='%s'>%s</a> was unpinned by <a href='%s'>%s</a>"
case "move":
out = "<a href='%s'>%s</a> was moved by <a href='%s'>%s</a>" // 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 <a href='%s'>%s</a>", 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 = "<a href='%s'>%s</a> was banned by <a href='%s'>%s</a>"
case "unban":
out = "<a href='%s'>%s</a> was unbanned by <a href='%s'>%s</a>"
case "activate":
out = "<a href='%s'>%s</a> was activated by <a href='%s'>%s</a>"
}
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 <a href='%s'>%s</a> was deleted by <a href='%s'>%s</a>", topic.Link, topic.Title, actor.Link, actor.Name)
}
}
if out == "" {
out = fmt.Sprintf("Unknown action '%s' on elementType '%s' by <a href='%s'>%s</a>", 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)
}

118
routes/panel/settings.go Normal file
View File

@ -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
}

View File

@ -32,6 +32,8 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User) commo
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
// TODO: Preload this?
header.AddScript("profile.css")
var err error var err error
var replyCreatedAt time.Time var replyCreatedAt time.Time

92
routes/reports.go Normal file
View File

@ -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
}

View File

@ -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 { func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit string) common.RouteError {
page, _ := strconv.Atoi(r.FormValue("page")) page, _ := strconv.Atoi(r.FormValue("page"))
@ -64,7 +62,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit
topic.ClassName = "" topic.ClassName = ""
//log.Printf("topic: %+v\n", topic) //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 { if ferr != nil {
return ferr 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) //log.Printf("user.Perms: %+v\n", user.Perms)
return common.NoPermissions(w, r, user) 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 // TODO: Only include these on pages with polls
headerVars.AddSheet("chartist/chartist.min.css") header.AddSheet("chartist/chartist.min.css")
headerVars.AddScript("chartist/chartist.min.js") header.AddScript("chartist/chartist.min.js")
topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums") topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums")
topic.ContentLines = strings.Count(topic.Content, "\n") 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 // Calculate the offset
offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage) 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... // Get the replies if we have any...
if topic.PostCount > 0 { 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) { if common.RunPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) {
return nil return nil
} }
err = common.RunThemeTemplate(headerVars.Theme.Name, "topic", tpage, w) err = common.RunThemeTemplate(header.Theme.Name, "topic", tpage, w)
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }

View File

@ -1,5 +1,4 @@
INSERT INTO [sync] ([last_update]) VALUES (GETUTCDATE()); 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],[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 ('bigpost_min_words','250','int');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('megapost_min_words','1000','int'); INSERT INTO [settings] ([name],[content],[type]) VALUES ('megapost_min_words','1000','int');

View File

@ -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])
);

View File

@ -1,5 +1,4 @@
INSERT INTO `sync`(`last_update`) VALUES (UTC_TIMESTAMP()); 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`,`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 ('bigpost_min_words','250','int');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('megapost_min_words','1000','int'); INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('megapost_min_words','1000','int');

View File

@ -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;

View File

@ -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);
;

View File

@ -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`)
);

View File

@ -1,5 +1,5 @@
{ {
"DBVersion":"4", "DBVersion":"5",
"DynamicFileVersion":"0", "DynamicFileVersion":"0",
"MinGoVersion":"1.10", "MinGoVersion":"1.10",
"MinVersion":"" "MinVersion":""

View File

@ -9,7 +9,7 @@
<div class="rowitem passive"><a href="/user/edit/username/">{{lang "account_menu_username"}}</a></div> <div class="rowitem passive"><a href="/user/edit/username/">{{lang "account_menu_username"}}</a></div>
<div class="rowitem passive"><a href="/user/edit/critical/">{{lang "account_menu_password"}}</a></div> <div class="rowitem passive"><a href="/user/edit/critical/">{{lang "account_menu_password"}}</a></div>
<div class="rowitem passive"><a href="/user/edit/email/">{{lang "account_menu_email"}}</a></div> <div class="rowitem passive"><a href="/user/edit/email/">{{lang "account_menu_email"}}</a></div>
<div class="rowitem passive"><a href="/user/edit/notifications/">{{lang "account_menu_notifications"}}</a></div> <!--<div class="rowitem passive"><a href="/user/edit/notifications/">{{lang "account_menu_notifications"}}</a></div>-->
{{/** 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 **/}} {{/** 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 **/}}
</div> </div>
</nav> </nav>

View File

@ -1,3 +1,8 @@
</div>
<aside class="midRight sidebar">{{dock "rightSidebar" .Header }}</aside>
</div>
<div class="footBlock">
<div class="footLeft"></div>
<div class="footer"> <div class="footer">
{{dock "footer" .Header }} {{dock "footer" .Header }}
<div id="poweredByHolder" class="footerBit"> <div id="poweredByHolder" class="footerBit">
@ -15,8 +20,8 @@
</form> </form>
</div> </div>
</div> </div>
<div class="footRight"></div>
</div> </div>
<aside class="sidebar">{{dock "rightSidebar" .Header }}</aside>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>
</div> </div>

View File

@ -88,10 +88,17 @@
{{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="{{lang "status_closed_tooltip"}}"> | &#x1F512;&#xFE0E</span>{{end}} {{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="{{lang "status_closed_tooltip"}}"> | &#x1F512;&#xFE0E</span>{{end}}
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status_pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}} {{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status_pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}}
</span> </span>
<span class="topic_inner_right rowsmall" style="float: right;"> {{/** TODO: Phase this out of Cosora and remove it **/}}
<div class="topic_inner_right rowsmall">
<span class="replyCount">{{.PostCount}}</span><br /> <span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span> <span class="likeCount">{{.LikeCount}}</span>
</span> </div>
</div>
<div class="topic_middle">
<div class="topic_inner_middle rowsmall">
<span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span>
</div>
</div> </div>
<div class="rowitem topic_right passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}"> <div class="rowitem topic_right passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}">
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.Avatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a> <a href="{{.LastUser.Link}}"><img src="{{.LastUser.Avatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>

View File

@ -12,14 +12,13 @@
{{if .Desc}} {{if .Desc}}
<br /><span class="rowsmall" itemprop="description">{{.Desc}}</span> <br /><span class="rowsmall" itemprop="description">{{.Desc}}</span>
{{else}} {{else}}
<br /><span class="rowsmall" style="font-style: italic;">{{lang "forums_no_description"}}</span> <br /><span class="rowsmall forum_nodesc">{{lang "forums_no_description"}}</span>
{{end}} {{end}}
</span> </span>
<span class="forum_right shift_right"> <span class="forum_right shift_right">
{{if .LastReplyer.Avatar}}<img class="extra_little_row_avatar" src="{{.LastReplyer.Avatar}}" height=64 width=64 alt="{{.LastReplyer.Name}}'s Avatar" title="{{.LastReplyer.Name}}'s Avatar" />{{end}} {{if .LastReplyer.Avatar}}<img class="extra_little_row_avatar" src="{{.LastReplyer.Avatar}}" height=64 width=64 alt="{{.LastReplyer.Name}}'s Avatar" title="{{.LastReplyer.Name}}'s Avatar" />{{end}}
<span> <span>
<a href="{{.LastTopic.Link}}">{{if .LastTopic.Title}}{{.LastTopic.Title}}{{else}}{{lang "forums_none"}}{{end}}</a> <a {{if .LastTopic.Link}}href="{{.LastTopic.Link}}"{{else}}class="forum_no_poster"{{end}}>{{if .LastTopic.Title}}{{.LastTopic.Title}}{{else}}{{lang "forums_none"}}{{end}}</a>
{{if .LastTopicTime}}<br /><span class="rowsmall">{{.LastTopicTime}}</span>{{end}} {{if .LastTopicTime}}<br /><span class="rowsmall">{{.LastTopicTime}}</span>{{end}}
</span> </span>
</span> </span>

View File

@ -22,12 +22,13 @@
<body> <body>
{{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}} {{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}}
<div class="container"> <div class="container">
<!--<div class="navrow">-->
<div class="left_of_nav">{{dock "leftOfNav" .Header }}</div> <div class="left_of_nav">{{dock "leftOfNav" .Header }}</div>
<nav class="nav"> <nav class="nav">
<div class="move_left"> <div class="move_left">
<div class="move_right"> <div class="move_right">
<ul> <ul>{{/** TODO: Have the theme control whether the long or short form of the name is used **/}}
<li id="menu_overview" class="menu_left"><a href="/" rel="home">{{.Header.Site.ShortName}}</a></li> <li id="menu_overview" class="menu_left"><a href="/" rel="home">{{if eq .Header.Theme.Name "nox"}}{{.Header.Site.Name}}{{else}}{{.Header.Site.ShortName}}{{end}}</a></li>
{{dock "topMenu" .Header }} {{dock "topMenu" .Header }}
<li class="menu_left menu_hamburger" title="{{lang "menu_hamburger_tooltip"}}"><a></a></li> <li class="menu_left menu_hamburger" title="{{lang "menu_hamburger_tooltip"}}"><a></a></li>
</ul> </ul>
@ -35,8 +36,23 @@
</div> </div>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</nav> </nav>
<div class="right_of_nav">{{dock "rightOfNav" .Header }}</div> <div class="right_of_nav"><!--{{dock "rightOfNav" .Header }}-->
<div id="back"><div id="main" {{if .Header.Widgets.RightSidebar}}class="shrink_main"{{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"}}
<div class="user_box">
<img src="{{.CurrentUser.Avatar}}" />
<div class="option_box">
<span class="username">{{.CurrentUser.Name}}</span>
<span class="alerts">21 new alerts</span>
</div>
</div>
{{end}}
</div>
<!--</div>-->
<div class="midRow">
<div class="midLeft"></div>
<div id="back" class="zone_{{.Header.Zone}}{{if .Header.Widgets.RightSidebar}} shrink_main{{end}}">
<div id="main" >
<div class="alertbox">{{range .Header.NoticeList}} <div class="alertbox">{{range .Header.NoticeList}}
{{template "notice.html" . }}{{end}} {{template "notice.html" . }}{{end}}
</div> </div>

View File

@ -4,15 +4,11 @@
{{template "panel-menu.html" . }} {{template "panel-menu.html" . }}
<main class="colstack_right"> <main class="colstack_right">
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_setting_head"}}</h1></div> <div class="rowitem"><h1>{{.Setting.FriendlyName}}</h1></div>
</div> </div>
<div id="panel_setting" class="colstack_item"> <div id="panel_setting" class="colstack_item">
<form action="/panel/settings/edit/submit/{{.Something.Name}}?session={{.CurrentUser.Session}}" method="post"> <form action="/panel/settings/edit/submit/{{.Setting.Name}}?session={{.CurrentUser.Session}}" method="post">
<div class="formrow"> {{if eq .Setting.Type "list"}}
<div class="formitem formlabel"><a>{{lang "panel_setting_name"}}</a></div>
<div class="formitem formlabel">{{.Something.Name}}</div>
</div>
{{if eq .Something.Type "list"}}
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_setting_value"}}</a></div> <div class="formitem formlabel"><a>{{lang "panel_setting_value"}}</a></div>
<div class="formitem"> <div class="formitem">
@ -21,19 +17,23 @@
</select> </select>
</div> </div>
</div> </div>
{{else if eq .Something.Type "bool"}} {{else if eq .Setting.Type "bool"}}
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_setting_value"}}</a></div> <div class="formitem formlabel"><a>{{lang "panel_setting_value"}}</a></div>
<div class="formitem"> <div class="formitem">
<select name="setting-value"> <select name="setting-value">
<option{{if eq .Something.Content "1"}} selected{{end}} value="1">{{lang "option_yes"}}</option> <option{{if eq .Setting.Content "1"}} selected{{end}} value="1">{{lang "option_yes"}}</option>
<option{{if eq .Something.Content "0"}} selected{{end}} value="0">{{lang "option_no"}}</option> <option{{if eq .Setting.Content "0"}} selected{{end}} value="0">{{lang "option_no"}}</option>
</select> </select>
</div> </div>
</div> </div>
{{else if eq .Setting.Type "textarea"}}
<div class="formrow">
<div class="formitem"><textarea name="setting-value">{{.Setting.Content}}</textarea></div>
</div>
{{else}}<div class="formrow"> {{else}}<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_setting_value"}}</a></div> <div class="formitem formlabel"><a>{{lang "panel_setting_value"}}</a></div>
<div class="formitem"><input name="setting-value" type="text" value="{{.Something.Content}}" /></div> <div class="formitem"><input name="setting-value" type="text" value="{{.Setting.Content}}" /></div>
</div>{{end}} </div>{{end}}
<div class="formrow"> <div class="formrow">
<div class="formitem"><button name="panel-button" class="formbutton">{{lang "panel_setting_update_button"}}</button></div> <div class="formitem"><button name="panel-button" class="formbutton">{{lang "panel_setting_update_button"}}</button></div>

View File

@ -6,10 +6,10 @@
<div class="rowitem"><h1>{{lang "panel_settings_head"}}</h1></div> <div class="rowitem"><h1>{{lang "panel_settings_head"}}</h1></div>
</div> </div>
<div id="panel_settings" class="colstack_item rowlist"> <div id="panel_settings" class="colstack_item rowlist">
{{range $key, $value := .Something}} {{range .Something}}
<div class="rowitem panel_compactrow editable_parent"> <div class="rowitem panel_compactrow editable_parent">
<a href="/panel/settings/edit/{{$key}}" class="editable_block panel_upshift">{{$key}}</a> <a href="/panel/settings/edit/{{.Name}}" class="editable_block panel_upshift">{{.FriendlyName}}</a>
<a class="panel_compacttext to_right">{{$value}}</a> <a class="panel_compacttext to_right">{{.Content}}</a>
</div> </div>
{{end}} {{end}}
</div> </div>

View File

@ -11,9 +11,8 @@
<div class="rowitem editable_parent" style="background-image: url('{{.Avatar}}');"> <div class="rowitem editable_parent" style="background-image: url('{{.Avatar}}');">
<img class="bgsub" src="{{.Avatar}}" alt="{{.Name}}'s Avatar" /> <img class="bgsub" src="{{.Avatar}}" alt="{{.Name}}'s Avatar" />
<a class="rowTitle editable_block"{{if $.CurrentUser.Perms.EditUser}} href="/panel/users/edit/{{.ID}}?session={{$.CurrentUser.Session}}"{{end}}>{{.Name}}</a> <a class="rowTitle editable_block"{{if $.CurrentUser.Perms.EditUser}} href="/panel/users/edit/{{.ID}}?session={{$.CurrentUser.Session}}"{{end}}>{{.Name}}</a>
<a href="/user/{{.ID}}" class="tag-mini profile_url">{{lang "panel_users_profile"}}</a> <a href="{{.Link}}" class="tag-mini profile_url">{{lang "panel_users_profile"}}</a>
{{if (.Tag) and (.IsSuperMod)}}<span style="float: right;"><span class="panel_tag" style="margin-left 4px;">{{.Tag}}</span></span>{{end}} {{if (.Tag) and (.IsSuperMod)}}<span style="float: right;"><span class="panel_tag" style="margin-left 4px;">{{.Tag}}</span></span>{{end}}
<span class="panel_floater"> <span class="panel_floater">
{{if .IsBanned}}<a href="/users/unban/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button ban_button">{{lang "panel_users_unban"}}</a>{{else if not .IsSuperMod}}<a href="/user/{{.ID}}#ban_user" class="panel_tag panel_right_button ban_button">{{lang "panel_users_ban"}}</a>{{end}} {{if .IsBanned}}<a href="/users/unban/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button ban_button">{{lang "panel_users_unban"}}</a>{{else if not .IsSuperMod}}<a href="/user/{{.ID}}#ban_user" class="panel_tag panel_right_button ban_button">{{lang "panel_users_ban"}}</a>{{end}}
{{if not .Active}}<a href="/users/activate/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button">{{lang "panel_users_activate"}}</a>{{end}} {{if not .Active}}<a href="/users/activate/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button">{{lang "panel_users_activate"}}</a>{{end}}

View File

@ -113,18 +113,27 @@
{{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="{{lang "status_closed_tooltip"}}"> | &#x1F512;&#xFE0E</span>{{end}} {{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="{{lang "status_closed_tooltip"}}"> | &#x1F512;&#xFE0E</span>{{end}}
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status_pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}} {{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status_pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}}
</span> </span>
<span class="topic_inner_right rowsmall" style="float: right;"> {{/** TODO: Phase this out of Cosora and remove it **/}}
<div class="topic_inner_right rowsmall">
<span class="replyCount">{{.PostCount}}</span><br /> <span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span> <span class="likeCount">{{.LikeCount}}</span>
</span> </div>
</div>
<div class="topic_middle">
<div class="topic_middle_inside rowsmall">
<span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span>
</div>
</div> </div>
<div class="rowitem topic_right passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}} topic_closed{{end}}"> <div class="rowitem topic_right passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}} topic_closed{{end}}">
<div class="topic_right_inside">
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.Avatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a> <a href="{{.LastUser.Link}}"><img src="{{.LastUser.Avatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<span> <span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br> <a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<span class="rowsmall lastReplyAt">{{.RelativeLastReplyAt}}</span> <span class="rowsmall lastReplyAt">{{.RelativeLastReplyAt}}</span>
</span> </span>
</div> </div>
</div>
</div>{{else}}<div class="rowitem passive rowmsg">{{lang "topics_no_topics"}}{{if .CurrentUser.Perms.CreateTopic}} <a href="/topics/create/">{{lang "topics_start_one"}}</a>{{end}}</div>{{end}} </div>{{else}}<div class="rowitem passive rowmsg">{{lang "topics_no_topics"}}{{if .CurrentUser.Perms.CreateTopic}} <a href="/topics/create/">{{lang "topics_start_one"}}</a>{{end}}</div>{{end}}
</div> </div>

View File

@ -53,6 +53,10 @@ body, #main {
padding-right: 0px; padding-right: 0px;
padding-bottom: 0px; padding-bottom: 0px;
} }
.footBlock {
padding-left: 8px;
padding-right: 8px;
}
.container { .container {
background-color: var(--element-background-color); background-color: var(--element-background-color);
} }
@ -62,6 +66,7 @@ body, #main {
padding-top: 14px; padding-top: 14px;
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
padding-bottom: 14px;
} }
.sidebar { .sidebar {
width: 200px; width: 200px;
@ -386,10 +391,10 @@ h1, h3 {
} }
.topic_list_title_block .pre_opt { .topic_list_title_block .pre_opt {
border-left: 1px solid var(--element-border-color); border-left: 1px solid var(--element-border-color);
padding-left: 12px; padding-left: 11px;
height: 20px; height: 20px;
color: var(--light-text-color); color: var(--light-text-color);
margin-right: 10px; margin-right: 9px;
} }
.topic_list_title_block .pre_opt:before { .topic_list_title_block .pre_opt:before {
content: "{{index .Phrases "topics_click_topics_to_select"}}"; content: "{{index .Phrases "topics_click_topics_to_select"}}";
@ -413,6 +418,9 @@ h1, h3 {
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
font-size: 18px; font-size: 18px;
} }
.mod_opt .moderate_open {
display: none;
}
.topic_create_form { .topic_create_form {
display: flex !important; display: flex !important;
@ -686,6 +694,9 @@ textarea {
border: 1px solid var(--element-border-color); border: 1px solid var(--element-border-color);
border-bottom: 2px solid var(--element-border-color); border-bottom: 2px solid var(--element-border-color);
} }
.topic_middle {
display: none;
}
.rowlist .rowitem { .rowlist .rowitem {
background-color: var(--element-background-color); background-color: var(--element-background-color);
padding: 12px; padding: 12px;
@ -816,6 +827,9 @@ textarea {
flex: 1 1 0px; flex: 1 1 0px;
border-left: none; border-left: none;
} }
.topic_right_inside {
display: flex;
}
.topic_left img { .topic_left img {
border-radius: 30px; border-radius: 30px;
@ -824,7 +838,7 @@ textarea {
margin-top: 8px; margin-top: 8px;
margin-left: 4px; margin-left: 4px;
} }
.topic_right img { .topic_right_inside img {
border-radius: 30px; border-radius: 30px;
height: 42px; height: 42px;
width: 42px; width: 42px;
@ -837,7 +851,7 @@ textarea {
margin-bottom: 14px; margin-bottom: 14px;
width: 220px; width: 220px;
} }
.topic_right > span { .topic_right_inside > span {
margin-top: 12px; margin-top: 12px;
margin-left: 8px; margin-left: 8px;
} }
@ -885,6 +899,9 @@ textarea {
border-bottom: 2px solid var(--element-border-color); border-bottom: 2px solid var(--element-border-color);
padding: 14px; padding: 14px;
} }
.forum_list .forum_nodesc {
font-style: italic;
}
.forum_right { .forum_right {
display: flex; display: flex;
} }
@ -1325,9 +1342,6 @@ textarea {
} }
/* TODO: Make widget_about's CSS less footer centric */ /* TODO: Make widget_about's CSS less footer centric */
.footer {
margin-top: 14px;
}
.footerBit, .footer .widget { .footerBit, .footer .widget {
border-top: 1px solid var(--element-border-color); border-top: 1px solid var(--element-border-color);
padding: 12px; padding: 12px;
@ -1456,15 +1470,29 @@ textarea {
max-width: 1000px; max-width: 1000px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
}
.footer {
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
#main {
padding-top: 18px; padding-top: 18px;
padding-left: 16px; padding-left: 16px;
padding-right: 16px; padding-right: 16px;
border-left: 1px solid hsl(20,0%,95%); border-left: 1px solid hsl(20,0%,95%);
border-right: 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%); background-color: hsl(0,0%,95%);
} }
#back:not(.zone_panel) .footBlock {
display: flex;
}
} }
@media(min-width: 721px) { @media(min-width: 721px) {
@ -1591,11 +1619,16 @@ textarea {
font-size: 18px; font-size: 18px;
} }
main > .rowhead, #main > .rowhead { main > .rowhead, #main > .rowhead {
margin-left: 0px;
margin-right: 0px;
border: none; border: none;
border-bottom: 2px solid var(--header-border-color); 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 { .topic_list {
display: flex; display: flex;
@ -1622,13 +1655,13 @@ textarea {
border-left: 1px solid var(--element-border-color); border-left: 1px solid var(--element-border-color);
background-color: hsl(0,0%,95%); background-color: hsl(0,0%,95%);
} }
.topic_right br, .topic_right img { .topic_right_inside br, .topic_right_inside img {
display: none; display: none;
} }
.topic_right.topic_sticky { .topic_right.topic_sticky {
border-bottom: 2px solid var(--element-border-color); border-bottom: 2px solid var(--element-border-color);
} }
.topic_right > span { .topic_right_inside > span {
margin-top: 6px; margin-top: 6px;
margin-bottom: 6px; margin-bottom: 6px;
} }
@ -1729,13 +1762,6 @@ textarea {
} }
} }
@media(max-width: 520px) { @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) { .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; display: none;
} }

View File

@ -5,6 +5,7 @@
border-left: none; border-left: none;
border-right: none; border-right: none;
padding-left: 0px; padding-left: 0px;
padding-bottom: 0px;
} }
#back { #back {
background-color: inherit; background-color: inherit;
@ -147,6 +148,10 @@
padding-bottom: 4px; padding-bottom: 4px;
margin-bottom: 6px; margin-bottom: 6px;
} }
#panel_setting textarea {
width: 100%;
height: 80px;
}
#forum_quick_perms .formitem { #forum_quick_perms .formitem {
display: flex; display: flex;
@ -257,3 +262,16 @@
margin-top: -14.5px; 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;
}
}

View File

View File

@ -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;
}
}

View File

View File

@ -21,7 +21,7 @@ body {
background-color: var(--main-background-color); background-color: var(--main-background-color);
margin: 0; margin: 0;
} }
p::selection, span::selection, a::selection { *::selection {
background-color: hsl(0,0%,75%); background-color: hsl(0,0%,75%);
color: hsl(0,0%,20%); color: hsl(0,0%,20%);
font-weight: 100; font-weight: 100;
@ -226,6 +226,9 @@ a {
} }
/* TODO: Add the avatars to the forum list */ /* TODO: Add the avatars to the forum list */
.forum_list .forum_nodesc {
font-style: italic;
}
.extra_little_row_avatar { .extra_little_row_avatar {
display: none; display: none;
} }
@ -377,7 +380,7 @@ textarea.large {
display: block; display: block;
} }
.formitem button, .formbutton { .formitem button, .formbutton, .mod_floater_submit, .pane_buttons button {
background-color: var(--input-background-color); background-color: var(--input-background-color);
border: 1px solid var(--input-border-color); border: 1px solid var(--input-border-color);
color: var(--input-text-color); color: var(--input-text-color);
@ -385,6 +388,15 @@ textarea.large {
padding-bottom: 6px; padding-bottom: 6px;
font-size: 13px; font-size: 13px;
} }
.mod_floater_submit {
padding: 5px;
padding-bottom: 4px;
margin-left: 2px;
}
.pane_buttons button {
padding: 5px;
padding-bottom: 4px;
}
.formrow { .formrow {
flex-direction: row; flex-direction: row;
@ -666,16 +678,97 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
font-size: 11px; 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 { .create_topic_opt a:before {
content: "{{index .Phrases "topics_new_topic"}}"; content: "{{index .Phrases "topics_new_topic"}}";
margin-left: 3px;
} }
.locked_opt a:before { .locked_opt a:before {
content: "{{index .Phrases "forum_locked"}}"; 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 { .topic_list .topic_row {
display: flex; 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 */ /* Temporary hack, so that I don't break the topic lists of the other themes */
.topic_list .topic_inner_right { .topic_list .topic_inner_right {
display: none; display: none;
@ -706,10 +799,13 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
width: 284px; width: 284px;
padding: 0px; padding: 0px;
} }
.topic_right_inside {
display: flex;
}
.topic_list .topic_left img, .topic_list .topic_right img { .topic_list .topic_left img, .topic_list .topic_right img {
width: 64px; width: 64px;
} }
.topic_list .topic_inner_left, .topic_right > span { .topic_list .topic_inner_left, .topic_right_inside > span {
margin-left: 8px; margin-left: 8px;
margin-top: 12px; margin-top: 12px;
} }
@ -725,6 +821,9 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
.topic_list .starter:before { .topic_list .starter:before {
content: "{{index .Phrases "topics_starter"}}: "; content: "{{index .Phrases "topics_starter"}}: ";
} }
.topic_middle {
display: none;
}
.topic_name_input { .topic_name_input {
width: 100%; width: 100%;

View File

View File

@ -158,6 +158,10 @@ li a {
#back { #back {
padding: 12px; padding: 12px;
padding-top: 0px; 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 */ /* 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 */ /* TODO: Add the avatars to the forum list */
.forum_list .forum_nodesc {
font-style: italic;
}
.extra_little_row_avatar { .extra_little_row_avatar {
display: none; display: none;
} }
@ -434,6 +441,7 @@ li a {
.topic_list .topic_row { .topic_list .topic_row {
display: grid; display: grid;
grid-template-columns: calc(100% - 204px) 204px; grid-template-columns: calc(100% - 204px) 204px;
overflow: hidden;
} }
.topic_list .topic_inner_right { .topic_list .topic_inner_right {
display: none; display: none;
@ -466,14 +474,20 @@ li a {
padding: 0px; padding: 0px;
height: 58px; height: 58px;
} }
.topic_left img, .topic_right img { .topic_right_inside {
display: flex;
}
.topic_left img, .topic_right_inside img {
width: 64px; width: 64px;
height: auto; height: auto;
} }
.topic_left .topic_inner_left, .topic_right > span { .topic_left .topic_inner_left, .topic_right_inside > span {
margin-top: 10px; margin-top: 10px;
margin-left: 8px; margin-left: 8px;
} }
.topic_middle {
display: none;
}
.postImage { .postImage {
max-width: 100%; max-width: 100%;
@ -969,9 +983,13 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
content: "{{index .Phrases "topic_report_button_text"}}"; content: "{{index .Phrases "topic_report_button_text"}}";
} }
.footer {
margin-left: 12px;
margin-right: 12px;
margin-bottom: 12px;
}
#poweredByHolder { #poweredByHolder {
border: 1px solid var(--main-border-color); border: 1px solid var(--main-border-color);
margin-top: 12px;
clear: both; clear: both;
height: 40px; height: 40px;
padding: 6px; padding: 6px;

View File

View File

@ -1,4 +0,0 @@
# Theme Notes
/public/post-avatar-bg.jpg is a solid rgb(255,255,255) white.

View File

@ -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; }
}

View File

@ -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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

View File

@ -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"]
}

View File

@ -149,6 +149,12 @@ li a {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
#back {
display: flex;
}
#main {
width: 100%;
}
.rowblock { .rowblock {
border: 1px solid hsl(0, 0%, 80%); border: 1px solid hsl(0, 0%, 80%);
@ -420,6 +426,9 @@ input, select {
} }
/* TODO: Add the avatars to the forum list */ /* TODO: Add the avatars to the forum list */
.forum_list .forum_nodesc {
font-style: italic;
}
.extra_little_row_avatar { .extra_little_row_avatar {
display: none; display: none;
} }
@ -474,14 +483,20 @@ input, select {
height: 58px; height: 58px;
overflow: hidden; overflow: hidden;
} }
.topic_left img, .topic_right img { .topic_right_inside {
display: flex;
}
.topic_left img, .topic_right_inside img {
width: 64px; width: 64px;
height: auto; height: auto;
} }
.topic_left .topic_inner_left, .topic_right > span { .topic_left .topic_inner_left, .topic_right_inside > span {
margin-top: 10px; margin-top: 10px;
margin-left: 8px; margin-left: 8px;
} }
.topic_middle {
display: none;
}
.postImage { .postImage {
max-width: 100%; max-width: 100%;

View File

View File

@ -10,6 +10,9 @@ go get -u github.com/denisenkom/go-mssqldb
echo "Updating bcrypt" echo "Updating bcrypt"
go get -u golang.org/x/crypto/bcrypt go get -u golang.org/x/crypto/bcrypt
echo "Updating Argon2"
go get -u golang.org/x/crypto/argon2
echo "Updating gopsutil" echo "Updating gopsutil"
go get -u github.com/Azareal/gopsutil go get -u github.com/Azareal/gopsutil

View File

@ -26,6 +26,13 @@ if %errorlevel% neq 0 (
exit /b %errorlevel% 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) echo Updating /x/system/windows (dependency for gopsutil)
go get -u golang.org/x/sys/windows go get -u golang.org/x/sys/windows
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (