UNSTABLE: 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:36:35 +10:00
parent 8ff8ce8e51
commit 23a686fe96
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/EQCSS.min.js"
- "public/EQCSS.js"
- "template_list.go"
- "template_forum.go"
- "template_forums.go"
- "template_topic.go"
- "template_topic_alt.go"
- "template_topics.go"
- "template_profile.go"
- "public/Sortable-1.4.0/*"

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/argon2
go get -u github.com/StackExchange/wmi
go get -u github.com/Azareal/gopsutil
@ -189,8 +191,6 @@ We're looking for ways to clean-up the plugin system so that all of them (except
![Tempra Simple Mobile](https://github.com/Azareal/Gosora/blob/master/images/tempra-simple-mobile-375px.png)
![Tempra Cursive Theme](https://github.com/Azareal/Gosora/blob/master/images/tempra-cursive.png)
![Tempra Conflux Theme](https://github.com/Azareal/Gosora/blob/master/images/tempra-conflux.png)
![Tempra Conflux Mobile](https://github.com/Azareal/Gosora/blob/master/images/tempra-conflux-mobile-320px.png)
@ -207,7 +207,7 @@ More images in the /images/ folder. Beware though, some of them are *really* out
* github.com/go-sql-driver/mysql For interfacing with MariaDB.
* golang.org/x/crypto/bcrypt For hashing passwords.
* golang.org/x/crypto/bcrypt and go get -u golang.org/x/crypto/argon2 For hashing passwords.
* github.com/Azareal/gopsutil For pulling information on CPU and memory usage. I've temporarily forked this, as we were having stability issues with the latest build.
@ -229,6 +229,8 @@ More images in the /images/ folder. Beware though, some of them are *really* out
* github.com/fsnotify/fsnotify A library for watching events on the file system.
* More items to come here, our dependencies are going through a lot of changes, and I'll be documenting those soon ;)
# Bundled Plugins
There are several plugins which are bundled with the software by default. These cover various common tasks which aren't common enough to clutter the core with or which have competing implementation methods (E.g. plugin_markdown vs plugin_bbcode for post mark-up).
@ -239,7 +241,7 @@ There are several plugins which are bundled with the software by default. These
* Markdown - An extremely simple plugin for converting Markdown into HTML.
* Social Groups - A WIP plugin which lets users create their own little discussion areas which they can administrate / moderate on their own.
* Social Groups - An extremely unstable WIP plugin which lets users create their own little discussion areas which they can administrate / moderate on their own.
# Developers

View File

@ -1,30 +1,58 @@
/*
*
* Gosora Authentication Interface
* Copyright Azareal 2017 - 2018
* Copyright Azareal 2017 - 2019
*
*/
package common
import "errors"
import "strconv"
import "net/http"
import "database/sql"
import (
"database/sql"
"errors"
"net/http"
"strconv"
"strings"
import "golang.org/x/crypto/bcrypt"
import "../query_gen/lib"
"../query_gen/lib"
//"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
)
var Auth AuthInt
const SaltLength int = 32
const SessionLength int = 80
// ErrMismatchedHashAndPassword is thrown whenever a hash doesn't match it's unhashed password
var ErrMismatchedHashAndPassword = bcrypt.ErrMismatchedHashAndPassword
// nolint
var ErrHashNotExist = errors.New("We don't recognise that hashing algorithm")
var ErrTooFewHashParams = errors.New("You haven't provided enough hash parameters")
// ErrPasswordTooLong is silly, but we don't want bcrypt to bork on us
var ErrPasswordTooLong = errors.New("The password you selected is too long")
var ErrWrongPassword = errors.New("That's not the correct password.")
var ErrSecretError = errors.New("There was a glitch in the system. Please contact your local administrator.")
var ErrNoUserByName = errors.New("We couldn't find an account with that username.")
var DefaultHashAlgo = "bcrypt" // Override this in the configuration file, not here
//func(realPassword string, password string, salt string) (err error)
var CheckPasswordFuncs = map[string]func(string, string, string) error{
"bcrypt": BcryptCheckPassword,
//"argon2": Argon2CheckPassword,
}
//func(password string) (hashedPassword string, salt string, err error)
var GeneratePasswordFuncs = map[string]func(string) (string, string, error){
"bcrypt": BcryptGeneratePassword,
//"argon2": Argon2GeneratePassword,
}
var HashPrefixes = map[string]string{
"$2a$": "bcrypt",
//"argon2$": "argon2",
}
// AuthInt is the main authentication interface.
type AuthInt interface {
@ -176,3 +204,75 @@ func (auth *DefaultAuth) CreateSession(uid int) (session string, err error) {
}
return session, nil
}
func CheckPassword(realPassword string, password string, salt string) (err error) {
blasted := strings.Split(realPassword, "$")
prefix := blasted[0]
if len(blasted) > 1 {
prefix += blasted[1]
}
algo, ok := HashPrefixes[prefix]
if !ok {
return ErrHashNotExist
}
checker := CheckPasswordFuncs[algo]
return checker(realPassword, password, salt)
}
func GeneratePassword(password string) (hash string, salt string, err error) {
gen, ok := GeneratePasswordFuncs[DefaultHashAlgo]
if !ok {
return "", "", ErrHashNotExist
}
return gen(password)
}
func BcryptCheckPassword(realPassword string, password string, salt string) (err error) {
return bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password+salt))
}
// Note: The salt is in the hash, therefore the salt parameter is blank
func BcryptGeneratePassword(password string) (hash string, salt string, err error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", "", err
}
return string(hashedPassword), salt, nil
}
/*const (
argon2Time uint32 = 3
argon2Memory uint32 = 32 * 1024
argon2Threads uint8 = 4
argon2KeyLen uint32 = 32
)
func Argon2CheckPassword(realPassword string, password string, salt string) (err error) {
split := strings.Split(realPassword, "$")
// TODO: Better validation
if len(split) < 5 {
return ErrTooFewHashParams
}
realKey, _ := base64.StdEncoding.DecodeString(split[len(split)-1])
time, _ := strconv.Atoi(split[1])
memory, _ := strconv.Atoi(split[2])
threads, _ := strconv.Atoi(split[3])
keyLen, _ := strconv.Atoi(split[4])
key := argon2.Key([]byte(password), []byte(salt), uint32(time), uint32(memory), uint8(threads), uint32(keyLen))
if subtle.ConstantTimeCompare(realKey, key) != 1 {
return ErrMismatchedHashAndPassword
}
return nil
}
func Argon2GeneratePassword(password string) (hash string, salt string, err error) {
sbytes := make([]byte, SaltLength)
_, err = rand.Read(sbytes)
if err != nil {
return "", "", err
}
key := argon2.Key([]byte(password), sbytes, argon2Time, argon2Memory, argon2Threads, argon2KeyLen)
hash = base64.StdEncoding.EncodeToString(key)
return fmt.Sprintf("argon2$%d%d%d%d%s%s", argon2Time, argon2Memory, argon2Threads, argon2KeyLen, salt, hash), string(sbytes), nil
}
*/

View File

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

View File

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

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()
}
// TODO: Simplify these four conditionals into two
if lower == 0 {
if higher < 0 {
return nil, errors.New("higher may not be lower than 0")

View File

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

View File

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

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 SimpleForumUserCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (headerLite *HeaderLite, err RouteError) = simpleForumUserCheck
var ForumUserCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (header *Header, err RouteError) = forumUserCheck
var MemberCheck func(w http.ResponseWriter, r *http.Request, user *User) (header *Header, err RouteError) = memberCheck
var SimpleUserCheck func(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, err RouteError) = simpleUserCheck
var UserCheck func(w http.ResponseWriter, r *http.Request, user *User) (header *Header, err RouteError) = userCheck
@ -166,28 +165,15 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
}
func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) {
return &HeaderLite{
Site: Site,
Settings: SettingBox.Load().(SettingMap),
}, nil
}
// TODO: Add this to the member routes
func memberCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Header, rerr RouteError) {
header, rerr = UserCheck(w, r, user)
if !user.Loggedin {
return header, NoPermissions(w, r, *user)
}
return header, rerr
return simpleUserCheck(w, r, user)
}
// SimpleUserCheck is back from the grave, yay :D
func simpleUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) {
headerLite = &HeaderLite{
return &HeaderLite{
Site: Site,
Settings: SettingBox.Load().(SettingMap),
}
return headerLite, nil
}, nil
}
// TODO: Add the ability for admins to restrict certain themes to certain groups?

View File

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

View File

