Added Quick Topic.

Added Attachments.
Added Attachment Media Embeds.

Renamed a load of *Store and *Cache methods to reduce the amount of unneccesary typing.
Added petabytes as a unit and cleaned up a few of the friendly units.
Refactored the username change logic to make it easier to maintain.
Refactored the avatar change logic to make it easier to maintain.
Shadow now uses CSS Variables for most of it's colours. We have plans to transpile this to support older browsers later on!
Snuck some CSS Variables into Tempra Conflux.
Added the GroupCache interface to MemoryGroupStore.
Added the Length method to MemoryGroupStore.
Added support for a site short name.
Added the UploadFiles permission.
Renamed more functions.
Fixed the background for the left gutter on the postbit for Tempra Simple and Shadow.
Added support for if statements operating on int8, int16, int32, int32, int64, uint, uint8, uint16, uint32, uint64, float32, and float64 for the template compiler.
Added support for if statements operating on slices and maps for the template compiler.
Fixed a security exploit in reply editing.
Fixed a bug in the URL detector in the parser where it couldn't find URLs with non-standard ports.
Fixed buttons having blue outlines on focus on Shadow.
Refactored the topic creation logic to make it easier to maintain.

Made a few responsive fixes, but there's still more to do in the following commits!
This commit is contained in:
Azareal 2017-10-05 11:20:28 +01:00
parent c0834ec7c0
commit 22650aad27
73 changed files with 1906 additions and 1269 deletions

4
.gitignore vendored
View File

@ -5,11 +5,15 @@ tmp.txt
run_notemplategen.bat run_notemplategen.bat
brun.bat brun.bat
attachs/*
!attachs/filler.txt
uploads/avatar_* uploads/avatar_*
uploads/socialgroup_* uploads/socialgroup_*
backups/*.sql backups/*.sql
node_modules/*
bin/* bin/*
out/* out/*
logs/*
*.exe *.exe
*.exe~ *.exe~
*.prof *.prof

1
attachs/filler.txt Normal file
View File

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

View File

@ -117,6 +117,7 @@ func (auth *DefaultAuth) SetCookies(w http.ResponseWriter, uid int, session stri
http.SetCookie(w, &cookie) http.SetCookie(w, &cookie)
} }
// GetCookies fetches the current user's session cookies
func (auth *DefaultAuth) GetCookies(r *http.Request) (uid int, session string, err error) { func (auth *DefaultAuth) GetCookies(r *http.Request) (uid int, session string, err error) {
// Are there any session cookies..? // Are there any session cookies..?
cookie, err := r.Cookie("uid") cookie, err := r.Cookie("uid")
@ -134,6 +135,7 @@ func (auth *DefaultAuth) GetCookies(r *http.Request) (uid int, session string, e
return uid, cookie.Value, err return uid, cookie.Value, err
} }
// SessionCheck checks if a user has session cookies and whether they're valid
func (auth *DefaultAuth) SessionCheck(w http.ResponseWriter, r *http.Request) (user *User, halt bool) { func (auth *DefaultAuth) SessionCheck(w http.ResponseWriter, r *http.Request) (user *User, halt bool) {
uid, session, err := auth.GetCookies(r) uid, session, err := auth.GetCookies(r)
if err != nil { if err != nil {
@ -156,6 +158,7 @@ func (auth *DefaultAuth) SessionCheck(w http.ResponseWriter, r *http.Request) (u
return user, false return user, false
} }
// CreateSession generates a new session to allow a remote client to stay logged in as a specific user
func (auth *DefaultAuth) CreateSession(uid int) (session string, err error) { func (auth *DefaultAuth) CreateSession(uid int) (session string, err error) {
session, err = GenerateSafeString(sessionLength) session, err = GenerateSafeString(sessionLength)
if err != nil { if err != nil {

View File

@ -8,6 +8,7 @@ const CACHE_STATIC int = 0
const CACHE_DYNAMIC int = 1 const CACHE_DYNAMIC int = 1
const CACHE_SQL int = 2 const CACHE_SQL int = 2
// nolint
// ErrCacheDesync is thrown whenever a piece of data, for instance, a user is out of sync with the database. Currently unused. // ErrCacheDesync is thrown whenever a piece of data, for instance, a user is out of sync with the database. Currently unused.
var ErrCacheDesync = errors.New("The cache is out of sync with the database.") // TODO: A cross-server synchronisation mechanism var ErrCacheDesync = errors.New("The cache is out of sync with the database.") // TODO: A cross-server synchronisation mechanism

View File

@ -2,7 +2,8 @@ package main
func init() { func init() {
// Site Info // Site Info
site.Name = "TS" site.ShortName = "TS" // This should be less than three letters to fit in the navbar
site.Name = "Test Site"
site.Email = "" site.Email = ""
site.URL = "localhost" site.URL = "localhost"
site.Port = "8080" // 8080 site.Port = "8080" // 8080
@ -47,7 +48,7 @@ func init() {
config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png" config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png"
config.ItemsPerPage = 25 config.ItemsPerPage = 25
// Developer flag // Developer flags
dev.DebugMode = true dev.DebugMode = true
//dev.SuperDebug = true //dev.SuperDebug = true
//dev.TemplateDebug = true //dev.TemplateDebug = true

View File

@ -230,6 +230,7 @@ func SecurityError(w http.ResponseWriter, r *http.Request, user User) {
} }
} }
// NotFound is used when the requested page doesn't exist
// ? - Add a JSQ and JS version of this? // ? - Add a JSQ and JS version of this?
// ? - Add a user parameter? // ? - Add a user parameter?
func NotFound(w http.ResponseWriter, r *http.Request) { func NotFound(w http.ResponseWriter, r *http.Request) {
@ -243,7 +244,7 @@ func NotFound(w http.ResponseWriter, r *http.Request) {
} }
} }
// nolint // CustomError lets us make custom error types which aren't covered by the generic functions above
func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User) { func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User) {
w.WriteHeader(errcode) w.WriteHeader(errcode)
pi := Page{errtitle, user, getDefaultHeaderVar(), tList, errmsg} pi := Page{errtitle, user, getDefaultHeaderVar(), tList, errmsg}
@ -258,7 +259,7 @@ func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWri
} }
} }
// nolint // CustomErrorJSQ is a version of CustomError which lets us handle both JSON and regular pages depending on how it's being accessed
func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User, isJs bool) { func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User, isJs bool) {
if !isJs { if !isJs {
CustomError(errmsg, errcode, errtitle, w, r, user) CustomError(errmsg, errcode, errtitle, w, r, user)
@ -267,7 +268,7 @@ func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.Response
} }
} }
// nolint // CustomErrorJS is the pure JSON version of CustomError
func CustomErrorJS(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User) { func CustomErrorJS(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User) {
w.WriteHeader(errcode) w.WriteHeader(errcode)
_, _ = w.Write([]byte(`{"errmsg":"` + errmsg + `"}`)) _, _ = w.Write([]byte(`{"errmsg":"` + errmsg + `"}`))

View File

@ -1,333 +0,0 @@
* {
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
body
{
font-family: arial;
}
@font-face {
font-family: 'EmojiFont';
src: url('https://github.com/Ranks/emojione/raw/master/assets/fonts/emojione-svg.woff2') format('woff2'),
url('https://github.com/Ranks/emojione/raw/master/assets/fonts/emojione-svg.woff') format('woff'), local("arial");
}
@supports (-ms-ime-align:auto) {
.user_content
{
font-family: EmojiFont, arial;
}
}
@-moz-document url-prefix() {
.user_content
{
font-family: EmojiFont, arial;
}
}
.move_left
{
float: left;
position: relative;
left: 50%;
}
.move_right
{
float: left;
position: relative;
left: -50%;
}
ul
{
padding-left: 0px;
padding-right: 0px;
height: 28px;
list-style-type: none;
}
li
{
height: 28px;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
padding-left: 10px;
padding-top: 5px;
padding-bottom: 5px;
font-weight: bold;
text-transform: uppercase;
}
li a
{
text-decoration: none;
color: #515151;
}
li a:hover
{
color: #7a7a7a;
}
.menu_left
{
float: left;
border-right: 1px solid #ccc;
padding-right: 10px;
}
.menu_left:first-child
{
border-left: 1px solid #ccc
}
.menu_right
{
float: right;
border-left: 1px solid #ccc;
padding-right: 10px;
}
.container
{
width: 90%;
padding: 0px;
margin-left: auto;
margin-right: auto;
}
.rowblock
{
border: 1px solid #ccc;
width: 100%;
padding: 0px;
padding-top: 0px;
}
.rowblock:empty
{
display: none;
}
.colblock_left
{
border: 1px solid #ccc;
padding: 0px;
padding-top: 0px;
width: 30%;
float: left;
margin-right: 8px;
}
.colblock_right
{
border: 1px solid #ccc;
padding: 0px;
padding-top: 0px;
width: 65%;
overflow: hidden;
word-wrap: break-word;
}
.colblock_left:empty
{
display: none;
}
.colblock_right:empty
{
display: none;
}
.rowitem
{
width: 100%;
padding-left: 8px;
padding-right: 8px;
padding-top: 17px;
padding-bottom: 12px;
font-weight: bold;
text-transform: uppercase;
}
.rowitem.passive
{
font-weight: normal;
text-transform: none;
}
.rowitem:not(:last-child)/*:not(:only-child)*/
{
border-bottom: 1px dotted #ccc;
}
.rowitem a
{
text-decoration: none;
color: black;
}
.rowitem a:hover
{
color: silver;
}
.col_left
{
width: 30%;
float: left;
}
.col_right
{
width: 69%;
overflow: hidden;
}
.colitem
{
padding-left: 8px;
padding-right: 8px;
padding-top: 17px;
padding-bottom: 12px;
font-weight: bold;
text-transform: uppercase;
}
.colitem.passive
{
font-weight: normal;
text-transform: none;
}
.colitem a
{
text-decoration: none;
color: black;
}
.colitem a:hover
{
color: silver;
}
.formrow
{
/*height: 40px;*/
width: 100%;
}
/*Clearfix*/
.formrow:before,
.formrow:after {
content: " ";
display: table;
}
.formrow:after {
clear: both;
}
.formrow:not(:last-child)
{
border-bottom: 1px dotted #ccc;
}
.formitem
{
float: left;
padding-left: 8px;
padding-right: 8px;
padding-top: 13px;
padding-bottom: 8px;
font-weight: bold;
}
.formitem:first-child
{
font-weight: bold;
}
.formitem:not(:last-child)
{
border-right: 1px dotted #ccc;
}
.formitem.invisible_border
{
border: none;
}
/* Mostly for textareas */
.formitem:only-child
{
width: 97%;
}
.formitem textarea
{
width: 100%;
height: 100px;
}
.formitem:has-child()
{
margin: 0 auto;
float: none;
}
button
{
background: white;
border: 1px solid #8e8e8e;
}
/* Topics */
.topic_status
{
text-transform: none;
margin-left: 8px;
padding-left: 2px;
padding-right: 2px;
padding-top: 2px;
padding-bottom: 2px;
background-color: #E8E8E8; /* 232,232,232. All three RGB colours being the same seems to create a shade of gray */
color: #505050; /* 80,80,80 */
border-radius: 2px;
}
.topic_status:empty
{
display: none;
}
.username
{
text-transform: none;
margin-left: 0px;
padding-left: 4px;
padding-right: 4px;
padding-top: 2px;
padding-bottom: 2px;
color: #505050; /* 80,80,80 */
background-color: #FFFFFF;
border-style: dotted;
border-color: #505050; /* 232,232,232. All three RGB colours being the same seems to create a shade of gray */
border-width: 1px;
font-size: 15px;
}
button.username
{
position: relative;
top: -0.25px;
}
.show_on_edit
{
display: none;
}
.alert
{
display: block;
padding: 5px;
margin-bottom: 10px;
border: 1px solid #ccc;
}
.alert_success
{
display: block;
padding: 5px;
border: 1px solid A2FC00;
margin-bottom: 10px;
background-color: DAF7A6;
}
.alert_error
{
display: block;
padding: 5px;
border: 1px solid #FF004B;
margin-bottom: 8px;
background-color: #FEB7CC;
}

View File

@ -32,6 +32,28 @@ var vhooks = map[string]func(...interface{}) interface{}{
"topic_create_pre_loop": nil, "topic_create_pre_loop": nil,
} }
// Coming Soon:
type Message interface {
ID() int
Poster() int
Contents() string
ParsedContents() string
}
// While the idea is nice, this might result in too much code duplication, as we have seventy billion page structs, what else could we do to get static typing with these in plugins?
type PageInt interface {
Title() string
HeaderVars() *HeaderVars
CurrentUser() *User
GetExtData(name string) interface{}
SetExtData(name string, contents interface{})
}
// Coming Soon:
var messageHooks = map[string][]func(Message, PageInt, ...interface{}) interface{}{
"topic_reply_row_assign": nil,
}
// Hooks which take in and spit out a string. This is usually used for parser components // Hooks which take in and spit out a string. This is usually used for parser components
var sshooks = map[string][]func(string) string{ var sshooks = map[string][]func(string) string{
"preparse_preassign": nil, "preparse_preassign": nil,

View File

@ -46,6 +46,7 @@ type ForumSimple struct {
Preset string Preset string
} }
// Copy gives you a non-pointer concurrency safe copy of the forum
func (forum *Forum) Copy() (fcopy Forum) { func (forum *Forum) Copy() (fcopy Forum) {
//forum.LastLock.RLock() //forum.LastLock.RLock()
fcopy = *forum fcopy = *forum

View File

@ -43,14 +43,14 @@ type ForumStore interface {
//GetFirstChild(parentID int, parentType string) (*Forum,error) //GetFirstChild(parentID int, parentType string) (*Forum,error)
Create(forumName string, forumDesc string, active bool, preset string) (int, error) Create(forumName string, forumDesc string, active bool, preset string) (int, error)
GetGlobalCount() int GlobalCount() int
} }
type ForumCache interface { type ForumCache interface {
CacheGet(id int) (*Forum, error) CacheGet(id int) (*Forum, error)
CacheSet(forum *Forum) error CacheSet(forum *Forum) error
CacheDelete(id int) CacheDelete(id int)
GetLength() int Length() int
} }
// MemoryForumStore is a struct which holds an arbitrary number of forums in memory, usually all of them, although we might introduce functionality to hold a smaller subset in memory for sites with an extremely large number of forums // MemoryForumStore is a struct which holds an arbitrary number of forums in memory, usually all of them, although we might introduce functionality to hold a smaller subset in memory for sites with an extremely large number of forums
@ -385,7 +385,8 @@ func (mfs *MemoryForumStore) Create(forumName string, forumDesc string, active b
} }
// ! Might be slightly inaccurate, if the sync.Map is constantly shifting and churning, but it'll stabilise eventually. Also, slow. Don't use this on every request x.x // ! Might be slightly inaccurate, if the sync.Map is constantly shifting and churning, but it'll stabilise eventually. Also, slow. Don't use this on every request x.x
func (mfs *MemoryForumStore) GetLength() (length int) { // Length returns the number of forums in the memory cache
func (mfs *MemoryForumStore) Length() (length int) {
mfs.forums.Range(func(_ interface{}, value interface{}) bool { mfs.forums.Range(func(_ interface{}, value interface{}) bool {
length++ length++
return true return true
@ -394,8 +395,8 @@ func (mfs *MemoryForumStore) GetLength() (length int) {
} }
// TODO: Get the total count of forums in the forum store minus the blanked forums rather than doing a heavy query for this? // TODO: Get the total count of forums in the forum store minus the blanked forums rather than doing a heavy query for this?
// GetGlobalCount returns the total number of forums // GlobalCount returns the total number of forums
func (mfs *MemoryForumStore) GetGlobalCount() (fcount int) { func (mfs *MemoryForumStore) GlobalCount() (fcount int) {
err := mfs.getForumCount.QueryRow().Scan(&fcount) err := mfs.getForumCount.QueryRow().Scan(&fcount)
if err != nil { if err != nil {
LogError(err) LogError(err)

View File

@ -43,6 +43,7 @@ var groupEntryExistsStmt *sql.Stmt
var getForumTopicsOffsetStmt *sql.Stmt var getForumTopicsOffsetStmt *sql.Stmt
var getExpiredScheduledGroupsStmt *sql.Stmt var getExpiredScheduledGroupsStmt *sql.Stmt
var getSyncStmt *sql.Stmt var getSyncStmt *sql.Stmt
var getAttachmentStmt *sql.Stmt
var getTopicRepliesOffsetStmt *sql.Stmt var getTopicRepliesOffsetStmt *sql.Stmt
var getTopicListStmt *sql.Stmt var getTopicListStmt *sql.Stmt
var getTopicUserStmt *sql.Stmt var getTopicUserStmt *sql.Stmt
@ -68,6 +69,7 @@ var addThemeStmt *sql.Stmt
var createGroupStmt *sql.Stmt var createGroupStmt *sql.Stmt
var addModlogEntryStmt *sql.Stmt var addModlogEntryStmt *sql.Stmt
var addAdminlogEntryStmt *sql.Stmt var addAdminlogEntryStmt *sql.Stmt
var addAttachmentStmt *sql.Stmt
var createWordFilterStmt *sql.Stmt var createWordFilterStmt *sql.Stmt
var addForumPermsToGroupStmt *sql.Stmt var addForumPermsToGroupStmt *sql.Stmt
var replaceScheduleGroupStmt *sql.Stmt var replaceScheduleGroupStmt *sql.Stmt
@ -341,6 +343,12 @@ func _gen_mysql() (err error) {
return err return err
} }
log.Print("Preparing getAttachment statement.")
getAttachmentStmt, err = db.Prepare("SELECT `sectionID`,`sectionTable`,`originID`,`originTable`,`uploadedBy`,`path` FROM `attachments` WHERE `path` = ? AND `sectionID` = ? AND `sectionTable` = ?")
if err != nil {
return err
}
log.Print("Preparing getTopicRepliesOffset statement.") log.Print("Preparing getTopicRepliesOffset statement.")
getTopicRepliesOffsetStmt, err = db.Prepare("SELECT `replies`.`rid`,`replies`.`content`,`replies`.`createdBy`,`replies`.`createdAt`,`replies`.`lastEdit`,`replies`.`lastEditBy`,`users`.`avatar`,`users`.`name`,`users`.`group`,`users`.`url_prefix`,`users`.`url_name`,`users`.`level`,`replies`.`ipaddress`,`replies`.`likeCount`,`replies`.`actionType` FROM `replies` LEFT JOIN `users` ON `replies`.`createdBy` = `users`.`uid` WHERE `tid` = ? LIMIT ?,?") getTopicRepliesOffsetStmt, err = db.Prepare("SELECT `replies`.`rid`,`replies`.`content`,`replies`.`createdBy`,`replies`.`createdAt`,`replies`.`lastEdit`,`replies`.`lastEditBy`,`users`.`avatar`,`users`.`name`,`users`.`group`,`users`.`url_prefix`,`users`.`url_name`,`users`.`level`,`replies`.`ipaddress`,`replies`.`likeCount`,`replies`.`actionType` FROM `replies` LEFT JOIN `users` ON `replies`.`createdBy` = `users`.`uid` WHERE `tid` = ? LIMIT ?,?")
if err != nil { if err != nil {
@ -491,6 +499,12 @@ func _gen_mysql() (err error) {
return err return err
} }
log.Print("Preparing addAttachment statement.")
addAttachmentStmt, err = db.Prepare("INSERT INTO `attachments`(`sectionID`,`sectionTable`,`originID`,`originTable`,`uploadedBy`,`path`) VALUES (?,?,?,?,?,?)")
if err != nil {
return err
}
log.Print("Preparing createWordFilter statement.") log.Print("Preparing createWordFilter statement.")
createWordFilterStmt, err = db.Prepare("INSERT INTO `word_filters`(`find`,`replacement`) VALUES (?,?)") createWordFilterStmt, err = db.Prepare("INSERT INTO `word_filters`(`find`,`replacement`) VALUES (?,?)")
if err != nil { if err != nil {

View File

@ -107,6 +107,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
case "/theme": case "/theme":
routeChangeTheme(w,req,user) routeChangeTheme(w,req,user)
return return
case "/attachs":
routeShowAttachment(w,req,user,extra_data)
return
case "/report": case "/report":
switch(req.URL.Path) { switch(req.URL.Path) {
case "/report/submit/": case "/report/submit/":

View File

@ -895,36 +895,37 @@ func BenchmarkCustomRouterSerial(b *testing.B) {
}) })
}*/ }*/
// TODO: Take the attachment system into account in these parser benches
func BenchmarkParserSerial(b *testing.B) { func BenchmarkParserSerial(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
b.Run("empty_post", func(b *testing.B) { b.Run("empty_post", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = parseMessage("") _ = parseMessage("", 0, "")
} }
}) })
b.Run("short_post", func(b *testing.B) { b.Run("short_post", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = parseMessage("Hey everyone, how's it going?") _ = parseMessage("Hey everyone, how's it going?", 0, "")
} }
}) })
b.Run("one_smily", func(b *testing.B) { b.Run("one_smily", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = parseMessage("Hey everyone, how's it going? :)") _ = parseMessage("Hey everyone, how's it going? :)", 0, "")
} }
}) })
b.Run("five_smilies", func(b *testing.B) { b.Run("five_smilies", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = parseMessage("Hey everyone, how's it going? :):):):):)") _ = parseMessage("Hey everyone, how's it going? :):):):):)", 0, "")
} }
}) })
b.Run("ten_smilies", func(b *testing.B) { b.Run("ten_smilies", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = parseMessage("Hey everyone, how's it going? :):):):):):):):):):)") _ = parseMessage("Hey everyone, how's it going? :):):):):):):):):):)", 0, "")
} }
}) })
b.Run("twenty_smilies", func(b *testing.B) { b.Run("twenty_smilies", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = parseMessage("Hey everyone, how's it going? :):):):):):):):):):):):):):):):):):):):)") _ = parseMessage("Hey everyone, how's it going? :):):):):):):):):):):):):):):):):):):):)", 0, "")
} }
}) })
} }

View File

@ -27,6 +27,8 @@ type Group struct {
CanSee []int // The IDs of the forums this group can see CanSee []int // The IDs of the forums this group can see
} }
// ! Ahem, don't listen to the comment below. It's not concurrency safe right now.
// Copy gives you a non-pointer concurrency safe copy of the group
func (group *Group) Copy() Group { func (group *Group) Copy() Group {
return *group return *group
} }

View File