@ -51,6 +51,7 @@ type dbConfig struct {
type config struct {
SslPrivkey string
SslFullchain string
HashAlgo string // Defaults to bcrypt, and in the future, possibly something stronger
MaxRequestSize int
CacheTopicUser int
@ -103,6 +104,11 @@ func ProcessConfig() error {
if Config.MaxUsernameLength == 0 {
Config.MaxUsernameLength = 100
}
GuestUser.Avatar = BuildAvatar(0, "")
if Config.HashAlgo != "" {
// TODO: Set the alternate hash algo, e.g. argon2
}
// We need this in here rather than verifyConfig as switchToTestDB() currently overwrites the values it verifies
if DbConfig.TestDbname == DbConfig.Dbname {

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")
var now = time.Now()
@ -167,7 +175,8 @@ func CompileTemplates() error {
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, RelativeTime(now), 0, 0, "", "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
var varList = make(map[string]tmpl.VarItem)
tpage := TopicPage{"Title", user, header, replyList, topic, poll, 1, 1}
header.Title = "Topic Name"
tpage := TopicPage{header, replyList, topic, poll, 1, 1}
topicIDTmpl, err := c.Compile("topic.html", "templates/", "common.TopicPage", tpage, varList)
if err != nil {
return err
@ -203,17 +212,16 @@ func CompileTemplates() error {
var topicsList []*TopicsRow
topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, "Date", time.Now(), "Date", user3.ID, 1, "", "127.0.0.1", 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"})
header.Title = "Topic List"
header2.Title = "Topic List"
topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, Paginator{[]int{1}, 1, 1}}
topicListTmpl, err := c.Compile("topics.html", "templates/", "common.TopicListPage", topicListPage, varList)
if err != nil {
return err
}
//var topicList []TopicUser
//topicList = append(topicList,TopicUser{1,"topic-title","Topic Title","The topic content.",1,false,false,"Date","Date",1,"","127.0.0.1",0,1,"classname","","admin-fred","Admin Fred",config.DefaultGroup,"",0,"","","","",58,false})
forumItem := BlankForum(1, "general-forum.1", "General Forum", "Where the general stuff happens", true, "all", 0, "", 0)
forumPage := ForumPage{"General Forum", user, header, topicsList, forumItem, Paginator{[]int{1}, 1, 1}}
header.Title = "General Forum"
forumPage := ForumPage{header, topicsList, forumItem, Paginator{[]int{1}, 1, 1}}
forumTmpl, err := c.Compile("forum.html", "templates/", "common.ForumPage", forumPage, varList)
if err != nil {
return err

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
import (
//"fmt"
"bytes"
"database/sql"
"encoding/json"
"errors"
"io"
"io/ioutil"
"log"
"mime"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"sync/atomic"
"text/template"
"../query_gen/lib"
)
type ThemeList map[string]*Theme
var Themes ThemeList = make(map[string]*Theme)
var Themes ThemeList = make(map[string]*Theme) // ? Refactor this into a store?
var DefaultThemeBox atomic.Value
var ChangeDefaultThemeMutex sync.Mutex
// TODO: Use this when the default theme doesn't exist
var fallbackTheme = "cosora"
var overridenTemplates = make(map[string]bool)
type Theme struct {
Name string
FriendlyName string
Version string
Creator string
FullImage string
MobileFriendly bool
Disabled bool
HideFromThemes bool
BgAvatars bool // For profiles, at the moment
ForkOf string
Tag string
URL string
Docks []string // Allowed Values: leftSidebar, rightSidebar, footer
Settings map[string]ThemeSetting
Templates []TemplateMapping
TemplatesMap map[string]string
TmplPtr map[string]interface{}
Resources []ThemeResource
ResourceTemplates *template.Template
// This variable should only be set and unset by the system, not the theme meta file
Active bool
}
type ThemeSetting struct {
FriendlyName string
Options []string
}
type TemplateMapping struct {
Name string
Source string
//When string
}
type ThemeResource struct {
Name string
Location string
Loggedin bool // Only serve this resource to logged in users
}
var overridenTemplates = make(map[string]bool) // ? What is this used for?
type ThemeStmts struct {
getThemes *sql.Stmt
@ -91,6 +42,89 @@ func init() {
})
}
func NewThemeList() (themes ThemeList, err error) {
themes = make(map[string]*Theme)
themeFiles, err := ioutil.ReadDir("./themes")
if err != nil {
return themes, err
}
for _, themeFile := range themeFiles {
if !themeFile.IsDir() {
continue
}
themeName := themeFile.Name()
log.Printf("Adding theme '%s'", themeName)
themePath := "./themes/" + themeName
themeFile, err := ioutil.ReadFile(themePath + "/theme.json")
if err != nil {
return themes, err
}
var theme = &Theme{Name: ""}
err = json.Unmarshal(themeFile, theme)
if err != nil {
return themes, err
}
// TODO: Implement the static file part of this and fsnotify
if theme.Path != "" {
log.Print("Resolving redirect to " + theme.Path)
themeFile, err := ioutil.ReadFile(theme.Path + "/theme.json")
if err != nil {
return themes, err
}
theme = &Theme{Name: "", Path: theme.Path}
err = json.Unmarshal(themeFile, theme)
if err != nil {
return themes, err
}
} else {
theme.Path = themePath
}
theme.Active = false // Set this to false, just in case someone explicitly overrode this value in the JSON file
// TODO: Let the theme specify where it's resources are via the JSON file?
// TODO: Let the theme inherit CSS from another theme?
// ? - This might not be too helpful, as it only searches for /public/ and not if /public/ is empty. Still, it might help some people with a slightly less cryptic error
log.Print(theme.Path + "/public/")
_, err = os.Stat(theme.Path + "/public/")
if err != nil {
if os.IsNotExist(err) {
return themes, errors.New("We couldn't find this theme's resources. E.g. the /public/ folder.")
} else {
log.Print("We weren't able to access this theme's resources due to a permissions issue or some other problem")
return themes, err
}
}
if theme.FullImage != "" {
DebugLog("Adding theme image")
err = StaticFiles.Add(theme.Path+"/"+theme.FullImage, themePath)
if err != nil {
return themes, err
}
}
theme.TemplatesMap = make(map[string]string)
theme.TmplPtr = make(map[string]interface{})
if theme.Templates != nil {
for _, themeTmpl := range theme.Templates {
theme.TemplatesMap[themeTmpl.Name] = themeTmpl.Source
theme.TmplPtr[themeTmpl.Name] = TmplPtrMap["o_"+themeTmpl.Source]
}
}
// TODO: Bind the built template, or an interpreted one for any dock overrides this theme has
themes[theme.Name] = theme
}
return themes, nil
}
// TODO: Make the initThemes and LoadThemes functions less confusing
// ? - Delete themes which no longer exist in the themes folder from the database?
func (themes ThemeList) LoadActiveStatus() error {
@ -141,221 +175,8 @@ func (themes ThemeList) LoadStaticFiles() error {
return nil
}
func InitThemes() error {
themeFiles, err := ioutil.ReadDir("./themes")
if err != nil {
return err
}
for _, themeFile := range themeFiles {
if !themeFile.IsDir() {
continue
}
themeName := themeFile.Name()
log.Printf("Adding theme '%s'", themeName)
themeFile, err := ioutil.ReadFile("./themes/" + themeName + "/theme.json")
if err != nil {
return err
}
var theme = &Theme{Name: ""}
err = json.Unmarshal(themeFile, theme)
if err != nil {
return err
}
theme.Active = false // Set this to false, just in case someone explicitly overrode this value in the JSON file
// TODO: Let the theme specify where it's resources are via the JSON file?
// TODO: Let the theme inherit CSS from another theme?
// ? - This might not be too helpful, as it only searches for /public/ and not if /public/ is empty. Still, it might help some people with a slightly less cryptic error
_, err = os.Stat("./themes/" + theme.Name + "/public/")
if err != nil {
if os.IsNotExist(err) {
return errors.New("We couldn't find this theme's resources. E.g. the /public/ folder.")
} else {
log.Print("We weren't able to access this theme's resources due to a permissions issue or some other problem")
return err
}
}
if theme.FullImage != "" {
DebugLog("Adding theme image")
err = StaticFiles.Add("./themes/"+themeName+"/"+theme.FullImage, "./themes/"+themeName)
if err != nil {
return err
}
}
theme.TemplatesMap = make(map[string]string)
theme.TmplPtr = make(map[string]interface{})
if theme.Templates != nil {
for _, themeTmpl := range theme.Templates {
theme.TemplatesMap[themeTmpl.Name] = themeTmpl.Source
theme.TmplPtr[themeTmpl.Name] = TmplPtrMap["o_"+themeTmpl.Source]
}
}
Themes[theme.Name] = theme
}
return nil
}
// TODO: It might be unsafe to call the template parsing functions with fsnotify, do something more concurrent
func (theme *Theme) LoadStaticFiles() error {
theme.ResourceTemplates = template.New("")
template.Must(theme.ResourceTemplates.ParseGlob("./themes/" + theme.Name + "/public/*.css"))
// It should be safe for us to load the files for all the themes in memory, as-long as the admin hasn't setup a ridiculous number of themes
return theme.AddThemeStaticFiles()
}
func (theme *Theme) AddThemeStaticFiles() error {
phraseMap := GetTmplPhrases()
// TODO: Use a function instead of a closure to make this more testable? What about a function call inside the closure to take the theme variable into account?
return filepath.Walk("./themes/"+theme.Name+"/public", func(path string, f os.FileInfo, err error) error {
DebugLog("Attempting to add static file '" + path + "' for default theme '" + theme.Name + "'")
if err != nil {
return err
}
if f.IsDir() {
return nil
}
path = strings.Replace(path, "\\", "/", -1)
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
var ext = filepath.Ext(path)
if ext == ".css" && len(data) != 0 {
var b bytes.Buffer
var pieces = strings.Split(path, "/")
var filename = pieces[len(pieces)-1]
err = theme.ResourceTemplates.ExecuteTemplate(&b, filename, CSSData{Phrases: phraseMap})
if err != nil {
return err
}
data = b.Bytes()
}
path = strings.TrimPrefix(path, "themes/"+theme.Name+"/public")
gzipData := compressBytesGzip(data)
StaticFiles.Set("/static/"+theme.Name+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
DebugLog("Added the '/" + theme.Name + path + "' static file for theme " + theme.Name + ".")
return nil
})
}
func (theme *Theme) MapTemplates() {
if theme.Templates != nil {
for _, themeTmpl := range theme.Templates {
if themeTmpl.Name == "" {
LogError(errors.New("Invalid destination template name"))
}
if themeTmpl.Source == "" {
LogError(errors.New("Invalid source template name"))
}
// `go generate` is one possibility for letting plugins inject custom page structs, but it would simply add another step of compilation. It might be simpler than the current build process from the perspective of the administrator?
destTmplPtr, ok := TmplPtrMap[themeTmpl.Name]
if !ok {
return
}
sourceTmplPtr, ok := TmplPtrMap[themeTmpl.Source]
if !ok {
LogError(errors.New("The source template doesn't exist!"))
}
switch dTmplPtr := destTmplPtr.(type) {
case *func(TopicPage, io.Writer) error:
switch sTmplPtr := sourceTmplPtr.(type) {
case *func(TopicPage, io.Writer) error:
//overridenTemplates[themeTmpl.Name] = d_tmpl_ptr
overridenTemplates[themeTmpl.Name] = true
*dTmplPtr = *sTmplPtr
default:
LogError(errors.New("The source and destination templates are incompatible"))
}
case *func(TopicListPage, io.Writer) error:
switch sTmplPtr := sourceTmplPtr.(type) {
case *func(TopicListPage, io.Writer) error:
//overridenTemplates[themeTmpl.Name] = d_tmpl_ptr
overridenTemplates[themeTmpl.Name] = true
*dTmplPtr = *sTmplPtr
default:
LogError(errors.New("The source and destination templates are incompatible"))
}
case *func(ForumPage, io.Writer) error:
switch sTmplPtr := sourceTmplPtr.(type) {
case *func(ForumPage, io.Writer) error:
//overridenTemplates[themeTmpl.Name] = d_tmpl_ptr
overridenTemplates[themeTmpl.Name] = true
*dTmplPtr = *sTmplPtr
default:
LogError(errors.New("The source and destination templates are incompatible"))
}
case *func(ForumsPage, io.Writer) error:
switch sTmplPtr := sourceTmplPtr.(type) {
case *func(ForumsPage, io.Writer) error:
//overridenTemplates[themeTmpl.Name] = d_tmpl_ptr
overridenTemplates[themeTmpl.Name] = true
*dTmplPtr = *sTmplPtr
default:
LogError(errors.New("The source and destination templates are incompatible"))
}
case *func(ProfilePage, io.Writer) error:
switch sTmplPtr := sourceTmplPtr.(type) {
case *func(ProfilePage, io.Writer) error:
//overridenTemplates[themeTmpl.Name] = d_tmpl_ptr
overridenTemplates[themeTmpl.Name] = true
*dTmplPtr = *sTmplPtr
default:
LogError(errors.New("The source and destination templates are incompatible"))
}
case *func(CreateTopicPage, io.Writer) error:
switch sTmplPtr := sourceTmplPtr.(type) {
case *func(CreateTopicPage, io.Writer) error:
//overridenTemplates[themeTmpl.Name] = d_tmpl_ptr
overridenTemplates[themeTmpl.Name] = true
*dTmplPtr = *sTmplPtr
default:
LogError(errors.New("The source and destination templates are incompatible"))
}
case *func(IPSearchPage, io.Writer) error:
switch sTmplPtr := sourceTmplPtr.(type) {
case *func(IPSearchPage, io.Writer) error:
//overridenTemplates[themeTmpl.Name] = d_tmpl_ptr
overridenTemplates[themeTmpl.Name] = true
*dTmplPtr = *sTmplPtr
default:
LogError(errors.New("The source and destination templates are incompatible"))
}
case *func(Page, io.Writer) error:
switch sTmplPtr := sourceTmplPtr.(type) {
case *func(Page, io.Writer) error:
//overridenTemplates[themeTmpl.Name] = d_tmpl_ptr
overridenTemplates[themeTmpl.Name] = true
*dTmplPtr = *sTmplPtr
default:
LogError(errors.New("The source and destination templates are incompatible"))
}
default:
log.Print("themeTmpl.Name: ", themeTmpl.Name)
log.Print("themeTmpl.Source: ", themeTmpl.Source)
LogError(errors.New("Unknown destination template type!"))
}
}
}
}
func ResetTemplateOverrides() {
log.Print("Resetting the template overrides")
for name := range overridenTemplates {
log.Print("Resetting '" + name + "' template override")
@ -542,17 +363,3 @@ func GetDefaultThemeName() string {
func SetDefaultThemeName(name string) {
DefaultThemeBox.Store(name)
}
func (theme Theme) HasDock(name string) bool {
for _, dock := range theme.Docks {
if dock == name {
return true
}
}
return false
}
// TODO: Implement this
func (theme Theme) BuildDock(dock string) (sbody string) {
return ""
}

View File

@ -14,20 +14,14 @@ import (
"time"
"../query_gen/lib"
"golang.org/x/crypto/bcrypt"
)
// TODO: Replace any literals with this
var BanGroup = 4
// TODO: Use something else as the guest avatar, maybe a question mark of some sort?
// GuestUser is an instance of user which holds guest data to avoid having to initialise a guest every time
var GuestUser = User{ID: 0, Link: "#", Group: 6, Perms: GuestPerms}
//func(real_password string, password string, salt string) (err error)
var CheckPassword = BcryptCheckPassword
//func(password string) (hashed_password string, salt string, err error)
var GeneratePassword = BcryptGeneratePassword
var GuestUser = User{ID: 0, Name: "Guest", Link: "#", Group: 6, Perms: GuestPerms} // BuildAvatar is done in site.go to make sure it's done after init
var ErrNoTempGroup = errors.New("We couldn't find a temporary group for this user")
type User struct {
@ -369,33 +363,6 @@ func BuildAvatar(uid int, avatar string) string {
return strings.Replace(Config.Noavatar, "{id}", strconv.Itoa(uid), 1)
}
func BcryptCheckPassword(realPassword string, password string, salt string) (err error) {
return bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password+salt))
}
// Investigate. Do we need the extra salt?
func BcryptGeneratePassword(password string) (hashedPassword string, salt string, err error) {
salt, err = GenerateSafeString(SaltLength)
if err != nil {
return "", "", err
}
password = password + salt
hashedPassword, err = BcryptGeneratePasswordNoSalt(password)
if err != nil {
return "", "", err
}
return hashedPassword, salt, nil
}
func BcryptGeneratePasswordNoSalt(password string) (hash string, err error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashedPassword), nil
}
// TODO: Move this to *User
func SetPassword(uid int, password string) error {
hashedPassword, salt, err := GeneratePassword(password)

View File

@ -20,6 +20,7 @@ type UserStore interface {
DirtyGet(id int) *User
Get(id int) (*User, error)
Exists(id int) bool
GetOffset(offset int, perPage int) (users []*User, err error)
//BulkGet(ids []int) ([]*User, error)
BulkGetMap(ids []int) (map[int]*User, error)
BypassGet(id int) (*User, error)
@ -35,6 +36,7 @@ type DefaultUserStore struct {
cache UserCache
get *sql.Stmt
getOffset *sql.Stmt
exists *sql.Stmt
register *sql.Stmt
usernameExists *sql.Stmt
@ -51,6 +53,7 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) {
return &DefaultUserStore{
cache: cache,
get: acc.SimpleSelect("users", "name, group, active, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, liked, last_ip, temp_group", "uid = ?", "", ""),
getOffset: acc.Select("users").Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, liked, last_ip, temp_group").Orderby("uid ASC").Limit("?,?").Prepare(),
exists: acc.SimpleSelect("users", "uid", "uid = ?", "", ""),
register: acc.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()"), // TODO: Implement user_count on users_groups here
usernameExists: acc.SimpleSelect("users", "name", "name = ?", "", ""),
@ -92,6 +95,29 @@ func (mus *DefaultUserStore) Get(id int) (*User, error) {
return user, err
}
// TODO: Optimise this, so we don't wind up hitting the database every-time for small gaps
// TODO: Make this a little more consistent with DefaultGroupStore's GetRange method
func (store *DefaultUserStore) GetOffset(offset int, perPage int) (users []*User, err error) {
rows, err := store.getOffset.Query(offset, perPage)
if err != nil {
return users, err
}
defer rows.Close()
for rows.Next() {
user := &User{Loggedin: true}
err := rows.Scan(&user.ID, &user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Liked, &user.LastIP, &user.TempGroup)
if err != nil {
return nil, err
}
user.Init()
store.cache.Set(user)
users = append(users, user)
}
return users, rows.Err()
}
// TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts?
// TODO: ID of 0 should always error?
func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) {

View File

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

View File

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

View File

@ -29,6 +29,13 @@ if %errorlevel% neq 0 (
exit /b %errorlevel%
)
echo Updating the Argon2 library
go get -u golang.org/x/crypto/argon2
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Updating /x/sys/windows (dependency for gopsutil)
go get -u golang.org/x/sys/windows
if %errorlevel% neq 0 (

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%
)
echo Updating the Argon2 library
go get -u golang.org/x/crypto/argon2
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Updating /x/sys/windows (dependency for gopsutil)
go get -u golang.org/x/sys/windows
if %errorlevel% neq 0 (

View File

@ -10,14 +10,10 @@ import "./common"
// nolint
type Stmts struct {
isPluginActive *sql.Stmt
getUsersOffset *sql.Stmt
isThemeDefault *sql.Stmt
getEmailsByUser *sql.Stmt
getTopicBasic *sql.Stmt
forumEntryExists *sql.Stmt
groupEntryExists *sql.Stmt
getForumTopics *sql.Stmt
createReport *sql.Stmt
addForumPermsToForum *sql.Stmt
addPlugin *sql.Stmt
addTheme *sql.Stmt
@ -29,13 +25,11 @@ type Stmts struct {
updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt
updateEmail *sql.Stmt
verifyEmail *sql.Stmt
setTempGroup *sql.Stmt
updateWordFilter *sql.Stmt
bumpSync *sql.Stmt
deleteActivityStreamMatch *sql.Stmt
deleteWordFilter *sql.Stmt
reportExists *sql.Stmt
getActivityFeedByWatcher *sql.Stmt
getActivityCountByWatcher *sql.Stmt
@ -59,14 +53,6 @@ func _gen_mssql() (err error) {
return err
}
common.DebugLog("Preparing getUsersOffset statement.")
stmts.getUsersOffset, err = db.Prepare("SELECT [uid],[name],[group],[active],[is_super_admin],[avatar] FROM [users] ORDER BY uid ASC OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY")
if err != nil {
log.Print("Error in getUsersOffset statement.")
log.Print("Bad Query: ","SELECT [uid],[name],[group],[active],[is_super_admin],[avatar] FROM [users] ORDER BY uid ASC OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY")
return err
}
common.DebugLog("Preparing isThemeDefault statement.")
stmts.isThemeDefault, err = db.Prepare("SELECT [default] FROM [themes] WHERE [uname] = ?1")
if err != nil {
@ -75,22 +61,6 @@ func _gen_mssql() (err error) {
return err
}
common.DebugLog("Preparing getEmailsByUser statement.")
stmts.getEmailsByUser, err = db.Prepare("SELECT [email],[validated],[token] FROM [emails] WHERE [uid] = ?1")
if err != nil {
log.Print("Error in getEmailsByUser statement.")
log.Print("Bad Query: ","SELECT [email],[validated],[token] FROM [emails] WHERE [uid] = ?1")
return err
}
common.DebugLog("Preparing getTopicBasic statement.")
stmts.getTopicBasic, err = db.Prepare("SELECT [title],[content] FROM [topics] WHERE [tid] = ?1")
if err != nil {
log.Print("Error in getTopicBasic statement.")
log.Print("Bad Query: ","SELECT [title],[content] FROM [topics] WHERE [tid] = ?1")
return err
}
common.DebugLog("Preparing forumEntryExists statement.")
stmts.forumEntryExists, err = db.Prepare("SELECT [fid] FROM [forums] WHERE [name] = '' ORDER BY fid ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY")
if err != nil {
@ -115,14 +85,6 @@ func _gen_mssql() (err error) {
return err
}
common.DebugLog("Preparing createReport statement.")
stmts.createReport, err = db.Prepare("INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[createdBy],[lastReplyBy],[data],[parentID],[css_class]) VALUES (?,?,?,GETUTCDATE(),GETUTCDATE(),?,?,?,1,'report')")
if err != nil {
log.Print("Error in createReport statement.")
log.Print("Bad Query: ","INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[createdBy],[lastReplyBy],[data],[parentID],[css_class]) VALUES (?,?,?,GETUTCDATE(),GETUTCDATE(),?,?,?,1,'report')")
return err
}
common.DebugLog("Preparing addForumPermsToForum statement.")
stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) VALUES (?,?,?,?)")
if err != nil {
@ -211,14 +173,6 @@ func _gen_mssql() (err error) {
return err
}
common.DebugLog("Preparing verifyEmail statement.")
stmts.verifyEmail, err = db.Prepare("UPDATE [emails] SET [validated] = 1,[token] = '' WHERE [email] = ?")
if err != nil {
log.Print("Error in verifyEmail statement.")
log.Print("Bad Query: ","UPDATE [emails] SET [validated] = 1,[token] = '' WHERE [email] = ?")
return err
}
common.DebugLog("Preparing setTempGroup statement.")
stmts.setTempGroup, err = db.Prepare("UPDATE [users] SET [temp_group] = ? WHERE [uid] = ?")
if err != nil {
@ -258,14 +212,6 @@ func _gen_mssql() (err error) {
log.Print("Bad Query: ","DELETE FROM [word_filters] WHERE [wfid] = ?")
return err
}
common.DebugLog("Preparing reportExists statement.")
stmts.reportExists, err = db.Prepare("SELECT COUNT(*) AS [count] FROM [topics] WHERE [data] = ? AND [data] != '' AND [parentID] = 1")
if err != nil {
log.Print("Error in reportExists statement.")
log.Print("Bad Query: ","SELECT COUNT(*) AS [count] FROM [topics] WHERE [data] = ? AND [data] != '' AND [parentID] = 1")
return err
}
return nil
}

View File

@ -12,14 +12,10 @@ import "./common"
// nolint
type Stmts struct {
isPluginActive *sql.Stmt
getUsersOffset *sql.Stmt
isThemeDefault *sql.Stmt
getEmailsByUser *sql.Stmt
getTopicBasic *sql.Stmt
forumEntryExists *sql.Stmt
groupEntryExists *sql.Stmt
getForumTopics *sql.Stmt
createReport *sql.Stmt
addForumPermsToForum *sql.Stmt
addPlugin *sql.Stmt
addTheme *sql.Stmt
@ -31,13 +27,11 @@ type Stmts struct {
updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt
updateEmail *sql.Stmt
verifyEmail *sql.Stmt
setTempGroup *sql.Stmt
updateWordFilter *sql.Stmt
bumpSync *sql.Stmt
deleteActivityStreamMatch *sql.Stmt
deleteWordFilter *sql.Stmt
reportExists *sql.Stmt
getActivityFeedByWatcher *sql.Stmt
getActivityCountByWatcher *sql.Stmt
@ -60,13 +54,6 @@ func _gen_mysql() (err error) {
return err
}
common.DebugLog("Preparing getUsersOffset statement.")
stmts.getUsersOffset, err = db.Prepare("SELECT `uid`,`name`,`group`,`active`,`is_super_admin`,`avatar` FROM `users` ORDER BY `uid` ASC LIMIT ?,?")
if err != nil {
log.Print("Error in getUsersOffset statement.")
return err
}
common.DebugLog("Preparing isThemeDefault statement.")
stmts.isThemeDefault, err = db.Prepare("SELECT `default` FROM `themes` WHERE `uname` = ?")
if err != nil {
@ -74,20 +61,6 @@ func _gen_mysql() (err error) {
return err
}
common.DebugLog("Preparing getEmailsByUser statement.")
stmts.getEmailsByUser, err = db.Prepare("SELECT `email`,`validated`,`token` FROM `emails` WHERE `uid` = ?")
if err != nil {
log.Print("Error in getEmailsByUser statement.")
return err
}
common.DebugLog("Preparing getTopicBasic statement.")
stmts.getTopicBasic, err = db.Prepare("SELECT `title`,`content` FROM `topics` WHERE `tid` = ?")
if err != nil {
log.Print("Error in getTopicBasic statement.")
return err
}
common.DebugLog("Preparing forumEntryExists statement.")
stmts.forumEntryExists, err = db.Prepare("SELECT `fid` FROM `forums` WHERE `name` = '' ORDER BY `fid` ASC LIMIT 0,1")
if err != nil {
@ -109,13 +82,6 @@ func _gen_mysql() (err error) {
return err
}
common.DebugLog("Preparing createReport statement.")
stmts.createReport, err = db.Prepare("INSERT INTO `topics`(`title`,`content`,`parsed_content`,`createdAt`,`lastReplyAt`,`createdBy`,`lastReplyBy`,`data`,`parentID`,`css_class`) VALUES (?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,1,'report')")
if err != nil {
log.Print("Error in createReport statement.")
return err
}
common.DebugLog("Preparing addForumPermsToForum statement.")
stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) VALUES (?,?,?,?)")
if err != nil {
@ -193,13 +159,6 @@ func _gen_mysql() (err error) {
return err
}
common.DebugLog("Preparing verifyEmail statement.")
stmts.verifyEmail, err = db.Prepare("UPDATE `emails` SET `validated` = 1,`token` = '' WHERE `email` = ?")
if err != nil {
log.Print("Error in verifyEmail statement.")
return err
}
common.DebugLog("Preparing setTempGroup statement.")
stmts.setTempGroup, err = db.Prepare("UPDATE `users` SET `temp_group` = ? WHERE `uid` = ?")
if err != nil {
@ -234,13 +193,6 @@ func _gen_mysql() (err error) {
log.Print("Error in deleteWordFilter statement.")
return err
}
common.DebugLog("Preparing reportExists statement.")
stmts.reportExists, err = db.Prepare("SELECT COUNT(*) AS `count` FROM `topics` WHERE `data` = ? AND `data` != '' AND `parentID` = 1")
if err != nil {
log.Print("Error in reportExists statement.")
return err
}
return nil
}

View File

@ -9,6 +9,10 @@ import "./common"
// nolint
type Stmts struct {
addForumPermsToForum *sql.Stmt
addPlugin *sql.Stmt
addTheme *sql.Stmt
createWordFilter *sql.Stmt
updatePlugin *sql.Stmt
updatePluginInstall *sql.Stmt
updateTheme *sql.Stmt
@ -16,7 +20,6 @@ type Stmts struct {
updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt
updateEmail *sql.Stmt
verifyEmail *sql.Stmt
setTempGroup *sql.Stmt
updateWordFilter *sql.Stmt
bumpSync *sql.Stmt
@ -35,6 +38,34 @@ type Stmts struct {
func _gen_pgsql() (err error) {
common.DebugLog("Building the generated statements")
common.DebugLog("Preparing addForumPermsToForum statement.")
stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO "forums_permissions"("gid","fid","preset","permissions") VALUES (?,?,?,?)")
if err != nil {
log.Print("Error in addForumPermsToForum statement.")
return err
}
common.DebugLog("Preparing addPlugin statement.")
stmts.addPlugin, err = db.Prepare("INSERT INTO "plugins"("uname","active","installed") VALUES (?,?,?)")
if err != nil {
log.Print("Error in addPlugin statement.")
return err
}
common.DebugLog("Preparing addTheme statement.")
stmts.addTheme, err = db.Prepare("INSERT INTO "themes"("uname","default") VALUES (?,?)")
if err != nil {
log.Print("Error in addTheme statement.")
return err
}
common.DebugLog("Preparing createWordFilter statement.")
stmts.createWordFilter, err = db.Prepare("INSERT INTO "word_filters"("find","replacement") VALUES (?,?)")
if err != nil {
log.Print("Error in createWordFilter statement.")
return err
}
common.DebugLog("Preparing updatePlugin statement.")
stmts.updatePlugin, err = db.Prepare("UPDATE `plugins` SET `active` = ? WHERE `uname` = ?")
if err != nil {
@ -84,13 +115,6 @@ func _gen_pgsql() (err error) {
return err
}
common.DebugLog("Preparing verifyEmail statement.")
stmts.verifyEmail, err = db.Prepare("UPDATE `emails` SET `validated` = 1,`token` = '' WHERE `email` = ?")
if err != nil {
log.Print("Error in verifyEmail statement.")
return err
}
common.DebugLog("Preparing setTempGroup statement.")
stmts.setTempGroup, err = db.Prepare("UPDATE `users` SET `temp_group` = ? WHERE `uid` = ?")
if err != nil {

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,13 @@ if %errorlevel% neq 0 (
exit /b %errorlevel%
)
echo Installing the Argon2 library
go get -u golang.org/x/crypto/argon2
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Installing /x/sys/windows (dependency for gopsutil)
go get -u golang.org/x/sys/windows
if %errorlevel% neq 0 (

View File

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

View File

@ -44,8 +44,12 @@
"MoveTopic": "Can move topics in or out"
},
"SettingLabels": {
"activation_type": "Activate All,Email Activation,Admin Approval"
"SettingPhrases": {
"activation_type":"Activation Type",
"activation_type_label": "Activate All,Email Activation,Admin Approval",
"bigpost_min_words":"Big Post Minimum Words",
"megapost_min_words":"Mega Post Minimum Words",
"meta_desc":"Meta Description"
},
"PermPresets": {
@ -263,6 +267,7 @@
"topics_click_topics_to_select":"Click the topics to select them",
"topics_new_topic":"New Topic",
"forum_locked":"Locked",
"topics_moderate":"Moderate",
"topics_replies_suffix":" replies",
"forums_topics_suffix":" topics",
"topics_gap_likes_suffix":" likes",

39
main.go
View File

@ -29,7 +29,6 @@ import (
var version = common.Version{Major: 0, Minor: 1, Patch: 0, Tag: "dev"}
var router *GenRouter
var startTime time.Time
var logWriter = io.MultiWriter(os.Stderr)
// TODO: Wrap the globals in here so we can pass pointers to them to subpackages
@ -107,6 +106,14 @@ func afterDBInit() (err error) {
}
log.Print("Initialising the stores")
common.Reports, err = common.NewDefaultReportStore(acc)
if err != nil {
return err
}
common.Emails, err = common.NewDefaultEmailStore(acc)
if err != nil {
return err
}
common.RegLogs, err = common.NewRegLogStore(acc)
if err != nil {
return err
@ -140,7 +147,8 @@ func afterDBInit() (err error) {
return err
}
counters.GlobalViewCounter, err = counters.NewGlobalViewCounter()
log.Print("Initialising the view counters")
counters.GlobalViewCounter, err = counters.NewGlobalViewCounter(acc)
if err != nil {
return err
}
@ -195,18 +203,6 @@ func main() {
return
}
}()*/
// WIP: Mango Test
/*res, err := ioutil.ReadFile("./templates/topic.html")
if err != nil {
log.Fatal(err)
}
tagIndices, err := mangoParse(string(res))
if err != nil {
log.Fatal(err)
}
log.Printf("tagIndices: %+v\n", tagIndices)
log.Fatal("")*/
config.Config()
// TODO: Have a file for each run with the time/date the server started as the file name?
@ -217,18 +213,9 @@ func main() {
}
logWriter = io.MultiWriter(os.Stderr, f)
log.SetOutput(logWriter)
//if profiling {
// f, err := os.Create("startup_cpu.prof")
// if err != nil {
// log.Fatal(err)
// }
// pprof.StartCPUProfile(f)
//}
log.Print("Running Gosora v" + version.String())
fmt.Println("")
startTime = time.Now()
common.StartTime = time.Now()
log.Print("Processing configuration data")
err = common.ProcessConfig()
@ -236,7 +223,7 @@ func main() {
log.Fatal(err)
}
err = common.InitThemes()
common.Themes, err = common.NewThemeList()
if err != nil {
log.Fatal(err)
}
@ -271,6 +258,7 @@ func main() {
log.Fatal(err)
}
log.Print("Initialising the file watcher")
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
@ -337,6 +325,7 @@ func main() {
}
}
log.Print("Initialising the task system")
var runTasks = func(tasks []func() error) {
for _, task := range tasks {
if task() != nil {

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"
t.Logf("Set realPassword to '%s'", realPassword)
t.Log("Hashing the real password")
hashedPassword, err = common.BcryptGeneratePasswordNoSalt(realPassword)
hashedPassword, err = common.BcryptGeneratePassword(realPassword)
if err != nil {
t.Error(err)
}

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ func init() {
addPatch(1, patch1)
addPatch(2, patch2)
addPatch(3, patch3)
addPatch(4, patch4)
}
func patch0(scanner *bufio.Scanner) (err error) {
@ -235,3 +236,214 @@ func patch3(scanner *bufio.Scanner) error {
return nil
}
func patch4(scanner *bufio.Scanner) error {
// ! Don't reuse this function blindly, it doesn't escape apostrophes
var replaceTextWhere = func(replaceThis string, withThis string) error {
return execStmt(qgen.Builder.SimpleUpdate("viewchunks", "route = '"+withThis+"'", "route = '"+replaceThis+"'"))
}
err := replaceTextWhere("routeReportSubmit", "routes.ReportSubmit")
if err != nil {
return err
}
err = replaceTextWhere("routeAccountEditEmail", "routes.AccountEditEmail")
if err != nil {
return err
}
err = replaceTextWhere("routeAccountEditEmailTokenSubmit", "routes.AccountEditEmailTokenSubmit")
if err != nil {
return err
}
err = replaceTextWhere("routePanelLogsRegs", "panel.LogsRegs")
if err != nil {
return err
}
err = replaceTextWhere("routePanelLogsMod", "panel.LogsMod")
if err != nil {
return err
}
err = replaceTextWhere("routePanelLogsAdmin", "panel.LogsAdmin")
if err != nil {
return err
}
err = replaceTextWhere("routePanelDebug", "panel.Debug")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsViews", "panel.AnalyticsViews")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsRouteViews", "panel.AnalyticsRouteViews")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsRouteViews", "panel.AnalyticsRouteViews")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsAgentViews", "panel.AnalyticsAgentViews")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsForumViews", "panel.AnalyticsForumViews")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsSystemViews", "panel.AnalyticsSystemViews")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsLanguageViews", "panel.AnalyticsLanguageViews")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsReferrerViews", "panel.AnalyticsReferrerViews")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsTopics", "panel.AnalyticsTopics")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsPosts", "panel.AnalyticsPosts")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsForums", "panel.AnalyticsForums")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsRoutes", "panel.AnalyticsRoutes")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsAgents", "panel.AnalyticsAgents")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsSystems", "panel.AnalyticsSystems")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsLanguages", "panel.AnalyticsLanguages")
if err != nil {
return err
}
err = replaceTextWhere("routePanelAnalyticsReferrers", "panel.AnalyticsReferrers")
if err != nil {
return err
}
err = replaceTextWhere("routePanelSettings", "panel.Settings")
if err != nil {
return err
}
err = replaceTextWhere("routePanelSettingEdit", "panel.SettingEdit")
if err != nil {
return err
}
err = replaceTextWhere("routePanelSettingEditSubmit", "panel.SettingEditSubmit")
if err != nil {
return err
}
err = replaceTextWhere("routePanelForums", "panel.Forums")
if err != nil {
return err
}
err = replaceTextWhere("routePanelForumsCreateSubmit", "panel.ForumsCreateSubmit")
if err != nil {
return err
}
err = replaceTextWhere("routePanelForumsDelete", "panel.ForumsDelete")
if err != nil {
return err
}
err = replaceTextWhere("routePanelForumsDeleteSubmit", "panel.ForumsDeleteSubmit")
if err != nil {
return err
}
err = replaceTextWhere("routePanelForumsEdit", "panel.ForumsEdit")
if err != nil {
return err
}
err = replaceTextWhere("routePanelForumsEditSubmit", "panel.ForumsEditSubmit")
if err != nil {
return err
}
err = replaceTextWhere("routePanelForumsEditPermsSubmit", "panel.ForumsEditPermsSubmit")
if err != nil {
return err
}
err = replaceTextWhere("routePanelForumsEditPermsAdvance", "panel.ForumsEditPermsAdvance")
if err != nil {
return err
}
err = replaceTextWhere("routePanelForumsEditPermsAdvanceSubmit", "panel.ForumsEditPermsAdvanceSubmit")
if err != nil {
return err
}
err = replaceTextWhere("routePanelBackups", "panel.Backups")
if err != nil {
return err
}
err = execStmt(qgen.Builder.SimpleDelete("settings", "name='url_tags'"))
if err != nil {
return err
}
err = execStmt(qgen.Builder.CreateTable("pages", "utf8mb4", "utf8mb4_general_ci",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"pid", "int", 0, false, true, ""},
qgen.DBTableColumn{"name", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"title", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"body", "text", 0, false, false, ""},
qgen.DBTableColumn{"allowedGroups", "text", 0, false, false, ""},
qgen.DBTableColumn{"menuID", "int", 0, false, false, "-1"},
},
[]qgen.DBTableKey{
qgen.DBTableKey{"pid", "primary"},
},
))
if err != nil {
return err
}
return nil
}

View File

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

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))
}
func (build *builder) AddColumn(table string, column DBTableColumn) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.AddColumn("_builder", table, column))
}
func (build *builder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsert("_builder", table, columns, fields))
}

View File

@ -71,53 +71,7 @@ func (adapter *MssqlAdapter) CreateTable(name string, table string, charset stri
var querystr = "CREATE TABLE [" + table + "] ("
for _, column := range columns {
var max bool
var createdAt bool
switch column.Type {
case "createdAt":
column.Type = "datetime"
createdAt = true
case "varchar":
column.Type = "nvarchar"
case "text":
column.Type = "nvarchar"
max = true
case "json":
column.Type = "nvarchar"
max = true
case "boolean":
column.Type = "bit"
}
var size string
if column.Size > 0 {
size = " (" + strconv.Itoa(column.Size) + ")"
}
if max {
size = " (MAX)"
}
var end string
if column.Default != "" {
end = " DEFAULT "
if createdAt {
end += "GETUTCDATE()" // TODO: Use GETUTCDATE() in updates instead of the neutral format
} else if adapter.stringyType(column.Type) && column.Default != "''" {
end += "'" + column.Default + "'"
} else {
end += column.Default
}
}
if !column.Null {
end += " not null"
}
// ! Not exactly the meaning of auto increment...
if column.AutoIncrement {
end += " IDENTITY"
}
column, size, end := adapter.parseColumn(column)
querystr += "\n\t[" + column.Name + "] " + column.Type + size + end + ","
}
@ -140,6 +94,67 @@ func (adapter *MssqlAdapter) CreateTable(name string, table string, charset stri
return querystr, nil
}
func (adapter *MssqlAdapter) parseColumn(column DBTableColumn) (col DBTableColumn, size string, end string) {
var max, createdAt bool
switch column.Type {
case "createdAt":
column.Type = "datetime"
createdAt = true
case "varchar":
column.Type = "nvarchar"
case "text":
column.Type = "nvarchar"
max = true
case "json":
column.Type = "nvarchar"
max = true
case "boolean":
column.Type = "bit"
}
if column.Size > 0 {
size = " (" + strconv.Itoa(column.Size) + ")"
}
if max {
size = " (MAX)"
}
if column.Default != "" {
end = " DEFAULT "
if createdAt {
end += "GETUTCDATE()" // TODO: Use GETUTCDATE() in updates instead of the neutral format
} else if adapter.stringyType(column.Type) && column.Default != "''" {
end += "'" + column.Default + "'"
} else {
end += column.Default
}
}
if !column.Null {
end += " not null"
}
// ! Not exactly the meaning of auto increment...
if column.AutoIncrement {
end += " IDENTITY"
}
return column, size, end
}
// TODO: Test this, not sure if some things work
func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
column, size, end := adapter.parseColumn(column)
querystr := "ALTER TABLE [" + table + "] ADD [" + column.Name + "] " + column.Type + size + end + ";"
adapter.pushStatement(name, "add-column", querystr)
return querystr, nil
}
func (adapter *MssqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")

View File

@ -86,39 +86,7 @@ func (adapter *MysqlAdapter) CreateTable(name string, table string, charset stri
var querystr = "CREATE TABLE `" + table + "` ("
for _, column := range columns {
// Make it easier to support Cassandra in the future
if column.Type == "createdAt" {
column.Type = "datetime"
} else if column.Type == "json" {
column.Type = "text"
}
var size string
if column.Size > 0 {
size = "(" + strconv.Itoa(column.Size) + ")"
}
var end string
// TODO: Exclude the other variants of text like mediumtext and longtext too
if column.Default != "" && column.Type != "text" {
end = " DEFAULT "
if adapter.stringyType(column.Type) && column.Default != "''" {
end += "'" + column.Default + "'"
} else {
end += column.Default
}
}
if column.Null {
end += " null"
} else {
end += " not null"
}
if column.AutoIncrement {
end += " AUTO_INCREMENT"
}
column, size, end := adapter.parseColumn(column)
querystr += "\n\t`" + column.Name + "` " + column.Type + size + end + ","
}
@ -148,6 +116,54 @@ func (adapter *MysqlAdapter) CreateTable(name string, table string, charset stri
return querystr + ";", nil
}
func (adapter *MysqlAdapter) parseColumn(column DBTableColumn) (col DBTableColumn, size string, end string) {
// Make it easier to support Cassandra in the future
if column.Type == "createdAt" {
column.Type = "datetime"
} else if column.Type == "json" {
column.Type = "text"
}
if column.Size > 0 {
size = "(" + strconv.Itoa(column.Size) + ")"
}
// TODO: Exclude the other variants of text like mediumtext and longtext too
if column.Default != "" && column.Type != "text" {
end = " DEFAULT "
if adapter.stringyType(column.Type) && column.Default != "''" {
end += "'" + column.Default + "'"
} else {
end += column.Default
}
}
if column.Null {
end += " null"
} else {
end += " not null"
}
if column.AutoIncrement {
end += " AUTO_INCREMENT"
}
return column, size, end
}
// TODO: Support AFTER column
// TODO: Test to make sure everything works here
func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
column, size, end := adapter.parseColumn(column)
querystr := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end + ";"
adapter.pushStatement(name, "add-column", querystr)
return querystr, nil
}
func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")

View File

@ -119,7 +119,7 @@ func (adapter *PgsqlAdapter) CreateTable(name string, table string, charset stri
}
// TODO: Implement this
func (adapter *PgsqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
@ -129,6 +129,54 @@ func (adapter *PgsqlAdapter) SimpleInsert(name string, table string, columns str
return "", nil
}
// TODO: Test this
// ! We need to get the last ID out of this somehow, maybe add returning to every query? Might require some sort of wrapper over the sql statements
func (adapter *PgsqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
var querystr = "INSERT INTO \"" + table + "\"("
if columns != "" {
querystr += adapter.buildColumns(columns) + ") VALUES ("
for _, field := range processFields(fields) {
nameLen := len(field.Name)
if field.Name[0] == '"' && field.Name[nameLen-1] == '"' && nameLen >= 3 {
field.Name = "'" + field.Name[1:nameLen-1] + "'"
}
if field.Name[0] == '\'' && field.Name[nameLen-1] == '\'' && nameLen >= 3 {
field.Name = "'" + strings.Replace(field.Name[1:nameLen-1], "'", "''", -1) + "'"
}
querystr += field.Name + ","
}
querystr = querystr[0 : len(querystr)-1]
} else {
querystr += ") VALUES ("
}
querystr += ")"
adapter.pushStatement(name, "insert", querystr)
return querystr, nil
}
func (adapter *PgsqlAdapter) buildColumns(columns string) (querystr string) {
if columns == "" {
return ""
}
// Escape the column names, just in case we've used a reserved keyword
for _, column := range processColumns(columns) {
if column.Type == "function" {
querystr += column.Left + ","
} else {
querystr += "\"" + column.Left + "\","
}
}
return querystr[0 : len(querystr)-1]
}
// TODO: Implement this
func (adapter *PgsqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) {
if name == "" {

View File

@ -106,6 +106,9 @@ type Adapter interface {
DropTable(name string, table string) (string, error)
CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error)
// TODO: Some way to add indices and keys
// TODO: Test this
AddColumn(name string, table string, column DBTableColumn) (string, error)
SimpleInsert(name string, table string, columns string, fields string) (string, error)
SimpleUpdate(name string, table string, set string, where string) (string, error)
SimpleDelete(name string, table string, where string) (string, error)

View File

@ -111,8 +111,6 @@ func writeStatements(adapter qgen.Adapter) error {
func seedTables(adapter qgen.Adapter) error {
qgen.Install.SimpleInsert("sync", "last_update", "UTC_TIMESTAMP()")
qgen.Install.SimpleInsert("settings", "name, content, type", "'url_tags','1','bool'")
qgen.Install.SimpleInsert("settings", "name, content, type, constraints", "'activation_type','1','list','1-3'")
qgen.Install.SimpleInsert("settings", "name, content, type", "'bigpost_min_words','250','int'")
qgen.Install.SimpleInsert("settings", "name, content, type", "'megapost_min_words','1000','int'")
@ -261,14 +259,8 @@ func writeSelects(adapter qgen.Adapter) error {
//build.Select("isPluginInstalled").Table("plugins").Columns("installed").Where("uname = ?").Parse()
build.Select("getUsersOffset").Table("users").Columns("uid, name, group, active, is_super_admin, avatar").Orderby("uid ASC").Limit("?,?").Parse()
build.Select("isThemeDefault").Table("themes").Columns("default").Where("uname = ?").Parse()
build.Select("getEmailsByUser").Table("emails").Columns("email, validated, token").Where("uid = ?").Parse()
build.Select("getTopicBasic").Table("topics").Columns("title, content").Where("tid = ?").Parse() // TODO: Comment this out and see if anything breaks
build.Select("forumEntryExists").Table("forums").Columns("fid").Where("name = ''").Orderby("fid ASC").Limit("0,1").Parse()
build.Select("groupEntryExists").Table("users_groups").Columns("gid").Where("name = ''").Orderby("gid ASC").Limit("0,1").Parse()
@ -289,8 +281,6 @@ func writeInnerJoins(adapter qgen.Adapter) (err error) {
func writeInserts(adapter qgen.Adapter) error {
build := adapter.Builder()
build.Insert("createReport").Table("topics").Columns("title, content, parsed_content, createdAt, lastReplyAt, createdBy, lastReplyBy, data, parentID, css_class").Fields("?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,1,'report'").Parse()
build.Insert("addForumPermsToForum").Table("forums_permissions").Columns("gid,fid,preset,permissions").Fields("?,?,?,?").Parse()
build.Insert("addPlugin").Table("plugins").Columns("uname, active, installed").Fields("?,?,?").Parse()
@ -319,8 +309,6 @@ func writeUpdates(adapter qgen.Adapter) error {
build.Update("updateEmail").Table("emails").Set("email = ?, uid = ?, validated = ?, token = ?").Where("email = ?").Parse()
build.Update("verifyEmail").Table("emails").Set("validated = 1, token = ''").Where("email = ?").Parse() // Need to fix this: Empty string isn't working, it gets set to 1 instead x.x -- Has this been fixed?
build.Update("setTempGroup").Table("users").Set("temp_group = ?").Where("uid = ?").Parse()
build.Update("updateWordFilter").Table("word_filters").Set("find = ?, replacement = ?").Where("wfid = ?").Parse()
@ -344,8 +332,6 @@ func writeDeletes(adapter qgen.Adapter) error {
}
func writeSimpleCounts(adapter qgen.Adapter) error {
adapter.SimpleCount("reportExists", "topics", "data = ? AND data != '' AND parentID = 1", "")
return nil
}

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.DBTableColumn{
qgen.DBTableColumn{"rlid", "int", 0, false, true, ""},

View File

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

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)
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 {
return common.InternalError(err, w, r)
}
header.Title = forum.Name
// TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete
offset, page, lastPage := common.PageOffset(forum.TopicCount, page, common.Config.ItemsPerPage)
@ -112,7 +113,7 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, sfid st
}
pageList := common.Paginate(forum.TopicCount, common.Config.ItemsPerPage, 5)
pi := common.ForumPage{forum.Name, user, header, topicList, forum, common.Paginator{pageList, page, lastPage}}
pi := common.ForumPage{header, topicList, forum, common.Paginator{pageList, page, lastPage}}
if common.RunPreRenderHook("pre_render_forum", w, r, &user, &pi) {
return nil
}

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

View File

@ -1,5 +1,4 @@
INSERT INTO [sync] ([last_update]) VALUES (GETUTCDATE());
INSERT INTO [settings] ([name],[content],[type]) VALUES ('url_tags','1','bool');
INSERT INTO [settings] ([name],[content],[type],[constraints]) VALUES ('activation_type','1','list','1-3');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('bigpost_min_words','250','int');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('megapost_min_words','1000','int');

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 `settings`(`name`,`content`,`type`) VALUES ('url_tags','1','bool');
INSERT INTO `settings`(`name`,`content`,`type`,`constraints`) VALUES ('activation_type','1','list','1-3');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('bigpost_min_words','250','int');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('megapost_min_words','1000','int');

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",
"MinGoVersion":"1.10",
"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/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/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 **/}}
</div>
</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">
{{dock "footer" .Header }}
<div id="poweredByHolder" class="footerBit">
@ -15,8 +20,8 @@
</form>
</div>
</div>
</div>
<aside class="sidebar">{{dock "rightSidebar" .Header }}</aside>
<div class="footRight"></div>
</div>
<div style="clear: both;"></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 .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status_pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}}
</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="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 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>

View File

@ -12,14 +12,13 @@
{{if .Desc}}
<br /><span class="rowsmall" itemprop="description">{{.Desc}}</span>
{{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}}
</span>
<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}}
<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}}
</span>
</span>

View File

@ -22,12 +22,13 @@
<body>
{{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}}
<div class="container">
<!--<div class="navrow">-->
<div class="left_of_nav">{{dock "leftOfNav" .Header }}</div>
<nav class="nav">
<div class="move_left">
<div class="move_right">
<ul>
<li id="menu_overview" class="menu_left"><a href="/" rel="home">{{.Header.Site.ShortName}}</a></li>
<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">{{if eq .Header.Theme.Name "nox"}}{{.Header.Site.Name}}{{else}}{{.Header.Site.ShortName}}{{end}}</a></li>
{{dock "topMenu" .Header }}
<li class="menu_left menu_hamburger" title="{{lang "menu_hamburger_tooltip"}}"><a></a></li>
</ul>
@ -35,8 +36,23 @@
</div>
<div style="clear: both;"></div>
</nav>
<div class="right_of_nav">{{dock "rightOfNav" .Header }}</div>
<div id="back"><div id="main" {{if .Header.Widgets.RightSidebar}}class="shrink_main"{{end}}>
<div class="alertbox">{{range .Header.NoticeList}}
{{template "notice.html" . }}{{end}}
<div class="right_of_nav"><!--{{dock "rightOfNav" .Header }}-->
{{/** 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}}
{{template "notice.html" . }}{{end}}
</div>

View File

@ -4,15 +4,11 @@
{{template "panel-menu.html" . }}
<main class="colstack_right">
<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 id="panel_setting" class="colstack_item">
<form action="/panel/settings/edit/submit/{{.Something.Name}}?session={{.CurrentUser.Session}}" method="post">
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_setting_name"}}</a></div>
<div class="formitem formlabel">{{.Something.Name}}</div>
</div>
{{if eq .Something.Type "list"}}
<form action="/panel/settings/edit/submit/{{.Setting.Name}}?session={{.CurrentUser.Session}}" method="post">
{{if eq .Setting.Type "list"}}
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_setting_value"}}</a></div>
<div class="formitem">
@ -21,19 +17,23 @@
</select>
</div>
</div>
{{else if eq .Something.Type "bool"}}
{{else if eq .Setting.Type "bool"}}
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_setting_value"}}</a></div>
<div class="formitem">
<select name="setting-value">
<option{{if eq .Something.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 "1"}} selected{{end}} value="1">{{lang "option_yes"}}</option>
<option{{if eq .Setting.Content "0"}} selected{{end}} value="0">{{lang "option_no"}}</option>
</select>
</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">
<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 class="formrow">
<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>
<div id="panel_settings" class="colstack_item rowlist">
{{range $key, $value := .Something}}
{{range .Something}}
<div class="rowitem panel_compactrow editable_parent">
<a href="/panel/settings/edit/{{$key}}" class="editable_block panel_upshift">{{$key}}</a>
<a class="panel_compacttext to_right">{{$value}}</a>
<a href="/panel/settings/edit/{{.Name}}" class="editable_block panel_upshift">{{.FriendlyName}}</a>
<a class="panel_compacttext to_right">{{.Content}}</a>
</div>
{{end}}
</div>

View File

@ -11,9 +11,8 @@
<div class="rowitem editable_parent" style="background-image: url('{{.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 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}}
<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 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,17 +113,26 @@
{{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}}
</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="likeCount">{{.LikeCount}}</span>
</span>
</div>
</div>
<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>
<span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<span class="rowsmall lastReplyAt">{{.RelativeLastReplyAt}}</span>
</span>
<div class="topic_middle">
<div class="topic_middle_inside rowsmall">
<span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span>
</div>
</div>
<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>
<span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<span class="rowsmall lastReplyAt">{{.RelativeLastReplyAt}}</span>
</span>
</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>

View File

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

View File

@ -5,6 +5,7 @@
border-left: none;
border-right: none;
padding-left: 0px;
padding-bottom: 0px;
}
#back {
background-color: inherit;
@ -147,6 +148,10 @@
padding-bottom: 4px;
margin-bottom: 6px;
}
#panel_setting textarea {
width: 100%;
height: 80px;
}
#forum_quick_perms .formitem {
display: flex;
@ -256,4 +261,17 @@
.colstack_left {
margin-top: -14.5px;
}
}
@media(min-width: 1000px) {
.footBlock {
padding-left: 0px;
padding-right: 0px;
}
.footer {
max-width: none;
width: 100%;
margin-left: 0px;
margin-right: 0px;
}
}

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);
margin: 0;
}
p::selection, span::selection, a::selection {
*::selection {
background-color: hsl(0,0%,75%);
color: hsl(0,0%,20%);
font-weight: 100;
@ -226,6 +226,9 @@ a {
}
/* TODO: Add the avatars to the forum list */
.forum_list .forum_nodesc {
font-style: italic;
}
.extra_little_row_avatar {
display: none;
}
@ -377,7 +380,7 @@ textarea.large {
display: block;
}
.formitem button, .formbutton {
.formitem button, .formbutton, .mod_floater_submit, .pane_buttons button {
background-color: var(--input-background-color);
border: 1px solid var(--input-border-color);
color: var(--input-text-color);
@ -385,6 +388,15 @@ textarea.large {
padding-bottom: 6px;
font-size: 13px;
}
.mod_floater_submit {
padding: 5px;
padding-bottom: 4px;
margin-left: 2px;
}
.pane_buttons button {
padding: 5px;
padding-bottom: 4px;
}
.formrow {
flex-direction: row;
@ -666,16 +678,97 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
font-size: 11px;
}
.topic_list_title_block .pre_opt:before {
content: "{{index .Phrases "topics_click_topics_to_select"}}";
font-size: 14px;
}
.create_topic_opt a:before {
content: "{{index .Phrases "topics_new_topic"}}";
margin-left: 3px;
}
.locked_opt a:before {
content: "{{index .Phrases "forum_locked"}}";
}
.mod_opt a {
margin-left: 4px;
}
.mod_opt a:after {
content: "{{index .Phrases "topics_moderate"}}";
padding-left: 1px;
}
.create_topic_opt {
order: 1;
}
.mod_opt {
order: 2;
}
.pre_opt {
order: 3;
margin-left: auto;
margin-right: 12px;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
.mod_floater {
position: fixed;
bottom: 15px;
right: 15px;
width: 150px;
height: 65px;
font-size: 14px;
padding: 14px;
z-index: 9999;
animation: fadein 0.8s;
background-color: var(--main-block-color);
}
.mod_floater_head {
margin-bottom: 8px;
}
.modal_pane {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: var(--main-block-color);
border: 2px solid #333333;
padding-left: 24px;
padding-right: 24px;
z-index: 9999;
animation: fadein 0.8s;
}
.pane_header {
font-size: 15px;
}
.pane_header h3 {
-webkit-margin-before: 0;
-webkit-margin-after: 0;
margin-block-start: 0;
margin-block-end: 0;
margin-top: 10px;
margin-bottom: 10px;
font-weight: normal;
}
.pane_row {
font-size: 14px;
margin-bottom: 1px;
}
.pane_selected {
font-weight: bold;
}
.pane_buttons {
margin-top: 7px;
margin-bottom: 8px;
}
.topic_list .topic_row {
display: flex;
}
.topic_selected .rowitem {
background-color: hsla(0, 0%, 29%, 1);
}
/* Temporary hack, so that I don't break the topic lists of the other themes */
.topic_list .topic_inner_right {
display: none;
@ -706,10 +799,13 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
width: 284px;
padding: 0px;
}
.topic_right_inside {
display: flex;
}
.topic_list .topic_left img, .topic_list .topic_right img {
width: 64px;
}
.topic_list .topic_inner_left, .topic_right > span {
.topic_list .topic_inner_left, .topic_right_inside > span {
margin-left: 8px;
margin-top: 12px;
}
@ -725,6 +821,9 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
.topic_list .starter:before {
content: "{{index .Phrases "topics_starter"}}: ";
}
.topic_middle {
display: none;
}
.topic_name_input {
width: 100%;

View File

View File

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

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

View File

View File

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

View File

@ -26,6 +26,13 @@ if %errorlevel% neq 0 (
exit /b %errorlevel%
)
echo Updating Argon2
go get -u golang.org/x/crypto/argon2
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Updating /x/system/windows (dependency for gopsutil)
go get -u golang.org/x/sys/windows
if %errorlevel% neq 0 (