@ -12,6 +12,7 @@ var groupCreateMutex sync.Mutex
var groupUpdateMutex sync.Mutex var groupUpdateMutex sync.Mutex
var gstore GroupStore var gstore GroupStore
// ? - We could fallback onto the database when an item can't be found in the cache?
type GroupStore interface { type GroupStore interface {
LoadGroups() error LoadGroups() error
DirtyGet(id int) *Group DirtyGet(id int) *Group
@ -23,6 +24,10 @@ type GroupStore interface {
GetRange(lower int, higher int) ([]*Group, error) GetRange(lower int, higher int) ([]*Group, error)
} }
type GroupCache interface {
Length() int
}
type MemoryGroupStore struct { type MemoryGroupStore struct {
groups []*Group // TODO: Use a sync.Map instead of a slice groups []*Group // TODO: Use a sync.Map instead of a slice
groupCapCount int groupCapCount int
@ -203,3 +208,7 @@ func (mgs *MemoryGroupStore) GetRange(lower int, higher int) (groups []*Group, e
} }
return groups, nil return groups, nil
} }
func (mgs *MemoryGroupStore) Length() int {
return len(mgs.groups)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 301 KiB

View File

@ -28,12 +28,17 @@ var dbUsername string
var dbPassword string var dbPassword string
var dbName string var dbName string
var dbPort string var dbPort string
var siteName, siteURL, serverPort string
var siteShortName string
var siteName string
var siteURL string
var serverPort string
var defaultAdapter = "mysql" var defaultAdapter = "mysql"
var defaultHost = "localhost" var defaultHost = "localhost"
var defaultUsername = "root" var defaultUsername = "root"
var defaultDbname = "gosora" var defaultDbname = "gosora"
var defaultSiteShortName = "SN"
var defaultSiteName = "Site Name" var defaultSiteName = "Site Name"
var defaultsiteURL = "localhost" var defaultsiteURL = "localhost"
var defaultServerPort = "80" // 8080's a good one, if you're testing and don't want it to clash with port 80 var defaultServerPort = "80" // 8080's a good one, if you're testing and don't want it to clash with port 80
@ -146,6 +151,7 @@ func main() {
func init() { func init() {
// Site Info // Site Info
site.ShortName = "` + siteShortName + `" // This should be less than three letters to fit in the navbar
site.Name = "` + siteName + `" site.Name = "` + siteName + `"
site.Email = "" site.Email = ""
site.URL = "` + siteURL + `" site.URL = "` + siteURL + `"
@ -191,7 +197,7 @@ config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation
config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png" config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png"
config.ItemsPerPage = 25 config.ItemsPerPage = 25
// Developer flag // Developer flags
dev.DebugMode = true dev.DebugMode = true
//dev.SuperDebug = true //dev.SuperDebug = true
//dev.TemplateDebug = true //dev.TemplateDebug = true
@ -294,6 +300,17 @@ func getSiteDetails() bool {
} }
fmt.Println("Set the site name to " + siteName) fmt.Println("Set the site name to " + siteName)
// ? - We could compute this based on the first letter of each word in the site's name, if it's name spans multiple words. I'm not sure how to do this for single word names.
fmt.Println("Can we have a short abbreviation for your site? Default: " + defaultSiteShortName)
if !scanner.Scan() {
return false
}
siteShortName = scanner.Text()
if siteShortName == "" {
siteShortName = defaultSiteShortName
}
fmt.Println("Set the site name to " + siteShortName)
fmt.Println("What's your site's url? Default: " + defaultsiteURL) fmt.Println("What's your site's url? Default: " + defaultsiteURL)
if !scanner.Scan() { if !scanner.Scan() {
return false return false

View File

@ -23,7 +23,9 @@
"ManageThemes": "Can manage themes", "ManageThemes": "Can manage themes",
"ManagePlugins": "Can manage plugins", "ManagePlugins": "Can manage plugins",
"ViewAdminLogs": "Can view the administrator action logs", "ViewAdminLogs": "Can view the administrator action logs",
"ViewIPs": "Can view IP addresses" "ViewIPs": "Can view IP addresses",
"UploadFiles": "Can upload files"
}, },
"LocalPerms": { "LocalPerms": {
"ViewTopic": "Can view topics", "ViewTopic": "Can view topics",

25
main.go
View File

@ -27,6 +27,7 @@ const kilobyte int = 1024
const megabyte int = kilobyte * 1024 const megabyte int = kilobyte * 1024
const gigabyte int = megabyte * 1024 const gigabyte int = megabyte * 1024
const terabyte int = gigabyte * 1024 const terabyte int = gigabyte * 1024
const petabyte int = terabyte * 1024
const saltLength int = 32 const saltLength int = 32
const sessionLength int = 80 const sessionLength int = 80
@ -37,6 +38,30 @@ var startTime time.Time
var externalSites = map[string]string{ var externalSites = map[string]string{
"YT": "https://www.youtube.com/", "YT": "https://www.youtube.com/",
} }
type StringList []string
// ? - Should we allow users to upload .php or .go files? It could cause security issues. We could store them with a mangled extension to render them inert
// TODO: Let admins manage this from the Control Panel
var allowedFileExts = StringList{
"png", "jpg", "jpeg", "svg", "bmp", "gif",
"txt", "xml", "json", "yaml", "js", "py", "rb",
"mp3", "mp4", "avi", "wmv",
}
var imageFileExts = StringList{
"png", "jpg", "jpeg", "svg", "bmp", "gif",
}
// TODO: Write a test for this
func (slice StringList) Contains(needle string) bool {
for _, item := range slice {
if item == needle {
return true
}
}
return false
}
var staticFiles = make(map[string]SFile) var staticFiles = make(map[string]SFile)
var logWriter = io.MultiWriter(os.Stderr) var logWriter = io.MultiWriter(os.Stderr)

View File

@ -1,12 +1,15 @@
package main package main
import ( import (
"crypto/sha256"
"encoding/hex"
"html" "html"
"io" "io"
"log" "log"
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -101,9 +104,17 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid st
// POST functions. Authorised users only. // POST functions. Authorised users only.
func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) {
err := r.ParseForm() // TODO: Reduce this to 1MB for attachments for each file?
if r.ContentLength > int64(config.MaxRequestSize) {
size, unit := convertByteUnit(float64(config.MaxRequestSize))
CustomError("Your attachments are too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, user)
return
}
r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxRequestSize))
err := r.ParseMultipartForm(int64(megabyte))
if err != nil { if err != nil {
PreError("Bad Form", w, r) LocalError("Unable to parse the form", w, r, user)
return return
} }
@ -131,36 +142,111 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) {
return return
} }
wcount := wordCount(content) tid, err := topics.Create(fid, topicName, content, user.ID, ipaddress)
res, err := createTopicStmt.Exec(fid, topicName, content, parseMessage(content), user.ID, ipaddress, wcount, user.ID)
if err != nil { if err != nil {
switch err {
case ErrNoRows:
LocalError("Something went wrong, perhaps the forum got deleted?", w, r, user)
case ErrNoTitle:
LocalError("This topic doesn't have a title", w, r, user)
case ErrNoBody:
LocalError("This topic doesn't have a body", w, r, user)
default:
InternalError(err, w) InternalError(err, w)
}
return return
} }
lastID, err := res.LastInsertId()
_, err = addSubscriptionStmt.Exec(user.ID, tid, "topic")
if err != nil { if err != nil {
InternalError(err, w) InternalError(err, w)
return return
} }
_, err = addSubscriptionStmt.Exec(user.ID, lastID, "topic") err = user.increasePostStats(wordCount(content), true)
if err != nil { if err != nil {
InternalError(err, w) InternalError(err, w)
return return
} }
http.Redirect(w, r, "/topic/"+strconv.FormatInt(lastID, 10), http.StatusSeeOther) // Handle the file attachments
err = user.increasePostStats(wcount, true) if user.Perms.UploadFiles {
var mpartFiles = r.MultipartForm.File
if len(mpartFiles) > 5 {
LocalError("You can't attach more than five files", w, r, user)
return
}
for _, fheaders := range r.MultipartForm.File {
for _, hdr := range fheaders {
log.Print("hdr.Filename ", hdr.Filename)
extarr := strings.Split(hdr.Filename, ".")
if len(extarr) < 2 {
LocalError("Bad file", w, r, user)
return
}
ext := extarr[len(extarr)-1]
// TODO: Can we do this without a regex?
reg, err := regexp.Compile("[^A-Za-z0-9]+")
if err != nil {
LocalError("Bad file extension", w, r, user)
return
}
ext = strings.ToLower(reg.ReplaceAllString(ext, ""))
if !allowedFileExts.Contains(ext) {
LocalError("You're not allowed this upload files with this extension", w, r, user)
return
}
infile, err := hdr.Open()
if err != nil {
LocalError("Upload failed", w, r, user)
return
}
defer infile.Close()
hasher := sha256.New()
_, err = io.Copy(hasher, infile)
if err != nil {
LocalError("Upload failed [Hashing Failed]", w, r, user)
return
}
infile.Close()
checksum := hex.EncodeToString(hasher.Sum(nil))
filename := checksum + "." + ext
outfile, err := os.Create("." + "/attachs/" + filename)
if err != nil {
LocalError("Upload failed [File Creation Failed]", w, r, user)
return
}
defer outfile.Close()
infile, err = hdr.Open()
if err != nil {
LocalError("Upload failed", w, r, user)
return
}
defer infile.Close()
_, err = io.Copy(outfile, infile)
if err != nil {
LocalError("Upload failed [Copy Failed]", w, r, user)
return
}
_, err = addAttachmentStmt.Exec(fid, "forums", tid, "topics", user.ID, filename)
if err != nil { if err != nil {
InternalError(err, w) InternalError(err, w)
return return
} }
err = fstore.AddTopic(int(lastID), user.ID, fid)
if err != nil && err != ErrNoRows {
InternalError(err, w)
} }
} }
}
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
}
func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) {
err := r.ParseForm() err := r.ParseForm()
@ -201,7 +287,7 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) {
} }
wcount := wordCount(content) wcount := wordCount(content)
_, err = createReplyStmt.Exec(tid, content, parseMessage(content), ipaddress, wcount, user.ID) _, err = createReplyStmt.Exec(tid, content, parseMessage(content, topic.ParentID, "forums"), ipaddress, wcount, user.ID)
if err != nil { if err != nil {
InternalError(err, w) InternalError(err, w)
return return
@ -475,7 +561,8 @@ func routeProfileReplyCreate(w http.ResponseWriter, r *http.Request, user User)
return return
} }
_, err = createProfileReplyStmt.Exec(uid, html.EscapeString(preparseMessage(r.PostFormValue("reply-content"))), parseMessage(html.EscapeString(preparseMessage(r.PostFormValue("reply-content")))), user.ID, ipaddress) content := html.EscapeString(preparseMessage(r.PostFormValue("reply-content")))
_, err = createProfileReplyStmt.Exec(uid, content, parseMessage(content, 0, ""), user.ID, ipaddress)
if err != nil { if err != nil {
InternalError(err, w) InternalError(err, w)
return return
@ -605,8 +692,9 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI
return return
} }
// TODO: Repost attachments in the reports forum, so that the mods can see them
// ? - Can we do this via the TopicStore? // ? - Can we do this via the TopicStore?
res, err := createReportStmt.Exec(title, content, parseMessage(content), user.ID, itemType+"_"+strconv.Itoa(itemID)) res, err := createReportStmt.Exec(title, content, parseMessage(content, 0, ""), user.ID, itemType+"_"+strconv.Itoa(itemID))
if err != nil { if err != nil {
InternalError(err, w) InternalError(err, w)
return return
@ -728,7 +816,8 @@ func routeAccountOwnEditAvatar(w http.ResponseWriter, r *http.Request, user User
func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user User) { func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user User) {
if r.ContentLength > int64(config.MaxRequestSize) { if r.ContentLength > int64(config.MaxRequestSize) {
http.Error(w, "Request too large", http.StatusExpectationFailed) size, unit := convertByteUnit(float64(config.MaxRequestSize))
CustomError("Your avatar's too big. Avatars must be smaller than "+strconv.Itoa(int(size))+unit, http.StatusExpectationFailed, "Error", w, r, user)
return return
} }
r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxRequestSize)) r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxRequestSize))
@ -742,14 +831,13 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use
return return
} }
err := r.ParseMultipartForm(int64(config.MaxRequestSize)) err := r.ParseMultipartForm(int64(megabyte))
if err != nil { if err != nil {
LocalError("Upload failed", w, r, user) LocalError("Upload failed", w, r, user)
return return
} }
var filename string var filename, ext string
var ext string
for _, fheaders := range r.MultipartForm.File { for _, fheaders := range r.MultipartForm.File {
for _, hdr := range fheaders { for _, hdr := range fheaders {
infile, err := hdr.Open() infile, err := hdr.Open()
@ -760,6 +848,7 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use
defer infile.Close() defer infile.Close()
// We don't want multiple files // We don't want multiple files
// TODO: Check the length of r.MultipartForm.File and error rather than doing this x.x
if filename != "" { if filename != "" {
if filename != hdr.Filename { if filename != hdr.Filename {
os.Remove("./uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext) os.Remove("./uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext)
@ -778,6 +867,7 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use
} }
ext = extarr[len(extarr)-1] ext = extarr[len(extarr)-1]
// TODO: Can we do this without a regex?
reg, err := regexp.Compile("[^A-Za-z0-9]+") reg, err := regexp.Compile("[^A-Za-z0-9]+")
if err != nil { if err != nil {
LocalError("Bad file extension", w, r, user) LocalError("Bad file extension", w, r, user)
@ -802,16 +892,12 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use
} }
} }
_, err = setAvatarStmt.Exec("."+ext, strconv.Itoa(user.ID)) err = user.ChangeAvatar("." + ext)
if err != nil { if err != nil {
InternalError(err, w) InternalError(err, w)
return return
} }
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext
ucache, ok := users.(UserCache)
if ok {
ucache.CacheRemove(user.ID)
}
headerVars.NoticeList = append(headerVars.NoticeList, "Your avatar was successfully updated") headerVars.NoticeList = append(headerVars.NoticeList, "Your avatar was successfully updated")
pi := Page{"Edit Avatar", user, headerVars, tList, nil} pi := Page{"Edit Avatar", user, headerVars, tList, nil}
@ -857,18 +943,12 @@ func routeAccountOwnEditUsernameSubmit(w http.ResponseWriter, r *http.Request, u
} }
newUsername := html.EscapeString(r.PostFormValue("account-new-username")) newUsername := html.EscapeString(r.PostFormValue("account-new-username"))
_, err = setUsernameStmt.Exec(newUsername, strconv.Itoa(user.ID)) err = user.ChangeName(newUsername)
if err != nil { if err != nil {
LocalError("Unable to change the username. Does someone else already have this name?", w, r, user) LocalError("Unable to change the username. Does someone else already have this name?", w, r, user)
return return
} }
// TODO: Use the reloaded data instead for the name?
user.Name = newUsername user.Name = newUsername
ucache, ok := users.(UserCache)
if ok {
ucache.CacheRemove(user.ID)
}
headerVars.NoticeList = append(headerVars.NoticeList, "Your username was successfully updated") headerVars.NoticeList = append(headerVars.NoticeList, "Your username was successfully updated")
pi := Page{"Edit Username", user, headerVars, tList, nil} pi := Page{"Edit Username", user, headerVars, tList, nil}
@ -1022,3 +1102,60 @@ func routeLogout(w http.ResponseWriter, r *http.Request, user User) {
auth.Logout(w, user.ID) auth.Logout(w, user.ID)
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
} }
func routeShowAttachment(w http.ResponseWriter, r *http.Request, user User, filename string) {
err := r.ParseForm()
if err != nil {
PreError("Bad Form", w, r)
return
}
filename = Stripslashes(filename)
var ext = filepath.Ext("./attachs/" + filename)
//log.Print("ext ", ext)
//log.Print("filename ", filename)
if !allowedFileExts.Contains(strings.TrimPrefix(ext, ".")) {
LocalError("Bad extension", w, r, user)
return
}
sectionID, err := strconv.Atoi(r.FormValue("sectionID"))
if err != nil {
LocalError("The sectionID is not an integer", w, r, user)
return
}
var sectionTable = r.FormValue("sectionType")
var originTable string
var originID, uploadedBy int
err = getAttachmentStmt.QueryRow(filename, sectionID, sectionTable).Scan(&sectionID, &sectionTable, &originID, &originTable, &uploadedBy, &filename)
if err == ErrNoRows {
NotFound(w, r)
return
} else if err != nil {
InternalError(err, w)
return
}
if sectionTable == "forums" {
_, ok := SimpleForumUserCheck(w, r, &user, sectionID)
if !ok {
return
}
if !user.Perms.ViewTopic {
NoPermissions(w, r, user)
return
}
} else {
LocalError("Unknown section", w, r, user)
return
}
if originTable != "topics" && originTable != "replies" {
LocalError("Unknown origin", w, r, user)
return
}
// TODO: Fix the problem where non-existent files aren't greeted with custom 404s on ServeFile()'s side
http.ServeFile(w, r, "./attachs/"+filename)
}

View File

@ -28,7 +28,7 @@ func userStoreTest(t *testing.T) {
var length int var length int
ucache, hasCache := users.(UserCache) ucache, hasCache := users.(UserCache)
if hasCache && ucache.GetLength() != 0 { if hasCache && ucache.Length() != 0 {
t.Error("Initial ucache length isn't zero") t.Error("Initial ucache length isn't zero")
} }
@ -39,7 +39,7 @@ func userStoreTest(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if hasCache && ucache.GetLength() != 0 { if hasCache && ucache.Length() != 0 {
t.Error("There shouldn't be anything in the user cache") t.Error("There shouldn't be anything in the user cache")
} }
@ -50,7 +50,7 @@ func userStoreTest(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if hasCache && ucache.GetLength() != 0 { if hasCache && ucache.Length() != 0 {
t.Error("There shouldn't be anything in the user cache") t.Error("There shouldn't be anything in the user cache")
} }
@ -66,7 +66,7 @@ func userStoreTest(t *testing.T) {
} }
if hasCache { if hasCache {
length = ucache.GetLength() length = ucache.Length()
if length != 1 { if length != 1 {
t.Error("User cache length should be 1, not " + strconv.Itoa(length)) t.Error("User cache length should be 1, not " + strconv.Itoa(length))
} }
@ -83,7 +83,7 @@ func userStoreTest(t *testing.T) {
} }
ucache.Flush() ucache.Flush()
length = ucache.GetLength() length = ucache.Length()
if length != 0 { if length != 0 {
t.Error("User cache length should be 0, not " + strconv.Itoa(length)) t.Error("User cache length should be 0, not " + strconv.Itoa(length))
} }
@ -97,7 +97,7 @@ func userStoreTest(t *testing.T) {
} }
if hasCache { if hasCache {
length = ucache.GetLength() length = ucache.Length()
if length != 0 { if length != 0 {
t.Error("User cache length should be 0, not " + strconv.Itoa(length)) t.Error("User cache length should be 0, not " + strconv.Itoa(length))
} }
@ -109,7 +109,7 @@ func userStoreTest(t *testing.T) {
} }
if hasCache { if hasCache {
length = ucache.GetLength() length = ucache.Length()
if length != 0 { if length != 0 {
t.Error("User cache length should be 0, not " + strconv.Itoa(length)) t.Error("User cache length should be 0, not " + strconv.Itoa(length))
} }
@ -133,7 +133,7 @@ func userStoreTest(t *testing.T) {
} }
if hasCache { if hasCache {
length = ucache.GetLength() length = ucache.Length()
if length != 1 { if length != 1 {
t.Error("User cache length should be 1, not " + strconv.Itoa(length)) t.Error("User cache length should be 1, not " + strconv.Itoa(length))
} }
@ -168,13 +168,13 @@ func userStoreTest(t *testing.T) {
} }
if hasCache { if hasCache {
length = ucache.GetLength() length = ucache.Length()
if length != 0 { if length != 0 {
t.Error("User cache length should be 0, not " + strconv.Itoa(length)) t.Error("User cache length should be 0, not " + strconv.Itoa(length))
} }
} }
count := users.GetGlobalCount() count := users.GlobalCount()
if count <= 0 { if count <= 0 {
t.Error("The number of users should be bigger than zero") t.Error("The number of users should be bigger than zero")
t.Error("count", count) t.Error("count", count)
@ -243,7 +243,7 @@ func topicStoreTest(t *testing.T) {
t.Error("TID #1 should exist") t.Error("TID #1 should exist")
} }
count := topics.GetGlobalCount() count := topics.GlobalCount()
if count <= 0 { if count <= 0 {
t.Error("The number of topics should be bigger than zero") t.Error("The number of topics should be bigger than zero")
t.Error("count", count) t.Error("count", count)

View File

@ -345,13 +345,6 @@ func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) {
return return
} }
content := html.EscapeString(preparseMessage(r.PostFormValue("edit_item")))
_, err = editReplyStmt.Exec(content, parseMessage(content), rid)
if err != nil {
InternalErrorJSQ(err, w, r, isJs)
return
}
// Get the Reply ID.. // Get the Reply ID..
var tid int var tid int
err = getReplyTIDStmt.QueryRow(rid).Scan(&tid) err = getReplyTIDStmt.QueryRow(rid).Scan(&tid)
@ -380,6 +373,13 @@ func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) {
return return
} }
content := html.EscapeString(preparseMessage(r.PostFormValue("edit_item")))
_, err = editReplyStmt.Exec(content, parseMessage(content, fid, "forums"), rid)
if err != nil {
InternalErrorJSQ(err, w, r, isJs)
return
}
if !isJs { if !isJs {
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther) http.Redirect(w, r, "/topic/"+strconv.Itoa(tid)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther)
} else { } else {
@ -504,7 +504,7 @@ func routeProfileReplyEditSubmit(w http.ResponseWriter, r *http.Request, user Us
} }
content := html.EscapeString(preparseMessage(r.PostFormValue("edit_item"))) content := html.EscapeString(preparseMessage(r.PostFormValue("edit_item")))
_, err = editProfileReplyStmt.Exec(content, parseMessage(content), rid) _, err = editProfileReplyStmt.Exec(content, parseMessage(content, 0, ""), rid)
if err != nil { if err != nil {
InternalErrorJSQ(err, w, r, isJs) InternalErrorJSQ(err, w, r, isJs)
return return

View File

@ -78,6 +78,17 @@ CREATE TABLE `replies`(
primary key(`rid`) primary key(`rid`)
) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; ) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;
CREATE TABLE `attachments`(
`attachID` int not null AUTO_INCREMENT,
`sectionID` int DEFAULT 0 not null, /* section ID */
`sectionTable` varchar(200) DEFAULT 'forums' not null, /* section table */
`originID` int not null,
`originTable` varchar(200) DEFAULT 'replies' not null,
`uploadedBy` int not null,
`path` varchar(200) not null,
primary key(`attachID`)
) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;
CREATE TABLE `revisions`( CREATE TABLE `revisions`(
`index` int not null, `index` int not null,
`content` text not null, `content` text not null,
@ -190,6 +201,7 @@ INSERT INTO emails(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1);
/* /*
The Permissions: The Permissions:
Global Permissions:
BanUsers BanUsers
ActivateUsers ActivateUsers
EditUser EditUser
@ -210,6 +222,10 @@ ManagePlugins
ViewAdminLogs ViewAdminLogs
ViewIPs ViewIPs
Non-staff Global Permissions:
UploadFiles
Forum Permissions:
ViewTopic ViewTopic
LikeItem LikeItem
CreateTopic CreateTopic
@ -222,9 +238,9 @@ PinTopic
CloseTopic CloseTopic
*/ */
INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`tag`) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,1,"Admin"); INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`tag`) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,1,"Admin");
INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`is_mod`,`tag`) VALUES ('Moderator','{"BanUsers":true,"ActivateUsers":false,"EditUser":true,"EditUserEmail":false,"EditUserGroup":true,"ViewIPs":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,"Mod"); INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`is_mod`,`tag`) VALUES ('Moderator','{"BanUsers":true,"ActivateUsers":false,"EditUser":true,"EditUserEmail":false,"EditUserGroup":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,"Mod");
INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`) VALUES ('Member','{"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"CreateReply":true}','{}'); 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`,`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`) VALUES ('Awaiting Activation','{"ViewTopic":true}','{}');
INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`tag`) VALUES ('Not Loggedin','{"ViewTopic":true}','{}','Guest'); INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`tag`) VALUES ('Not Loggedin','{"ViewTopic":true}','{}','Guest');

183
pages.go
View File

@ -4,6 +4,7 @@ import (
//"fmt" //"fmt"
"bytes" "bytes"
"html/template" "html/template"
"net/url"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -64,7 +65,9 @@ type TopicsPage struct {
Title string Title string
CurrentUser User CurrentUser User
Header *HeaderVars Header *HeaderVars
ItemList []*TopicsRow TopicList []*TopicsRow
ForumList []Forum
DefaultForum int
} }
type ForumPage struct { type ForumPage struct {
@ -287,12 +290,16 @@ var invalidURL = []byte("<span style='color: red;'>[Invalid URL]</span>")
var invalidTopic = []byte("<span style='color: red;'>[Invalid Topic]</span>") var invalidTopic = []byte("<span style='color: red;'>[Invalid Topic]</span>")
var invalidProfile = []byte("<span style='color: red;'>[Invalid Profile]</span>") var invalidProfile = []byte("<span style='color: red;'>[Invalid Profile]</span>")
var invalidForum = []byte("<span style='color: red;'>[Invalid Forum]</span>") var invalidForum = []byte("<span style='color: red;'>[Invalid Forum]</span>")
var unknownMedia = []byte("<span style='color: red;'>[Unknown Media]</span>")
var urlOpen = []byte("<a href='") var urlOpen = []byte("<a href='")
var urlOpen2 = []byte("'>") var urlOpen2 = []byte("'>")
var bytesSinglequote = []byte("'") var bytesSinglequote = []byte("'")
var bytesGreaterthan = []byte(">") var bytesGreaterthan = []byte(">")
var urlMention = []byte(" class='mention'") var urlMention = []byte(" class='mention'")
var urlClose = []byte("</a>") var urlClose = []byte("</a>")
var imageOpen = []byte("<a href=\"")
var imageOpen2 = []byte("\"><img src='")
var imageClose = []byte("' class='postImage' /></a>")
var urlpattern = `(?s)([ {1}])((http|https|ftp|mailto)*)(:{??)\/\/([\.a-zA-Z\/]+)([ {1}])` var urlpattern = `(?s)([ {1}])((http|https|ftp|mailto)*)(:{??)\/\/([\.a-zA-Z\/]+)([ {1}])`
var urlReg *regexp.Regexp var urlReg *regexp.Regexp
@ -440,7 +447,8 @@ func preparseMessage(msg string) string {
} }
// TODO: Write a test for this // TODO: Write a test for this
func parseMessage(msg string /*, user User*/) string { // TODO: We need a lot more hooks here. E.g. To add custom media types and handlers.
func parseMessage(msg string, sectionID int, sectionType string /*, user User*/) string {
msg = strings.Replace(msg, ":)", "😀", -1) msg = strings.Replace(msg, ":)", "😀", -1)
msg = strings.Replace(msg, ":(", "😞", -1) msg = strings.Replace(msg, ":(", "😞", -1)
msg = strings.Replace(msg, ":D", "😃", -1) msg = strings.Replace(msg, ":D", "😃", -1)
@ -461,19 +469,13 @@ func parseMessage(msg string /*, user User*/) string {
var msgbytes = []byte(msg) var msgbytes = []byte(msg)
var outbytes []byte var outbytes []byte
msgbytes = append(msgbytes, spaceGap...) msgbytes = append(msgbytes, spaceGap...)
//log.Print(`"`+string(msgbytes)+`"`) //log.Printf("string(msgbytes) %+v\n", `"`+string(msgbytes)+`"`)
lastItem := 0 var lastItem = 0
i := 0 var i = 0
for ; len(msgbytes) > (i + 1); i++ { for ; len(msgbytes) > (i + 1); i++ {
//log.Print("Index:",i) //log.Print("Index:",i)
//log.Print("Index Item: ",msgbytes[i]) //log.Print("Index Item: ",msgbytes[i])
//if msgbytes[i] == 10 { //log.Print("string(msgbytes[i]): ",string(msgbytes[i]))
// log.Print("NEWLINE")
//} else if msgbytes[i] == 32 {
// log.Print("SPACE")
//} else {
// log.Print("string(msgbytes[i])",string(msgbytes[i]))
//}
//log.Print("End Index") //log.Print("End Index")
if (i == 0 && (msgbytes[0] > 32)) || ((msgbytes[i] < 33) && (msgbytes[i+1] > 32)) { if (i == 0 && (msgbytes[0] > 32)) || ((msgbytes[i] < 33) && (msgbytes[i+1] > 32)) {
//log.Print("IN") //log.Print("IN")
@ -508,11 +510,11 @@ func parseMessage(msg string /*, user User*/) string {
lastItem = i lastItem = i
//log.Print("string(msgbytes) ",string(msgbytes)) //log.Print("string(msgbytes) ",string(msgbytes))
//log.Print(msgbytes) //log.Print("msgbytes ",msgbytes)
//log.Print(msgbytes[lastItem - 1]) //log.Print(msgbytes[lastItem - 1])
//log.Print(lastItem - 1) //log.Print(lastItem - 1)
//log.Print(msgbytes[lastItem]) //log.Print(msgbytes[lastItem])
//log.Print(lastItem) //log.Print("lastItem ",lastItem)
} else if bytes.Equal(msgbytes[i+1:i+5], []byte("rid-")) { } else if bytes.Equal(msgbytes[i+1:i+5], []byte("rid-")) {
outbytes = append(outbytes, msgbytes[lastItem:i]...) outbytes = append(outbytes, msgbytes[lastItem:i]...)
i += 5 i += 5
@ -611,11 +613,57 @@ func parseMessage(msg string /*, user User*/) string {
outbytes = append(outbytes, msgbytes[lastItem:i]...) outbytes = append(outbytes, msgbytes[lastItem:i]...)
urlLen := partialURLBytesLen(msgbytes[i:]) urlLen := partialURLBytesLen(msgbytes[i:])
if msgbytes[i+urlLen] != ' ' && msgbytes[i+urlLen] != 10 { if msgbytes[i+urlLen] > 32 { // space and invisibles
outbytes = append(outbytes, invalidURL...) outbytes = append(outbytes, invalidURL...)
i += urlLen i += urlLen
continue continue
} }
outbytes = append(outbytes, urlOpen...)
outbytes = append(outbytes, msgbytes[i:i+urlLen]...)
outbytes = append(outbytes, urlOpen2...)
outbytes = append(outbytes, msgbytes[i:i+urlLen]...)
outbytes = append(outbytes, urlClose...)
i += urlLen
lastItem = i
} else if msgbytes[i] == '/' && msgbytes[i+1] == '/' {
outbytes = append(outbytes, msgbytes[lastItem:i]...)
urlLen := partialURLBytesLen(msgbytes[i:])
if msgbytes[i+urlLen] > 32 { // space and invisibles
//log.Print("INVALID URL")
//log.Print("msgbytes[i+urlLen]", msgbytes[i+urlLen])
//log.Print("string(msgbytes[i+urlLen])", string(msgbytes[i+urlLen]))
//log.Print("msgbytes[i:i+urlLen]", msgbytes[i:i+urlLen])
//log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen]))
outbytes = append(outbytes, invalidURL...)
i += urlLen
continue
}
//log.Print("VALID URL")
//log.Print("msgbytes[i:i+urlLen]", msgbytes[i:i+urlLen])
//log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen]))
media, ok := parseMediaBytes(msgbytes[i : i+urlLen])
if !ok {
outbytes = append(outbytes, invalidURL...)
i += urlLen
continue
}
if media.Type == "image" {
outbytes = append(outbytes, imageOpen...)
outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"&sectionType="+sectionType)...)
outbytes = append(outbytes, imageOpen2...)
outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"&sectionType="+sectionType)...)
outbytes = append(outbytes, imageClose...)
i += urlLen
lastItem = i
continue
} else if media.Type != "" {
outbytes = append(outbytes, unknownMedia...)
i += urlLen
continue
}
outbytes = append(outbytes, urlOpen...) outbytes = append(outbytes, urlOpen...)
outbytes = append(outbytes, msgbytes[i:i+urlLen]...) outbytes = append(outbytes, msgbytes[i:i+urlLen]...)
outbytes = append(outbytes, urlOpen2...) outbytes = append(outbytes, urlOpen2...)
@ -629,12 +677,9 @@ func parseMessage(msg string /*, user User*/) string {
if lastItem != i && len(outbytes) != 0 { if lastItem != i && len(outbytes) != 0 {
//log.Print("lastItem: ",msgbytes[lastItem]) //log.Print("lastItem: ",msgbytes[lastItem])
//log.Print("lastItem index:") //log.Print("lastItem index: ",lastItem)
//log.Print(lastItem) //log.Print("i: ",i)
//log.Print("i:") //log.Print("lastItem to end: ",msgbytes[lastItem:])
//log.Print(i)
//log.Print("lastItem to end:")
//log.Print(msgbytes[lastItem:])
//log.Print("-----") //log.Print("-----")
calclen := len(msgbytes) - 10 calclen := len(msgbytes) - 10
if calclen <= lastItem { if calclen <= lastItem {
@ -666,8 +711,8 @@ func regexParseMessage(msg string) string {
return msg return msg
} }
// 6, 7, 8, 6, 7 // 6, 7, 8, 6, 2, 7
// ftp://, http://, https:// git://, mailto: (not a URL, just here for length comparison purposes) // ftp://, http://, https:// git://, //, mailto: (not a URL, just here for length comparison purposes)
// TODO: Write a test for this // TODO: Write a test for this
func validateURLBytes(data []byte) bool { func validateURLBytes(data []byte) bool {
datalen := len(data) datalen := len(data)
@ -681,10 +726,13 @@ func validateURLBytes(data []byte) bool {
} else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) { } else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) {
i = 8 i = 8
} }
} else if datalen >= 2 && data[0] == '/' && data[1] == '/' {
i = 2
} }
// ? - There should only be one : and that's only if the URL is on a non-standard port
for ; datalen > i; i++ { for ; datalen > i; i++ {
if data[i] != '\\' && data[i] != '_' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) {
return false return false
} }
} }
@ -704,10 +752,13 @@ func validatedURLBytes(data []byte) (url []byte) {
} else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) { } else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) {
i = 8 i = 8
} }
} else if datalen >= 2 && data[0] == '/' && data[1] == '/' {
i = 2
} }
// ? - There should only be one : and that's only if the URL is on a non-standard port
for ; datalen > i; i++ { for ; datalen > i; i++ {
if data[i] != '\\' && data[i] != '_' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) {
return invalidURL return invalidURL
} }
} }
@ -730,10 +781,13 @@ func partialURLBytes(data []byte) (url []byte) {
} else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) { } else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) {
i = 8 i = 8
} }
} else if datalen >= 2 && data[0] == '/' && data[1] == '/' {
i = 2
} }
// ? - There should only be one : and that's only if the URL is on a non-standard port
for ; end >= i; i++ { for ; end >= i; i++ {
if data[i] != '\\' && data[i] != '_' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) {
end = i end = i
} }
} }
@ -756,47 +810,80 @@ func partialURLBytesLen(data []byte) int {
} else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) { } else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) {
i = 8 i = 8
} }
} else if datalen >= 2 && data[0] == '/' && data[1] == '/' {
i = 2
} }
// ? - There should only be one : and that's only if the URL is on a non-standard port
for ; datalen > i; i++ { for ; datalen > i; i++ {
if data[i] != '\\' && data[i] != '_' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { if data[i] != '\\' && data[i] != '_' && data[i] != ':' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) {
//log.Print("Bad Character: ", data[i]) //log.Print("Bad Character: ", data[i])
return i return i
} }
} }
//log.Print("Data Length: ",datalen) //log.Print("Data Length: ",datalen)
return datalen return datalen
} }
type MediaEmbed struct {
Type string //image
URL string
}
// TODO: Write a test for this // TODO: Write a test for this
func parseMediaBytes(data []byte) (protocol []byte, url []byte) { func parseMediaBytes(data []byte) (media MediaEmbed, ok bool) {
datalen := len(data) if !validateURLBytes(data) {
i := 0 return media, false
if datalen >= 6 {
if bytes.Equal(data[0:6], []byte("ftp://")) || bytes.Equal(data[0:6], []byte("git://")) {
i = 6
protocol = data[0:2]
} else if datalen >= 7 && bytes.Equal(data[0:7], httpProtBytes) {
i = 7
protocol = []byte("http")
} else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) {
i = 8
protocol = []byte("https")
} }
url, err := parseURL(data)
if err != nil {
return media, false
} }
for ; datalen > i; i++ { //log.Print("url ", url)
if data[i] != '\\' && data[i] != '_' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) { hostname := url.Hostname()
return []byte(""), invalidURL scheme := url.Scheme
port := url.Port()
//log.Print("hostname ", hostname)
//log.Print("scheme ", scheme)
var samesite = hostname == "localhost" || hostname == site.URL
if samesite {
//log.Print("samesite")
hostname = strings.Split(site.URL, ":")[0]
// ?- Test this as I'm not sure it'll do what it should. If someone's running SSL on port 80 or non-SSL on port 443 then... Well... They're in far worse trouble than this...
port = site.Port
if scheme == "" && site.EnableSsl {
scheme = "https"
} }
} }
if scheme == "" {
scheme = "http"
}
if len(protocol) == 0 { path := url.EscapedPath()
protocol = []byte("http") //log.Print("path", path)
pathFrags := strings.Split(path, "/")
//log.Printf("pathFrags %+v\n", pathFrags)
//log.Print("scheme ", scheme)
//log.Print("hostname ", hostname)
if len(pathFrags) >= 2 {
if samesite && pathFrags[1] == "attachs" && (scheme == "http" || scheme == "https") {
//log.Print("Attachment")
media.Type = "image"
var sport string
// ? - Assumes the sysadmin hasn't mixed up the two standard ports
if port != "443" && port != "80" {
sport = ":" + port
} }
return protocol, data[i:] media.URL = scheme + "://" + hostname + sport + path
}
}
return media, true
}
func parseURL(data []byte) (*url.URL, error) {
return url.Parse(string(data))
} }
// TODO: Write a test for this // TODO: Write a test for this

View File

@ -1531,6 +1531,7 @@ func routePanelGroupsEditPerms(w http.ResponseWriter, r *http.Request, user User
globalPerms = append(globalPerms, NameLangToggle{"ManagePlugins", GetGlobalPermPhrase("ManagePlugins"), group.Perms.ManagePlugins}) globalPerms = append(globalPerms, NameLangToggle{"ManagePlugins", GetGlobalPermPhrase("ManagePlugins"), group.Perms.ManagePlugins})
globalPerms = append(globalPerms, NameLangToggle{"ViewAdminLogs", GetGlobalPermPhrase("ViewAdminLogs"), group.Perms.ViewAdminLogs}) globalPerms = append(globalPerms, NameLangToggle{"ViewAdminLogs", GetGlobalPermPhrase("ViewAdminLogs"), group.Perms.ViewAdminLogs})
globalPerms = append(globalPerms, NameLangToggle{"ViewIPs", GetGlobalPermPhrase("ViewIPs"), group.Perms.ViewIPs}) globalPerms = append(globalPerms, NameLangToggle{"ViewIPs", GetGlobalPermPhrase("ViewIPs"), group.Perms.ViewIPs})
globalPerms = append(globalPerms, NameLangToggle{"UploadFiles", GetGlobalPermPhrase("UploadFiles"), group.Perms.UploadFiles})
pi := PanelEditGroupPermsPage{"Group Editor", user, headerVars, stats, group.ID, group.Name, localPerms, globalPerms} pi := PanelEditGroupPermsPage{"Group Editor", user, headerVars, stats, group.ID, group.Name, localPerms, globalPerms}
if preRenderHooks["pre_render_panel_edit_group_perms"] != nil { if preRenderHooks["pre_render_panel_edit_group_perms"] != nil {

View File

@ -16,6 +16,8 @@ var AllPerms Perms
var AllForumPerms ForumPerms var AllForumPerms ForumPerms
var AllPluginPerms = make(map[string]bool) var AllPluginPerms = make(map[string]bool)
// ? - Can we avoid duplicating the items in this list in a bunch of places?
var LocalPermList = []string{ var LocalPermList = []string{
"ViewTopic", "ViewTopic",
"LikeItem", "LikeItem",
@ -29,6 +31,7 @@ var LocalPermList = []string{
"CloseTopic", "CloseTopic",
} }
// ? - Can we avoid duplicating the items in this list in a bunch of places?
var GlobalPermList = []string{ var GlobalPermList = []string{
"BanUsers", "BanUsers",
"ActivateUsers", "ActivateUsers",
@ -49,6 +52,7 @@ var GlobalPermList = []string{
"ManagePlugins", "ManagePlugins",
"ViewAdminLogs", "ViewAdminLogs",
"ViewIPs", "ViewIPs",
"UploadFiles",
} }
// Permission Structure: ActionComponent[Subcomponent]Flag // Permission Structure: ActionComponent[Subcomponent]Flag
@ -74,6 +78,10 @@ type Perms struct {
ViewAdminLogs bool ViewAdminLogs bool
ViewIPs bool ViewIPs bool
// Global non-staff permissions
UploadFiles bool
// TODO: Add a permission for enabling avatars
// Forum permissions // Forum permissions
ViewTopic bool ViewTopic bool
LikeItem bool LikeItem bool
@ -147,6 +155,8 @@ func init() {
ViewAdminLogs: true, ViewAdminLogs: true,
ViewIPs: true, ViewIPs: true,
UploadFiles: true,
ViewTopic: true, ViewTopic: true,
LikeItem: true, LikeItem: true,
CreateTopic: true, CreateTopic: true,

View File

@ -78,6 +78,7 @@ function load_alerts(menu_alerts)
bind_to_alerts(); bind_to_alerts();
}, },
error: function(magic,theStatus,error) { error: function(magic,theStatus,error) {
var errtxt
try { try {
var data = JSON.parse(magic.responseText); var data = JSON.parse(magic.responseText);
if("errmsg" in data) errtxt = data.errmsg; if("errmsg" in data) errtxt = data.errmsg;
@ -94,16 +95,16 @@ function load_alerts(menu_alerts)
function SplitN(data,ch,n) { function SplitN(data,ch,n) {
var out = []; var out = [];
if(data.length == 0) return out; if(data.length === 0) return out;
var lastIndex = 0; var lastIndex = 0;
var j = 0; var j = 0;
var lastN = 1; var lastN = 1;
for(var i = 0; i < data.length; i++) { for(let i = 0; i < data.length; i++) {
if(data[i] == ch) { if(data[i] === ch) {
out[j++] = data.substring(lastIndex,i); out[j++] = data.substring(lastIndex,i);
lastIndex = i; lastIndex = i;
if(lastN == n) break; if(lastN === n) break;
lastN++; lastN++;
} }
} }
@ -118,19 +119,23 @@ $(document).ready(function(){
else conn = new WebSocket("ws://" + document.location.host + "/ws/"); else conn = new WebSocket("ws://" + document.location.host + "/ws/");
conn.onopen = function() { conn.onopen = function() {
console.log("The WebSockets connection was opened");
conn.send("page " + document.location.pathname + '\r'); conn.send("page " + document.location.pathname + '\r');
// TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on // TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on
Notification.requestPermission(); Notification.requestPermission();
} }
conn.onclose = function() { conn.onclose = function() {
conn = false; conn = false;
console.log("The WebSockets connection was closed");
} }
conn.onmessage = function(event) { conn.onmessage = function(event) {
//console.log("WS_Message: ",event.data); //console.log("WS_Message: ",event.data);
if(event.data[0] == "{") { if(event.data[0] == "{") {
try { try {
var data = JSON.parse(event.data); var data = JSON.parse(event.data);
} catch(err) { console.log(err); } } catch(err) {
console.log(err);
}
if ("msg" in data) { if ("msg" in data) {
var msg = data.msg var msg = data.msg
@ -175,11 +180,11 @@ $(document).ready(function(){
//console.log(messages[i]); //console.log(messages[i]);
if(messages[i].startsWith("set ")) { if(messages[i].startsWith("set ")) {
//msgblocks = messages[i].split(' ',3); //msgblocks = messages[i].split(' ',3);
msgblocks = SplitN(messages[i]," ",3); let msgblocks = SplitN(messages[i]," ",3);
if(msgblocks.length < 3) continue; if(msgblocks.length < 3) continue;
document.querySelector(msgblocks[1]).innerHTML = msgblocks[2]; document.querySelector(msgblocks[1]).innerHTML = msgblocks[2];
} else if(messages[i].startsWith("set-class ")) { } else if(messages[i].startsWith("set-class ")) {
msgblocks = SplitN(messages[i]," ",3); let msgblocks = SplitN(messages[i]," ",3);
if(msgblocks.length < 3) continue; if(msgblocks.length < 3) continue;
document.querySelector(msgblocks[1]).className = msgblocks[2]; document.querySelector(msgblocks[1]).className = msgblocks[2];
} }
@ -328,7 +333,7 @@ $(document).ready(function(){
//console.log("running .submit_edit event"); //console.log("running .submit_edit event");
var out_data = {isJs: "1"} var out_data = {isJs: "1"}
var block_parent = $(this).closest('.editable_parent'); var block_parent = $(this).closest('.editable_parent');
var block = block_parent.find('.editable_block').each(function(){ block_parent.find('.editable_block').each(function(){
var field_name = this.getAttribute("data-field"); var field_name = this.getAttribute("data-field");
var field_type = this.getAttribute("data-type"); var field_type = this.getAttribute("data-type");
if(field_type=="list") { if(field_type=="list") {
@ -397,6 +402,71 @@ $(document).ready(function(){
event.stopPropagation(); event.stopPropagation();
}) })
$(".create_topic_link").click(function(event){
event.preventDefault();
$(".topic_create_form").show();
});
$(".topic_create_form .close_form").click(function(){
event.preventDefault();
$(".topic_create_form").hide();
});
function uploadFileHandler() {
var fileList = this.files;
// Truncate the number of files to 5
let files = [];
for(var i = 0; i < fileList.length && i < 5; i++)
files[i] = fileList[i];
// Iterate over the files
for(let i = 0; i < files.length; i++) {
console.log("files[" + i + "]",files[i]);
let reader = new FileReader();
reader.onload = function(e) {
var fileDock = document.getElementById("upload_file_dock");
var fileItem = document.createElement("label");
console.log("fileItem",fileItem);
if(!files[i]["name"].indexOf('.' > -1)) {
// TODO: Surely, there's a prettier and more elegant way of doing this?
alert("This file doesn't have an extension");
return;
}
var ext = files[i]["name"].split('.').pop();
fileItem.innerText = "." + ext;
fileItem.className = "formbutton uploadItem";
fileItem.style.backgroundImage = "url("+e.target.result+")";
fileDock.appendChild(fileItem);
let reader = new FileReader();
reader.onload = function(e) {
crypto.subtle.digest('SHA-256',e.target.result).then(function(hash) {
const hashArray = Array.from(new Uint8Array(hash))
return hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('')
}).then(function(hash) {
console.log("hash",hash);
let content = document.getElementById("topic_content")
console.log("content.value",content.value);
if(content.value == "") content.value = content.value + "//" + siteURL + "/attachs/" + hash + "." + ext;
else content.value = content.value + "\r\n//" + siteURL + "/attachs/" + hash + "." + ext;
console.log("content.value",content.value);
});
}
reader.readAsArrayBuffer(files[i]);
}
reader.readAsDataURL(files[i]);
}
}
var uploadFiles = document.getElementById("quick_topic_upload_files");
if(uploadFiles != null) {
uploadFiles.addEventListener("change", uploadFileHandler, false);
}
$("#themeSelectorSelect").change(function(){ $("#themeSelectorSelect").change(function(){
console.log("Changing the theme to " + this.options[this.selectedIndex].getAttribute("val")); console.log("Changing the theme to " + this.options[this.selectedIndex].getAttribute("val"));
$.ajax({ $.ajax({
@ -408,6 +478,7 @@ $(document).ready(function(){
console.log("Theme successfully switched"); console.log("Theme successfully switched");
console.log("data",data); console.log("data",data);
console.log("status",status); console.log("status",status);
console.log("xhr",xhr);
window.location.reload(); window.location.reload();
}, },
// TODO: Use a standard error handler for the AJAX calls in here which throws up the response (if JSON) in a .notice? Might be difficult to trace errors in the console, if we reuse the same function every-time // TODO: Use a standard error handler for the AJAX calls in here which throws up the response (if JSON) in a .notice? Might be difficult to trace errors in the console, if we reuse the same function every-time

View File

@ -1,12 +0,0 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
<defs>
<pattern id="wooblies" width="50" height="43.4" patternUnits="userSpaceOnUse" patternTransform="scale(0.5) rotate(30)">
<polygon points="24.8,22 37.3,29.2 37.3,43.7 24.8,50.9 2.3,33.7 12.3,29.2" id="hex" fill="rgb(240,240,240)" stroke="darkgray" stroke-width="1" />
<use xlink:href="#hex" x="25" />
<use xlink:href="#hex" x="-25" />
<use xlink:href="#hex" x="12.5" y="-21.7" />
<use xlink:href="#hex" x="-12.5" y="-21.7" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#wooblies)" />
</svg>

Before

Width:  |  Height:  |  Size: 686 B

View File

@ -1,12 +0,0 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
<defs>
<pattern id="wooblies" width="50" height="43.4" patternUnits="userSpaceOnUse" patternTransform="scale(0.5) rotate(30)">
<polygon points="24.8,22 37.3,29.2 37.3,43.7 24.8,50.9 2.3,33.7 12.3,29.2" id="hex" fill="rgba(240,240,240)" stroke="darkgray" stroke-width="0.5" fill-opacity="0.1" />
<use xlink:href="#hex" x="25" />
<use xlink:href="#hex" x="-25" />
<use xlink:href="#hex" x="12.5" y="-21.7" />
<use xlink:href="#hex" x="-12.5" y="-21.7" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#wooblies)" />
</svg>

Before

Width:  |  Height:  |  Size: 708 B

View File

@ -13,11 +13,12 @@ func init() {
} }
type Mysql_Adapter struct { type Mysql_Adapter struct {
Name string Name string // ? - Do we really need this? Can't we hard-code this?
Buffer map[string]DB_Stmt Buffer map[string]DB_Stmt
BufferOrder []string // Map iteration order is random, so we need this to track the order, so we don't get huge diffs every commit BufferOrder []string // Map iteration order is random, so we need this to track the order, so we don't get huge diffs every commit
} }
// GetName gives you the name of the database adapter. In this case, it's mysql
func (adapter *Mysql_Adapter) GetName() string { func (adapter *Mysql_Adapter) GetName() string {
return adapter.Name return adapter.Name
} }
@ -120,7 +121,7 @@ func (adapter *Mysql_Adapter) SimpleInsert(name string, table string, columns st
var querystr = "INSERT INTO `" + table + "`(" var querystr = "INSERT INTO `" + table + "`("
// Escape the column names, just in case we've used a reserved keyword // Escape the column names, just in case we've used a reserved keyword
for _, column := range _process_columns(columns) { for _, column := range processColumns(columns) {
if column.Type == "function" { if column.Type == "function" {
querystr += column.Left + "," querystr += column.Left + ","
} else { } else {
@ -132,7 +133,7 @@ func (adapter *Mysql_Adapter) SimpleInsert(name string, table string, columns st
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
querystr += ") VALUES (" querystr += ") VALUES ("
for _, field := range _processFields(fields) { for _, field := range processFields(fields) {
querystr += field.Name + "," querystr += field.Name + ","
} }
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
@ -158,7 +159,7 @@ func (adapter *Mysql_Adapter) SimpleReplace(name string, table string, columns s
var querystr = "REPLACE INTO `" + table + "`(" var querystr = "REPLACE INTO `" + table + "`("
// Escape the column names, just in case we've used a reserved keyword // Escape the column names, just in case we've used a reserved keyword
for _, column := range _process_columns(columns) { for _, column := range processColumns(columns) {
if column.Type == "function" { if column.Type == "function" {
querystr += column.Left + "," querystr += column.Left + ","
} else { } else {
@ -169,7 +170,7 @@ func (adapter *Mysql_Adapter) SimpleReplace(name string, table string, columns s
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
querystr += ") VALUES (" querystr += ") VALUES ("
for _, field := range _processFields(fields) { for _, field := range processFields(fields) {
querystr += field.Name + "," querystr += field.Name + ","
} }
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
@ -190,7 +191,7 @@ func (adapter *Mysql_Adapter) SimpleUpdate(name string, table string, set string
} }
var querystr = "UPDATE `" + table + "` SET " var querystr = "UPDATE `" + table + "` SET "
for _, item := range _process_set(set) { for _, item := range processSet(set) {
querystr += "`" + item.Column + "` =" querystr += "`" + item.Column + "` ="
for _, token := range item.Expr { for _, token := range item.Expr {
switch token.Type { switch token.Type {
@ -211,7 +212,7 @@ func (adapter *Mysql_Adapter) SimpleUpdate(name string, table string, set string
// Add support for BETWEEN x.x // Add support for BETWEEN x.x
if len(where) != 0 { if len(where) != 0 {
querystr += " WHERE" querystr += " WHERE"
for _, loc := range _processWhere(where) { for _, loc := range processWhere(where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function", "operator", "number", "substitute": case "function", "operator", "number", "substitute":
@ -247,7 +248,7 @@ func (adapter *Mysql_Adapter) SimpleDelete(name string, table string, where stri
var querystr = "DELETE FROM `" + table + "` WHERE" var querystr = "DELETE FROM `" + table + "` WHERE"
// Add support for BETWEEN x.x // Add support for BETWEEN x.x
for _, loc := range _processWhere(where) { for _, loc := range processWhere(where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function", "operator", "number", "substitute": case "function", "operator", "number", "substitute":
@ -308,7 +309,7 @@ func (adapter *Mysql_Adapter) SimpleSelect(name string, table string, columns st
// Add support for BETWEEN x.x // Add support for BETWEEN x.x
if len(where) != 0 { if len(where) != 0 {
querystr += " WHERE" querystr += " WHERE"
for _, loc := range _processWhere(where) { for _, loc := range processWhere(where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function", "operator", "number", "substitute": case "function", "operator", "number", "substitute":
@ -328,7 +329,7 @@ func (adapter *Mysql_Adapter) SimpleSelect(name string, table string, columns st
if len(orderby) != 0 { if len(orderby) != 0 {
querystr += " ORDER BY " querystr += " ORDER BY "
for _, column := range _process_orderby(orderby) { for _, column := range processOrderby(orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + "," querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
} }
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
@ -362,7 +363,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2
var querystr = "SELECT " var querystr = "SELECT "
for _, column := range _process_columns(columns) { for _, column := range processColumns(columns) {
var source, alias string var source, alias string
// Escape the column names, just in case we've used a reserved keyword // Escape the column names, just in case we've used a reserved keyword
@ -384,7 +385,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
querystr += " FROM `" + table1 + "` LEFT JOIN `" + table2 + "` ON " querystr += " FROM `" + table1 + "` LEFT JOIN `" + table2 + "` ON "
for _, joiner := range _processJoiner(joiners) { for _, joiner := range processJoiner(joiners) {
querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND " querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND "
} }
// Remove the trailing AND // Remove the trailing AND
@ -393,7 +394,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2
// Add support for BETWEEN x.x // Add support for BETWEEN x.x
if len(where) != 0 { if len(where) != 0 {
querystr += " WHERE" querystr += " WHERE"
for _, loc := range _processWhere(where) { for _, loc := range processWhere(where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function", "operator", "number", "substitute": case "function", "operator", "number", "substitute":
@ -418,7 +419,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2
if len(orderby) != 0 { if len(orderby) != 0 {
querystr += " ORDER BY " querystr += " ORDER BY "
for _, column := range _process_orderby(orderby) { for _, column := range processOrderby(orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + "," querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
} }
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
@ -452,7 +453,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2
var querystr = "SELECT " var querystr = "SELECT "
for _, column := range _process_columns(columns) { for _, column := range processColumns(columns) {
var source, alias string var source, alias string
// Escape the column names, just in case we've used a reserved keyword // Escape the column names, just in case we've used a reserved keyword
@ -474,7 +475,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
querystr += " FROM `" + table1 + "` INNER JOIN `" + table2 + "` ON " querystr += " FROM `" + table1 + "` INNER JOIN `" + table2 + "` ON "
for _, joiner := range _processJoiner(joiners) { for _, joiner := range processJoiner(joiners) {
querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND " querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND "
} }
// Remove the trailing AND // Remove the trailing AND
@ -483,7 +484,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2
// Add support for BETWEEN x.x // Add support for BETWEEN x.x
if len(where) != 0 { if len(where) != 0 {
querystr += " WHERE" querystr += " WHERE"
for _, loc := range _processWhere(where) { for _, loc := range processWhere(where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function", "operator", "number", "substitute": case "function", "operator", "number", "substitute":
@ -508,7 +509,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2
if len(orderby) != 0 { if len(orderby) != 0 {
querystr += " ORDER BY " querystr += " ORDER BY "
for _, column := range _process_orderby(orderby) { for _, column := range processOrderby(orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + "," querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
} }
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
@ -529,7 +530,7 @@ func (adapter *Mysql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel
var querystr = "INSERT INTO `" + ins.Table + "`(" var querystr = "INSERT INTO `" + ins.Table + "`("
// Escape the column names, just in case we've used a reserved keyword // Escape the column names, just in case we've used a reserved keyword
for _, column := range _process_columns(ins.Columns) { for _, column := range processColumns(ins.Columns) {
if column.Type == "function" { if column.Type == "function" {
querystr += column.Left + "," querystr += column.Left + ","
} else { } else {
@ -540,7 +541,7 @@ func (adapter *Mysql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel
/* Select Portion */ /* Select Portion */
for _, column := range _process_columns(sel.Columns) { for _, column := range processColumns(sel.Columns) {
var source, alias string var source, alias string
// Escape the column names, just in case we've used a reserved keyword // Escape the column names, just in case we've used a reserved keyword
@ -562,7 +563,7 @@ func (adapter *Mysql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel
// Add support for BETWEEN x.x // Add support for BETWEEN x.x
if len(sel.Where) != 0 { if len(sel.Where) != 0 {
querystr += " WHERE" querystr += " WHERE"
for _, loc := range _processWhere(sel.Where) { for _, loc := range processWhere(sel.Where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function", "operator", "number", "substitute": case "function", "operator", "number", "substitute":
@ -582,7 +583,7 @@ func (adapter *Mysql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel
if len(sel.Orderby) != 0 { if len(sel.Orderby) != 0 {
querystr += " ORDER BY " querystr += " ORDER BY "
for _, column := range _process_orderby(sel.Orderby) { for _, column := range processOrderby(sel.Orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + "," querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
} }
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
@ -603,7 +604,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s
var querystr = "INSERT INTO `" + ins.Table + "`(" var querystr = "INSERT INTO `" + ins.Table + "`("
// Escape the column names, just in case we've used a reserved keyword // Escape the column names, just in case we've used a reserved keyword
for _, column := range _process_columns(ins.Columns) { for _, column := range processColumns(ins.Columns) {
if column.Type == "function" { if column.Type == "function" {
querystr += column.Left + "," querystr += column.Left + ","
} else { } else {
@ -614,7 +615,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s
/* Select Portion */ /* Select Portion */
for _, column := range _process_columns(sel.Columns) { for _, column := range processColumns(sel.Columns) {
var source, alias string var source, alias string
// Escape the column names, just in case we've used a reserved keyword // Escape the column names, just in case we've used a reserved keyword
@ -634,7 +635,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
querystr += " FROM `" + sel.Table1 + "` LEFT JOIN `" + sel.Table2 + "` ON " querystr += " FROM `" + sel.Table1 + "` LEFT JOIN `" + sel.Table2 + "` ON "
for _, joiner := range _processJoiner(sel.Joiners) { for _, joiner := range processJoiner(sel.Joiners) {
querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND " querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND "
} }
querystr = querystr[0 : len(querystr)-4] querystr = querystr[0 : len(querystr)-4]
@ -642,7 +643,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s
// Add support for BETWEEN x.x // Add support for BETWEEN x.x
if len(sel.Where) != 0 { if len(sel.Where) != 0 {
querystr += " WHERE" querystr += " WHERE"
for _, loc := range _processWhere(sel.Where) { for _, loc := range processWhere(sel.Where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function", "operator", "number", "substitute": case "function", "operator", "number", "substitute":
@ -667,7 +668,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s
if len(sel.Orderby) != 0 { if len(sel.Orderby) != 0 {
querystr += " ORDER BY " querystr += " ORDER BY "
for _, column := range _process_orderby(sel.Orderby) { for _, column := range processOrderby(sel.Orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + "," querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
} }
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
@ -688,7 +689,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert,
var querystr = "INSERT INTO `" + ins.Table + "`(" var querystr = "INSERT INTO `" + ins.Table + "`("
// Escape the column names, just in case we've used a reserved keyword // Escape the column names, just in case we've used a reserved keyword
for _, column := range _process_columns(ins.Columns) { for _, column := range processColumns(ins.Columns) {
if column.Type == "function" { if column.Type == "function" {
querystr += column.Left + "," querystr += column.Left + ","
} else { } else {
@ -699,7 +700,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert,
/* Select Portion */ /* Select Portion */
for _, column := range _process_columns(sel.Columns) { for _, column := range processColumns(sel.Columns) {
var source, alias string var source, alias string
// Escape the column names, just in case we've used a reserved keyword // Escape the column names, just in case we've used a reserved keyword
@ -719,7 +720,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert,
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
querystr += " FROM `" + sel.Table1 + "` INNER JOIN `" + sel.Table2 + "` ON " querystr += " FROM `" + sel.Table1 + "` INNER JOIN `" + sel.Table2 + "` ON "
for _, joiner := range _processJoiner(sel.Joiners) { for _, joiner := range processJoiner(sel.Joiners) {
querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND " querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND "
} }
querystr = querystr[0 : len(querystr)-4] querystr = querystr[0 : len(querystr)-4]
@ -727,7 +728,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert,
// Add support for BETWEEN x.x // Add support for BETWEEN x.x
if len(sel.Where) != 0 { if len(sel.Where) != 0 {
querystr += " WHERE" querystr += " WHERE"
for _, loc := range _processWhere(sel.Where) { for _, loc := range processWhere(sel.Where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function", "operator", "number", "substitute": case "function", "operator", "number", "substitute":
@ -752,7 +753,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert,
if len(sel.Orderby) != 0 { if len(sel.Orderby) != 0 {
querystr += " ORDER BY " querystr += " ORDER BY "
for _, column := range _process_orderby(sel.Orderby) { for _, column := range processOrderby(sel.Orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + "," querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
} }
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
@ -783,7 +784,7 @@ func (adapter *Mysql_Adapter) SimpleCount(name string, table string, where strin
//fmt.Println("SimpleCount:",name) //fmt.Println("SimpleCount:",name)
//fmt.Println("where:",where) //fmt.Println("where:",where)
//fmt.Println("_process_where:",_process_where(where)) //fmt.Println("_process_where:",_process_where(where))
for _, loc := range _processWhere(where) { for _, loc := range processWhere(where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function", "operator", "number", "substitute": case "function", "operator", "number", "substitute":

View File

@ -12,11 +12,12 @@ func init() {
} }
type Pgsql_Adapter struct { type Pgsql_Adapter struct {
Name string Name string // ? - Do we really need this? Can't we hard-code this?
Buffer map[string]DB_Stmt Buffer map[string]DB_Stmt
BufferOrder []string // Map iteration order is random, so we need this to track the order, so we don't get huge diffs every commit BufferOrder []string // Map iteration order is random, so we need this to track the order, so we don't get huge diffs every commit
} }
// GetName gives you the name of the database adapter. In this case, it's pgsql
func (adapter *Pgsql_Adapter) GetName() string { func (adapter *Pgsql_Adapter) GetName() string {
return adapter.Name return adapter.Name
} }
@ -139,7 +140,7 @@ func (adapter *Pgsql_Adapter) SimpleUpdate(name string, table string, set string
return "", errors.New("You need to set data in this update statement") return "", errors.New("You need to set data in this update statement")
} }
var querystr = "UPDATE `" + table + "` SET " var querystr = "UPDATE `" + table + "` SET "
for _, item := range _process_set(set) { for _, item := range processSet(set) {
querystr += "`" + item.Column + "` =" querystr += "`" + item.Column + "` ="
for _, token := range item.Expr { for _, token := range item.Expr {
switch token.Type { switch token.Type {
@ -166,7 +167,7 @@ func (adapter *Pgsql_Adapter) SimpleUpdate(name string, table string, set string
// Add support for BETWEEN x.x // Add support for BETWEEN x.x
if len(where) != 0 { if len(where) != 0 {
querystr += " WHERE" querystr += " WHERE"
for _, loc := range _processWhere(where) { for _, loc := range processWhere(where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function": case "function":

View File

@ -1,25 +1,31 @@
/* WIP Under Construction */ /*
*
* Query Generator Library
* WIP Under Construction
* Copyright Azareal 2017 - 2018
*
*/
package qgen package qgen
//import "fmt" //import "fmt"
import "strings" import "strings"
import "os" import "os"
func _process_columns(colstr string) (columns []DB_Column) { func processColumns(colstr string) (columns []DB_Column) {
if colstr == "" { if colstr == "" {
return columns return columns
} }
colstr = strings.Replace(colstr, " as ", " AS ", -1) colstr = strings.Replace(colstr, " as ", " AS ", -1)
for _, segment := range strings.Split(colstr, ",") { for _, segment := range strings.Split(colstr, ",") {
var outcol DB_Column var outcol DB_Column
dothalves := strings.Split(strings.TrimSpace(segment), ".") dotHalves := strings.Split(strings.TrimSpace(segment), ".")
var halves []string var halves []string
if len(dothalves) == 2 { if len(dotHalves) == 2 {
outcol.Table = dothalves[0] outcol.Table = dotHalves[0]
halves = strings.Split(dothalves[1], " AS ") halves = strings.Split(dotHalves[1], " AS ")
} else { } else {
halves = strings.Split(dothalves[0], " AS ") halves = strings.Split(dotHalves[0], " AS ")
} }
halves[0] = strings.TrimSpace(halves[0]) halves[0] = strings.TrimSpace(halves[0])
@ -40,7 +46,7 @@ func _process_columns(colstr string) (columns []DB_Column) {
return columns return columns
} }
func _process_orderby(orderstr string) (order []DB_Order) { func processOrderby(orderstr string) (order []DB_Order) {
if orderstr == "" { if orderstr == "" {
return order return order
} }
@ -57,7 +63,7 @@ func _process_orderby(orderstr string) (order []DB_Order) {
return order return order
} }
func _processJoiner(joinstr string) (joiner []DB_Joiner) { func processJoiner(joinstr string) (joiner []DB_Joiner) {
if joinstr == "" { if joinstr == "" {
return joiner return joiner
} }
@ -68,9 +74,9 @@ func _processJoiner(joinstr string) (joiner []DB_Joiner) {
var parseOffset int var parseOffset int
var left, right string var left, right string
left, parseOffset = _getIdentifier(segment, parseOffset) left, parseOffset = getIdentifier(segment, parseOffset)
outjoin.Operator, parseOffset = _getOperator(segment, parseOffset+1) outjoin.Operator, parseOffset = getOperator(segment, parseOffset+1)
right, parseOffset = _getIdentifier(segment, parseOffset+1) right, parseOffset = getIdentifier(segment, parseOffset+1)
left_column := strings.Split(left, ".") left_column := strings.Split(left, ".")
right_column := strings.Split(right, ".") right_column := strings.Split(right, ".")
@ -84,7 +90,7 @@ func _processJoiner(joinstr string) (joiner []DB_Joiner) {
return joiner return joiner
} }
func _processWhere(wherestr string) (where []DB_Where) { func processWhere(wherestr string) (where []DB_Where) {
if wherestr == "" { if wherestr == "" {
return where return where
} }
@ -144,7 +150,7 @@ func _processWhere(wherestr string) (where []DB_Where) {
//fmt.Println("len(halves)",len(halves[1])) //fmt.Println("len(halves)",len(halves[1]))
//fmt.Println("preI",string(halves[1][preI])) //fmt.Println("preI",string(halves[1][preI]))
//fmt.Println("msg prior to preI",halves[1][0:preI]) //fmt.Println("msg prior to preI",halves[1][0:preI])
i = _skipFunctionCall(segment, i-1) i = skipFunctionCall(segment, i-1)
//fmt.Println("i",i) //fmt.Println("i",i)
//fmt.Println("msg prior to i-1",halves[1][0:i-1]) //fmt.Println("msg prior to i-1",halves[1][0:i-1])
//fmt.Println("string(i-1)",string(halves[1][i-1])) //fmt.Println("string(i-1)",string(halves[1][i-1]))
@ -179,7 +185,7 @@ func _processWhere(wherestr string) (where []DB_Where) {
return where return where
} }
func _process_set(setstr string) (setter []DB_Setter) { func processSet(setstr string) (setter []DB_Setter) {
if setstr == "" { if setstr == "" {
return setter return setter
} }
@ -192,7 +198,7 @@ func _process_set(setstr string) (setter []DB_Setter) {
setstr += "," setstr += ","
for i := 0; i < len(setstr); i++ { for i := 0; i < len(setstr); i++ {
if setstr[i] == '(' { if setstr[i] == '(' {
i = _skipFunctionCall(setstr, i-1) i = skipFunctionCall(setstr, i-1)
setset = append(setset, setstr[lastItem:i+1]) setset = append(setset, setstr[lastItem:i+1])
buffer = "" buffer = ""
lastItem = i + 2 lastItem = i + 2
@ -208,12 +214,12 @@ func _process_set(setstr string) (setter []DB_Setter) {
// Second pass. Break this setitem into manageable chunks // Second pass. Break this setitem into manageable chunks
buffer = "" buffer = ""
for _, setitem := range setset { for _, setitem := range setset {
var tmp_setter DB_Setter var tmpSetter DB_Setter
halves := strings.Split(setitem, "=") halves := strings.Split(setitem, "=")
if len(halves) != 2 { if len(halves) != 2 {
continue continue
} }
tmp_setter.Column = strings.TrimSpace(halves[0]) tmpSetter.Column = strings.TrimSpace(halves[0])
halves[1] += ")" halves[1] += ")"
var optype int // 0: None, 1: Number, 2: Column, 3: Function, 4: String, 5: Operator var optype int // 0: None, 1: Number, 2: Column, 3: Function, 4: String, 5: Operator
@ -237,7 +243,7 @@ func _process_set(setstr string) (setter []DB_Setter) {
buffer = string(char) buffer = string(char)
} else if char == '?' { } else if char == '?' {
//fmt.Println("Expr:","?") //fmt.Println("Expr:","?")
tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{"?", "substitute"}) tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{"?", "substitute"})
} }
case 1: // number case 1: // number
if '0' <= char && char <= '9' { if '0' <= char && char <= '9' {
@ -246,7 +252,7 @@ func _process_set(setstr string) (setter []DB_Setter) {
optype = 0 optype = 0
i-- i--
//fmt.Println("Expr:",buffer) //fmt.Println("Expr:",buffer)
tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{buffer, "number"}) tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{buffer, "number"})
} }
case 2: // column case 2: // column
if ('a' <= char && char <= 'z') || ('A' <= char && char <= 'Z') || char == '_' { if ('a' <= char && char <= 'z') || ('A' <= char && char <= 'Z') || char == '_' {
@ -258,7 +264,7 @@ func _process_set(setstr string) (setter []DB_Setter) {
optype = 0 optype = 0
i-- i--
//fmt.Println("Expr:",buffer) //fmt.Println("Expr:",buffer)
tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{buffer, "column"}) tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{buffer, "column"})
} }
case 3: // function case 3: // function
var preI = i var preI = i
@ -266,14 +272,14 @@ func _process_set(setstr string) (setter []DB_Setter) {
//fmt.Println("len(halves)",len(halves[1])) //fmt.Println("len(halves)",len(halves[1]))
//fmt.Println("preI",string(halves[1][preI])) //fmt.Println("preI",string(halves[1][preI]))
//fmt.Println("msg prior to preI",halves[1][0:preI]) //fmt.Println("msg prior to preI",halves[1][0:preI])
i = _skipFunctionCall(halves[1], i-1) i = skipFunctionCall(halves[1], i-1)
//fmt.Println("i",i) //fmt.Println("i",i)
//fmt.Println("msg prior to i-1",halves[1][0:i-1]) //fmt.Println("msg prior to i-1",halves[1][0:i-1])
//fmt.Println("string(i-1)",string(halves[1][i-1])) //fmt.Println("string(i-1)",string(halves[1][i-1]))
//fmt.Println("string(i)",string(halves[1][i])) //fmt.Println("string(i)",string(halves[1][i]))
buffer += halves[1][preI:i] + string(halves[1][i]) buffer += halves[1][preI:i] + string(halves[1][i])
//fmt.Println("Expr:",buffer) //fmt.Println("Expr:",buffer)
tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{buffer, "function"}) tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{buffer, "function"})
optype = 0 optype = 0
case 4: // string case 4: // string
if char != '\'' { if char != '\'' {
@ -281,7 +287,7 @@ func _process_set(setstr string) (setter []DB_Setter) {
} else { } else {
optype = 0 optype = 0
//fmt.Println("Expr:",buffer) //fmt.Println("Expr:",buffer)
tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{buffer, "string"}) tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{buffer, "string"})
} }
case 5: // operator case 5: // operator
if _is_op_byte(char) { if _is_op_byte(char) {
@ -290,19 +296,19 @@ func _process_set(setstr string) (setter []DB_Setter) {
optype = 0 optype = 0
i-- i--
//fmt.Println("Expr:",buffer) //fmt.Println("Expr:",buffer)
tmp_setter.Expr = append(tmp_setter.Expr, DB_Token{buffer, "operator"}) tmpSetter.Expr = append(tmpSetter.Expr, DB_Token{buffer, "operator"})
} }
default: default:
panic("Bad optype in _process_set") panic("Bad optype in _process_set")
} }
} }
setter = append(setter, tmp_setter) setter = append(setter, tmpSetter)
} }
//fmt.Println("setter",setter) //fmt.Println("setter",setter)
return setter return setter
} }
func _processLimit(limitstr string) (limiter DB_Limit) { func processLimit(limitstr string) (limiter DB_Limit) {
halves := strings.Split(limitstr, ",") halves := strings.Split(limitstr, ",")
if len(halves) == 2 { if len(halves) == 2 {
limiter.Offset = halves[0] limiter.Offset = halves[0]
@ -321,7 +327,7 @@ func _is_op_rune(char rune) bool {
return char == '<' || char == '>' || char == '=' || char == '!' || char == '*' || char == '%' || char == '+' || char == '-' || char == '/' return char == '<' || char == '>' || char == '=' || char == '!' || char == '*' || char == '%' || char == '+' || char == '-' || char == '/'
} }
func _processFields(fieldstr string) (fields []DB_Field) { func processFields(fieldstr string) (fields []DB_Field) {
if fieldstr == "" { if fieldstr == "" {
return fields return fields
} }
@ -330,12 +336,12 @@ func _processFields(fieldstr string) (fields []DB_Field) {
fieldstr += "," fieldstr += ","
for i := 0; i < len(fieldstr); i++ { for i := 0; i < len(fieldstr); i++ {
if fieldstr[i] == '(' { if fieldstr[i] == '(' {
i = _skipFunctionCall(fieldstr, i-1) i = skipFunctionCall(fieldstr, i-1)
fields = append(fields, DB_Field{Name: fieldstr[lastItem : i+1], Type: _getIdentifierType(fieldstr[lastItem : i+1])}) fields = append(fields, DB_Field{Name: fieldstr[lastItem : i+1], Type: getIdentifierType(fieldstr[lastItem : i+1])})
buffer = "" buffer = ""
lastItem = i + 2 lastItem = i + 2
} else if fieldstr[i] == ',' && buffer != "" { } else if fieldstr[i] == ',' && buffer != "" {
fields = append(fields, DB_Field{Name: buffer, Type: _getIdentifierType(buffer)}) fields = append(fields, DB_Field{Name: buffer, Type: getIdentifierType(buffer)})
buffer = "" buffer = ""
lastItem = i + 1 lastItem = i + 1
} else if (fieldstr[i] > 32) && fieldstr[i] != ',' && fieldstr[i] != ')' { } else if (fieldstr[i] > 32) && fieldstr[i] != ',' && fieldstr[i] != ')' {
@ -345,7 +351,7 @@ func _processFields(fieldstr string) (fields []DB_Field) {
return fields return fields
} }
func _getIdentifierType(identifier string) string { func getIdentifierType(identifier string) string {
if ('a' <= identifier[0] && identifier[0] <= 'z') || ('A' <= identifier[0] && identifier[0] <= 'Z') { if ('a' <= identifier[0] && identifier[0] <= 'z') || ('A' <= identifier[0] && identifier[0] <= 'Z') {
if identifier[len(identifier)-1] == ')' { if identifier[len(identifier)-1] == ')' {
return "function" return "function"
@ -358,12 +364,12 @@ func _getIdentifierType(identifier string) string {
return "literal" return "literal"
} }
func _getIdentifier(segment string, startOffset int) (out string, i int) { func getIdentifier(segment string, startOffset int) (out string, i int) {
segment = strings.TrimSpace(segment) segment = strings.TrimSpace(segment)
segment += " " // Avoid overflow bugs with slicing segment += " " // Avoid overflow bugs with slicing
for i = startOffset; i < len(segment); i++ { for i = startOffset; i < len(segment); i++ {
if segment[i] == '(' { if segment[i] == '(' {
i = _skipFunctionCall(segment, i) i = skipFunctionCall(segment, i)
return strings.TrimSpace(segment[startOffset:i]), (i - 1) return strings.TrimSpace(segment[startOffset:i]), (i - 1)
} }
if (segment[i] == ' ' || _is_op_byte(segment[i])) && i != startOffset { if (segment[i] == ' ' || _is_op_byte(segment[i])) && i != startOffset {
@ -373,7 +379,7 @@ func _getIdentifier(segment string, startOffset int) (out string, i int) {
return strings.TrimSpace(segment[startOffset:]), (i - 1) return strings.TrimSpace(segment[startOffset:]), (i - 1)
} }
func _getOperator(segment string, startOffset int) (out string, i int) { func getOperator(segment string, startOffset int) (out string, i int) {
segment = strings.TrimSpace(segment) segment = strings.TrimSpace(segment)
segment += " " // Avoid overflow bugs with slicing segment += " " // Avoid overflow bugs with slicing
for i = startOffset; i < len(segment); i++ { for i = startOffset; i < len(segment); i++ {
@ -384,7 +390,7 @@ func _getOperator(segment string, startOffset int) (out string, i int) {
return strings.TrimSpace(segment[startOffset:]), (i - 1) return strings.TrimSpace(segment[startOffset:]), (i - 1)
} }
func _skipFunctionCall(data string, index int) int { func skipFunctionCall(data string, index int) int {
var braceCount int var braceCount int
for ; index < len(data); index++ { for ; index < len(data); index++ {
char := data[index] char := data[index]

View File

@ -269,6 +269,8 @@ func write_selects(adapter qgen.DB_Adapter) error {
adapter.SimpleSelect("getSync", "sync", "last_update", "", "", "") adapter.SimpleSelect("getSync", "sync", "last_update", "", "", "")
adapter.SimpleSelect("getAttachment", "attachments", "sectionID, sectionTable, originID, originTable, uploadedBy, path", "path = ? AND sectionID = ? AND sectionTable = ?", "", "")
return nil return nil
} }
@ -334,6 +336,8 @@ func write_inserts(adapter qgen.DB_Adapter) error {
adapter.SimpleInsert("addAdminlogEntry", "administration_logs", "action, elementID, elementType, ipaddress, actorID, doneAt", "?,?,?,?,?,UTC_TIMESTAMP()") adapter.SimpleInsert("addAdminlogEntry", "administration_logs", "action, elementID, elementType, ipaddress, actorID, doneAt", "?,?,?,?,?,UTC_TIMESTAMP()")
adapter.SimpleInsert("addAttachment", "attachments", "sectionID, sectionTable, originID, originTable, uploadedBy, path", "?,?,?,?,?,?")
adapter.SimpleInsert("createWordFilter", "word_filters", "find, replacement", "?,?") adapter.SimpleInsert("createWordFilter", "word_filters", "find, replacement", "?,?")
return nil return nil

View File

@ -50,6 +50,7 @@ type Reply struct {
LikeCount int LikeCount int
} }
// Copy gives you a non-pointer concurrency safe copy of the reply
func (reply *Reply) Copy() Reply { func (reply *Reply) Copy() Reply {
return *reply return *reply
} }

View File

@ -6,8 +6,8 @@ import "log"
//import "strings" //import "strings"
import "os" import "os"
var route_list []Route var routeList []Route
var route_groups []RouteGroup var routeGroups []RouteGroup
func main() { func main() {
log.Println("Generating the router...") log.Println("Generating the router...")
@ -16,9 +16,9 @@ func main() {
routes() routes()
var out string var out string
var fdata = "// Code generated by. DO NOT EDIT.\n/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */\n" var fileData = "// Code generated by. DO NOT EDIT.\n/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */\n"
for _, route := range route_list { for _, route := range routeList {
var end int var end int
if route.Path[len(route.Path)-1] == '/' { if route.Path[len(route.Path)-1] == '/' {
end = len(route.Path) - 1 end = len(route.Path) - 1
@ -36,7 +36,7 @@ func main() {
out += ")\n\t\t\treturn" out += ")\n\t\t\treturn"
} }
for _, group := range route_groups { for _, group := range routeGroups {
var end int var end int
if group.Path[len(group.Path)-1] == '/' { if group.Path[len(group.Path)-1] == '/' {
end = len(group.Path) - 1 end = len(group.Path) - 1
@ -46,10 +46,10 @@ func main() {
out += ` out += `
case "` + group.Path[0:end] + `": case "` + group.Path[0:end] + `":
switch(req.URL.Path) {` switch(req.URL.Path) {`
var default_route Route var defaultRoute Route
for _, route := range group.Routes { for _, route := range group.Routes {
if group.Path == route.Path { if group.Path == route.Path {
default_route = route defaultRoute = route
continue continue
} }
@ -64,13 +64,13 @@ func main() {
out += ")\n\t\t\t\t\treturn" out += ")\n\t\t\t\t\treturn"
} }
if default_route.Name != "" { if defaultRoute.Name != "" {
out += "\n\t\t\t\tdefault:" out += "\n\t\t\t\tdefault:"
if default_route.Before != "" { if defaultRoute.Before != "" {
out += "\n\t\t\t\t\t" + default_route.Before out += "\n\t\t\t\t\t" + defaultRoute.Before
} }
out += "\n\t\t\t\t\t" + default_route.Name + "(w,req,user" out += "\n\t\t\t\t\t" + defaultRoute.Name + "(w,req,user"
for _, item := range default_route.Vars { for _, item := range defaultRoute.Vars {
out += ", " + item out += ", " + item
} }
out += ")\n\t\t\t\t\treturn" out += ")\n\t\t\t\t\treturn"
@ -78,7 +78,7 @@ func main() {
out += "\n\t\t\t}" out += "\n\t\t\t}"
} }
fdata += `package main fileData += `package main
import "log" import "log"
import "strings" import "strings"
@ -209,11 +209,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
NotFound(w,req) NotFound(w,req)
} }
` `
write_file("./gen_router.go", fdata) writeFile("./gen_router.go", fileData)
log.Println("Successfully generated the router") log.Println("Successfully generated the router")
} }
func write_file(name string, content string) { func writeFile(name string, content string) {
f, err := os.Create(name) f, err := os.Create(name)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -13,11 +13,11 @@ type RouteGroup struct {
} }
func addRoute(fname string, path string, before string, vars ...string) { func addRoute(fname string, path string, before string, vars ...string) {
route_list = append(route_list, Route{fname, path, before, vars}) routeList = append(routeList, Route{fname, path, before, vars})
} }
func addRouteGroup(path string, routes ...Route) { func addRouteGroup(path string, routes ...Route) {
route_groups = append(route_groups, RouteGroup{path, routes}) routeGroups = append(routeGroups, RouteGroup{path, routes})
} }
func routes() { func routes() {
@ -31,6 +31,7 @@ func routes() {
//addRoute("routeTopicCreate","/topics/create/","","extra_data") //addRoute("routeTopicCreate","/topics/create/","","extra_data")
//addRoute("routeTopics","/topics/",""/*,"&groups","&forums"*/) //addRoute("routeTopics","/topics/",""/*,"&groups","&forums"*/)
addRoute("routeChangeTheme", "/theme/", "") addRoute("routeChangeTheme", "/theme/", "")
addRoute("routeShowAttachment", "/attachs/", "", "extra_data")
addRouteGroup("/report/", addRouteGroup("/report/",
Route{"routeReportSubmit", "/report/submit/", "", []string{"extra_data"}}, Route{"routeReportSubmit", "/report/submit/", "", []string{"extra_data"}},

View File

@ -26,7 +26,7 @@ var tList []interface{}
//var nList []string //var nList []string
var successJSONBytes = []byte(`{"success":"1"}`) var successJSONBytes = []byte(`{"success":"1"}`)
var cacheControlMaxAge = "max-age=" + strconv.Itoa(day) var cacheControlMaxAge = "max-age=" + strconv.Itoa(day) // TODO: Make this a config value
// HTTPSRedirect is a connection handler which redirects all HTTP requests to HTTPS // HTTPSRedirect is a connection handler which redirects all HTTP requests to HTTPS
type HTTPSRedirect struct { type HTTPSRedirect struct {
@ -171,11 +171,25 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user User) {
canSee = group.CanSee canSee = group.CanSee
} }
// We need a list of the visible forums for Quick Topic
var forumList []Forum
for _, fid := range canSee { for _, fid := range canSee {
forum := fstore.DirtyGet(fid) forum := fstore.DirtyGet(fid)
if forum.Name != "" && forum.Active { if forum.Name != "" && forum.Active {
if forum.ParentType == "" || forum.ParentType == "forum" {
// Optimise Quick Topic away for guests
if user.Loggedin {
fcopy := forum.Copy()
// TODO: Add a hook here for plugin_socialgroups
forumList = append(forumList, fcopy)
}
}
// ? - Should we be showing plugin_socialgroups posts on /topics/?
// ? - Would it be useful, if we could post in social groups from /topics/?
fidList = append(fidList, strconv.Itoa(fid)) fidList = append(fidList, strconv.Itoa(fid))
qlist += "?," qlist += "?,"
} }
} }
@ -265,7 +279,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user User) {
topicItem.LastUser = userList[topicItem.LastReplyBy] topicItem.LastUser = userList[topicItem.LastReplyBy]
} }
pi := TopicsPage{"Topic List", user, headerVars, topicList} pi := TopicsPage{"All Topics", user, headerVars, topicList, forumList, config.DefaultForum}
if preRenderHooks["pre_render_topic_list"] != nil { if preRenderHooks["pre_render_topic_list"] != nil {
if runPreRenderHook("pre_render_topic_list", w, r, &user, &pi) { if runPreRenderHook("pre_render_topic_list", w, r, &user, &pi) {
return return
@ -495,7 +509,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
BuildWidgets("view_topic", &topic, headerVars, r) BuildWidgets("view_topic", &topic, headerVars, r)
topic.ContentHTML = parseMessage(topic.Content) topic.ContentHTML = parseMessage(topic.Content, topic.ParentID, "forums")
topic.ContentLines = strings.Count(topic.Content, "\n") topic.ContentLines = strings.Count(topic.Content, "\n")
// We don't want users posting in locked topics... // We don't want users posting in locked topics...
@ -573,7 +587,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
replyItem.UserLink = buildProfileURL(nameToSlug(replyItem.CreatedByName), replyItem.CreatedBy) replyItem.UserLink = buildProfileURL(nameToSlug(replyItem.CreatedByName), replyItem.CreatedBy)
replyItem.ParentID = topic.ID replyItem.ParentID = topic.ID
replyItem.ContentHtml = parseMessage(replyItem.Content) replyItem.ContentHtml = parseMessage(replyItem.Content, topic.ParentID, "forums")
replyItem.ContentLines = strings.Count(replyItem.Content, "\n") replyItem.ContentLines = strings.Count(replyItem.Content, "\n")
postGroup, err = gstore.Get(replyItem.Group) postGroup, err = gstore.Get(replyItem.Group)
@ -744,7 +758,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user User) {
// TODO: Add a hook here // TODO: Add a hook here
replyList = append(replyList, ReplyUser{rid, puser.ID, replyContent, parseMessage(replyContent), replyCreatedBy, buildProfileURL(nameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""}) replyList = append(replyList, ReplyUser{rid, puser.ID, replyContent, parseMessage(replyContent, 0, ""), replyCreatedBy, buildProfileURL(nameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""})
} }
err = rows.Err() err = rows.Err()
if err != nil { if err != nil {

View File

@ -192,8 +192,8 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerV
return headerVars, stats, false return headerVars, stats, false
} }
stats.Users = users.GetGlobalCount() stats.Users = users.GlobalCount()
stats.Forums = fstore.GetGlobalCount() // TODO: Stop it from showing the blanked forums stats.Forums = fstore.GlobalCount() // TODO: Stop it from showing the blanked forums
stats.Settings = len(headerVars.Settings) stats.Settings = len(headerVars.Settings)
stats.WordFilters = len(wordFilterBox.Load().(WordFilterBox)) stats.WordFilters = len(wordFilterBox.Load().(WordFilterBox))
stats.Themes = len(themes) stats.Themes = len(themes)

View File

@ -12,6 +12,7 @@ var config Config
var dev DevConfig var dev DevConfig
type Site struct { type Site struct {
ShortName string
Name string // ? - Move this into the settings table? Should we make a second version of this for the abbreviation shown in the navbar? Name string // ? - Move this into the settings table? Should we make a second version of this for the abbreviation shown in the navbar?
Email string // ? - Move this into the settings table? Email string // ? - Move this into the settings table?
URL string URL string

View File

@ -20,33 +20,37 @@ func template_forum(tmpl_forum_vars ForumPage, w http.ResponseWriter) {
w.Write(header_0) w.Write(header_0)
w.Write([]byte(tmpl_forum_vars.Title)) w.Write([]byte(tmpl_forum_vars.Title))
w.Write(header_1) w.Write(header_1)
w.Write([]byte(tmpl_forum_vars.Header.ThemeName)) w.Write([]byte(tmpl_forum_vars.Header.Site.Name))
w.Write(header_2) w.Write(header_2)
w.Write([]byte(tmpl_forum_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_forum_vars.Header.Stylesheets) != 0 { if len(tmpl_forum_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_forum_vars.Header.Stylesheets { for _, item := range tmpl_forum_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4) w.Write(header_4)
} w.Write([]byte(item))
}
w.Write(header_5) w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_forum_vars.Header.Scripts) != 0 { if len(tmpl_forum_vars.Header.Scripts) != 0 {
for _, item := range tmpl_forum_vars.Header.Scripts { for _, item := range tmpl_forum_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7) w.Write(header_7)
} w.Write([]byte(item))
}
w.Write(header_8) w.Write(header_8)
w.Write([]byte(tmpl_forum_vars.CurrentUser.Session))
w.Write(header_9)
if !tmpl_forum_vars.CurrentUser.IsSuperMod {
w.Write(header_10)
} }
}
w.Write(header_9)
w.Write([]byte(tmpl_forum_vars.CurrentUser.Session))
w.Write(header_10)
w.Write([]byte(tmpl_forum_vars.Header.Site.URL))
w.Write(header_11) w.Write(header_11)
if !tmpl_forum_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0) w.Write(menu_0)
w.Write(menu_1) w.Write(menu_1)
w.Write([]byte(tmpl_forum_vars.Header.Site.Name)) w.Write([]byte(tmpl_forum_vars.Header.Site.ShortName))
w.Write(menu_2) w.Write(menu_2)
if tmpl_forum_vars.CurrentUser.Loggedin { if tmpl_forum_vars.CurrentUser.Loggedin {
w.Write(menu_3) w.Write(menu_3)
@ -58,16 +62,16 @@ w.Write(menu_5)
w.Write(menu_6) w.Write(menu_6)
} }
w.Write(menu_7) w.Write(menu_7)
w.Write(header_12)
if tmpl_forum_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
w.Write(header_14) w.Write(header_14)
if tmpl_forum_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_15)
}
w.Write(header_16)
if len(tmpl_forum_vars.Header.NoticeList) != 0 { if len(tmpl_forum_vars.Header.NoticeList) != 0 {
for _, item := range tmpl_forum_vars.Header.NoticeList { for _, item := range tmpl_forum_vars.Header.NoticeList {
w.Write(header_15) w.Write(header_17)
w.Write([]byte(item)) w.Write([]byte(item))
w.Write(header_16) w.Write(header_18)
} }
} }
if tmpl_forum_vars.Page > 1 { if tmpl_forum_vars.Page > 1 {
@ -106,65 +110,75 @@ w.Write(forum_14)
w.Write(forum_15) w.Write(forum_15)
} }
w.Write(forum_16) w.Write(forum_16)
if len(tmpl_forum_vars.ItemList) != 0 { if tmpl_forum_vars.CurrentUser.Perms.CreateTopic {
for _, item := range tmpl_forum_vars.ItemList {
w.Write(forum_17) w.Write(forum_17)
if item.Sticky { w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID)))
w.Write(forum_18) w.Write(forum_18)
} else { if tmpl_forum_vars.CurrentUser.Perms.UploadFiles {
if item.IsClosed {
w.Write(forum_19) w.Write(forum_19)
} }
}
w.Write(forum_20) w.Write(forum_20)
if item.Creator.Avatar != "" {
w.Write(forum_21)
w.Write([]byte(item.Creator.Avatar))
w.Write(forum_22)
} }
w.Write(forum_21)
if len(tmpl_forum_vars.ItemList) != 0 {
for _, item := range tmpl_forum_vars.ItemList {
w.Write(forum_22)
if item.Sticky {
w.Write(forum_23) w.Write(forum_23)
w.Write([]byte(strconv.Itoa(item.PostCount))) } else {
w.Write(forum_24)
w.Write([]byte(item.LastReplyAt))
w.Write(forum_25)
w.Write([]byte(item.Link))
w.Write(forum_26)
w.Write([]byte(item.Title))
w.Write(forum_27)
w.Write([]byte(item.Creator.Link))
w.Write(forum_28)
w.Write([]byte(item.Creator.Name))
w.Write(forum_29)
if item.IsClosed { if item.IsClosed {
w.Write(forum_24)
}
}
w.Write(forum_25)
if item.Creator.Avatar != "" {
w.Write(forum_26)
w.Write([]byte(item.Creator.Avatar))
w.Write(forum_27)
}
w.Write(forum_28)
w.Write([]byte(strconv.Itoa(item.PostCount)))
w.Write(forum_29)
w.Write([]byte(item.LastReplyAt))
w.Write(forum_30) w.Write(forum_30)
w.Write([]byte(item.Link))
w.Write(forum_31)
w.Write([]byte(item.Title))
w.Write(forum_32)
w.Write([]byte(item.Creator.Link))
w.Write(forum_33)
w.Write([]byte(item.Creator.Name))
w.Write(forum_34)
if item.IsClosed {
w.Write(forum_35)
} }
if item.Sticky { if item.Sticky {
w.Write(forum_31)
}
w.Write(forum_32)
if item.LastUser.Avatar != "" {
w.Write(forum_33)
w.Write([]byte(item.LastUser.Avatar))
w.Write(forum_34)
}
w.Write(forum_35)
w.Write([]byte(item.LastUser.Link))
w.Write(forum_36) w.Write(forum_36)
w.Write([]byte(item.LastUser.Name)) }
w.Write(forum_37) w.Write(forum_37)
w.Write([]byte(item.LastReplyAt)) if item.LastUser.Avatar != "" {
w.Write(forum_38) w.Write(forum_38)
w.Write([]byte(item.LastUser.Avatar))
w.Write(forum_39)
}
w.Write(forum_40)
w.Write([]byte(item.LastUser.Link))
w.Write(forum_41)
w.Write([]byte(item.LastUser.Name))
w.Write(forum_42)
w.Write([]byte(item.LastReplyAt))
w.Write(forum_43)
} }
} else { } else {
w.Write(forum_39) w.Write(forum_44)
if tmpl_forum_vars.CurrentUser.Perms.CreateTopic { if tmpl_forum_vars.CurrentUser.Perms.CreateTopic {
w.Write(forum_40) w.Write(forum_45)
w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID)))
w.Write(forum_41) w.Write(forum_46)
} }
w.Write(forum_42) w.Write(forum_47)
} }
w.Write(forum_43) w.Write(forum_48)
w.Write(footer_0) w.Write(footer_0)
if len(tmpl_forum_vars.Header.Themes) != 0 { if len(tmpl_forum_vars.Header.Themes) != 0 {
for _, item := range tmpl_forum_vars.Header.Themes { for _, item := range tmpl_forum_vars.Header.Themes {

View File

@ -19,33 +19,37 @@ func template_forums(tmpl_forums_vars ForumsPage, w http.ResponseWriter) {
w.Write(header_0) w.Write(header_0)
w.Write([]byte(tmpl_forums_vars.Title)) w.Write([]byte(tmpl_forums_vars.Title))
w.Write(header_1) w.Write(header_1)
w.Write([]byte(tmpl_forums_vars.Header.ThemeName)) w.Write([]byte(tmpl_forums_vars.Header.Site.Name))
w.Write(header_2) w.Write(header_2)
w.Write([]byte(tmpl_forums_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_forums_vars.Header.Stylesheets) != 0 { if len(tmpl_forums_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_forums_vars.Header.Stylesheets { for _, item := range tmpl_forums_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4) w.Write(header_4)
} w.Write([]byte(item))
}
w.Write(header_5) w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_forums_vars.Header.Scripts) != 0 { if len(tmpl_forums_vars.Header.Scripts) != 0 {
for _, item := range tmpl_forums_vars.Header.Scripts { for _, item := range tmpl_forums_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7) w.Write(header_7)
} w.Write([]byte(item))
}
w.Write(header_8) w.Write(header_8)
w.Write([]byte(tmpl_forums_vars.CurrentUser.Session))
w.Write(header_9)
if !tmpl_forums_vars.CurrentUser.IsSuperMod {
w.Write(header_10)
} }
}
w.Write(header_9)
w.Write([]byte(tmpl_forums_vars.CurrentUser.Session))
w.Write(header_10)
w.Write([]byte(tmpl_forums_vars.Header.Site.URL))
w.Write(header_11) w.Write(header_11)
if !tmpl_forums_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0) w.Write(menu_0)
w.Write(menu_1) w.Write(menu_1)
w.Write([]byte(tmpl_forums_vars.Header.Site.Name)) w.Write([]byte(tmpl_forums_vars.Header.Site.ShortName))
w.Write(menu_2) w.Write(menu_2)
if tmpl_forums_vars.CurrentUser.Loggedin { if tmpl_forums_vars.CurrentUser.Loggedin {
w.Write(menu_3) w.Write(menu_3)
@ -57,16 +61,16 @@ w.Write(menu_5)
w.Write(menu_6) w.Write(menu_6)
} }
w.Write(menu_7) w.Write(menu_7)
w.Write(header_12)
if tmpl_forums_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
w.Write(header_14) w.Write(header_14)
if tmpl_forums_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_15)
}
w.Write(header_16)
if len(tmpl_forums_vars.Header.NoticeList) != 0 { if len(tmpl_forums_vars.Header.NoticeList) != 0 {
for _, item := range tmpl_forums_vars.Header.NoticeList { for _, item := range tmpl_forums_vars.Header.NoticeList {
w.Write(header_15) w.Write(header_17)
w.Write([]byte(item)) w.Write([]byte(item))
w.Write(header_16) w.Write(header_18)
} }
} }
w.Write(forums_0) w.Write(forums_0)

View File

@ -82,6 +82,7 @@ var template_create_topic_handle func(CreateTopicPage, http.ResponseWriter) = fu
} }
} }
// ? - Add template hooks?
func compileTemplates() error { func compileTemplates() error {
var c CTemplateSet var c CTemplateSet
@ -128,6 +129,7 @@ func compileTemplates() error {
return err return err
} }
// TODO: Use a dummy forum list to avoid o(n) problems
var forumList []Forum var forumList []Forum
forums, err := fstore.GetAll() forums, err := fstore.GetAll()
if err != nil { if err != nil {
@ -147,7 +149,7 @@ func compileTemplates() error {
var topicsList []*TopicsRow var topicsList []*TopicsRow
topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, "Date", "Date", user3.ID, 1, "", "127.0.0.1", 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}) topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, "Date", "Date", user3.ID, 1, "", "127.0.0.1", 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"})
topicsPage := TopicsPage{"Topic List", user, headerVars, topicsList} topicsPage := TopicsPage{"Topic List", user, headerVars, topicsList, forumList, config.DefaultForum}
topicsTmpl, err := c.compileTemplate("topics.html", "templates/", "TopicsPage", topicsPage, varList) topicsTmpl, err := c.compileTemplate("topics.html", "templates/", "TopicsPage", topicsPage, varList)
if err != nil { if err != nil {
return err return err

View File

@ -5,31 +5,36 @@ var header_0 = []byte(`<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<title>`) <title>`)
var header_1 = []byte(`</title> var header_1 = []byte(` | `)
var header_2 = []byte(`</title>
<link href="/static/`) <link href="/static/`)
var header_2 = []byte(`/main.css" rel="stylesheet" type="text/css"> var header_3 = []byte(`/main.css" rel="stylesheet" type="text/css">
`) `)
var header_3 = []byte(` var header_4 = []byte(`
<link href="/static/`) <link href="/static/`)
var header_4 = []byte(`" rel="stylesheet" type="text/css"> var header_5 = []byte(`" rel="stylesheet" type="text/css">
`)
var header_5 = []byte(`
<script type="text/javascript" src="/static/jquery-3.1.1.min.js"></script>
`) `)
var header_6 = []byte(` var header_6 = []byte(`
<script type="text/javascript" src="/static/`) <script type="text/javascript" src="/static/jquery-3.1.1.min.js"></script>
var header_7 = []byte(`"></script>
`) `)
var header_8 = []byte(` var header_7 = []byte(`
<script type="text/javascript">var session = "`) <script type="text/javascript" src="/static/`)
var header_9 = []byte(`";</script> var header_8 = []byte(`"></script>
`)
var header_9 = []byte(`
<script type="text/javascript">
var session = "`)
var header_10 = []byte(`";
var siteURL = "`)
var header_11 = []byte(`";
</script>
<script type="text/javascript" src="/static/global.js"></script> <script type="text/javascript" src="/static/global.js"></script>
<meta name="viewport" content="width=device-width,initial-scale = 1.0, maximum-scale=1.0,user-scalable=no" /> <meta name="viewport" content="width=device-width,initial-scale = 1.0, maximum-scale=1.0,user-scalable=no" />
</head> </head>
<body> <body>
<style>`) <style>`)
var header_10 = []byte(`.supermod_only { display: none !important; }`) var header_12 = []byte(`.supermod_only { display: none !important; }`)
var header_11 = []byte(`</style> var header_13 = []byte(`</style>
<div class="container"> <div class="container">
`) `)
var menu_0 = []byte(`<nav class="nav"> var menu_0 = []byte(`<nav class="nav">
@ -41,7 +46,6 @@ var menu_1 = []byte(`
var menu_2 = []byte(`</a></li> var menu_2 = []byte(`</a></li>
<li class="menu_left menu_forums"><a href="/forums/">Forums</a></li> <li class="menu_left menu_forums"><a href="/forums/">Forums</a></li>
<li class="menu_left menu_topics"><a href="/">Topics</a></li> <li class="menu_left menu_topics"><a href="/">Topics</a></li>
<li class="menu_left menu_create_topic"><a href="/topics/create/">Create Topic</a></li>
<li id="general_alerts" class="menu_right menu_alerts"> <li id="general_alerts" class="menu_right menu_alerts">
<div class="alert_bell"></div> <div class="alert_bell"></div>
<div class="alert_counter"></div> <div class="alert_counter"></div>
@ -68,13 +72,13 @@ var menu_7 = []byte(`
<div style="clear: both;"></div> <div style="clear: both;"></div>
</nav> </nav>
`) `)
var header_12 = []byte(` var header_14 = []byte(`
<div id="back"><div id="main" `) <div id="back"><div id="main" `)
var header_13 = []byte(`class="shrink_main"`) var header_15 = []byte(`class="shrink_main"`)
var header_14 = []byte(`> var header_16 = []byte(`>
`) `)
var header_15 = []byte(`<div class="alert">`) var header_17 = []byte(`<div class="alert">`)
var header_16 = []byte(`</div>`) var header_18 = []byte(`</div>`)
var topic_0 = []byte(` var topic_0 = []byte(`
<form id="edit_topic_form" action='/topic/edit/submit/`) <form id="edit_topic_form" action='/topic/edit/submit/`)
@ -117,120 +121,122 @@ var topic_18 = []byte(`
<div class="rowitem passive editable_parent post_item `) <div class="rowitem passive editable_parent post_item `)
var topic_19 = []byte(`" style="`) var topic_19 = []byte(`" style="`)
var topic_20 = []byte(`background-image:url(`) var topic_20 = []byte(`background-image:url(`)
var topic_21 = []byte(`), url(/static/post-avatar-bg.jpg);background-position: 0px `) var topic_21 = []byte(`), url(/static/`)
var topic_22 = []byte(`-1`) var topic_22 = []byte(`/post-avatar-bg.jpg);background-position: 0px `)
var topic_23 = []byte(`0px;background-repeat:no-repeat, repeat-y;`) var topic_23 = []byte(`-1`)
var topic_24 = []byte(`"> var topic_24 = []byte(`0px;background-repeat:no-repeat, repeat-y;`)
var topic_25 = []byte(`">
<p class="hide_on_edit topic_content user_content" style="margin:0;padding:0;">`) <p class="hide_on_edit topic_content user_content" style="margin:0;padding:0;">`)
var topic_25 = []byte(`</p> var topic_26 = []byte(`</p>
<textarea name="topic_content" class="show_on_edit topic_content_input">`) <textarea name="topic_content" class="show_on_edit topic_content_input">`)
var topic_26 = []byte(`</textarea> var topic_27 = []byte(`</textarea>
<span class="controls"> <span class="controls">
<a href="`) <a href="`)
var topic_27 = []byte(`" class="username real_username">`) var topic_28 = []byte(`" class="username real_username">`)
var topic_28 = []byte(`</a>&nbsp;&nbsp; var topic_29 = []byte(`</a>&nbsp;&nbsp;
`) `)
var topic_29 = []byte(`<a href="/topic/like/submit/`) var topic_30 = []byte(`<a href="/topic/like/submit/`)
var topic_30 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"> var topic_31 = []byte(`" class="mod_button" title="Love it" style="color:#202020;">
<button class="username like_label"`) <button class="username like_label"`)
var topic_31 = []byte(` style="background-color:#D6FFD6;"`) var topic_32 = []byte(` style="background-color:#D6FFD6;"`)
var topic_32 = []byte(`></button></a>`) var topic_33 = []byte(`></button></a>`)
var topic_33 = []byte(`<a href='/topic/edit/`) var topic_34 = []byte(`<a href='/topic/edit/`)
var topic_34 = []byte(`' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic"><button class="username edit_label"></button></a>`) var topic_35 = []byte(`' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic"><button class="username edit_label"></button></a>`)
var topic_35 = []byte(`<a href='/topic/delete/submit/`) var topic_36 = []byte(`<a href='/topic/delete/submit/`)
var topic_36 = []byte(`' class="mod_button" style="font-weight:normal;" title="Delete Topic"><button class="username trash_label"></button></a>`) var topic_37 = []byte(`' class="mod_button" style="font-weight:normal;" title="Delete Topic"><button class="username trash_label"></button></a>`)
var topic_37 = []byte(`<a class="mod_button" href='/topic/unlock/submit/`) var topic_38 = []byte(`<a class="mod_button" href='/topic/unlock/submit/`)
var topic_38 = []byte(`' style="font-weight:normal;" title="Unlock Topic"><button class="username unlock_label"></button></a>`) var topic_39 = []byte(`' style="font-weight:normal;" title="Unlock Topic"><button class="username unlock_label"></button></a>`)
var topic_39 = []byte(`<a href='/topic/lock/submit/`) var topic_40 = []byte(`<a href='/topic/lock/submit/`)
var topic_40 = []byte(`' class="mod_button" style="font-weight:normal;" title="Lock Topic"><button class="username lock_label"></button></a>`) var topic_41 = []byte(`' class="mod_button" style="font-weight:normal;" title="Lock Topic"><button class="username lock_label"></button></a>`)
var topic_41 = []byte(`<a class="mod_button" href='/topic/unstick/submit/`) var topic_42 = []byte(`<a class="mod_button" href='/topic/unstick/submit/`)
var topic_42 = []byte(`' style="font-weight:normal;" title="Unpin Topic"><button class="username unpin_label"></button></a>`) var topic_43 = []byte(`' style="font-weight:normal;" title="Unpin Topic"><button class="username unpin_label"></button></a>`)
var topic_43 = []byte(`<a href='/topic/stick/submit/`) var topic_44 = []byte(`<a href='/topic/stick/submit/`)
var topic_44 = []byte(`' class="mod_button" style="font-weight:normal;" title="Pin Topic"><button class="username pin_label"></button></a>`) var topic_45 = []byte(`' class="mod_button" style="font-weight:normal;" title="Pin Topic"><button class="username pin_label"></button></a>`)
var topic_45 = []byte(`<a class="mod_button" href='/users/ips/?ip=`) var topic_46 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_46 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`) var topic_47 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_47 = []byte(` var topic_48 = []byte(`
<a href="/report/submit/`) <a href="/report/submit/`)
var topic_48 = []byte(`?session=`) var topic_49 = []byte(`?session=`)
var topic_49 = []byte(`&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag Topic"><button class="username flag_label"></button></a> var topic_50 = []byte(`&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag Topic"><button class="username flag_label"></button></a>
`) `)
var topic_50 = []byte(`<a class="username hide_on_micro like_count">`) var topic_51 = []byte(`<a class="username hide_on_micro like_count">`)
var topic_51 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`) var topic_52 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_52 = []byte(`<a class="username hide_on_micro user_tag">`) var topic_53 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_53 = []byte(`</a>`) var topic_54 = []byte(`</a>`)
var topic_54 = []byte(`<a class="username hide_on_micro level">`) var topic_55 = []byte(`<a class="username hide_on_micro level">`)
var topic_55 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`) var topic_56 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_56 = []byte(` var topic_57 = []byte(`
</span> </span>
</div> </div>
</article> </article>
<div class="rowblock post_container" style="overflow: hidden;">`) <div class="rowblock post_container" style="overflow: hidden;">`)
var topic_57 = []byte(` var topic_58 = []byte(`
<article class="rowitem passive deletable_block editable_parent post_item action_item"> <article class="rowitem passive deletable_block editable_parent post_item action_item">
<span class="action_icon" style="font-size: 18px;padding-right: 5px;">`) <span class="action_icon" style="font-size: 18px;padding-right: 5px;">`)
var topic_58 = []byte(`</span>
<span>`)
var topic_59 = []byte(`</span> var topic_59 = []byte(`</span>
<span>`)
var topic_60 = []byte(`</span>
</article> </article>
`) `)
var topic_60 = []byte(` var topic_61 = []byte(`
<article class="rowitem passive deletable_block editable_parent post_item `) <article class="rowitem passive deletable_block editable_parent post_item `)
var topic_61 = []byte(`" style="`) var topic_62 = []byte(`" style="`)
var topic_62 = []byte(`background-image:url(`) var topic_63 = []byte(`background-image:url(`)
var topic_63 = []byte(`), url(/static/post-avatar-bg.jpg);background-position: 0px `) var topic_64 = []byte(`), url(/static/`)
var topic_64 = []byte(`-1`) var topic_65 = []byte(`/post-avatar-bg.jpg);background-position: 0px `)
var topic_65 = []byte(`0px;background-repeat:no-repeat, repeat-y;`) var topic_66 = []byte(`-1`)
var topic_66 = []byte(`"> var topic_67 = []byte(`0px;background-repeat:no-repeat, repeat-y;`)
var topic_68 = []byte(`">
`) `)
var topic_67 = []byte(` var topic_69 = []byte(`
<p class="editable_block user_content" style="margin:0;padding:0;">`) <p class="editable_block user_content" style="margin:0;padding:0;">`)
var topic_68 = []byte(`</p> var topic_70 = []byte(`</p>
<span class="controls"> <span class="controls">
<a href="`) <a href="`)
var topic_69 = []byte(`" class="username real_username">`) var topic_71 = []byte(`" class="username real_username">`)
var topic_70 = []byte(`</a>&nbsp;&nbsp; var topic_72 = []byte(`</a>&nbsp;&nbsp;
`) `)
var topic_71 = []byte(`<a href="/reply/like/submit/`) var topic_73 = []byte(`<a href="/reply/like/submit/`)
var topic_72 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`) var topic_74 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`)
var topic_73 = []byte(` style="background-color:#D6FFD6;"`) var topic_75 = []byte(` style="background-color:#D6FFD6;"`)
var topic_74 = []byte(`></button></a>`) var topic_76 = []byte(`></button></a>`)
var topic_75 = []byte(`<a href="/reply/edit/submit/`) var topic_77 = []byte(`<a href="/reply/edit/submit/`)
var topic_76 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`) var topic_78 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`)
var topic_77 = []byte(`<a href="/reply/delete/submit/`) var topic_79 = []byte(`<a href="/reply/delete/submit/`)
var topic_78 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`) var topic_80 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`)
var topic_79 = []byte(`<a class="mod_button" href='/users/ips/?ip=`) var topic_81 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_80 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`) var topic_82 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_81 = []byte(` var topic_83 = []byte(`
<a href="/report/submit/`) <a href="/report/submit/`)
var topic_82 = []byte(`?session=`) var topic_84 = []byte(`?session=`)
var topic_83 = []byte(`&type=reply" class="mod_button report_item" title="Flag Reply"><button class="username report_item flag_label"></button></a> var topic_85 = []byte(`&type=reply" class="mod_button report_item" title="Flag Reply"><button class="username report_item flag_label"></button></a>
`) `)
var topic_84 = []byte(`<a class="username hide_on_micro like_count">`) var topic_86 = []byte(`<a class="username hide_on_micro like_count">`)
var topic_85 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`) var topic_87 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_86 = []byte(`<a class="username hide_on_micro user_tag">`) var topic_88 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_87 = []byte(`</a>`) var topic_89 = []byte(`</a>`)
var topic_88 = []byte(`<a class="username hide_on_micro level">`) var topic_90 = []byte(`<a class="username hide_on_micro level">`)
var topic_89 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`) var topic_91 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_90 = []byte(` var topic_92 = []byte(`
</span> </span>
</article> </article>
`) `)
var topic_91 = []byte(`</div> var topic_93 = []byte(`</div>
`) `)
var topic_92 = []byte(` var topic_94 = []byte(`
<div class="rowblock topic_reply_form"> <div class="rowblock topic_reply_form">
<form action="/reply/create/" method="post"> <form action="/reply/create/" method="post">
<input name="tid" value='`) <input name="tid" value='`)
var topic_93 = []byte(`' type="hidden" /> var topic_95 = []byte(`' type="hidden" />
<div class="formrow real_first_child"> <div class="formrow real_first_child">
<div class="formitem"><textarea name="reply-content" placeholder="Insert reply here" required></textarea></div> <div class="formitem"><textarea name="reply-content" placeholder="Insert reply here" required></textarea></div>
</div> </div>
@ -240,7 +246,7 @@ var topic_93 = []byte(`' type="hidden" />
</form> </form>
</div> </div>
`) `)
var topic_94 = []byte(` var topic_96 = []byte(`
</main> </main>
@ -654,60 +660,111 @@ var topics_0 = []byte(`
<main> <main>
<div class="rowblock rowhead"> <div class="rowblock rowhead">
<div class="rowitem"><h1>Topic List</h1></div> <div class="rowitem topic_list_title`)
</div> var topics_1 = []byte(` has_opt`)
<div id="topic_list" class="rowblock topic_list" aria-label="The main topic list"> var topics_2 = []byte(`"><h1>All Topics</h1></div>
`) `)
var topics_1 = []byte(`<div class="rowitem topic_left passive datarow `) var topics_3 = []byte(`
var topics_2 = []byte(`topic_sticky`) <div class="opt create_topic_opt" title="Create Topic"><a class="create_topic_link" href="/topics/create/"></a></div>
var topics_3 = []byte(`topic_closed`) `)
var topics_4 = []byte(`" style="`) var topics_4 = []byte(`<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>`)
var topics_5 = []byte(`background-image: url(`) var topics_5 = []byte(`
var topics_6 = []byte(`);background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;`) <div style="clear: both;"></div>
var topics_7 = []byte(`"> `)
var topics_6 = []byte(`
</div>
`)
var topics_7 = []byte(`
<div class="rowblock topic_create_form" style="display: none;">
<form name="topic_create_form_form" id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<div class="formrow topic_board_row real_first_child">
<div class="formitem"><select form="topic_create_form_form" id="topic_board_input" name="topic-board">
`)
var topics_8 = []byte(`<option `)
var topics_9 = []byte(`selected`)
var topics_10 = []byte(` value="`)
var topics_11 = []byte(`">`)
var topics_12 = []byte(`</option>`)
var topics_13 = []byte(`
</select></div>
</div>
<div class="formrow topic_name_row">
<div class="formitem">
<input form="topic_create_form_form" name="topic-name" placeholder="Topic title" required>
</div>
</div>
<div class="formrow topic_content_row">
<div class="formitem">
<textarea form="topic_create_form_form" id="topic_content" name="topic-content" placeholder="Insert post here" required></textarea>
</div>
</div>
<div class="formrow topic_button_row">
<div class="formitem">
<button form="topic_create_form_form" class="formbutton">Create Topic</button>
`)
var topics_14 = []byte(`
<input name="quick_topic_upload_files" form="topic_create_form_form" id="quick_topic_upload_files" multiple type="file" style="display: none;" />
<label for="quick_topic_upload_files" class="formbutton add_file_button">Add File</label>`)
var topics_15 = []byte(`
<div id="upload_file_dock"></div>
<button class="formbutton close_form">Cancel</button>
</div>
</div>
</div>
`)
var topics_16 = []byte(`
<div id="topic_list" class="rowblock topic_list" aria-label="A list containing topics from every forum">
`)
var topics_17 = []byte(`<div class="rowitem topic_left passive datarow `)
var topics_18 = []byte(`topic_sticky`)
var topics_19 = []byte(`topic_closed`)
var topics_20 = []byte(`" style="`)
var topics_21 = []byte(`background-image: url(`)
var topics_22 = []byte(`);background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;`)
var topics_23 = []byte(`">
<span class="topic_inner_right rowsmall" style="float: right;"> <span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">`) <span class="replyCount">`)
var topics_8 = []byte(` replies</span><br /> var topics_24 = []byte(` replies</span><br />
<span class="lastReplyAt">`) <span class="lastReplyAt">`)
var topics_9 = []byte(`</span> var topics_25 = []byte(`</span>
</span> </span>
<span> <span>
<a class="rowtopic" href="`) <a class="rowtopic" href="`)
var topics_10 = []byte(`">`) var topics_26 = []byte(`">`)
var topics_11 = []byte(`</a> `) var topics_27 = []byte(`</a> `)
var topics_12 = []byte(`<a class="rowsmall" href="`) var topics_28 = []byte(`<a class="rowsmall" href="`)
var topics_13 = []byte(`">`) var topics_29 = []byte(`">`)
var topics_14 = []byte(`</a>`) var topics_30 = []byte(`</a>`)
var topics_15 = []byte(` var topics_31 = []byte(`
<br /><a class="rowsmall" href="`) <br /><a class="rowsmall" href="`)
var topics_16 = []byte(`">Starter: `) var topics_32 = []byte(`">Starter: `)
var topics_17 = []byte(`</a> var topics_33 = []byte(`</a>
`) `)
var topics_18 = []byte(`<span class="rowsmall topic_status_e topic_status_closed" title="Status: Closed"> | &#x1F512;&#xFE0E</span>`) var topics_34 = []byte(`<span class="rowsmall topic_status_e topic_status_closed" title="Status: Closed"> | &#x1F512;&#xFE0E</span>`)
var topics_19 = []byte(`<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>`) var topics_35 = []byte(`<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>`)
var topics_20 = []byte(` var topics_36 = []byte(`
</span> </span>
</div> </div>
<div class="rowitem topic_right passive datarow `) <div class="rowitem topic_right passive datarow `)
var topics_21 = []byte(`topic_sticky`) var topics_37 = []byte(`topic_sticky`)
var topics_22 = []byte(`topic_closed`) var topics_38 = []byte(`topic_closed`)
var topics_23 = []byte(`" style="`) var topics_39 = []byte(`" style="`)
var topics_24 = []byte(`background-image: url(`) var topics_40 = []byte(`background-image: url(`)
var topics_25 = []byte(`);background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;`) var topics_41 = []byte(`);background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;`)
var topics_26 = []byte(`"> var topics_42 = []byte(`">
<span> <span>
<a href="`) <a href="`)
var topics_27 = []byte(`" class="lastName" style="font-size: 14px;">`) var topics_43 = []byte(`" class="lastName" style="font-size: 14px;">`)
var topics_28 = []byte(`</a><br> var topics_44 = []byte(`</a><br>
<span class="rowsmall lastReplyAt">Last: `) <span class="rowsmall lastReplyAt">Last: `)
var topics_29 = []byte(`</span> var topics_45 = []byte(`</span>
</span> </span>
</div> </div>
`) `)
var topics_30 = []byte(`<div class="rowitem passive">There aren't any topics yet.`) var topics_46 = []byte(`<div class="rowitem passive">There aren't any topics yet.`)
var topics_31 = []byte(` <a href="/topics/create/">Start one?</a>`) var topics_47 = []byte(` <a href="/topics/create/">Start one?</a>`)
var topics_32 = []byte(`</div>`) var topics_48 = []byte(`</div>`)
var topics_33 = []byte(` var topics_49 = []byte(`
</div> </div>
</main> </main>
@ -733,7 +790,7 @@ var forum_11 = []byte(`</h1>
</div> </div>
`) `)
var forum_12 = []byte(` var forum_12 = []byte(`
<div class="opt create_topic_opt" title="Create Topic"><a href="/topics/create/`) <div class="opt create_topic_opt" title="Create Topic"><a class="create_topic_link" href="/topics/create/`)
var forum_13 = []byte(`"></a></div> var forum_13 = []byte(`"></a></div>
`) `)
var forum_14 = []byte(`<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>`) var forum_14 = []byte(`<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>`)
@ -742,52 +799,83 @@ var forum_15 = []byte(`
`) `)
var forum_16 = []byte(` var forum_16 = []byte(`
</div> </div>
`)
var forum_17 = []byte(`
<div class="rowblock topic_create_form" style="display: none;">
<form id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<input form="topic_create_form_form" id="topic_board_input" name="topic-board" value="`)
var forum_18 = []byte(`" type="hidden">
<div class="formrow topic_name_row real_first_child">
<div class="formitem">
<input form="topic_create_form_form" name="topic-name" placeholder="Topic title" required>
</div>
</div>
<div class="formrow topic_content_row">
<div class="formitem">
<textarea form="topic_create_form_form" id="topic_content" name="topic-content" placeholder="Insert post here" required></textarea>
</div>
</div>
<div class="formrow topic_button_row">
<div class="formitem">
<button form="topic_create_form_form" name="topic-button" class="formbutton">Create Topic</button>
`)
var forum_19 = []byte(`
<input name="quick_topic_upload_files" form="topic_create_form_form" id="quick_topic_upload_files" multiple type="file" style="display: none;" />
<label for="quick_topic_upload_files" class="formbutton add_file_button">Add File</label>`)
var forum_20 = []byte(`
<div id="upload_file_dock"></div>
<button class="formbutton close_form">Cancel</button>
</div>
</div>
</div>
`)
var forum_21 = []byte(`
<div id="forum_topic_list" class="rowblock topic_list"> <div id="forum_topic_list" class="rowblock topic_list">
`) `)
var forum_17 = []byte(`<div class="rowitem topic_left passive datarow `) var forum_22 = []byte(`<div class="rowitem topic_left passive datarow `)
var forum_18 = []byte(`topic_sticky`) var forum_23 = []byte(`topic_sticky`)
var forum_19 = []byte(`topic_closed`) var forum_24 = []byte(`topic_closed`)
var forum_20 = []byte(`" style="`) var forum_25 = []byte(`" style="`)
var forum_21 = []byte(`background-image: url(`) var forum_26 = []byte(`background-image: url(`)
var forum_22 = []byte(`);background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;`) var forum_27 = []byte(`);background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;`)
var forum_23 = []byte(`"> var forum_28 = []byte(`">
<span class="topic_inner_right rowsmall" style="float: right;"> <span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">`) <span class="replyCount">`)
var forum_24 = []byte(` replies</span><br /> var forum_29 = []byte(` replies</span><br />
<span class="lastReplyAt">`) <span class="lastReplyAt">`)
var forum_25 = []byte(`</span> var forum_30 = []byte(`</span>
</span> </span>
<span> <span>
<a class="rowtopic" href="`) <a class="rowtopic" href="`)
var forum_26 = []byte(`">`) var forum_31 = []byte(`">`)
var forum_27 = []byte(`</a> var forum_32 = []byte(`</a>
<br /><a class="rowsmall" href="`) <br /><a class="rowsmall" href="`)
var forum_28 = []byte(`">Starter: `) var forum_33 = []byte(`">Starter: `)
var forum_29 = []byte(`</a> var forum_34 = []byte(`</a>
`) `)
var forum_30 = []byte(`<span class="rowsmall topic_status_e topic_status_closed" title="Status: Closed"> | &#x1F512;&#xFE0E</span>`) var forum_35 = []byte(`<span class="rowsmall topic_status_e topic_status_closed" title="Status: Closed"> | &#x1F512;&#xFE0E</span>`)
var forum_31 = []byte(`<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>`) var forum_36 = []byte(`<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>`)
var forum_32 = []byte(` var forum_37 = []byte(`
</span> </span>
</div> </div>
<div class="rowitem topic_right passive datarow" style="`) <div class="rowitem topic_right passive datarow" style="`)
var forum_33 = []byte(`background-image: url(`) var forum_38 = []byte(`background-image: url(`)
var forum_34 = []byte(`);background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;`) var forum_39 = []byte(`);background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;`)
var forum_35 = []byte(`"> var forum_40 = []byte(`">
<span> <span>
<a href="`) <a href="`)
var forum_36 = []byte(`" class="lastName" style="font-size: 14px;">`) var forum_41 = []byte(`" class="lastName" style="font-size: 14px;">`)
var forum_37 = []byte(`</a><br> var forum_42 = []byte(`</a><br>
<span class="rowsmall lastReplyAt">Last: `) <span class="rowsmall lastReplyAt">Last: `)
var forum_38 = []byte(`</span> var forum_43 = []byte(`</span>
</span> </span>
</div> </div>
`) `)
var forum_39 = []byte(`<div class="rowitem passive">There aren't any topics in this forum yet.`) var forum_44 = []byte(`<div class="rowitem passive">There aren't any topics in this forum yet.`)
var forum_40 = []byte(` <a href="/topics/create/`) var forum_45 = []byte(` <a href="/topics/create/`)
var forum_41 = []byte(`">Start one?</a>`) var forum_46 = []byte(`">Start one?</a>`)
var forum_42 = []byte(`</div>`) var forum_47 = []byte(`</div>`)
var forum_43 = []byte(` var forum_48 = []byte(`
</div> </div>
</main> </main>

View File

@ -20,33 +20,37 @@ func template_profile(tmpl_profile_vars ProfilePage, w http.ResponseWriter) {
w.Write(header_0) w.Write(header_0)
w.Write([]byte(tmpl_profile_vars.Title)) w.Write([]byte(tmpl_profile_vars.Title))
w.Write(header_1) w.Write(header_1)
w.Write([]byte(tmpl_profile_vars.Header.ThemeName)) w.Write([]byte(tmpl_profile_vars.Header.Site.Name))
w.Write(header_2) w.Write(header_2)
w.Write([]byte(tmpl_profile_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_profile_vars.Header.Stylesheets) != 0 { if len(tmpl_profile_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_profile_vars.Header.Stylesheets { for _, item := range tmpl_profile_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4) w.Write(header_4)
} w.Write([]byte(item))
}
w.Write(header_5) w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_profile_vars.Header.Scripts) != 0 { if len(tmpl_profile_vars.Header.Scripts) != 0 {
for _, item := range tmpl_profile_vars.Header.Scripts { for _, item := range tmpl_profile_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7) w.Write(header_7)
} w.Write([]byte(item))
}
w.Write(header_8) w.Write(header_8)
w.Write([]byte(tmpl_profile_vars.CurrentUser.Session))
w.Write(header_9)
if !tmpl_profile_vars.CurrentUser.IsSuperMod {
w.Write(header_10)
} }
}
w.Write(header_9)
w.Write([]byte(tmpl_profile_vars.CurrentUser.Session))
w.Write(header_10)
w.Write([]byte(tmpl_profile_vars.Header.Site.URL))
w.Write(header_11) w.Write(header_11)
if !tmpl_profile_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0) w.Write(menu_0)
w.Write(menu_1) w.Write(menu_1)
w.Write([]byte(tmpl_profile_vars.Header.Site.Name)) w.Write([]byte(tmpl_profile_vars.Header.Site.ShortName))
w.Write(menu_2) w.Write(menu_2)
if tmpl_profile_vars.CurrentUser.Loggedin { if tmpl_profile_vars.CurrentUser.Loggedin {
w.Write(menu_3) w.Write(menu_3)
@ -58,16 +62,16 @@ w.Write(menu_5)
w.Write(menu_6) w.Write(menu_6)
} }
w.Write(menu_7) w.Write(menu_7)
w.Write(header_12)
if tmpl_profile_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
w.Write(header_14) w.Write(header_14)
if tmpl_profile_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_15)
}
w.Write(header_16)
if len(tmpl_profile_vars.Header.NoticeList) != 0 { if len(tmpl_profile_vars.Header.NoticeList) != 0 {
for _, item := range tmpl_profile_vars.Header.NoticeList { for _, item := range tmpl_profile_vars.Header.NoticeList {
w.Write(header_15) w.Write(header_17)
w.Write([]byte(item)) w.Write([]byte(item))
w.Write(header_16) w.Write(header_18)
} }
} }
w.Write(profile_0) w.Write(profile_0)

View File

@ -20,33 +20,37 @@ func template_topic(tmpl_topic_vars TopicPage, w http.ResponseWriter) {
w.Write(header_0) w.Write(header_0)
w.Write([]byte(tmpl_topic_vars.Title)) w.Write([]byte(tmpl_topic_vars.Title))
w.Write(header_1) w.Write(header_1)
w.Write([]byte(tmpl_topic_vars.Header.ThemeName)) w.Write([]byte(tmpl_topic_vars.Header.Site.Name))
w.Write(header_2) w.Write(header_2)
w.Write([]byte(tmpl_topic_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_topic_vars.Header.Stylesheets) != 0 { if len(tmpl_topic_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_topic_vars.Header.Stylesheets { for _, item := range tmpl_topic_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4) w.Write(header_4)
} w.Write([]byte(item))
}
w.Write(header_5) w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_topic_vars.Header.Scripts) != 0 { if len(tmpl_topic_vars.Header.Scripts) != 0 {
for _, item := range tmpl_topic_vars.Header.Scripts { for _, item := range tmpl_topic_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7) w.Write(header_7)
} w.Write([]byte(item))
}
w.Write(header_8) w.Write(header_8)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(header_9)
if !tmpl_topic_vars.CurrentUser.IsSuperMod {
w.Write(header_10)
} }
}
w.Write(header_9)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(header_10)
w.Write([]byte(tmpl_topic_vars.Header.Site.URL))
w.Write(header_11) w.Write(header_11)
if !tmpl_topic_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0) w.Write(menu_0)
w.Write(menu_1) w.Write(menu_1)
w.Write([]byte(tmpl_topic_vars.Header.Site.Name)) w.Write([]byte(tmpl_topic_vars.Header.Site.ShortName))
w.Write(menu_2) w.Write(menu_2)
if tmpl_topic_vars.CurrentUser.Loggedin { if tmpl_topic_vars.CurrentUser.Loggedin {
w.Write(menu_3) w.Write(menu_3)
@ -58,16 +62,16 @@ w.Write(menu_5)
w.Write(menu_6) w.Write(menu_6)
} }
w.Write(menu_7) w.Write(menu_7)
w.Write(header_12)
if tmpl_topic_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
w.Write(header_14) w.Write(header_14)
if tmpl_topic_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_15)
}
w.Write(header_16)
if len(tmpl_topic_vars.Header.NoticeList) != 0 { if len(tmpl_topic_vars.Header.NoticeList) != 0 {
for _, item := range tmpl_topic_vars.Header.NoticeList { for _, item := range tmpl_topic_vars.Header.NoticeList {
w.Write(header_15) w.Write(header_17)
w.Write([]byte(item)) w.Write([]byte(item))
w.Write(header_16) w.Write(header_18)
} }
} }
w.Write(topic_0) w.Write(topic_0)
@ -117,169 +121,173 @@ if tmpl_topic_vars.Topic.Avatar != "" {
w.Write(topic_20) w.Write(topic_20)
w.Write([]byte(tmpl_topic_vars.Topic.Avatar)) w.Write([]byte(tmpl_topic_vars.Topic.Avatar))
w.Write(topic_21) w.Write(topic_21)
if tmpl_topic_vars.Topic.ContentLines <= 5 { w.Write([]byte(tmpl_topic_vars.Header.ThemeName))
w.Write(topic_22) w.Write(topic_22)
} if tmpl_topic_vars.Topic.ContentLines <= 5 {
w.Write(topic_23) w.Write(topic_23)
} }
w.Write(topic_24) w.Write(topic_24)
w.Write([]byte(tmpl_topic_vars.Topic.ContentHTML))
w.Write(topic_25)
w.Write([]byte(tmpl_topic_vars.Topic.Content))
w.Write(topic_26)
w.Write([]byte(tmpl_topic_vars.Topic.UserLink))
w.Write(topic_27)
w.Write([]byte(tmpl_topic_vars.Topic.CreatedByName))
w.Write(topic_28)
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write(topic_29)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_30)
if tmpl_topic_vars.Topic.Liked {
w.Write(topic_31)
} }
w.Write(topic_25)
w.Write([]byte(tmpl_topic_vars.Topic.ContentHTML))
w.Write(topic_26)
w.Write([]byte(tmpl_topic_vars.Topic.Content))
w.Write(topic_27)
w.Write([]byte(tmpl_topic_vars.Topic.UserLink))
w.Write(topic_28)
w.Write([]byte(tmpl_topic_vars.Topic.CreatedByName))
w.Write(topic_29)
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write(topic_30)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_31)
if tmpl_topic_vars.Topic.Liked {
w.Write(topic_32) w.Write(topic_32)
} }
if tmpl_topic_vars.CurrentUser.Perms.EditTopic {
w.Write(topic_33) w.Write(topic_33)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) }
if tmpl_topic_vars.CurrentUser.Perms.EditTopic {
w.Write(topic_34) w.Write(topic_34)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_35)
} }
if tmpl_topic_vars.CurrentUser.Perms.DeleteTopic { if tmpl_topic_vars.CurrentUser.Perms.DeleteTopic {
w.Write(topic_35)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_36) w.Write(topic_36)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_37)
} }
if tmpl_topic_vars.CurrentUser.Perms.CloseTopic { if tmpl_topic_vars.CurrentUser.Perms.CloseTopic {
if tmpl_topic_vars.Topic.IsClosed { if tmpl_topic_vars.Topic.IsClosed {
w.Write(topic_37)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_38) w.Write(topic_38)
} else {
w.Write(topic_39)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_39)
} else {
w.Write(topic_40) w.Write(topic_40)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_41)
} }
} }
if tmpl_topic_vars.CurrentUser.Perms.PinTopic { if tmpl_topic_vars.CurrentUser.Perms.PinTopic {
if tmpl_topic_vars.Topic.Sticky { if tmpl_topic_vars.Topic.Sticky {
w.Write(topic_41)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_42) w.Write(topic_42)
} else {
w.Write(topic_43)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_43)
} else {
w.Write(topic_44) w.Write(topic_44)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_45)
} }
} }
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs { if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
w.Write(topic_45)
w.Write([]byte(tmpl_topic_vars.Topic.IPAddress))
w.Write(topic_46) w.Write(topic_46)
} w.Write([]byte(tmpl_topic_vars.Topic.IPAddress))
w.Write(topic_47) w.Write(topic_47)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID))) }
w.Write(topic_48) w.Write(topic_48)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session)) w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_49) w.Write(topic_49)
if tmpl_topic_vars.Topic.LikeCount > 0 { w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_50) w.Write(topic_50)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.LikeCount))) if tmpl_topic_vars.Topic.LikeCount > 0 {
w.Write(topic_51) w.Write(topic_51)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.LikeCount)))
w.Write(topic_52)
} }
if tmpl_topic_vars.Topic.Tag != "" { if tmpl_topic_vars.Topic.Tag != "" {
w.Write(topic_52)
w.Write([]byte(tmpl_topic_vars.Topic.Tag))
w.Write(topic_53) w.Write(topic_53)
} else { w.Write([]byte(tmpl_topic_vars.Topic.Tag))
w.Write(topic_54) w.Write(topic_54)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.Level))) } else {
w.Write(topic_55) w.Write(topic_55)
} w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.Level)))
w.Write(topic_56) w.Write(topic_56)
}
w.Write(topic_57)
if len(tmpl_topic_vars.ItemList) != 0 { if len(tmpl_topic_vars.ItemList) != 0 {
for _, item := range tmpl_topic_vars.ItemList { for _, item := range tmpl_topic_vars.ItemList {
if item.ActionType != "" { if item.ActionType != "" {
w.Write(topic_57)
w.Write([]byte(item.ActionIcon))
w.Write(topic_58) w.Write(topic_58)
w.Write([]byte(item.ActionType)) w.Write([]byte(item.ActionIcon))
w.Write(topic_59) w.Write(topic_59)
} else { w.Write([]byte(item.ActionType))
w.Write(topic_60) w.Write(topic_60)
w.Write([]byte(item.ClassName)) } else {
w.Write(topic_61) w.Write(topic_61)
if item.Avatar != "" { w.Write([]byte(item.ClassName))
w.Write(topic_62) w.Write(topic_62)
w.Write([]byte(item.Avatar)) if item.Avatar != "" {
w.Write(topic_63) w.Write(topic_63)
if item.ContentLines <= 5 { w.Write([]byte(item.Avatar))
w.Write(topic_64) w.Write(topic_64)
} w.Write([]byte(tmpl_topic_vars.Header.ThemeName))
w.Write(topic_65) w.Write(topic_65)
} if item.ContentLines <= 5 {
w.Write(topic_66) w.Write(topic_66)
}
w.Write(topic_67) w.Write(topic_67)
w.Write([]byte(item.ContentHtml)) }
w.Write(topic_68) w.Write(topic_68)
w.Write([]byte(item.UserLink))
w.Write(topic_69) w.Write(topic_69)
w.Write([]byte(item.CreatedByName)) w.Write([]byte(item.ContentHtml))
w.Write(topic_70) w.Write(topic_70)
if tmpl_topic_vars.CurrentUser.Perms.LikeItem { w.Write([]byte(item.UserLink))
w.Write(topic_71) w.Write(topic_71)
w.Write([]byte(strconv.Itoa(item.ID))) w.Write([]byte(item.CreatedByName))
w.Write(topic_72) w.Write(topic_72)
if item.Liked { if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write(topic_73) w.Write(topic_73)
}
w.Write(topic_74)
}
if tmpl_topic_vars.CurrentUser.Perms.EditReply {
w.Write(topic_75)
w.Write([]byte(strconv.Itoa(item.ID))) w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_74)
if item.Liked {
w.Write(topic_75)
}
w.Write(topic_76) w.Write(topic_76)
} }
if tmpl_topic_vars.CurrentUser.Perms.DeleteReply { if tmpl_topic_vars.CurrentUser.Perms.EditReply {
w.Write(topic_77) w.Write(topic_77)
w.Write([]byte(strconv.Itoa(item.ID))) w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_78) w.Write(topic_78)
} }
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs { if tmpl_topic_vars.CurrentUser.Perms.DeleteReply {
w.Write(topic_79) w.Write(topic_79)
w.Write([]byte(item.IPAddress)) w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_80) w.Write(topic_80)
} }
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
w.Write(topic_81) w.Write(topic_81)
w.Write([]byte(strconv.Itoa(item.ID))) w.Write([]byte(item.IPAddress))
w.Write(topic_82) w.Write(topic_82)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session)) }
w.Write(topic_83) w.Write(topic_83)
if item.LikeCount > 0 { w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_84) w.Write(topic_84)
w.Write([]byte(strconv.Itoa(item.LikeCount))) w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_85) w.Write(topic_85)
if item.LikeCount > 0 {
w.Write(topic_86)
w.Write([]byte(strconv.Itoa(item.LikeCount)))
w.Write(topic_87)
} }
if item.Tag != "" { if item.Tag != "" {
w.Write(topic_86)
w.Write([]byte(item.Tag))
w.Write(topic_87)
} else {
w.Write(topic_88) w.Write(topic_88)
w.Write([]byte(strconv.Itoa(item.Level))) w.Write([]byte(item.Tag))
w.Write(topic_89) w.Write(topic_89)
} } else {
w.Write(topic_90) w.Write(topic_90)
} w.Write([]byte(strconv.Itoa(item.Level)))
}
}
w.Write(topic_91) w.Write(topic_91)
if tmpl_topic_vars.CurrentUser.Perms.CreateReply {
w.Write(topic_92)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_93)
} }
w.Write(topic_92)
}
}
}
w.Write(topic_93)
if tmpl_topic_vars.CurrentUser.Perms.CreateReply {
w.Write(topic_94) w.Write(topic_94)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_95)
}
w.Write(topic_96)
w.Write(footer_0) w.Write(footer_0)
if len(tmpl_topic_vars.Header.Themes) != 0 { if len(tmpl_topic_vars.Header.Themes) != 0 {
for _, item := range tmpl_topic_vars.Header.Themes { for _, item := range tmpl_topic_vars.Header.Themes {

View File

@ -20,33 +20,37 @@ func template_topic_alt(tmpl_topic_alt_vars TopicPage, w http.ResponseWriter) {
w.Write(header_0) w.Write(header_0)
w.Write([]byte(tmpl_topic_alt_vars.Title)) w.Write([]byte(tmpl_topic_alt_vars.Title))
w.Write(header_1) w.Write(header_1)
w.Write([]byte(tmpl_topic_alt_vars.Header.ThemeName)) w.Write([]byte(tmpl_topic_alt_vars.Header.Site.Name))
w.Write(header_2) w.Write(header_2)
w.Write([]byte(tmpl_topic_alt_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_topic_alt_vars.Header.Stylesheets) != 0 { if len(tmpl_topic_alt_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_topic_alt_vars.Header.Stylesheets { for _, item := range tmpl_topic_alt_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4) w.Write(header_4)
} w.Write([]byte(item))
}
w.Write(header_5) w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_topic_alt_vars.Header.Scripts) != 0 { if len(tmpl_topic_alt_vars.Header.Scripts) != 0 {
for _, item := range tmpl_topic_alt_vars.Header.Scripts { for _, item := range tmpl_topic_alt_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7) w.Write(header_7)
} w.Write([]byte(item))
}
w.Write(header_8) w.Write(header_8)
w.Write([]byte(tmpl_topic_alt_vars.CurrentUser.Session))
w.Write(header_9)
if !tmpl_topic_alt_vars.CurrentUser.IsSuperMod {
w.Write(header_10)
} }
}
w.Write(header_9)
w.Write([]byte(tmpl_topic_alt_vars.CurrentUser.Session))
w.Write(header_10)
w.Write([]byte(tmpl_topic_alt_vars.Header.Site.URL))
w.Write(header_11) w.Write(header_11)
if !tmpl_topic_alt_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0) w.Write(menu_0)
w.Write(menu_1) w.Write(menu_1)
w.Write([]byte(tmpl_topic_alt_vars.Header.Site.Name)) w.Write([]byte(tmpl_topic_alt_vars.Header.Site.ShortName))
w.Write(menu_2) w.Write(menu_2)
if tmpl_topic_alt_vars.CurrentUser.Loggedin { if tmpl_topic_alt_vars.CurrentUser.Loggedin {
w.Write(menu_3) w.Write(menu_3)
@ -58,16 +62,16 @@ w.Write(menu_5)
w.Write(menu_6) w.Write(menu_6)
} }
w.Write(menu_7) w.Write(menu_7)
w.Write(header_12)
if tmpl_topic_alt_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
w.Write(header_14) w.Write(header_14)
if tmpl_topic_alt_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_15)
}
w.Write(header_16)
if len(tmpl_topic_alt_vars.Header.NoticeList) != 0 { if len(tmpl_topic_alt_vars.Header.NoticeList) != 0 {
for _, item := range tmpl_topic_alt_vars.Header.NoticeList { for _, item := range tmpl_topic_alt_vars.Header.NoticeList {
w.Write(header_15) w.Write(header_17)
w.Write([]byte(item)) w.Write([]byte(item))
w.Write(header_16) w.Write(header_18)
} }
} }
if tmpl_topic_alt_vars.Page > 1 { if tmpl_topic_alt_vars.Page > 1 {

View File

@ -20,33 +20,37 @@ func template_topics(tmpl_topics_vars TopicsPage, w http.ResponseWriter) {
w.Write(header_0) w.Write(header_0)
w.Write([]byte(tmpl_topics_vars.Title)) w.Write([]byte(tmpl_topics_vars.Title))
w.Write(header_1) w.Write(header_1)
w.Write([]byte(tmpl_topics_vars.Header.ThemeName)) w.Write([]byte(tmpl_topics_vars.Header.Site.Name))
w.Write(header_2) w.Write(header_2)
w.Write([]byte(tmpl_topics_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_topics_vars.Header.Stylesheets) != 0 { if len(tmpl_topics_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_topics_vars.Header.Stylesheets { for _, item := range tmpl_topics_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4) w.Write(header_4)
} w.Write([]byte(item))
}
w.Write(header_5) w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_topics_vars.Header.Scripts) != 0 { if len(tmpl_topics_vars.Header.Scripts) != 0 {
for _, item := range tmpl_topics_vars.Header.Scripts { for _, item := range tmpl_topics_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7) w.Write(header_7)
} w.Write([]byte(item))
}
w.Write(header_8) w.Write(header_8)
w.Write([]byte(tmpl_topics_vars.CurrentUser.Session))
w.Write(header_9)
if !tmpl_topics_vars.CurrentUser.IsSuperMod {
w.Write(header_10)
} }
}
w.Write(header_9)
w.Write([]byte(tmpl_topics_vars.CurrentUser.Session))
w.Write(header_10)
w.Write([]byte(tmpl_topics_vars.Header.Site.URL))
w.Write(header_11) w.Write(header_11)
if !tmpl_topics_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0) w.Write(menu_0)
w.Write(menu_1) w.Write(menu_1)
w.Write([]byte(tmpl_topics_vars.Header.Site.Name)) w.Write([]byte(tmpl_topics_vars.Header.Site.ShortName))
w.Write(menu_2) w.Write(menu_2)
if tmpl_topics_vars.CurrentUser.Loggedin { if tmpl_topics_vars.CurrentUser.Loggedin {
w.Write(menu_3) w.Write(menu_3)
@ -58,92 +62,129 @@ w.Write(menu_5)
w.Write(menu_6) w.Write(menu_6)
} }
w.Write(menu_7) w.Write(menu_7)
w.Write(header_12)
if tmpl_topics_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
w.Write(header_14) w.Write(header_14)
if tmpl_topics_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_15)
}
w.Write(header_16)
if len(tmpl_topics_vars.Header.NoticeList) != 0 { if len(tmpl_topics_vars.Header.NoticeList) != 0 {
for _, item := range tmpl_topics_vars.Header.NoticeList { for _, item := range tmpl_topics_vars.Header.NoticeList {
w.Write(header_15) w.Write(header_17)
w.Write([]byte(item)) w.Write([]byte(item))
w.Write(header_16) w.Write(header_18)
} }
} }
w.Write(topics_0) w.Write(topics_0)
if len(tmpl_topics_vars.ItemList) != 0 { if tmpl_topics_vars.CurrentUser.ID != 0 {
for _, item := range tmpl_topics_vars.ItemList {
w.Write(topics_1) w.Write(topics_1)
if item.Sticky { }
w.Write(topics_2) w.Write(topics_2)
} else { if tmpl_topics_vars.CurrentUser.ID != 0 {
if item.IsClosed { if len(tmpl_topics_vars.ForumList) != 0 {
w.Write(topics_3) w.Write(topics_3)
} } else {
}
w.Write(topics_4) w.Write(topics_4)
if item.Creator.Avatar != "" {
w.Write(topics_5)
w.Write([]byte(item.Creator.Avatar))
w.Write(topics_6)
} }
w.Write(topics_5)
}
w.Write(topics_6)
if tmpl_topics_vars.CurrentUser.ID != 0 {
if len(tmpl_topics_vars.ForumList) != 0 {
w.Write(topics_7) w.Write(topics_7)
w.Write([]byte(strconv.Itoa(item.PostCount))) if len(tmpl_topics_vars.ForumList) != 0 {
for _, item := range tmpl_topics_vars.ForumList {
w.Write(topics_8) w.Write(topics_8)
w.Write([]byte(item.LastReplyAt)) if item.ID == tmpl_topics_vars.DefaultForum {
w.Write(topics_9) w.Write(topics_9)
w.Write([]byte(item.Link)) }
w.Write(topics_10) w.Write(topics_10)
w.Write([]byte(item.Title)) w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topics_11) w.Write(topics_11)
if item.ForumName != "" { w.Write([]byte(item.Name))
w.Write(topics_12) w.Write(topics_12)
w.Write([]byte(item.ForumLink)) }
}
w.Write(topics_13) w.Write(topics_13)
w.Write([]byte(item.ForumName)) if tmpl_topics_vars.CurrentUser.Perms.UploadFiles {
w.Write(topics_14) w.Write(topics_14)
} }
w.Write(topics_15) w.Write(topics_15)
w.Write([]byte(item.Creator.Link))
w.Write(topics_16)
w.Write([]byte(item.Creator.Name))
w.Write(topics_17)
if item.IsClosed {
w.Write(topics_18)
} }
}
w.Write(topics_16)
if len(tmpl_topics_vars.TopicList) != 0 {
for _, item := range tmpl_topics_vars.TopicList {
w.Write(topics_17)
if item.Sticky { if item.Sticky {
w.Write(topics_18)
} else {
if item.IsClosed {
w.Write(topics_19) w.Write(topics_19)
} }
}
w.Write(topics_20) w.Write(topics_20)
if item.Sticky { if item.Creator.Avatar != "" {
w.Write(topics_21) w.Write(topics_21)
} else { w.Write([]byte(item.Creator.Avatar))
if item.IsClosed {
w.Write(topics_22) w.Write(topics_22)
} }
}
w.Write(topics_23) w.Write(topics_23)
if item.LastUser.Avatar != "" { w.Write([]byte(strconv.Itoa(item.PostCount)))
w.Write(topics_24) w.Write(topics_24)
w.Write([]byte(item.LastUser.Avatar))
w.Write(topics_25)
}
w.Write(topics_26)
w.Write([]byte(item.LastUser.Link))
w.Write(topics_27)
w.Write([]byte(item.LastUser.Name))
w.Write(topics_28)
w.Write([]byte(item.LastReplyAt)) w.Write([]byte(item.LastReplyAt))
w.Write(topics_25)
w.Write([]byte(item.Link))
w.Write(topics_26)
w.Write([]byte(item.Title))
w.Write(topics_27)
if item.ForumName != "" {
w.Write(topics_28)
w.Write([]byte(item.ForumLink))
w.Write(topics_29) w.Write(topics_29)
w.Write([]byte(item.ForumName))
w.Write(topics_30)
}
w.Write(topics_31)
w.Write([]byte(item.Creator.Link))
w.Write(topics_32)
w.Write([]byte(item.Creator.Name))
w.Write(topics_33)
if item.IsClosed {
w.Write(topics_34)
}
if item.Sticky {
w.Write(topics_35)
}
w.Write(topics_36)
if item.Sticky {
w.Write(topics_37)
} else {
if item.IsClosed {
w.Write(topics_38)
}
}
w.Write(topics_39)
if item.LastUser.Avatar != "" {
w.Write(topics_40)
w.Write([]byte(item.LastUser.Avatar))
w.Write(topics_41)
}
w.Write(topics_42)
w.Write([]byte(item.LastUser.Link))
w.Write(topics_43)
w.Write([]byte(item.LastUser.Name))
w.Write(topics_44)
w.Write([]byte(item.LastReplyAt))
w.Write(topics_45)
} }
} else { } else {
w.Write(topics_30) w.Write(topics_46)
if tmpl_topics_vars.CurrentUser.Perms.CreateTopic { if tmpl_topics_vars.CurrentUser.Perms.CreateTopic {
w.Write(topics_31) w.Write(topics_47)
} }
w.Write(topics_32) w.Write(topics_48)
} }
w.Write(topics_33) w.Write(topics_49)
w.Write(footer_0) w.Write(footer_0)
if len(tmpl_topics_vars.Header.Themes) != 0 { if len(tmpl_topics_vars.Header.Themes) != 0 {
for _, item := range tmpl_topics_vars.Header.Themes { for _, item := range tmpl_topics_vars.Header.Themes {

View File

@ -876,14 +876,15 @@ func (c *CTemplateSet) compileBoolsub(varname string, varholder string, template
fmt.Println("in compileBoolsub") fmt.Println("in compileBoolsub")
} }
out, val := c.compileIfVarsub(varname, varholder, templateName, val) out, val := c.compileIfVarsub(varname, varholder, templateName, val)
// TODO: What if it's a pointer or an interface? I *think* we've got pointers handled somewhere, but not interfaces which we don't know the types of at compile time
switch val.Kind() { switch val.Kind() {
case reflect.Int: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
out += " > 0" out += " > 0"
case reflect.Bool: // Do nothing case reflect.Bool: // Do nothing
case reflect.String: case reflect.String:
out += " != \"\"" out += " != \"\""
case reflect.Int64: case reflect.Slice, reflect.Map:
out += " > 0" out = "len(" + out + ") != 0"
default: default:
fmt.Println("Variable Name:", varname) fmt.Println("Variable Name:", varname)
fmt.Println("Variable Holder:", varholder) fmt.Println("Variable Holder:", varholder)

View File

@ -4,25 +4,29 @@
<div class="rowitem"><h1>Create Topic</h1></div> <div class="rowitem"><h1>Create Topic</h1></div>
</div> </div>
<div class="rowblock"> <div class="rowblock">
<form action="/topic/create/submit/" method="post"> <form id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<div class="formrow real_first_child"> <div class="formrow real_first_child">
<div class="formitem formlabel"><a>Board</a></div> <div class="formitem formlabel"><a>Board</a></div>
<div class="formitem"><select name="topic-board"> <div class="formitem"><select form="topic_create_form_form" id="topic_board_input" name="topic-board">
{{range .ItemList}}<option {{if eq .ID $.FID}}selected{{end}} value="{{.ID}}">{{.Name}}</option>{{end}} {{range .ItemList}}<option {{if eq .ID $.FID}}selected{{end}} value="{{.ID}}">{{.Name}}</option>{{end}}
</select></div> </select></div>
</div> </div>
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>Topic Name</a></div> <div class="formitem formlabel"><a>Topic Name</a></div>
<div class="formitem"><input name="topic-name" type="text" placeholder="Topic Name" required /></div> <div class="formitem"><input form="topic_create_form_form" name="topic-name" type="text" placeholder="Topic Name" required /></div>
</div> </div>
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>Content</a></div> <div class="formitem formlabel"><a>Content</a></div>
<div class="formitem"><textarea class="large" name="topic-content" placeholder="Insert content here" required></textarea></div> <div class="formitem"><textarea form="topic_create_form_form" class="large" id="topic_content" name="topic-content" placeholder="Insert content here" required></textarea></div>
</div> </div>
<div class="formrow"> <div class="formrow">
<div class="formitem"><button name="topic-button" class="formbutton form_middle_button">Create Topic</button></div> <button form="topic_create_form_form" name="topic-button" class="formbutton">Create Topic</button>
{{if .CurrentUser.Perms.UploadFiles}}
<input name="quick_topic_upload_files" form="topic_create_form_form" id="quick_topic_upload_files" multiple type="file" style="display: none;" />
<label for="quick_topic_upload_files" class="formbutton add_file_button">Add File</label>{{end}}
<div id="upload_file_dock"></div>
<button class="formbutton close_form">Cancel</button>
</div> </div>
</form>
</div> </div>
</main> </main>
{{template "footer.html" . }} {{template "footer.html" . }}

View File

@ -11,11 +11,37 @@
</div> </div>
{{if ne .CurrentUser.ID 0}} {{if ne .CurrentUser.ID 0}}
{{if .CurrentUser.Perms.CreateTopic}} {{if .CurrentUser.Perms.CreateTopic}}
<div class="opt create_topic_opt" title="Create Topic"><a href="/topics/create/{{.Forum.ID}}"></a></div> <div class="opt create_topic_opt" title="Create Topic"><a class="create_topic_link" href="/topics/create/{{.Forum.ID}}"></a></div>
{{else}}<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>{{end}} {{else}}<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>{{end}}
<div style="clear: both;"></div> <div style="clear: both;"></div>
{{end}} {{end}}
</div> </div>
{{if .CurrentUser.Perms.CreateTopic}}
<div class="rowblock topic_create_form" style="display: none;">
<form id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<input form="topic_create_form_form" id="topic_board_input" name="topic-board" value="{{.Forum.ID}}" type="hidden">
<div class="formrow topic_name_row real_first_child">
<div class="formitem">
<input form="topic_create_form_form" name="topic-name" placeholder="Topic title" required>
</div>
</div>
<div class="formrow topic_content_row">
<div class="formitem">
<textarea form="topic_create_form_form" id="topic_content" name="topic-content" placeholder="Insert post here" required></textarea>
</div>
</div>
<div class="formrow topic_button_row">
<div class="formitem">
<button form="topic_create_form_form" name="topic-button" class="formbutton">Create Topic</button>
{{if .CurrentUser.Perms.UploadFiles}}
<input name="quick_topic_upload_files" form="topic_create_form_form" id="quick_topic_upload_files" multiple type="file" style="display: none;" />
<label for="quick_topic_upload_files" class="formbutton add_file_button">Add File</label>{{end}}
<div id="upload_file_dock"></div>
<button class="formbutton close_form">Cancel</button>
</div>
</div>
</div>
{{end}}
<div id="forum_topic_list" class="rowblock topic_list"> <div id="forum_topic_list" class="rowblock topic_list">
{{range .ItemList}}<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}" style="{{if .Creator.Avatar}}background-image: url({{.Creator.Avatar}});background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;{{end}}"> {{range .ItemList}}<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}" style="{{if .Creator.Avatar}}background-image: url({{.Creator.Avatar}});background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;{{end}}">
<span class="topic_inner_right rowsmall" style="float: right;"> <span class="topic_inner_right rowsmall" style="float: right;">

View File

@ -1,7 +1,7 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}} | {{.Header.Site.Name}}</title>
<link href="/static/{{.Header.ThemeName}}/main.css" rel="stylesheet" type="text/css"> <link href="/static/{{.Header.ThemeName}}/main.css" rel="stylesheet" type="text/css">
{{range .Header.Stylesheets}} {{range .Header.Stylesheets}}
<link href="/static/{{.}}" rel="stylesheet" type="text/css"> <link href="/static/{{.}}" rel="stylesheet" type="text/css">
@ -10,7 +10,10 @@
{{range .Header.Scripts}} {{range .Header.Scripts}}
<script type="text/javascript" src="/static/{{.}}"></script> <script type="text/javascript" src="/static/{{.}}"></script>
{{end}} {{end}}
<script type="text/javascript">var session = "{{.CurrentUser.Session}}";</script> <script type="text/javascript">
var session = "{{.CurrentUser.Session}}";
var siteURL = "{{.Header.Site.URL}}";
</script>
<script type="text/javascript" src="/static/global.js"></script> <script type="text/javascript" src="/static/global.js"></script>
<meta name="viewport" content="width=device-width,initial-scale = 1.0, maximum-scale=1.0,user-scalable=no" /> <meta name="viewport" content="width=device-width,initial-scale = 1.0, maximum-scale=1.0,user-scalable=no" />
</head> </head>

View File

@ -2,10 +2,9 @@
<div class="move_left"> <div class="move_left">
<div class="move_right"> <div class="move_right">
<ul>{{/** Add a menu manager **/}} <ul>{{/** Add a menu manager **/}}
<li class="menu_left menu_overview"><a href="/" rel="home">{{.Header.Site.Name}}</a></li> <li class="menu_left menu_overview"><a href="/" rel="home">{{.Header.Site.ShortName}}</a></li>
<li class="menu_left menu_forums"><a href="/forums/">Forums</a></li> <li class="menu_left menu_forums"><a href="/forums/">Forums</a></li>
<li class="menu_left menu_topics"><a href="/">Topics</a></li> <li class="menu_left menu_topics"><a href="/">Topics</a></li>
<li class="menu_left menu_create_topic"><a href="/topics/create/">Create Topic</a></li>
<li id="general_alerts" class="menu_right menu_alerts"> <li id="general_alerts" class="menu_right menu_alerts">
<div class="alert_bell"></div> <div class="alert_bell"></div>
<div class="alert_counter"></div> <div class="alert_counter"></div>

View File

@ -22,7 +22,7 @@
</div> </div>
<article class="rowblock post_container top_post"> <article class="rowblock post_container top_post">
<div class="rowitem passive editable_parent post_item {{.Topic.ClassName}}" style="{{if .Topic.Avatar}}background-image:url({{.Topic.Avatar}}), url(/static/post-avatar-bg.jpg);background-position: 0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}"> <div class="rowitem passive editable_parent post_item {{.Topic.ClassName}}" style="{{if .Topic.Avatar}}background-image:url({{.Topic.Avatar}}), url(/static/{{.Header.ThemeName}}/post-avatar-bg.jpg);background-position: 0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}">
<p class="hide_on_edit topic_content user_content" style="margin:0;padding:0;">{{.Topic.ContentHTML}}</p> <p class="hide_on_edit topic_content user_content" style="margin:0;padding:0;">{{.Topic.ContentHTML}}</p>
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea> <textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
@ -55,7 +55,7 @@
<span>{{.ActionType}}</span> <span>{{.ActionType}}</span>
</article> </article>
{{else}} {{else}}
<article class="rowitem passive deletable_block editable_parent post_item {{.ClassName}}" style="{{if .Avatar}}background-image:url({{.Avatar}}), url(/static/post-avatar-bg.jpg);background-position: 0px {{if le .ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}"> <article class="rowitem passive deletable_block editable_parent post_item {{.ClassName}}" style="{{if .Avatar}}background-image:url({{.Avatar}}), url(/static/{{$.Header.ThemeName}}/post-avatar-bg.jpg);background-position: 0px {{if le .ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}">
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}} {{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
<p class="editable_block user_content" style="margin:0;padding:0;">{{.ContentHtml}}</p> <p class="editable_block user_content" style="margin:0;padding:0;">{{.ContentHtml}}</p>

View File

@ -2,10 +2,48 @@
<main> <main>
<div class="rowblock rowhead"> <div class="rowblock rowhead">
<div class="rowitem"><h1>Topic List</h1></div> <div class="rowitem topic_list_title{{if ne .CurrentUser.ID 0}} has_opt{{end}}"><h1>All Topics</h1></div>
{{if ne .CurrentUser.ID 0}}
{{if .ForumList}}
<div class="opt create_topic_opt" title="Create Topic"><a class="create_topic_link" href="/topics/create/"></a></div>
{{else}}<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>{{end}}
<div style="clear: both;"></div>
{{end}}
</div> </div>
<div id="topic_list" class="rowblock topic_list" aria-label="The main topic list"> {{if ne .CurrentUser.ID 0}}
{{range .ItemList}}<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}" style="{{if .Creator.Avatar}}background-image: url({{.Creator.Avatar}});background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;{{end}}"> {{if .ForumList}}
<div class="rowblock topic_create_form" style="display: none;">
<form name="topic_create_form_form" id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<div class="formrow topic_board_row real_first_child">
<div class="formitem"><select form="topic_create_form_form" id="topic_board_input" name="topic-board">
{{range .ForumList}}<option {{if eq .ID $.DefaultForum}}selected{{end}} value="{{.ID}}">{{.Name}}</option>{{end}}
</select></div>
</div>
<div class="formrow topic_name_row">
<div class="formitem">
<input form="topic_create_form_form" name="topic-name" placeholder="Topic title" required>
</div>
</div>
<div class="formrow topic_content_row">
<div class="formitem">
<textarea form="topic_create_form_form" id="topic_content" name="topic-content" placeholder="Insert post here" required></textarea>
</div>
</div>
<div class="formrow topic_button_row">
<div class="formitem">
<button form="topic_create_form_form" class="formbutton">Create Topic</button>
{{if .CurrentUser.Perms.UploadFiles}}
<input name="quick_topic_upload_files" form="topic_create_form_form" id="quick_topic_upload_files" multiple type="file" style="display: none;" />
<label for="quick_topic_upload_files" class="formbutton add_file_button">Add File</label>{{end}}
<div id="upload_file_dock"></div>
<button class="formbutton close_form">Cancel</button>
</div>
</div>
</div>
{{end}}
{{end}}
<div id="topic_list" class="rowblock topic_list" aria-label="A list containing topics from every forum">
{{range .TopicList}}<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}" style="{{if .Creator.Avatar}}background-image: url({{.Creator.Avatar}});background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;{{end}}">
<span class="topic_inner_right rowsmall" style="float: right;"> <span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">{{.PostCount}} replies</span><br /> <span class="replyCount">{{.PostCount}} replies</span><br />
<span class="lastReplyAt">{{.LastReplyAt}}</span> <span class="lastReplyAt">{{.LastReplyAt}}</span>

View File

@ -139,6 +139,19 @@ func initThemes() error {
theme.Active = false // Set this to false, just in case someone explicitly overrode this value in the JSON file theme.Active = false // Set this to false, just in case someone explicitly overrode this value in the JSON file
// TODO: Let the theme specify where it's resources are via the JSON file?
// TODO: Let the theme inherit CSS from another theme?
// ? - This might not be too helpful, as it only searches for /public/ and not if /public/ is empty. Still, it might help some people with a slightly less cryptic error
_, err = os.Stat("./themes/" + theme.Name + "/public/")
if err != nil {
if os.IsNotExist(err) {
return errors.New("We couldn't find this theme's resources. E.g. the /public/ folder.")
} else {
log.Print("We weren't able to access this theme's resources due to a permissions issue or some other problem")
return err
}
}
if theme.FullImage != "" { if theme.FullImage != "" {
if dev.DebugMode { if dev.DebugMode {
log.Print("Adding theme image") log.Print("Adding theme image")

View File

@ -796,7 +796,7 @@ blockquote p {
} }
@media(max-width: 620px) { @media(max-width: 620px) {
.menu_create_topic, .menu_overview, .hide_on_mobile { display: none; } .menu_overview, .hide_on_mobile { display: none; }
} }
/* This one is specifically for small mobiles.. */ /* This one is specifically for small mobiles.. */

View File

@ -807,7 +807,7 @@ blockquote p {
} }
@media(max-width: 620px) { @media(max-width: 620px) {
.menu_create_topic, .menu_overview, .hide_on_mobile { display: none; } .menu_overview, .hide_on_mobile { display: none; }
} }
/* This one is specifically for small mobiles.. */ /* This one is specifically for small mobiles.. */

View File

9
themes/cosora/theme.json Normal file
View File

@ -0,0 +1,9 @@
{
"Name": "cosora",
"FriendlyName": "Cosora",
"Version": "0.0.1",
"Creator": "Azareal",
"URL": "github.com/Azareal/Gosora",
"HideFromThemes":true,
"Tag": "WIP"
}

View File

@ -1,10 +1,21 @@
/* Patch for Edge, until they fix emojis in arial x.x */ /* Patch for Edge, until they fix emojis in arial x.x */
@supports (-ms-ime-align:auto) { .user_content { font-family: Segoe UI Emoji, arial; } } @supports (-ms-ime-align:auto) { .user_content { font-family: Segoe UI Emoji, arial; } }
:root {
--main-block-color: rgb(61,61,61);
--main-text-color: white;
--dim-text-color: rgb(205,205,205);
--main-background-color: #222222;
--inner-background-color: #333333;
--input-background-color: #444444;
--input-border-color: #555555;
--input-text-color: #999999;
}
body { body {
font-family: arial; font-family: arial;
color: white; color: var(--main-text-color);
background-color: #222222; background-color: var(--main-background-color);
margin: 0; margin: 0;
} }
p::selection, span::selection, a::selection { p::selection, span::selection, a::selection {
@ -17,15 +28,15 @@ p::selection, span::selection, a::selection {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
width: 70%; width: 70%;
background-color: #333333; background-color: var(--inner-background-color);
position: relative; position: relative;
top: -2px; top: -2px;
} }
ul { ul {
list-style-type: none; list-style-type: none;
background-color: rgb(61,61,61); background-color: var(--main-block-color);
border-bottom: 1px solid #222222; border-bottom: 1px solid var(--main-background-color);
padding-left: 15%; padding-left: 15%;
padding-right: 15%; padding-right: 15%;
margin: 0; margin: 0;
@ -89,7 +100,6 @@ li {
.menu_alerts .alertList { .menu_alerts .alertList {
display: none; display: none;
} }
.selectedAlert .alertList { .selectedAlert .alertList {
display: block; display: block;
position: absolute; position: absolute;
@ -99,7 +109,7 @@ li {
z-index: 50; z-index: 50;
right: 15%; right: 15%;
font-size: 13px; font-size: 13px;
background-color: #333333; background-color: var(--inner-background-color);
} }
.alertItem { .alertItem {
@ -109,19 +119,19 @@ li {
height: 40px; height: 40px;
background-size: 48px; background-size: 48px;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: rgb(61,61,61); background-color: var(--main-block-color);
padding-left: 56px; padding-left: 56px;
padding-top: 8px; padding-top: 8px;
} }
a { a {
text-decoration: none; text-decoration: none;
color: white; color: var(--main-text-color);
} }
.alert { .alert {
padding-bottom: 12px; padding-bottom: 12px;
background-color: rgb(61,61,61); background-color: var(--main-block-color);
padding: 12px; padding: 12px;
display: block; display: block;
} }
@ -147,7 +157,7 @@ a {
.rowitem, .formitem { .rowitem, .formitem {
padding-bottom: 12px; padding-bottom: 12px;
background-color: rgb(61,61,61); background-color: var(--main-block-color);
margin-top: 8px; margin-top: 8px;
padding: 12px; padding: 12px;
} }
@ -179,7 +189,7 @@ a {
.colline { .colline {
font-size: 14px; font-size: 14px;
background-color: rgb(61,61,61); background-color: var(--main-block-color);
margin-top: 5px; margin-top: 5px;
padding: 10px; padding: 10px;
} }
@ -218,7 +228,7 @@ a {
.user_tag { .user_tag {
float: right; float: right;
color: rgb(205,205,205); color: var(--dim-text-color);
} }
.real_username { .real_username {
@ -234,7 +244,7 @@ a {
.mod_button button { .mod_button button {
border: none; border: none;
background: none; background: none;
color: white; color: var(--main-text-color);
font-size: 12px; font-size: 12px;
padding: 0; padding: 0;
} }
@ -292,7 +302,7 @@ a {
} }
.level_label, .level { .level_label, .level {
color: rgb(205,205,205); color: var(--dim-text-color);
float: right; float: right;
} }
.level { .level {
@ -310,17 +320,15 @@ a {
} }
textarea { textarea {
background-color: #444444; background-color: var(--input-background-color);
border-color: #555555; border-color: var(--input-border-color);
color: #999999; color: var(--input-text-color);
width: calc(100% - 15px); width: calc(100% - 15px);
min-height: 80px; min-height: 80px;
} }
textarea:focus, input:focus, select:focus, button:focus {
textarea:focus, input:focus, select:focus {
outline-color: rgb(95,95,95); outline-color: rgb(95,95,95);
} }
textarea.large { textarea.large {
min-height: 120px; min-height: 120px;
margin-top: 1px; margin-top: 1px;
@ -328,15 +336,10 @@ textarea.large {
display: block; display: block;
} }
.topic_reply_form textarea {
width: calc(100% - 5px);
min-height: 80px;
}
.formitem button, .formbutton { .formitem button, .formbutton {
background-color: #444444; background-color: var(--input-background-color);
border: 1px solid #555555; border: 1px solid var(--input-border-color);
color: #999999; color: var(--input-text-color);
padding: 7px; padding: 7px;
padding-bottom: 6px; padding-bottom: 6px;
font-size: 13px; font-size: 13px;
@ -402,7 +405,7 @@ textarea.large {
flex-direction: row; flex-direction: row;
} }
.pageitem { .pageitem {
background-color: rgb(61,61,61); background-color: var(--main-block-color);
padding: 10px; padding: 10px;
margin-right: 4px; margin-right: 4px;
font-size: 13px; font-size: 13px;
@ -423,9 +426,9 @@ textarea.large {
} }
.formitem input { .formitem input {
background-color: #444444; background-color: var(--input-background-color);
border: 1px solid #555555; border: 1px solid var(--input-border-color);
color: #999999; color: var(--input-text-color);
padding-bottom: 6px; padding-bottom: 6px;
font-size: 13px; font-size: 13px;
@ -434,9 +437,9 @@ textarea.large {
} }
.formitem select { .formitem select {
background-color: #444444; background-color: var(--input-background-color);
border: 1px solid #555555; border: 1px solid var(--input-border-color);
color: #999999; color: var(--input-text-color);
font-size: 13px; font-size: 13px;
padding: 4px; padding: 4px;
} }
@ -458,8 +461,50 @@ input, select, textarea {
margin-top: 5px; margin-top: 5px;
} }
.topic_create_form .topic_board_row .formitem, .topic_create_form .topic_name_row .formitem {
padding-bottom: 5px;
}
.topic_create_form input, .topic_create_form select {
padding: 7px;
font-family: monospace;
}
.topic_create_form select {
padding: 6px;
}
.topic_create_form input {
width: calc(100% - 14px);
}
.topic_create_form textarea, .topic_reply_form textarea {
width: calc(100% - 5px);
min-height: 80px;
}
.topic_create_form textarea {
padding: 7px;
width: calc(100% - 14px);
}
.topic_button_row .formitem {
display: flex;
}
.topic_create_form .add_file_button {
margin-left: 8px;
}
.topic_create_form .close_form {
margin-left: auto;
}
.topic_create_form .upload_file_dock {
display: flex;
}
.topic_create_form .uploadItem {
display: inline-block;
margin-left: 8px;
background-size: 25px 30px;
background-repeat: no-repeat;
padding-left: 30px;
}
.footer { .footer {
background-color: rgb(61,61,61); background-color: var(--main-block-color);
margin-top: 5px; margin-top: 5px;
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
@ -469,9 +514,9 @@ input, select, textarea {
height: 25px; height: 25px;
} }
.footer select { .footer select {
background-color: #444444; background-color: var(--input-background-color);
border: 1px solid #555555; border: 1px solid var(--input-border-color);
color: #999999; color: var(--input-text-color);
font-size: 13px; font-size: 13px;
padding: 4px; padding: 4px;
} }
@ -505,7 +550,7 @@ input, select, textarea {
height: 30.4px; height: 30.4px;
padding-left: 5px; padding-left: 5px;
width: 100%; width: 100%;
background-color: rgb(61,61,61); background-color: var(--main-block-color);
padding-top: 11px; padding-top: 11px;
} }
.opt a { .opt a {
@ -545,9 +590,9 @@ input, select, textarea {
.topic_name_input { .topic_name_input {
width: 100%; width: 100%;
margin-right: 10px; margin-right: 10px;
background-color: #444444; background-color: var(--input-background-color);
border: 1px solid #555555; border: 1px solid var(--input-border-color);
color: #999999; color: var(--input-text-color);
padding-bottom: 6px; padding-bottom: 6px;
font-size: 13px; font-size: 13px;
padding: 5px; padding: 5px;
@ -561,6 +606,13 @@ input, select, textarea {
top: -5px; top: -5px;
} }
.postImage {
max-width: 100%;
max-height: 200px;/*300px;*/
background-color: rgb(71,71,71);
padding: 10px;
}
/* Profiles */ /* Profiles */
#profile_left_lane { #profile_left_lane {
width: 220px; width: 220px;
@ -608,9 +660,9 @@ input, select, textarea {
} }
.ip_search_block input { .ip_search_block input {
background-color: #444444; background-color: var(--input-background-color);
border: 1px solid #555555; border: 1px solid var(--input-border-color);
color: #999999; color: var(--input-text-color);
margin-top: -3px; margin-top: -3px;
margin-bottom: -3px; margin-bottom: -3px;
padding: 4px; padding: 4px;
@ -640,7 +692,7 @@ input, select, textarea {
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
font-size: 13px; font-size: 13px;
background-color: rgb(61,61,61); background-color: var(--main-block-color);
} }
#panel_dashboard_right .colstack_head .rowitem { #panel_dashboard_right .colstack_head .rowitem {
@ -808,7 +860,7 @@ input, select, textarea {
} }
@media(max-width: 470px) { @media(max-width: 470px) {
.menu_create_topic, .like_count_label, .like_count { .like_count_label, .like_count {
display: none; display: none;
} }
.post_item { .post_item {

View File

@ -4,6 +4,5 @@
"Version": "0.0.1", "Version": "0.0.1",
"Creator": "Azareal", "Creator": "Azareal",
"FullImage": "shadow.png", "FullImage": "shadow.png",
"URL": "github.com/Azareal/Gosora", "URL": "github.com/Azareal/Gosora"
"Tag": "WIP"
} }

View File

@ -4,6 +4,11 @@
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
} }
/* TODO: Run a find and replacer in Gosora to support browsers without CSS Variable support */
:root {
--main-border-color: hsl(0,0%,80%);
}
body { body {
font-family: arial; font-family: arial;
padding-bottom: 8px; padding-bottom: 8px;
@ -23,11 +28,11 @@ ul {
padding-right: 0px; padding-right: 0px;
height: 36px; height: 36px;
list-style-type: none; list-style-type: none;
border: 1px solid hsl(0,0%,80%); border: 1px solid var(--main-border-color);
background: hsl(0, 0%, 97%); background: hsl(0, 0%, 97%);
margin-bottom: 12px; margin-bottom: 12px;
margin-top: 0px; margin-top: 0px;
border-bottom: 1.5px inset hsl(0,0%,80%); border-bottom: 1.5px inset var(--main-border-color);
margin-left: -8px; margin-left: -8px;
margin-right: -8px; margin-right: -8px;
} }
@ -48,14 +53,14 @@ li a {
} }
.menu_left { .menu_left {
float: left; float: left;
border-right: 1px solid hsl(0,0%,80%); border-right: 1px solid var(--main-border-color);
border-bottom: 1.5px inset hsl(0,0%,80%); border-bottom: 1.5px inset var(--main-border-color);
padding-right: 10px; padding-right: 10px;
background: hsl(0, 0%, 98%); background: hsl(0, 0%, 98%);
} }
.menu_right { .menu_right {
float: right; float: right;
border-left: 1px solid hsl(0,0%,80%); border-left: 1px solid var(--main-border-color);
padding-right: 10px; padding-right: 10px;
} }
@ -109,10 +114,10 @@ li a {
line-height: 16px; line-height: 16px;
width: 300px; width: 300px;
right: calc(5% + 7px); right: calc(5% + 7px);
border-top: 1px solid hsl(0,0%,80%); border-top: 1px solid var(--main-border-color);
border-left: 1px solid hsl(0,0%,80%); border-left: 1px solid var(--main-border-color);
border-right: 1px solid hsl(0,0%,80%); border-right: 1px solid var(--main-border-color);
border-bottom: 1px solid hsl(0,0%,80%); border-bottom: 1px solid var(--main-border-color);
margin-bottom: 10px; margin-bottom: 10px;
} }
.alertItem { .alertItem {
@ -150,7 +155,7 @@ li a {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
background: hsl(0, 0%, 98%); background: hsl(0, 0%, 98%);
border: 1px solid hsl(0,0%,80%); border: 1px solid var(--main-border-color);
border-top: none; border-top: none;
} }
@ -161,14 +166,14 @@ li a {
/* Explict declaring each border direction to fix a bug in Chrome where an override to .rowhead was also applying to .rowblock in some cases */ /* Explict declaring each border direction to fix a bug in Chrome where an override to .rowhead was also applying to .rowblock in some cases */
.rowblock { .rowblock {
border: 1px solid hsl(0,0%,80%); border: 1px solid var(--main-border-color);
width: 100%; width: 100%;
padding: 0px; padding: 0px;
padding-top: 0px; padding-top: 0px;
border-top: 1px solid hsl(0,0%,80%); border-top: 1px solid var(--main-border-color);
border-left: 1px solid hsl(0,0%,80%); border-left: 1px solid var(--main-border-color);
border-right: 1px solid hsl(0,0%,80%); border-right: 1px solid var(--main-border-color);
border-bottom: 1.5px inset hsl(0,0%,80%); border-bottom: 1.5px inset var(--main-border-color);
} }
.rowblock:empty { .rowblock:empty {
display: none; display: none;
@ -211,7 +216,7 @@ li a {
width: calc(70% - 13px); width: calc(70% - 13px);
} }
.colstack_item { .colstack_item {
border: 1px solid hsl(0,0%,80%); border: 1px solid var(--main-border-color);
padding: 0px; padding: 0px;
padding-top: 0px; padding-top: 0px;
width: 100%; width: 100%;
@ -230,7 +235,7 @@ li a {
margin-top: 2px; margin-top: 2px;
} }
.grid_item { .grid_item {
border: 1px solid hsl(0,0%,80%); border: 1px solid var(--main-border-color);
word-wrap: break-word; word-wrap: break-word;
background-color: white; background-color: white;
width: 100%; width: 100%;
@ -291,6 +296,34 @@ li a {
} }
.opthead { display: none; } .opthead { display: none; }
.rowitem.has_opt {
float: left;
width: calc(100% - 50px);
border-right: 1px solid #ccc;
border-bottom: none;
}
.opt {
float: left;
font-size: 32px;
height: 100%;
background-color: hsl(0, 0%, 99%);
width: 50px;
text-align: center;
}
.create_topic_opt a.create_topic_link:before {
content: '🖊︎';
}
.create_topic_opt, .create_topic_opt a {
color: rgb(120,120,120);
text-decoration: none;
}
.locked_opt {
color: rgb(80,80,80);
}
.locked_opt:before {
content: '🔒︎';
}
.datarow { .datarow {
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
@ -310,7 +343,7 @@ li a {
clear: both; clear: both;
} }
.formrow:not(:last-child) { .formrow:not(:last-child) {
border-bottom: 1px dotted hsl(0,0%,80%); border-bottom: 1px dotted var(--main-border-color);
} }
.formitem { .formitem {
@ -321,7 +354,7 @@ li a {
font-weight: normal; font-weight: normal;
} }
.formitem:not(:last-child) { .formitem:not(:last-child) {
border-right: 1px dotted hsl(0,0%,80%); border-right: 1px dotted var(--main-border-color);
} }
.formitem.invisible_border { .formitem.invisible_border {
border: none; border: none;
@ -346,18 +379,18 @@ li a {
padding-bottom: 12px;/*16px;*/ padding-bottom: 12px;/*16px;*/
/*padding-left: 15px;*/ /*padding-left: 15px;*/
} }
.formbutton, button {
background: white;
border: 1px solid #8e8e8e;
}
.formbutton { .formbutton {
padding: 7px; padding: 7px;
display: block; display: block;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
font-size: 15px; font-size: 15px;
border-color: hsl(0,0%,80%); border-color: var(--main-border-color);
}
button {
background: white;
border: 1px solid #8e8e8e;
} }
/* Topics */ /* Topics */
@ -386,6 +419,39 @@ button {
} }
} }
.postImage {
max-width: 100%;
max-height: 200px;
background-color: white;
padding: 10px;
}
.topic_create_form .topic_button_row .formitem {
display: flex;
}
.topic_create_form .formbutton:first-child {
margin-left: 0px;
margin-right: 5px;
}
.topic_create_form .formbutton:not(:first-child) {
margin-left: 0px;
margin-right: 5px;
}
.topic_create_form .formbutton:last-child {
margin-left: auto;
}
.topic_create_form .upload_file_dock {
display: flex;
}
.topic_create_form .uploadItem {
display: inline-block;
margin-left: 8px;
margin-right: 8px;
background-size: 25px 35px;
background-repeat: no-repeat;
padding-left: 30px;
}
.topic_status_sticky { .topic_status_sticky {
display: none; display: none;
} }
@ -467,7 +533,7 @@ button {
color: #505050; /* 80,80,80 */ color: #505050; /* 80,80,80 */
background-color: #FFFFFF; background-color: #FFFFFF;
border-style: solid; border-style: solid;
border-color: hsl(0,0%,80%); border-color: var(--main-border-color);
border-width: 1px; border-width: 1px;
font-size: 15px; font-size: 15px;
} }
@ -499,7 +565,7 @@ button.username {
display: block; display: block;
padding: 5px; padding: 5px;
margin-bottom: 10px; margin-bottom: 10px;
border: 1px solid hsl(0,0%,80%); border: 1px solid var(--main-border-color);
background-color: white; background-color: white;
} }
.alert_success { .alert_success {
@ -580,10 +646,10 @@ button.username {
} }
#profile_left_lane { #profile_left_lane {
border: 1px solid hsl(0,0%,80%); border: 1px solid var(--main-border-color);
width: 220px; width: 220px;
margin-bottom: 10px; margin-bottom: 10px;
border-bottom: 1.5px inset hsl(0,0%,80%); border-bottom: 1.5px inset var(--main-border-color);
} }
#profile_left_lane .avatarRow { #profile_left_lane .avatarRow {
overflow: hidden; overflow: hidden;
@ -612,7 +678,7 @@ button.username {
margin-top: 20px; margin-top: 20px;
} }
#profile_right_lane .topic_reply_form { #profile_right_lane .topic_reply_form {
border-bottom: 1.5px inset hsl(0,0%,80%); border-bottom: 1.5px inset var(--main-border-color);
} }
.simple { .simple {
@ -652,7 +718,7 @@ button.username {
position: sticky; position: sticky;
top: 4px; top: 4px;
/*box-shadow: 0 1px 2px rgba(0,0,0,.1);*/ /*box-shadow: 0 1px 2px rgba(0,0,0,.1);*/
border-bottom: 1.5px inset hsl(0,0%,80%); border-bottom: 1.5px inset var(--main-border-color);
} }
.userinfo .avatar_item { .userinfo .avatar_item {
background-repeat: no-repeat, repeat-y; background-repeat: no-repeat, repeat-y;
@ -672,10 +738,12 @@ button.username {
margin-bottom: 0; margin-bottom: 0;
margin-right: 3px; margin-right: 3px;
/*box-shadow: 0 1px 2px rgba(0,0,0,.1);*/ /*box-shadow: 0 1px 2px rgba(0,0,0,.1);*/
border-bottom: 1.5px inset hsl(0,0%,80%); border-bottom: 1.5px inset var(--main-border-color);
} }
.action_item .userinfo { display: none; } .action_item .userinfo {
display: none;
}
.action_item .content_container { .action_item .content_container {
min-height: auto; min-height: auto;
padding: 15px; padding: 15px;
@ -689,7 +757,7 @@ button.username {
border-width: 1px; border-width: 1px;
background-color: #FFFFFF; background-color: #FFFFFF;
border-style: solid; border-style: solid;
border-color: hsl(0,0%,80%); border-color: var(--main-border-color);
padding: 0px; padding: 0px;
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
@ -720,7 +788,7 @@ button.username {
border-bottom: none; border-bottom: none;
} }
.topic_reply_form { .topic_reply_form {
border-top: 1px solid hsl(0,0%,80%); border-top: 1px solid var(--main-border-color);
} }
.post_container .post_item { .post_container .post_item {
background-color: #eaeaea; background-color: #eaeaea;
@ -739,7 +807,7 @@ button.username {
} }
.footer { .footer {
border: 1px solid hsl(0,0%,80%); border: 1px solid var(--main-border-color);
margin-top: 12px; margin-top: 12px;
clear: both; clear: both;
height: 40px; height: 40px;
@ -747,7 +815,7 @@ button.username {
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
background-color: white; background-color: white;
border-bottom: 1.5px inset hsl(0,0%,80%); border-bottom: 1.5px inset var(--main-border-color);
} }
.footer select { .footer select {
padding: 2px; padding: 2px;
@ -767,7 +835,7 @@ button.username {
margin-top: -5px; margin-top: -5px;
} }
.pageitem { .pageitem {
border: 1px solid hsl(0,0%,80%); border: 1px solid var(--main-border-color);
background-color: white; background-color: white;
padding: 5px; padding: 5px;
margin-right: 5px; margin-right: 5px;

View File

@ -13,6 +13,12 @@
} }
@media(max-width: 950px) { @media(max-width: 950px) {
.sidebar {
display: none;
}
}
@media (max-width: 700px) {
li { li {
height: 29px; height: 29px;
font-size: 15px; font-size: 15px;
@ -20,11 +26,20 @@
padding-top: 6px; padding-top: 6px;
padding-bottom: 6px; padding-bottom: 6px;
} }
li, li a {
font-size: 15px;
}
ul { ul {
height: 30px; height: 30px;
margin-top: 8px; margin-top: 8px;
margin-left: -1px;
margin-right: -1px;
border-bottom: 1px solid hsl(0,0%,80%);
}
.menu_left, .menu_right {
padding-right: 9px;
border-bottom: 1px solid hsl(0,0%,80%);
} }
.menu_left, .menu_right { padding-right: 9px; }
.menu_alerts { .menu_alerts {
padding-left: 7px; padding-left: 7px;
padding-right: 7px; padding-right: 7px;
@ -39,9 +54,16 @@
height: 100% !important; height: 100% !important;
overflow-x: hidden; overflow-x: hidden;
} }
.container { width: auto; } .container {
.sidebar { display: none; } width: auto;
.selectedAlert .alertList { top: 37px; right: 4px; } margin-left: 5px;
margin-right: 5px;
margin-top: 5px;
}
.selectedAlert .alertList {
top: 37px;
right: 4px;
}
} }
@media (max-width: 680px) { @media (max-width: 680px) {
@ -54,7 +76,7 @@
li a { font-size: 14px; } li a { font-size: 14px; }
ul { height: 26px; } ul { height: 26px; }
.menu_left, .menu_right { padding-right: 7px; } .menu_left, .menu_right { padding-right: 7px; }
.menu_create_topic, .hide_on_mobile { display: none; } .hide_on_mobile { display: none; }
.menu_alerts { .menu_alerts {
padding-left: 4px; padding-left: 4px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 301 KiB

View File

@ -588,7 +588,6 @@ button.username {
ul { height: 26px; } ul { height: 26px; }
.menu_left { padding-right: 5px; padding-top: 1px; } .menu_left { padding-right: 5px; padding-top: 1px; }
.menu_right { padding-right: 5px; } .menu_right { padding-right: 5px; }
.menu_create_topic { display: none;}
.menu_alerts { .menu_alerts {
padding-left: 4px; padding-left: 4px;
@ -596,8 +595,13 @@ button.username {
font-size: 16px; font-size: 16px;
padding-top: 1px; padding-top: 1px;
} }
.menu_alerts .alert_counter { top: -23px; left: 8px; } .menu_alerts .alert_counter {
.selectedAlert .alertList { top: 33px; } top: -23px;
left: 8px;
}
.selectedAlert .alertList {
top: 33px;
}
.hide_on_mobile { display: none; } .hide_on_mobile { display: none; }
.prev_button, .next_button { top: auto; bottom: 5px; } .prev_button, .next_button { top: auto; bottom: 5px; }

View File

@ -17,7 +17,7 @@ ul {
padding-right: 0px; padding-right: 0px;
height: 36px; height: 36px;
list-style-type: none; list-style-type: none;
border: 1px solid #ccc; border: 1px solid hsl(0, 0%, 80%);
background-color: rgb(252,252,252); background-color: rgb(252,252,252);
margin-bottom: 12px; margin-bottom: 12px;
} }
@ -27,7 +27,7 @@ li {
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
background: white; background: white;
border-bottom: 1px solid #ccc; border-bottom: 1px solid hsl(0, 0%, 80%);
} }
li:hover { background: rgb(252,252,252); } li:hover { background: rgb(252,252,252); }
li a { li a {
@ -38,12 +38,12 @@ li a {
} }
.menu_left { .menu_left {
float: left; float: left;
border-right: 1px solid #ccc; border-right: 1px solid hsl(0, 0%, 80%);
padding-right: 10px; padding-right: 10px;
} }
.menu_right { .menu_right {
float: right; float: right;
border-left: 1px solid #ccc; border-left: 1px solid hsl(0, 0%, 80%);
padding-right: 10px; padding-right: 10px;
} }
.menu_overview { .menu_overview {
@ -105,10 +105,10 @@ li a {
line-height: 16px; line-height: 16px;
width: 300px; width: 300px;
right: calc(5% + 7px); right: calc(5% + 7px);
border-top: 1px solid #ccc; border-top: 1px solid hsl(0, 0%, 80%);
border-left: 1px solid #ccc; border-left: 1px solid hsl(0, 0%, 80%);
border-right: 1px solid #ccc; border-right: 1px solid hsl(0, 0%, 80%);
border-bottom: 1px solid #ccc; border-bottom: 1px solid hsl(0, 0%, 80%);
margin-bottom: 10px; margin-bottom: 10px;
} }
.alertItem { .alertItem {
@ -150,7 +150,7 @@ li a {
} }
.rowblock { .rowblock {
border: 1px solid #ccc; border: 1px solid hsl(0, 0%, 80%);
width: 100%; width: 100%;
padding: 0px; padding: 0px;
padding-top: 0px; padding-top: 0px;
@ -159,14 +159,14 @@ li a {
display: none; display: none;
} }
.rowmenu { .rowmenu {
border: 1px solid #ccc; border: 1px solid hsl(0, 0%, 80%);
} }
.rowsmall { .rowsmall {
font-size: 12px; font-size: 12px;
} }
/*.colblock_left { /*.colblock_left {
border: 1px solid #ccc; border: 1px solid hsl(0, 0%, 80%);
padding: 0px; padding: 0px;
padding-top: 0px; padding-top: 0px;
width: 30%; width: 30%;
@ -174,7 +174,7 @@ li a {
margin-right: 8px; margin-right: 8px;
} }
.colblock_right { .colblock_right {
border: 1px solid #ccc; border: 1px solid hsl(0, 0%, 80%);
padding: 0px; padding: 0px;
padding-top: 0px; padding-top: 0px;
width: 65%; width: 65%;
@ -195,7 +195,7 @@ li a {
width: calc(70% - 15px); width: calc(70% - 15px);
} }
.colstack_item { .colstack_item {
border: 1px solid #ccc; border: 1px solid hsl(0, 0%, 80%);
padding: 0px; padding: 0px;
padding-top: 0px; padding-top: 0px;
width: 100%; width: 100%;
@ -215,7 +215,7 @@ li a {
margin-top: 2px; margin-top: 2px;
} }
.grid_item { .grid_item {
border: 1px solid #ccc; border: 1px solid hsl(0, 0%, 80%);
word-wrap: break-word; word-wrap: break-word;
background-color: white; background-color: white;
width: 100%; width: 100%;
@ -260,7 +260,7 @@ li a {
background-color: white; background-color: white;
} }
/*.rowitem:not(.passive) { font-size: 17px; }*/ /*.rowitem:not(.passive) { font-size: 17px; }*/
.rowitem:not(:last-child) { border-bottom: 1px dotted #ccc; } .rowitem:not(:last-child) { border-bottom: 1px dotted hsl(0, 0%, 80%); }
.rowitem a { .rowitem a {
text-decoration: none; text-decoration: none;
color: black; color: black;
@ -272,7 +272,7 @@ li a {
.rowitem.has_opt { .rowitem.has_opt {
float: left; float: left;
width: calc(100% - 50px); width: calc(100% - 50px);
border-right: 1px solid #ccc; border-right: 1px solid hsl(0, 0%, 80%);
border-bottom: none; border-bottom: none;
} }
.opt { .opt {
@ -283,8 +283,8 @@ li a {
width: 50px; width: 50px;
text-align: center; text-align: center;
} }
.create_topic_opt a:before { .create_topic_opt a.create_topic_link:before {
content '🖊︎'; content: '🖊︎';
} }
.create_topic_opt, .create_topic_opt a { .create_topic_opt, .create_topic_opt a {
color: rgb(120,120,120); color: rgb(120,120,120);
@ -321,7 +321,7 @@ li a {
display: table; display: table;
} }
.formrow:after { clear: both; } .formrow:after { clear: both; }
.formrow:not(:last-child) { border-bottom: 1px dotted #ccc; } .formrow:not(:last-child) { border-bottom: 1px dotted hsl(0, 0%, 80%); }
.formitem { .formitem {
float: left; float: left;
@ -330,8 +330,12 @@ li a {
/*font-size: 17px;*/ /*font-size: 17px;*/
font-weight: normal; font-weight: normal;
} }
.formitem:not(:last-child) { border-right: 1px dotted #ccc; } .formitem:not(:last-child) {
.formitem.invisible_border { border: none; } border-right: 1px dotted hsl(0, 0%, 80%);
}
.formitem.invisible_border {
border: none;
}
/* Mostly for textareas */ /* Mostly for textareas */
.formitem:only-child { width: 100%; } .formitem:only-child { width: 100%; }
@ -344,24 +348,26 @@ li a {
margin: 0 auto; margin: 0 auto;
float: none; float: none;
} }
.formitem:not(:only-child) input, .formitem:not(:only-child) select { padding: 3px;/*5px;*/ } .formitem:not(:only-child) input, .formitem:not(:only-child) select {
padding: 3px;/*5px;*/
}
.formitem:not(:only-child).formlabel { .formitem:not(:only-child).formlabel {
padding-top: 15px;/*18px;*/ padding-top: 15px;/*18px;*/
padding-bottom: 12px;/*16px;*/ padding-bottom: 12px;/*16px;*/
/*padding-left: 15px;*/ /*padding-left: 15px;*/
} }
.formbutton, button {
background: white;
border: 1px solid #8e8e8e;
}
.formbutton { .formbutton {
padding: 7px; padding: 7px;
display: block; display: block;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
font-size: 15px; font-size: 15px;
border-color: #ccc; border-color: hsl(0, 0%, 80%);
}
button {
background: white;
border: 1px solid #8e8e8e;
} }
/* Topics */ /* Topics */
@ -390,6 +396,39 @@ button {
} }
} }
.postImage {
max-width: 100%;
max-height: 200px;
background-color: white;
padding: 10px;
}
.topic_create_form .topic_button_row .formitem {
display: flex;
}
.topic_create_form .formbutton:first-child {
margin-left: 0px;
margin-right: 5px;
}
.topic_create_form .formbutton:not(:first-child) {
margin-left: 0px;
margin-right: 5px;
}
.topic_create_form .formbutton:last-child {
margin-left: auto;
}
.topic_create_form .upload_file_dock {
display: flex;
}
.topic_create_form .uploadItem {
display: inline-block;
margin-left: 8px;
margin-right: 8px;
background-size: 25px 35px;
background-repeat: no-repeat;
padding-left: 30px;
}
.username, .panel_tag { .username, .panel_tag {
text-transform: none; text-transform: none;
margin-left: 0px; margin-left: 0px;
@ -400,7 +439,7 @@ button {
color: #505050; /* 80,80,80 */ color: #505050; /* 80,80,80 */
background-color: #FFFFFF; background-color: #FFFFFF;
border-style: solid; border-style: solid;
border-color: #ccc; border-color: hsl(0, 0%, 80%);
border-width: 1px; border-width: 1px;
font-size: 15px; font-size: 15px;
} }
@ -553,7 +592,7 @@ button.username {
} }
.postQuote { .postQuote {
border: 1px solid #ccc; border: 1px solid hsl(0, 0%, 80%);
background: white; background: white;
padding: 5px; padding: 5px;
margin: 0px; margin: 0px;
@ -578,7 +617,7 @@ button.username {
display: block; display: block;
padding: 5px; padding: 5px;
margin-bottom: 10px; margin-bottom: 10px;
border: 1px solid #ccc; border: 1px solid hsl(0, 0%, 80%);
} }
.alert_success { .alert_success {
display: block; display: block;
@ -616,8 +655,12 @@ button.username {
text-decoration: none; text-decoration: none;
color: #505050; color: #505050;
} }
.prev_button { left: 14px; } .prev_button {
.next_button { right: 14px; } left: 14px;
}
.next_button {
right: 14px;
}
.head_tag_upshift { .head_tag_upshift {
float: right; float: right;
position: relative; position: relative;
@ -625,7 +668,7 @@ button.username {
} }
.footer { .footer {
border: 1px solid #ccc; border: 1px solid hsl(0, 0%, 80%);
margin-top: 12px; margin-top: 12px;
clear: both; clear: both;
height: 40px; height: 40px;
@ -697,7 +740,7 @@ button.username {
padding: 5px; padding: 5px;
margin-right: 5px; margin-right: 5px;
padding-bottom: 4px; padding-bottom: 4px;
border: 1px solid #ccc; border: 1px solid hsl(0, 0%, 80%);
} }
.pageitem a { .pageitem a {
color: black; color: black;

View File

@ -52,7 +52,6 @@
li a { font-size: 14px; } li a { font-size: 14px; }
ul { height: 26px; } ul { height: 26px; }
.menu_left, .menu_right { padding-right: 7px; } .menu_left, .menu_right { padding-right: 7px; }
.menu_create_topic { display: none; }
.menu_alerts { .menu_alerts {
padding-left: 4px; padding-left: 4px;

View File

@ -146,7 +146,7 @@ func (topic *Topic) RemoveLike(uid int) error {
func (topic *Topic) Update(name string, content string) error { func (topic *Topic) Update(name string, content string) error {
content = preparseMessage(content) content = preparseMessage(content)
parsed_content := parseMessage(html.EscapeString(content)) parsed_content := parseMessage(html.EscapeString(content), topic.ParentID, "forums")
_, err := editTopicStmt.Exec(name, content, parsed_content, topic.ID) _, err := editTopicStmt.Exec(name, content, parsed_content, topic.ID)
tcache, ok := topics.(TopicCache) tcache, ok := topics.(TopicCache)
@ -170,6 +170,7 @@ func (topic *Topic) CreateActionReply(action string, ipaddress string, user User
return err return err
} }
// Copy gives you a non-pointer concurrency safe copy of the topic
func (topic *Topic) Copy() Topic { func (topic *Topic) Copy() Topic {
return *topic return *topic
} }
@ -188,7 +189,7 @@ func getTopicUser(tid int) (TopicUser, error) {
// We might be better off just passing seperate topic and user structs to the caller? // We might be better off just passing seperate topic and user structs to the caller?
return copyTopicToTopicUser(topic, user), nil return copyTopicToTopicUser(topic, user), nil
} else if ucache.GetLength() < ucache.GetCapacity() { } else if ucache.Length() < ucache.GetCapacity() {
topic, err = topics.Get(tid) topic, err = topics.Get(tid)
if err != nil { if err != nil {
return TopicUser{ID: tid}, err return TopicUser{ID: tid}, err

View File

@ -8,7 +8,9 @@ package main
import ( import (
"database/sql" "database/sql"
"errors"
"log" "log"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -20,17 +22,20 @@ import (
// TODO: Add some sort of update method // TODO: Add some sort of update method
// ? - Should we add stick, lock, unstick, and unlock methods? These might be better on the Topics not the TopicStore // ? - Should we add stick, lock, unstick, and unlock methods? These might be better on the Topics not the TopicStore
var topics TopicStore var topics TopicStore
var ErrNoTitle = errors.New("This message is missing a title")
var ErrNoBody = errors.New("This message is missing a body")
type TopicStore interface { type TopicStore interface {
Get(id int) (*Topic, error) Get(id int) (*Topic, error)
BypassGet(id int) (*Topic, error) BypassGet(id int) (*Topic, error)
Delete(id int) error Delete(id int) error
Exists(id int) bool Exists(id int) bool
Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error)
AddLastTopic(item *Topic, fid int) error // unimplemented AddLastTopic(item *Topic, fid int) error // unimplemented
// TODO: Implement these two methods // TODO: Implement these two methods
//GetReplies() ([]*Reply, error) //Replies(tid int) ([]*Reply, error)
//GetRepliesRange(lower int, higher int) ([]*Reply, error) //RepliesRange(tid int, lower int, higher int) ([]*Reply, error)
GetGlobalCount() int GlobalCount() int
} }
type TopicCache interface { type TopicCache interface {
@ -43,7 +48,7 @@ type TopicCache interface {
CacheRemoveUnsafe(id int) error CacheRemoveUnsafe(id int) error
Flush() Flush()
Reload(id int) error Reload(id int) error
GetLength() int Length() int
SetCapacity(capacity int) SetCapacity(capacity int)
GetCapacity() int GetCapacity() int
} }
@ -176,6 +181,34 @@ func (mts *MemoryTopicStore) Exists(id int) bool {
return mts.exists.QueryRow(id).Scan(&id) == nil return mts.exists.QueryRow(id).Scan(&id) == nil
} }
func (mts *MemoryTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) {
topicName = strings.TrimSpace(topicName)
if topicName == "" {
return 0, ErrNoBody
}
content = strings.TrimSpace(content)
parsedContent := parseMessage(content, fid, "forums")
if strings.TrimSpace(parsedContent) == "" {
return 0, ErrNoBody
}
wcount := wordCount(content)
// TODO: Move this statement into the topic store
res, err := createTopicStmt.Exec(fid, topicName, content, parsedContent, uid, ipaddress, wcount, uid)
if err != nil {
return 0, err
}
lastID, err := res.LastInsertId()
if err != nil {
return 0, err
}
err = fstore.AddTopic(int(lastID), uid, fid)
return int(lastID), err
}
func (mts *MemoryTopicStore) CacheSet(item *Topic) error { func (mts *MemoryTopicStore) CacheSet(item *Topic) error {
mts.Lock() mts.Lock()
_, ok := mts.items[item.ID] _, ok := mts.items[item.ID]
@ -241,7 +274,9 @@ func (mts *MemoryTopicStore) Flush() {
mts.Unlock() mts.Unlock()
} }
func (mts *MemoryTopicStore) GetLength() int { // ! Is this concurrent?
// Length returns the number of topics in the memory cache
func (mts *MemoryTopicStore) Length() int {
return int(mts.length) return int(mts.length)
} }
@ -253,8 +288,8 @@ func (mts *MemoryTopicStore) GetCapacity() int {
return mts.capacity return mts.capacity
} }
// Return the total number of topics on these forums // GlobalCount returns the total number of topics on these forums
func (mts *MemoryTopicStore) GetGlobalCount() int { func (mts *MemoryTopicStore) GlobalCount() int {
var tcount int var tcount int
err := mts.topicCount.QueryRow().Scan(&tcount) err := mts.topicCount.QueryRow().Scan(&tcount)
if err != nil { if err != nil {
@ -314,6 +349,34 @@ func (sts *SQLTopicStore) Exists(id int) bool {
return sts.exists.QueryRow(id).Scan(&id) == nil return sts.exists.QueryRow(id).Scan(&id) == nil
} }
func (sts *SQLTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) {
topicName = strings.TrimSpace(topicName)
if topicName == "" {
return 0, ErrNoBody
}
content = strings.TrimSpace(content)
parsedContent := parseMessage(content, fid, "forums")
if strings.TrimSpace(parsedContent) == "" {
return 0, ErrNoBody
}
wcount := wordCount(content)
// TODO: Move this statement into the topic store
res, err := createTopicStmt.Exec(fid, topicName, content, parsedContent, uid, ipaddress, wcount, uid)
if err != nil {
return 0, err
}
lastID, err := res.LastInsertId()
if err != nil {
return 0, err
}
err = fstore.AddTopic(int(lastID), uid, fid)
return int(lastID), err
}
// TODO: Use a transaction here // TODO: Use a transaction here
func (sts *SQLTopicStore) Delete(id int) error { func (sts *SQLTopicStore) Delete(id int) error {
topic, err := sts.Get(id) topic, err := sts.Get(id)
@ -347,8 +410,8 @@ func (sts *SQLTopicStore) AddLastTopic(item *Topic, fid int) error {
return nil return nil
} }
// Return the total number of topics on these forums // GlobalCount returns the total number of topics on these forums
func (sts *SQLTopicStore) GetGlobalCount() int { func (sts *SQLTopicStore) GlobalCount() int {
var tcount int var tcount int
err := sts.topicCount.QueryRow().Scan(&tcount) err := sts.topicCount.QueryRow().Scan(&tcount)
if err != nil { if err != nil {

19
user.go
View File

@ -121,6 +121,24 @@ func (user *User) Activate() (err error) {
return err return err
} }
func (user *User) ChangeName(username string) (err error) {
_, err = setUsernameStmt.Exec(username, user.ID)
ucache, ok := users.(UserCache)
if ok {
ucache.CacheRemove(user.ID)
}
return err
}
func (user *User) ChangeAvatar(avatar string) (err error) {
_, err = setAvatarStmt.Exec(avatar, user.ID)
ucache, ok := users.(UserCache)
if ok {
ucache.CacheRemove(user.ID)
}
return err
}
func (user *User) increasePostStats(wcount int, topic bool) error { func (user *User) increasePostStats(wcount int, topic bool) error {
var mod int var mod int
baseScore := 1 baseScore := 1
@ -201,6 +219,7 @@ func (user *User) decreasePostStats(wcount int, topic bool) error {
return err return err
} }
// Copy gives you a non-pointer concurrency safe copy of the user
func (user *User) Copy() User { func (user *User) Copy() User {
return *user return *user
} }

View File

@ -25,7 +25,7 @@ type UserStore interface {
BulkGetMap(ids []int) (map[int]*User, error) BulkGetMap(ids []int) (map[int]*User, error)
BypassGet(id int) (*User, error) BypassGet(id int) (*User, error)
Create(username string, password string, email string, group int, active int) (int, error) Create(username string, password string, email string, group int, active int) (int, error)
GetGlobalCount() int GlobalCount() int
} }
type UserCache interface { type UserCache interface {
@ -38,7 +38,7 @@ type UserCache interface {
CacheRemoveUnsafe(id int) error CacheRemoveUnsafe(id int) error
Flush() Flush()
Reload(id int) error Reload(id int) error
GetLength() int Length() int
SetCapacity(capacity int) SetCapacity(capacity int)
GetCapacity() int GetCapacity() int
} }
@ -376,7 +376,9 @@ func (mus *MemoryUserStore) Flush() {
mus.Unlock() mus.Unlock()
} }
func (mus *MemoryUserStore) GetLength() int { // ! Is this concurrent?
// Length returns the number of users in the memory cache
func (mus *MemoryUserStore) Length() int {
return int(mus.length) return int(mus.length)
} }
@ -388,8 +390,8 @@ func (mus *MemoryUserStore) GetCapacity() int {
return mus.capacity return mus.capacity
} }
// Return the total number of users registered on the forums // GlobalCount returns the total number of users registered on the forums
func (mus *MemoryUserStore) GetGlobalCount() int { func (mus *MemoryUserStore) GlobalCount() int {
var ucount int var ucount int
err := mus.userCount.QueryRow().Scan(&ucount) err := mus.userCount.QueryRow().Scan(&ucount)
if err != nil { if err != nil {
@ -554,8 +556,8 @@ func (mus *SQLUserStore) Create(username string, password string, email string,
return int(lastID), err return int(lastID), err
} }
// Return the total number of users registered on the forums // GlobalCount returns the total number of users registered on the forums
func (mus *SQLUserStore) GetGlobalCount() int { func (mus *SQLUserStore) GlobalCount() int {
var ucount int var ucount int
err := mus.userCount.QueryRow().Scan(&ucount) err := mus.userCount.QueryRow().Scan(&ucount)
if err != nil { if err != nil {

View File

@ -57,9 +57,8 @@ func relativeTime(in string) (string, error) {
if in == "" { if in == "" {
return "", nil return "", nil
} }
layout := "2006-01-02 15:04:05"
t, err := time.Parse(layout, in) t, err := time.Parse("2006-01-02 15:04:05", in)
//t, err := time.ParseInLocation(layout, in, timeLocation)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -103,6 +102,8 @@ func relativeTime(in string) (string, error) {
// TODO: Write a test for this // TODO: Write a test for this
func convertByteUnit(bytes float64) (float64, string) { func convertByteUnit(bytes float64) (float64, string) {
switch { switch {
case bytes >= float64(petabyte):
return bytes / float64(petabyte), "PB"
case bytes >= float64(terabyte): case bytes >= float64(terabyte):
return bytes / float64(terabyte), "TB" return bytes / float64(terabyte), "TB"
case bytes >= float64(gigabyte): case bytes >= float64(gigabyte):
@ -119,6 +120,8 @@ func convertByteUnit(bytes float64) (float64, string) {
// TODO: Write a test for this // TODO: Write a test for this
func convertByteInUnit(bytes float64, unit string) (count float64) { func convertByteInUnit(bytes float64, unit string) (count float64) {
switch unit { switch unit {
case "PB":
count = bytes / float64(petabyte)
case "TB": case "TB":
count = bytes / float64(terabyte) count = bytes / float64(terabyte)
case "GB": case "GB":
@ -141,7 +144,7 @@ func convertByteInUnit(bytes float64, unit string) (count float64) {
func convertUnit(num int) (int, string) { func convertUnit(num int) (int, string) {
switch { switch {
case num >= 1000000000000: case num >= 1000000000000:
return 0, "∞" return num / 1000000000000, "T"
case num >= 1000000000: case num >= 1000000000:
return num / 1000000000, "B" return num / 1000000000, "B"
case num >= 1000000: case num >= 1000000:
@ -156,8 +159,10 @@ func convertUnit(num int) (int, string) {
// TODO: Write a test for this // TODO: Write a test for this
func convertFriendlyUnit(num int) (int, string) { func convertFriendlyUnit(num int) (int, string) {
switch { switch {
case num >= 1000000000000000:
return 0, " quadrillion"
case num >= 1000000000000: case num >= 1000000000000:
return 0, " zillion" return 0, " trillion"
case num >= 1000000000: case num >= 1000000000:
return num / 1000000000, " billion" return num / 1000000000, " billion"
case num >= 1000000: case num >= 1000000:
@ -347,7 +352,7 @@ func wordCount(input string) (count int) {
func getLevel(score int) (level int) { func getLevel(score int) (level int) {
var base float64 = 25 var base float64 = 25
var current, prev float64 var current, prev float64
expFactor := 2.8 var expFactor = 2.8
for i := 1; ; i++ { for i := 1; ; i++ {
_, bit := math.Modf(float64(i) / 10) _, bit := math.Modf(float64(i) / 10)
@ -390,7 +395,7 @@ func getLevelScore(getLevel int) (score int) {
func getLevels(maxLevel int) []float64 { func getLevels(maxLevel int) []float64 {
var base float64 = 25 var base float64 = 25
var current, prev float64 // = 0 var current, prev float64 // = 0
expFactor := 2.8 var expFactor = 2.8
var out []float64 var out []float64
out = append(out, 0) out = append(out, 0)