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
brun.bat
attachs/*
!attachs/filler.txt
uploads/avatar_*
uploads/socialgroup_*
backups/*.sql
node_modules/*
bin/*
out/*
logs/*
*.exe
*.exe~
*.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)
}
// GetCookies fetches the current user's session cookies
func (auth *DefaultAuth) GetCookies(r *http.Request) (uid int, session string, err error) {
// Are there any session cookies..?
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
}
// 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) {
uid, session, err := auth.GetCookies(r)
if err != nil {
@ -156,6 +158,7 @@ func (auth *DefaultAuth) SessionCheck(w http.ResponseWriter, r *http.Request) (u
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) {
session, err = GenerateSafeString(sessionLength)
if err != nil {

View File

@ -8,6 +8,7 @@ const CACHE_STATIC int = 0
const CACHE_DYNAMIC int = 1
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.
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() {
// 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.URL = "localhost"
site.Port = "8080" // 8080
@ -47,7 +48,7 @@ func init() {
config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png"
config.ItemsPerPage = 25
// Developer flag
// Developer flags
dev.DebugMode = true
//dev.SuperDebug = 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 user parameter?
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) {
w.WriteHeader(errcode)
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) {
if !isJs {
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) {
w.WriteHeader(errcode)
_, _ = 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,
}
// 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
var sshooks = map[string][]func(string) string{
"preparse_preassign": nil,

View File

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

View File

@ -43,14 +43,14 @@ type ForumStore interface {
//GetFirstChild(parentID int, parentType string) (*Forum,error)
Create(forumName string, forumDesc string, active bool, preset string) (int, error)
GetGlobalCount() int
GlobalCount() int
}
type ForumCache interface {
CacheGet(id int) (*Forum, error)
CacheSet(forum *Forum) error
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
@ -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
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 {
length++
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?
// GetGlobalCount returns the total number of forums
func (mfs *MemoryForumStore) GetGlobalCount() (fcount int) {
// GlobalCount returns the total number of forums
func (mfs *MemoryForumStore) GlobalCount() (fcount int) {
err := mfs.getForumCount.QueryRow().Scan(&fcount)
if err != nil {
LogError(err)

View File

@ -43,6 +43,7 @@ var groupEntryExistsStmt *sql.Stmt
var getForumTopicsOffsetStmt *sql.Stmt
var getExpiredScheduledGroupsStmt *sql.Stmt
var getSyncStmt *sql.Stmt
var getAttachmentStmt *sql.Stmt
var getTopicRepliesOffsetStmt *sql.Stmt
var getTopicListStmt *sql.Stmt
var getTopicUserStmt *sql.Stmt
@ -68,6 +69,7 @@ var addThemeStmt *sql.Stmt
var createGroupStmt *sql.Stmt
var addModlogEntryStmt *sql.Stmt
var addAdminlogEntryStmt *sql.Stmt
var addAttachmentStmt *sql.Stmt
var createWordFilterStmt *sql.Stmt
var addForumPermsToGroupStmt *sql.Stmt
var replaceScheduleGroupStmt *sql.Stmt
@ -341,6 +343,12 @@ func _gen_mysql() (err error) {
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.")
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 {
@ -491,6 +499,12 @@ func _gen_mysql() (err error) {
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.")
createWordFilterStmt, err = db.Prepare("INSERT INTO `word_filters`(`find`,`replacement`) VALUES (?,?)")
if err != nil {

View File

@ -107,6 +107,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
case "/theme":
routeChangeTheme(w,req,user)
return
case "/attachs":
routeShowAttachment(w,req,user,extra_data)
return
case "/report":
switch(req.URL.Path) {
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) {
b.ReportAllocs()
b.Run("empty_post", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = parseMessage("")
_ = parseMessage("", 0, "")
}
})
b.Run("short_post", func(b *testing.B) {
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) {
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) {
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) {
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) {
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
}
// ! 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 {
return *group
}

View File

@ -12,6 +12,7 @@ var groupCreateMutex sync.Mutex
var groupUpdateMutex sync.Mutex
var gstore GroupStore
// ? - We could fallback onto the database when an item can't be found in the cache?
type GroupStore interface {
LoadGroups() error
DirtyGet(id int) *Group
@ -23,6 +24,10 @@ type GroupStore interface {
GetRange(lower int, higher int) ([]*Group, error)
}
type GroupCache interface {
Length() int
}
type MemoryGroupStore struct {
groups []*Group // TODO: Use a sync.Map instead of a slice
groupCapCount int
@ -203,3 +208,7 @@ func (mgs *MemoryGroupStore) GetRange(lower int, higher int) (groups []*Group, e
}
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 dbName 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 defaultHost = "localhost"
var defaultUsername = "root"
var defaultDbname = "gosora"
var defaultSiteShortName = "SN"
var defaultSiteName = "Site Name"
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
@ -145,57 +150,58 @@ func main() {
configContents := []byte(`package main
func init() {
// Site Info
site.Name = "` + siteName + `"
site.Email = ""
site.URL = "` + siteURL + `"
site.Port = "` + serverPort + `"
site.EnableSsl = false
site.EnableEmails = false
site.HasProxy = false // Cloudflare counts as this, if it's sitting in the middle
config.SslPrivkey = ""
config.SslFullchain = ""
site.Language = "english"
// Site Info
site.ShortName = "` + siteShortName + `" // This should be less than three letters to fit in the navbar
site.Name = "` + siteName + `"
site.Email = ""
site.URL = "` + siteURL + `"
site.Port = "` + serverPort + `"
site.EnableSsl = false
site.EnableEmails = false
site.HasProxy = false // Cloudflare counts as this, if it's sitting in the middle
config.SslPrivkey = ""
config.SslFullchain = ""
site.Language = "english"
// Database details
dbConfig.Host = "` + dbHost + `"
dbConfig.Username = "` + dbUsername + `"
dbConfig.Password = "` + dbPassword + `"
dbConfig.Dbname = "` + dbName + `"
dbConfig.Port = "` + dbPort + `" // You probably won't need to change this
// Database details
dbConfig.Host = "` + dbHost + `"
dbConfig.Username = "` + dbUsername + `"
dbConfig.Password = "` + dbPassword + `"
dbConfig.Dbname = "` + dbName + `"
dbConfig.Port = "` + dbPort + `" // You probably won't need to change this
// Limiters
config.MaxRequestSize = 5 * megabyte
// Limiters
config.MaxRequestSize = 5 * megabyte
// Caching
config.CacheTopicUser = CACHE_STATIC
config.UserCacheCapacity = 120 // The max number of users held in memory
config.TopicCacheCapacity = 200 // The max number of topics held in memory
// Caching
config.CacheTopicUser = CACHE_STATIC
config.UserCacheCapacity = 120 // The max number of users held in memory
config.TopicCacheCapacity = 200 // The max number of topics held in memory
// Email
config.SMTPServer = ""
config.SMTPUsername = ""
config.SMTPPassword = ""
config.SMTPPort = "25"
// Email
config.SMTPServer = ""
config.SMTPUsername = ""
config.SMTPPassword = ""
config.SMTPPort = "25"
// Misc
config.DefaultRoute = routeTopics
config.DefaultGroup = 3 // Should be a setting in the database
config.ActivationGroup = 5 // Should be a setting in the database
config.StaffCSS = "staff_post"
config.DefaultForum = 2
config.MinifyTemplates = true
config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features
// Misc
config.DefaultRoute = routeTopics
config.DefaultGroup = 3 // Should be a setting in the database
config.ActivationGroup = 5 // Should be a setting in the database
config.StaffCSS = "staff_post"
config.DefaultForum = 2
config.MinifyTemplates = true
config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features
//config.Noavatar = "https://api.adorable.io/avatars/{width}/{id}@{site_url}.png"
config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png"
config.ItemsPerPage = 25
//config.Noavatar = "https://api.adorable.io/avatars/{width}/{id}@{site_url}.png"
config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png"
config.ItemsPerPage = 25
// Developer flag
dev.DebugMode = true
//dev.SuperDebug = true
//dev.TemplateDebug = true
//dev.Profiling = true
// Developer flags
dev.DebugMode = true
//dev.SuperDebug = true
//dev.TemplateDebug = true
//dev.Profiling = true
}
`)
@ -294,6 +300,17 @@ func getSiteDetails() bool {
}
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)
if !scanner.Scan() {
return false

View File

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

25
main.go
View File

@ -27,6 +27,7 @@ const kilobyte int = 1024
const megabyte int = kilobyte * 1024
const gigabyte int = megabyte * 1024
const terabyte int = gigabyte * 1024
const petabyte int = terabyte * 1024
const saltLength int = 32
const sessionLength int = 80
@ -37,6 +38,30 @@ var startTime time.Time
var externalSites = map[string]string{
"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 logWriter = io.MultiWriter(os.Stderr)

View File

@ -1,12 +1,15 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"html"
"io"
"log"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
@ -101,9 +104,17 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid st
// POST functions. Authorised users only.
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 {
PreError("Bad Form", w, r)
LocalError("Unable to parse the form", w, r, user)
return
}
@ -131,35 +142,110 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) {
return
}
wcount := wordCount(content)
res, err := createTopicStmt.Exec(fid, topicName, content, parseMessage(content), user.ID, ipaddress, wcount, user.ID)
tid, err := topics.Create(fid, topicName, content, user.ID, ipaddress)
if err != nil {
InternalError(err, w)
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)
}
return
}
lastID, err := res.LastInsertId()
_, err = addSubscriptionStmt.Exec(user.ID, tid, "topic")
if err != nil {
InternalError(err, w)
return
}
_, err = addSubscriptionStmt.Exec(user.ID, lastID, "topic")
err = user.increasePostStats(wordCount(content), true)
if err != nil {
InternalError(err, w)
return
}
http.Redirect(w, r, "/topic/"+strconv.FormatInt(lastID, 10), http.StatusSeeOther)
err = user.increasePostStats(wcount, true)
if err != nil {
InternalError(err, w)
return
// Handle the file attachments
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 {
InternalError(err, w)
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) {
@ -201,7 +287,7 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) {
}
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 {
InternalError(err, w)
return
@ -475,7 +561,8 @@ func routeProfileReplyCreate(w http.ResponseWriter, r *http.Request, user User)
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 {
InternalError(err, w)
return
@ -605,8 +692,9 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI
return
}
// TODO: Repost attachments in the reports forum, so that the mods can see them
// ? - 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 {
InternalError(err, w)
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) {
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
}
r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxRequestSize))
@ -742,14 +831,13 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use
return
}
err := r.ParseMultipartForm(int64(config.MaxRequestSize))
err := r.ParseMultipartForm(int64(megabyte))
if err != nil {
LocalError("Upload failed", w, r, user)
return
}
var filename string
var ext string
var filename, ext string
for _, fheaders := range r.MultipartForm.File {
for _, hdr := range fheaders {
infile, err := hdr.Open()
@ -760,6 +848,7 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use
defer infile.Close()
// 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 != hdr.Filename {
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]
// 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)
@ -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 {
InternalError(err, w)
return
}
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")
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"))
_, err = setUsernameStmt.Exec(newUsername, strconv.Itoa(user.ID))
err = user.ChangeName(newUsername)
if err != nil {
LocalError("Unable to change the username. Does someone else already have this name?", w, r, user)
return
}
// TODO: Use the reloaded data instead for the name?
user.Name = newUsername
ucache, ok := users.(UserCache)
if ok {
ucache.CacheRemove(user.ID)
}
headerVars.NoticeList = append(headerVars.NoticeList, "Your username was successfully updated")
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)
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
ucache, hasCache := users.(UserCache)
if hasCache && ucache.GetLength() != 0 {
if hasCache && ucache.Length() != 0 {
t.Error("Initial ucache length isn't zero")
}
@ -39,7 +39,7 @@ func userStoreTest(t *testing.T) {
t.Fatal(err)
}
if hasCache && ucache.GetLength() != 0 {
if hasCache && ucache.Length() != 0 {
t.Error("There shouldn't be anything in the user cache")
}
@ -50,7 +50,7 @@ func userStoreTest(t *testing.T) {
t.Fatal(err)
}
if hasCache && ucache.GetLength() != 0 {
if hasCache && ucache.Length() != 0 {
t.Error("There shouldn't be anything in the user cache")
}
@ -66,7 +66,7 @@ func userStoreTest(t *testing.T) {
}
if hasCache {
length = ucache.GetLength()
length = ucache.Length()
if length != 1 {
t.Error("User cache length should be 1, not " + strconv.Itoa(length))
}
@ -83,7 +83,7 @@ func userStoreTest(t *testing.T) {
}
ucache.Flush()
length = ucache.GetLength()
length = ucache.Length()
if length != 0 {
t.Error("User cache length should be 0, not " + strconv.Itoa(length))
}
@ -97,7 +97,7 @@ func userStoreTest(t *testing.T) {
}
if hasCache {
length = ucache.GetLength()
length = ucache.Length()
if length != 0 {
t.Error("User cache length should be 0, not " + strconv.Itoa(length))
}
@ -109,7 +109,7 @@ func userStoreTest(t *testing.T) {
}
if hasCache {
length = ucache.GetLength()
length = ucache.Length()
if length != 0 {
t.Error("User cache length should be 0, not " + strconv.Itoa(length))
}
@ -133,7 +133,7 @@ func userStoreTest(t *testing.T) {
}
if hasCache {
length = ucache.GetLength()
length = ucache.Length()
if length != 1 {
t.Error("User cache length should be 1, not " + strconv.Itoa(length))
}
@ -168,13 +168,13 @@ func userStoreTest(t *testing.T) {
}
if hasCache {
length = ucache.GetLength()
length = ucache.Length()
if length != 0 {
t.Error("User cache length should be 0, not " + strconv.Itoa(length))
}
}
count := users.GetGlobalCount()
count := users.GlobalCount()
if count <= 0 {
t.Error("The number of users should be bigger than zero")
t.Error("count", count)
@ -243,7 +243,7 @@ func topicStoreTest(t *testing.T) {
t.Error("TID #1 should exist")
}
count := topics.GetGlobalCount()
count := topics.GlobalCount()
if count <= 0 {
t.Error("The number of topics should be bigger than zero")
t.Error("count", count)

View File

@ -345,13 +345,6 @@ func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) {
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..
var tid int
err = getReplyTIDStmt.QueryRow(rid).Scan(&tid)
@ -380,6 +373,13 @@ func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) {
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 {
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther)
} else {
@ -504,7 +504,7 @@ func routeProfileReplyEditSubmit(w http.ResponseWriter, r *http.Request, user Us
}
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 {
InternalErrorJSQ(err, w, r, isJs)
return

View File

@ -78,6 +78,17 @@ CREATE TABLE `replies`(
primary key(`rid`)
) 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`(
`index` int not null,
`content` text not null,
@ -190,6 +201,7 @@ INSERT INTO emails(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1);
/*
The Permissions:
Global Permissions:
BanUsers
ActivateUsers
EditUser
@ -210,6 +222,10 @@ ManagePlugins
ViewAdminLogs
ViewIPs
Non-staff Global Permissions:
UploadFiles
Forum Permissions:
ViewTopic
LikeItem
CreateTopic
@ -222,9 +238,9 @@ PinTopic
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`,`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`) VALUES ('Member','{"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"CreateReply":true}','{}');
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,"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','{"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"CreateReply":true}','{}');
INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`is_banned`) VALUES ('Banned','{"ViewTopic":true}','{}',1);
INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`) VALUES ('Awaiting Activation','{"ViewTopic":true}','{}');
INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`tag`) VALUES ('Not Loggedin','{"ViewTopic":true}','{}','Guest');

199
pages.go
View File

@ -4,6 +4,7 @@ import (
//"fmt"
"bytes"
"html/template"
"net/url"
"regexp"
"strconv"
"strings"
@ -61,10 +62,12 @@ type TopicPage struct {
}
type TopicsPage struct {
Title string
CurrentUser User
Header *HeaderVars
ItemList []*TopicsRow
Title string
CurrentUser User
Header *HeaderVars
TopicList []*TopicsRow
ForumList []Forum
DefaultForum int
}
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 invalidProfile = []byte("<span style='color: red;'>[Invalid Profile]</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 urlOpen2 = []byte("'>")
var bytesSinglequote = []byte("'")
var bytesGreaterthan = []byte(">")
var urlMention = []byte(" class='mention'")
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 urlReg *regexp.Regexp
@ -440,7 +447,8 @@ func preparseMessage(msg string) string {
}
// 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, ":D", "😃", -1)
@ -461,19 +469,13 @@ func parseMessage(msg string /*, user User*/) string {
var msgbytes = []byte(msg)
var outbytes []byte
msgbytes = append(msgbytes, spaceGap...)
//log.Print(`"`+string(msgbytes)+`"`)
lastItem := 0
i := 0
//log.Printf("string(msgbytes) %+v\n", `"`+string(msgbytes)+`"`)
var lastItem = 0
var i = 0
for ; len(msgbytes) > (i + 1); i++ {
//log.Print("Index:",i)
//log.Print("Index Item:",msgbytes[i])
//if msgbytes[i] == 10 {
// log.Print("NEWLINE")
//} else if msgbytes[i] == 32 {
// log.Print("SPACE")
//} else {
// log.Print("string(msgbytes[i])",string(msgbytes[i]))
//}
//log.Print("Index Item: ",msgbytes[i])
//log.Print("string(msgbytes[i]): ",string(msgbytes[i]))
//log.Print("End Index")
if (i == 0 && (msgbytes[0] > 32)) || ((msgbytes[i] < 33) && (msgbytes[i+1] > 32)) {
//log.Print("IN")
@ -507,12 +509,12 @@ func parseMessage(msg string /*, user User*/) string {
outbytes = append(outbytes, urlClose...)
lastItem = i
//log.Print("string(msgbytes)",string(msgbytes))
//log.Print(msgbytes)
//log.Print("string(msgbytes) ",string(msgbytes))
//log.Print("msgbytes ",msgbytes)
//log.Print(msgbytes[lastItem - 1])
//log.Print(lastItem - 1)
//log.Print(msgbytes[lastItem])
//log.Print(lastItem)
//log.Print("lastItem ",lastItem)
} else if bytes.Equal(msgbytes[i+1:i+5], []byte("rid-")) {
outbytes = append(outbytes, msgbytes[lastItem:i]...)
i += 5
@ -611,11 +613,57 @@ func parseMessage(msg string /*, user User*/) string {
outbytes = append(outbytes, msgbytes[lastItem: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...)
i += urlLen
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, msgbytes[i:i+urlLen]...)
outbytes = append(outbytes, urlOpen2...)
@ -628,13 +676,10 @@ func parseMessage(msg string /*, user User*/) string {
}
if lastItem != i && len(outbytes) != 0 {
//log.Print("lastItem:",msgbytes[lastItem])
//log.Print("lastItem index:")
//log.Print(lastItem)
//log.Print("i:")
//log.Print(i)
//log.Print("lastItem to end:")
//log.Print(msgbytes[lastItem:])
//log.Print("lastItem: ",msgbytes[lastItem])
//log.Print("lastItem index: ",lastItem)
//log.Print("i: ",i)
//log.Print("lastItem to end: ",msgbytes[lastItem:])
//log.Print("-----")
calclen := len(msgbytes) - 10
if calclen <= lastItem {
@ -666,8 +711,8 @@ func regexParseMessage(msg string) string {
return msg
}
// 6, 7, 8, 6, 7
// ftp://, http://, https:// git://, mailto: (not a URL, just here for length comparison purposes)
// 6, 7, 8, 6, 2, 7
// ftp://, http://, https:// git://, //, mailto: (not a URL, just here for length comparison purposes)
// TODO: Write a test for this
func validateURLBytes(data []byte) bool {
datalen := len(data)
@ -681,10 +726,13 @@ func validateURLBytes(data []byte) bool {
} else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) {
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++ {
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
}
}
@ -704,10 +752,13 @@ func validatedURLBytes(data []byte) (url []byte) {
} else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) {
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++ {
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
}
}
@ -730,10 +781,13 @@ func partialURLBytes(data []byte) (url []byte) {
} else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) {
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++ {
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
}
}
@ -756,47 +810,80 @@ func partialURLBytesLen(data []byte) int {
} else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) {
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++ {
if 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])
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])
return i
}
}
//log.Print("Data Length:",datalen)
//log.Print("Data Length: ",datalen)
return datalen
}
type MediaEmbed struct {
Type string //image
URL string
}
// TODO: Write a test for this
func parseMediaBytes(data []byte) (protocol []byte, url []byte) {
datalen := len(data)
i := 0
func parseMediaBytes(data []byte) (media MediaEmbed, ok bool) {
if !validateURLBytes(data) {
return media, false
}
url, err := parseURL(data)
if err != nil {
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")
//log.Print("url ", url)
hostname := url.Hostname()
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"
}
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) {
return []byte(""), invalidURL
path := url.EscapedPath()
//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
}
media.URL = scheme + "://" + hostname + sport + path
}
}
return media, true
}
if len(protocol) == 0 {
protocol = []byte("http")
}
return protocol, data[i:]
func parseURL(data []byte) (*url.URL, error) {
return url.Parse(string(data))
}
// 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{"ViewAdminLogs", GetGlobalPermPhrase("ViewAdminLogs"), group.Perms.ViewAdminLogs})
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}
if preRenderHooks["pre_render_panel_edit_group_perms"] != nil {

View File

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

View File

@ -78,6 +78,7 @@ function load_alerts(menu_alerts)
bind_to_alerts();
},
error: function(magic,theStatus,error) {
var errtxt
try {
var data = JSON.parse(magic.responseText);
if("errmsg" in data) errtxt = data.errmsg;
@ -94,16 +95,16 @@ function load_alerts(menu_alerts)
function SplitN(data,ch,n) {
var out = [];
if(data.length == 0) return out;
if(data.length === 0) return out;
var lastIndex = 0;
var j = 0;
var lastN = 1;
for(var i = 0; i < data.length; i++) {
if(data[i] == ch) {
for(let i = 0; i < data.length; i++) {
if(data[i] === ch) {
out[j++] = data.substring(lastIndex,i);
lastIndex = i;
if(lastN == n) break;
if(lastN === n) break;
lastN++;
}
}
@ -118,19 +119,23 @@ $(document).ready(function(){
else conn = new WebSocket("ws://" + document.location.host + "/ws/");
conn.onopen = function() {
console.log("The WebSockets connection was opened");
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
Notification.requestPermission();
}
conn.onclose = function() {
conn = false;
console.log("The WebSockets connection was closed");
}
conn.onmessage = function(event) {
//console.log("WS_Message: ",event.data);
if(event.data[0] == "{") {
try {
var data = JSON.parse(event.data);
} catch(err) { console.log(err); }
} catch(err) {
console.log(err);
}
if ("msg" in data) {
var msg = data.msg
@ -175,11 +180,11 @@ $(document).ready(function(){
//console.log(messages[i]);
if(messages[i].startsWith("set ")) {
//msgblocks = messages[i].split(' ',3);
msgblocks = SplitN(messages[i]," ",3);
let msgblocks = SplitN(messages[i]," ",3);
if(msgblocks.length < 3) continue;
document.querySelector(msgblocks[1]).innerHTML = msgblocks[2];
} else if(messages[i].startsWith("set-class ")) {
msgblocks = SplitN(messages[i]," ",3);
let msgblocks = SplitN(messages[i]," ",3);
if(msgblocks.length < 3) continue;
document.querySelector(msgblocks[1]).className = msgblocks[2];
}
@ -328,7 +333,7 @@ $(document).ready(function(){
//console.log("running .submit_edit event");
var out_data = {isJs: "1"}
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_type = this.getAttribute("data-type");
if(field_type=="list") {
@ -397,6 +402,71 @@ $(document).ready(function(){
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(){
console.log("Changing the theme to " + this.options[this.selectedIndex].getAttribute("val"));
$.ajax({
@ -408,6 +478,7 @@ $(document).ready(function(){
console.log("Theme successfully switched");
console.log("data",data);
console.log("status",status);
console.log("xhr",xhr);
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

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 {
Name string
Name string // ? - Do we really need this? Can't we hard-code this?
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
}
// GetName gives you the name of the database adapter. In this case, it's mysql
func (adapter *Mysql_Adapter) GetName() string {
return adapter.Name
}
@ -120,7 +121,7 @@ func (adapter *Mysql_Adapter) SimpleInsert(name string, table string, columns st
var querystr = "INSERT INTO `" + table + "`("
// 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" {
querystr += column.Left + ","
} else {
@ -132,7 +133,7 @@ func (adapter *Mysql_Adapter) SimpleInsert(name string, table string, columns st
querystr = querystr[0 : len(querystr)-1]
querystr += ") VALUES ("
for _, field := range _processFields(fields) {
for _, field := range processFields(fields) {
querystr += field.Name + ","
}
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 + "`("
// 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" {
querystr += column.Left + ","
} else {
@ -169,7 +170,7 @@ func (adapter *Mysql_Adapter) SimpleReplace(name string, table string, columns s
querystr = querystr[0 : len(querystr)-1]
querystr += ") VALUES ("
for _, field := range _processFields(fields) {
for _, field := range processFields(fields) {
querystr += field.Name + ","
}
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 "
for _, item := range _process_set(set) {
for _, item := range processSet(set) {
querystr += "`" + item.Column + "` ="
for _, token := range item.Expr {
switch token.Type {
@ -211,7 +212,7 @@ func (adapter *Mysql_Adapter) SimpleUpdate(name string, table string, set string
// Add support for BETWEEN x.x
if len(where) != 0 {
querystr += " WHERE"
for _, loc := range _processWhere(where) {
for _, loc := range processWhere(where) {
for _, token := range loc.Expr {
switch token.Type {
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"
// Add support for BETWEEN x.x
for _, loc := range _processWhere(where) {
for _, loc := range processWhere(where) {
for _, token := range loc.Expr {
switch token.Type {
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
if len(where) != 0 {
querystr += " WHERE"
for _, loc := range _processWhere(where) {
for _, loc := range processWhere(where) {
for _, token := range loc.Expr {
switch token.Type {
case "function", "operator", "number", "substitute":
@ -328,7 +329,7 @@ func (adapter *Mysql_Adapter) SimpleSelect(name string, table string, columns st
if len(orderby) != 0 {
querystr += " ORDER BY "
for _, column := range _process_orderby(orderby) {
for _, column := range processOrderby(orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
}
querystr = querystr[0 : len(querystr)-1]
@ -362,7 +363,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2
var querystr = "SELECT "
for _, column := range _process_columns(columns) {
for _, column := range processColumns(columns) {
var source, alias string
// 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 += " 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 "
}
// Remove the trailing AND
@ -393,7 +394,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2
// Add support for BETWEEN x.x
if len(where) != 0 {
querystr += " WHERE"
for _, loc := range _processWhere(where) {
for _, loc := range processWhere(where) {
for _, token := range loc.Expr {
switch token.Type {
case "function", "operator", "number", "substitute":
@ -418,7 +419,7 @@ func (adapter *Mysql_Adapter) SimpleLeftJoin(name string, table1 string, table2
if len(orderby) != 0 {
querystr += " ORDER BY "
for _, column := range _process_orderby(orderby) {
for _, column := range processOrderby(orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
}
querystr = querystr[0 : len(querystr)-1]
@ -452,7 +453,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2
var querystr = "SELECT "
for _, column := range _process_columns(columns) {
for _, column := range processColumns(columns) {
var source, alias string
// 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 += " 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 "
}
// Remove the trailing AND
@ -483,7 +484,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2
// Add support for BETWEEN x.x
if len(where) != 0 {
querystr += " WHERE"
for _, loc := range _processWhere(where) {
for _, loc := range processWhere(where) {
for _, token := range loc.Expr {
switch token.Type {
case "function", "operator", "number", "substitute":
@ -508,7 +509,7 @@ func (adapter *Mysql_Adapter) SimpleInnerJoin(name string, table1 string, table2
if len(orderby) != 0 {
querystr += " ORDER BY "
for _, column := range _process_orderby(orderby) {
for _, column := range processOrderby(orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
}
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 + "`("
// 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" {
querystr += column.Left + ","
} else {
@ -540,7 +541,7 @@ func (adapter *Mysql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel
/* Select Portion */
for _, column := range _process_columns(sel.Columns) {
for _, column := range processColumns(sel.Columns) {
var source, alias string
// 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
if len(sel.Where) != 0 {
querystr += " WHERE"
for _, loc := range _processWhere(sel.Where) {
for _, loc := range processWhere(sel.Where) {
for _, token := range loc.Expr {
switch token.Type {
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 {
querystr += " ORDER BY "
for _, column := range _process_orderby(sel.Orderby) {
for _, column := range processOrderby(sel.Orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
}
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 + "`("
// 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" {
querystr += column.Left + ","
} else {
@ -614,7 +615,7 @@ func (adapter *Mysql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, s
/* Select Portion */
for _, column := range _process_columns(sel.Columns) {
for _, column := range processColumns(sel.Columns) {
var source, alias string
// 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 += " 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 = 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
if len(sel.Where) != 0 {
querystr += " WHERE"
for _, loc := range _processWhere(sel.Where) {
for _, loc := range processWhere(sel.Where) {
for _, token := range loc.Expr {
switch token.Type {
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 {
querystr += " ORDER BY "
for _, column := range _process_orderby(sel.Orderby) {
for _, column := range processOrderby(sel.Orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
}
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 + "`("
// 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" {
querystr += column.Left + ","
} else {
@ -699,7 +700,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert,
/* Select Portion */
for _, column := range _process_columns(sel.Columns) {
for _, column := range processColumns(sel.Columns) {
var source, alias string
// 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 += " 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 = 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
if len(sel.Where) != 0 {
querystr += " WHERE"
for _, loc := range _processWhere(sel.Where) {
for _, loc := range processWhere(sel.Where) {
for _, token := range loc.Expr {
switch token.Type {
case "function", "operator", "number", "substitute":
@ -752,7 +753,7 @@ func (adapter *Mysql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert,
if len(sel.Orderby) != 0 {
querystr += " ORDER BY "
for _, column := range _process_orderby(sel.Orderby) {
for _, column := range processOrderby(sel.Orderby) {
querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
}
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("where:",where)
//fmt.Println("_process_where:",_process_where(where))
for _, loc := range _processWhere(where) {
for _, loc := range processWhere(where) {
for _, token := range loc.Expr {
switch token.Type {
case "function", "operator", "number", "substitute":

View File

@ -12,11 +12,12 @@ func init() {
}
type Pgsql_Adapter struct {
Name string
Name string // ? - Do we really need this? Can't we hard-code this?
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
}
// GetName gives you the name of the database adapter. In this case, it's pgsql
func (adapter *Pgsql_Adapter) GetName() string {
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")
}
var querystr = "UPDATE `" + table + "` SET "
for _, item := range _process_set(set) {
for _, item := range processSet(set) {
querystr += "`" + item.Column + "` ="
for _, token := range item.Expr {
switch token.Type {
@ -166,7 +167,7 @@ func (adapter *Pgsql_Adapter) SimpleUpdate(name string, table string, set string
// Add support for BETWEEN x.x
if len(where) != 0 {
querystr += " WHERE"
for _, loc := range _processWhere(where) {
for _, loc := range processWhere(where) {
for _, token := range loc.Expr {
switch token.Type {
case "function":

View File

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

View File

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

View File

@ -6,8 +6,8 @@ import "log"
//import "strings"
import "os"
var route_list []Route
var route_groups []RouteGroup
var routeList []Route
var routeGroups []RouteGroup
func main() {
log.Println("Generating the router...")
@ -16,9 +16,9 @@ func main() {
routes()
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
if route.Path[len(route.Path)-1] == '/' {
end = len(route.Path) - 1
@ -36,7 +36,7 @@ func main() {
out += ")\n\t\t\treturn"
}
for _, group := range route_groups {
for _, group := range routeGroups {
var end int
if group.Path[len(group.Path)-1] == '/' {
end = len(group.Path) - 1
@ -46,10 +46,10 @@ func main() {
out += `
case "` + group.Path[0:end] + `":
switch(req.URL.Path) {`
var default_route Route
var defaultRoute Route
for _, route := range group.Routes {
if group.Path == route.Path {
default_route = route
defaultRoute = route
continue
}
@ -64,13 +64,13 @@ func main() {
out += ")\n\t\t\t\t\treturn"
}
if default_route.Name != "" {
if defaultRoute.Name != "" {
out += "\n\t\t\t\tdefault:"
if default_route.Before != "" {
out += "\n\t\t\t\t\t" + default_route.Before
if defaultRoute.Before != "" {
out += "\n\t\t\t\t\t" + defaultRoute.Before
}
out += "\n\t\t\t\t\t" + default_route.Name + "(w,req,user"
for _, item := range default_route.Vars {
out += "\n\t\t\t\t\t" + defaultRoute.Name + "(w,req,user"
for _, item := range defaultRoute.Vars {
out += ", " + item
}
out += ")\n\t\t\t\t\treturn"
@ -78,7 +78,7 @@ func main() {
out += "\n\t\t\t}"
}
fdata += `package main
fileData += `package main
import "log"
import "strings"
@ -209,11 +209,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
NotFound(w,req)
}
`
write_file("./gen_router.go", fdata)
writeFile("./gen_router.go", fileData)
log.Println("Successfully generated the router")
}
func write_file(name string, content string) {
func writeFile(name string, content string) {
f, err := os.Create(name)
if err != nil {
log.Fatal(err)

View File

@ -13,11 +13,11 @@ type RouteGroup struct {
}
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) {
route_groups = append(route_groups, RouteGroup{path, routes})
routeGroups = append(routeGroups, RouteGroup{path, routes})
}
func routes() {
@ -31,6 +31,7 @@ func routes() {
//addRoute("routeTopicCreate","/topics/create/","","extra_data")
//addRoute("routeTopics","/topics/",""/*,"&groups","&forums"*/)
addRoute("routeChangeTheme", "/theme/", "")
addRoute("routeShowAttachment", "/attachs/", "", "extra_data")
addRouteGroup("/report/",
Route{"routeReportSubmit", "/report/submit/", "", []string{"extra_data"}},

View File

@ -26,7 +26,7 @@ var tList []interface{}
//var nList []string
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
type HTTPSRedirect struct {
@ -171,11 +171,25 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user User) {
canSee = group.CanSee
}
// We need a list of the visible forums for Quick Topic
var forumList []Forum
for _, fid := range canSee {
forum := fstore.DirtyGet(fid)
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))
qlist += "?,"
}
}
@ -265,7 +279,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user User) {
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 runPreRenderHook("pre_render_topic_list", w, r, &user, &pi) {
return
@ -495,7 +509,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
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")
// 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.ParentID = topic.ID
replyItem.ContentHtml = parseMessage(replyItem.Content)
replyItem.ContentHtml = parseMessage(replyItem.Content, topic.ParentID, "forums")
replyItem.ContentLines = strings.Count(replyItem.Content, "\n")
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
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()
if err != nil {

View File

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

View File

@ -12,6 +12,7 @@ var config Config
var dev DevConfig
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?
Email string // ? - Move this into the settings table?
URL string

View File

@ -20,33 +20,37 @@ func template_forum(tmpl_forum_vars ForumPage, w http.ResponseWriter) {
w.Write(header_0)
w.Write([]byte(tmpl_forum_vars.Title))
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([]byte(tmpl_forum_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_forum_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_forum_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4)
}
}
w.Write([]byte(item))
w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_forum_vars.Header.Scripts) != 0 {
for _, item := range tmpl_forum_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7)
}
}
w.Write([]byte(item))
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)
if !tmpl_forum_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0)
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)
if tmpl_forum_vars.CurrentUser.Loggedin {
w.Write(menu_3)
@ -58,16 +62,16 @@ w.Write(menu_5)
w.Write(menu_6)
}
w.Write(menu_7)
w.Write(header_12)
if tmpl_forum_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
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 {
for _, item := range tmpl_forum_vars.Header.NoticeList {
w.Write(header_15)
w.Write(header_17)
w.Write([]byte(item))
w.Write(header_16)
w.Write(header_18)
}
}
if tmpl_forum_vars.Page > 1 {
@ -106,65 +110,75 @@ w.Write(forum_14)
w.Write(forum_15)
}
w.Write(forum_16)
if len(tmpl_forum_vars.ItemList) != 0 {
for _, item := range tmpl_forum_vars.ItemList {
if tmpl_forum_vars.CurrentUser.Perms.CreateTopic {
w.Write(forum_17)
if item.Sticky {
w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID)))
w.Write(forum_18)
} else {
if item.IsClosed {
if tmpl_forum_vars.CurrentUser.Perms.UploadFiles {
w.Write(forum_19)
}
}
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([]byte(strconv.Itoa(item.PostCount)))
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)
} else {
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([]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 {
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([]byte(item.LastUser.Name))
}
w.Write(forum_37)
w.Write([]byte(item.LastReplyAt))
if item.LastUser.Avatar != "" {
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 {
w.Write(forum_39)
w.Write(forum_44)
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(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)
if len(tmpl_forum_vars.Header.Themes) != 0 {
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([]byte(tmpl_forums_vars.Title))
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([]byte(tmpl_forums_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_forums_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_forums_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4)
}
}
w.Write([]byte(item))
w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_forums_vars.Header.Scripts) != 0 {
for _, item := range tmpl_forums_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7)
}
}
w.Write([]byte(item))
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)
if !tmpl_forums_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0)
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)
if tmpl_forums_vars.CurrentUser.Loggedin {
w.Write(menu_3)
@ -57,16 +61,16 @@ w.Write(menu_5)
w.Write(menu_6)
}
w.Write(menu_7)
w.Write(header_12)
if tmpl_forums_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
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 {
for _, item := range tmpl_forums_vars.Header.NoticeList {
w.Write(header_15)
w.Write(header_17)
w.Write([]byte(item))
w.Write(header_16)
w.Write(header_18)
}
}
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 {
var c CTemplateSet
@ -128,6 +129,7 @@ func compileTemplates() error {
return err
}
// TODO: Use a dummy forum list to avoid o(n) problems
var forumList []Forum
forums, err := fstore.GetAll()
if err != nil {
@ -147,7 +149,7 @@ func compileTemplates() error {
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"})
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)
if err != nil {
return err

View File

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

View File

@ -20,33 +20,37 @@ func template_profile(tmpl_profile_vars ProfilePage, w http.ResponseWriter) {
w.Write(header_0)
w.Write([]byte(tmpl_profile_vars.Title))
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([]byte(tmpl_profile_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_profile_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_profile_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4)
}
}
w.Write([]byte(item))
w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_profile_vars.Header.Scripts) != 0 {
for _, item := range tmpl_profile_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7)
}
}
w.Write([]byte(item))
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)
if !tmpl_profile_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0)
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)
if tmpl_profile_vars.CurrentUser.Loggedin {
w.Write(menu_3)
@ -58,16 +62,16 @@ w.Write(menu_5)
w.Write(menu_6)
}
w.Write(menu_7)
w.Write(header_12)
if tmpl_profile_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
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 {
for _, item := range tmpl_profile_vars.Header.NoticeList {
w.Write(header_15)
w.Write(header_17)
w.Write([]byte(item))
w.Write(header_16)
w.Write(header_18)
}
}
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([]byte(tmpl_topic_vars.Title))
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([]byte(tmpl_topic_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_topic_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_topic_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4)
}
}
w.Write([]byte(item))
w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_topic_vars.Header.Scripts) != 0 {
for _, item := range tmpl_topic_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7)
}
}
w.Write([]byte(item))
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)
if !tmpl_topic_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0)
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)
if tmpl_topic_vars.CurrentUser.Loggedin {
w.Write(menu_3)
@ -58,16 +62,16 @@ w.Write(menu_5)
w.Write(menu_6)
}
w.Write(menu_7)
w.Write(header_12)
if tmpl_topic_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
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 {
for _, item := range tmpl_topic_vars.Header.NoticeList {
w.Write(header_15)
w.Write(header_17)
w.Write([]byte(item))
w.Write(header_16)
w.Write(header_18)
}
}
w.Write(topic_0)
@ -117,169 +121,173 @@ if tmpl_topic_vars.Topic.Avatar != "" {
w.Write(topic_20)
w.Write([]byte(tmpl_topic_vars.Topic.Avatar))
w.Write(topic_21)
if tmpl_topic_vars.Topic.ContentLines <= 5 {
w.Write([]byte(tmpl_topic_vars.Header.ThemeName))
w.Write(topic_22)
}
if tmpl_topic_vars.Topic.ContentLines <= 5 {
w.Write(topic_23)
}
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)
}
if tmpl_topic_vars.CurrentUser.Perms.EditTopic {
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([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_35)
}
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([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_37)
}
if tmpl_topic_vars.CurrentUser.Perms.CloseTopic {
if tmpl_topic_vars.Topic.IsClosed {
w.Write(topic_37)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_38)
} else {
w.Write(topic_39)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_39)
} else {
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.Topic.Sticky {
w.Write(topic_41)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_42)
} else {
w.Write(topic_43)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_43)
} else {
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 {
w.Write(topic_45)
w.Write([]byte(tmpl_topic_vars.Topic.IPAddress))
w.Write(topic_46)
}
w.Write([]byte(tmpl_topic_vars.Topic.IPAddress))
w.Write(topic_47)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
}
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)
if tmpl_topic_vars.Topic.LikeCount > 0 {
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
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([]byte(strconv.Itoa(tmpl_topic_vars.Topic.LikeCount)))
w.Write(topic_52)
}
if tmpl_topic_vars.Topic.Tag != "" {
w.Write(topic_52)
w.Write([]byte(tmpl_topic_vars.Topic.Tag))
w.Write(topic_53)
} else {
w.Write([]byte(tmpl_topic_vars.Topic.Tag))
w.Write(topic_54)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.Level)))
} else {
w.Write(topic_55)
}
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.Level)))
w.Write(topic_56)
}
w.Write(topic_57)
if len(tmpl_topic_vars.ItemList) != 0 {
for _, item := range tmpl_topic_vars.ItemList {
if item.ActionType != "" {
w.Write(topic_57)
w.Write([]byte(item.ActionIcon))
w.Write(topic_58)
w.Write([]byte(item.ActionType))
w.Write([]byte(item.ActionIcon))
w.Write(topic_59)
} else {
w.Write([]byte(item.ActionType))
w.Write(topic_60)
w.Write([]byte(item.ClassName))
} else {
w.Write(topic_61)
if item.Avatar != "" {
w.Write([]byte(item.ClassName))
w.Write(topic_62)
w.Write([]byte(item.Avatar))
if item.Avatar != "" {
w.Write(topic_63)
if item.ContentLines <= 5 {
w.Write([]byte(item.Avatar))
w.Write(topic_64)
}
w.Write([]byte(tmpl_topic_vars.Header.ThemeName))
w.Write(topic_65)
}
if item.ContentLines <= 5 {
w.Write(topic_66)
}
w.Write(topic_67)
w.Write([]byte(item.ContentHtml))
}
w.Write(topic_68)
w.Write([]byte(item.UserLink))
w.Write(topic_69)
w.Write([]byte(item.CreatedByName))
w.Write([]byte(item.ContentHtml))
w.Write(topic_70)
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write([]byte(item.UserLink))
w.Write(topic_71)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write([]byte(item.CreatedByName))
w.Write(topic_72)
if item.Liked {
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
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(topic_74)
if item.Liked {
w.Write(topic_75)
}
w.Write(topic_76)
}
if tmpl_topic_vars.CurrentUser.Perms.DeleteReply {
if tmpl_topic_vars.CurrentUser.Perms.EditReply {
w.Write(topic_77)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_78)
}
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
if tmpl_topic_vars.CurrentUser.Perms.DeleteReply {
w.Write(topic_79)
w.Write([]byte(item.IPAddress))
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_80)
}
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
w.Write(topic_81)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write([]byte(item.IPAddress))
w.Write(topic_82)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
}
w.Write(topic_83)
if item.LikeCount > 0 {
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_84)
w.Write([]byte(strconv.Itoa(item.LikeCount)))
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
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 != "" {
w.Write(topic_86)
w.Write([]byte(item.Tag))
w.Write(topic_87)
} else {
w.Write(topic_88)
w.Write([]byte(strconv.Itoa(item.Level)))
w.Write([]byte(item.Tag))
w.Write(topic_89)
}
} else {
w.Write(topic_90)
}
}
}
w.Write([]byte(strconv.Itoa(item.Level)))
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([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_95)
}
w.Write(topic_96)
w.Write(footer_0)
if len(tmpl_topic_vars.Header.Themes) != 0 {
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([]byte(tmpl_topic_alt_vars.Title))
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([]byte(tmpl_topic_alt_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_topic_alt_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_topic_alt_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4)
}
}
w.Write([]byte(item))
w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_topic_alt_vars.Header.Scripts) != 0 {
for _, item := range tmpl_topic_alt_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7)
}
}
w.Write([]byte(item))
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)
if !tmpl_topic_alt_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0)
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)
if tmpl_topic_alt_vars.CurrentUser.Loggedin {
w.Write(menu_3)
@ -58,16 +62,16 @@ w.Write(menu_5)
w.Write(menu_6)
}
w.Write(menu_7)
w.Write(header_12)
if tmpl_topic_alt_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
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 {
for _, item := range tmpl_topic_alt_vars.Header.NoticeList {
w.Write(header_15)
w.Write(header_17)
w.Write([]byte(item))
w.Write(header_16)
w.Write(header_18)
}
}
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([]byte(tmpl_topics_vars.Title))
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([]byte(tmpl_topics_vars.Header.ThemeName))
w.Write(header_3)
if len(tmpl_topics_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_topics_vars.Header.Stylesheets {
w.Write(header_3)
w.Write([]byte(item))
w.Write(header_4)
}
}
w.Write([]byte(item))
w.Write(header_5)
}
}
w.Write(header_6)
if len(tmpl_topics_vars.Header.Scripts) != 0 {
for _, item := range tmpl_topics_vars.Header.Scripts {
w.Write(header_6)
w.Write([]byte(item))
w.Write(header_7)
}
}
w.Write([]byte(item))
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)
if !tmpl_topics_vars.CurrentUser.IsSuperMod {
w.Write(header_12)
}
w.Write(header_13)
w.Write(menu_0)
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)
if tmpl_topics_vars.CurrentUser.Loggedin {
w.Write(menu_3)
@ -58,92 +62,129 @@ w.Write(menu_5)
w.Write(menu_6)
}
w.Write(menu_7)
w.Write(header_12)
if tmpl_topics_vars.Header.Widgets.RightSidebar != "" {
w.Write(header_13)
}
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 {
for _, item := range tmpl_topics_vars.Header.NoticeList {
w.Write(header_15)
w.Write(header_17)
w.Write([]byte(item))
w.Write(header_16)
w.Write(header_18)
}
}
w.Write(topics_0)
if len(tmpl_topics_vars.ItemList) != 0 {
for _, item := range tmpl_topics_vars.ItemList {
if tmpl_topics_vars.CurrentUser.ID != 0 {
w.Write(topics_1)
if item.Sticky {
}
w.Write(topics_2)
} else {
if item.IsClosed {
if tmpl_topics_vars.CurrentUser.ID != 0 {
if len(tmpl_topics_vars.ForumList) != 0 {
w.Write(topics_3)
}
}
} else {
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([]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([]byte(item.LastReplyAt))
if item.ID == tmpl_topics_vars.DefaultForum {
w.Write(topics_9)
w.Write([]byte(item.Link))
}
w.Write(topics_10)
w.Write([]byte(item.Title))
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topics_11)
if item.ForumName != "" {
w.Write([]byte(item.Name))
w.Write(topics_12)
w.Write([]byte(item.ForumLink))
}
}
w.Write(topics_13)
w.Write([]byte(item.ForumName))
if tmpl_topics_vars.CurrentUser.Perms.UploadFiles {
w.Write(topics_14)
}
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 {
w.Write(topics_18)
} else {
if item.IsClosed {
w.Write(topics_19)
}
}
w.Write(topics_20)
if item.Sticky {
if item.Creator.Avatar != "" {
w.Write(topics_21)
} else {
if item.IsClosed {
w.Write([]byte(item.Creator.Avatar))
w.Write(topics_22)
}
}
w.Write(topics_23)
if item.LastUser.Avatar != "" {
w.Write([]byte(strconv.Itoa(item.PostCount)))
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(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([]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 {
w.Write(topics_30)
w.Write(topics_46)
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)
if len(tmpl_topics_vars.Header.Themes) != 0 {
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")
}
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() {
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"
case reflect.Bool: // Do nothing
case reflect.String:
out += " != \"\""
case reflect.Int64:
out += " > 0"
case reflect.Slice, reflect.Map:
out = "len(" + out + ") != 0"
default:
fmt.Println("Variable Name:", varname)
fmt.Println("Variable Holder:", varholder)

View File

@ -4,25 +4,29 @@
<div class="rowitem"><h1>Create Topic</h1></div>
</div>
<div class="rowblock">
<form action="/topic/create/submit/" method="post">
<div class="formrow real_first_child">
<div class="formitem formlabel"><a>Board</a></div>
<div class="formitem"><select name="topic-board">
{{range .ItemList}}<option {{if eq .ID $.FID}}selected{{end}} value="{{.ID}}">{{.Name}}</option>{{end}}
</select></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>Topic Name</a></div>
<div class="formitem"><input name="topic-name" type="text" placeholder="Topic Name" required /></div>
</div>
<div class="formrow">
<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>
<div class="formrow">
<div class="formitem"><button name="topic-button" class="formbutton form_middle_button">Create Topic</button></div>
</div>
</form>
<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="formitem formlabel"><a>Board</a></div>
<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}}
</select></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>Topic Name</a></div>
<div class="formitem"><input form="topic_create_form_form" name="topic-name" type="text" placeholder="Topic Name" required /></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>Content</a></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 class="formrow">
<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>
</main>
{{template "footer.html" . }}

View File

@ -11,11 +11,37 @@
</div>
{{if ne .CurrentUser.ID 0}}
{{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}}
<div style="clear: both;"></div>
{{end}}
</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">
{{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;">

View File

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

View File

@ -2,10 +2,9 @@
<div class="move_left">
<div class="move_right">
<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_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">
<div class="alert_bell"></div>
<div class="alert_counter"></div>

View File

@ -22,7 +22,7 @@
</div>
<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>
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
@ -55,7 +55,7 @@
<span>{{.ActionType}}</span>
</article>
{{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 **/}}
<p class="editable_block user_content" style="margin:0;padding:0;">{{.ContentHtml}}</p>

View File

@ -2,10 +2,48 @@
<main>
<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 id="topic_list" class="rowblock topic_list" aria-label="The main 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}}">
{{if ne .CurrentUser.ID 0}}
{{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="replyCount">{{.PostCount}} replies</span><br />
<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
// 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 dev.DebugMode {
log.Print("Adding theme image")

View File

@ -796,7 +796,7 @@ blockquote p {
}
@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.. */

View File

@ -807,7 +807,7 @@ blockquote p {
}
@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.. */

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

View File

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

View File

@ -4,6 +4,11 @@
-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 {
font-family: arial;
padding-bottom: 8px;
@ -23,11 +28,11 @@ ul {
padding-right: 0px;
height: 36px;
list-style-type: none;
border: 1px solid hsl(0,0%,80%);
border: 1px solid var(--main-border-color);
background: hsl(0, 0%, 97%);
margin-bottom: 12px;
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-right: -8px;
}
@ -48,14 +53,14 @@ li a {
}
.menu_left {
float: left;
border-right: 1px solid hsl(0,0%,80%);
border-bottom: 1.5px inset hsl(0,0%,80%);
border-right: 1px solid var(--main-border-color);
border-bottom: 1.5px inset var(--main-border-color);
padding-right: 10px;
background: hsl(0, 0%, 98%);
}
.menu_right {
float: right;
border-left: 1px solid hsl(0,0%,80%);
border-left: 1px solid var(--main-border-color);
padding-right: 10px;
}
@ -109,10 +114,10 @@ li a {
line-height: 16px;
width: 300px;
right: calc(5% + 7px);
border-top: 1px solid hsl(0,0%,80%);
border-left: 1px solid hsl(0,0%,80%);
border-right: 1px solid hsl(0,0%,80%);
border-bottom: 1px solid hsl(0,0%,80%);
border-top: 1px solid var(--main-border-color);
border-left: 1px solid var(--main-border-color);
border-right: 1px solid var(--main-border-color);
border-bottom: 1px solid var(--main-border-color);
margin-bottom: 10px;
}
.alertItem {
@ -150,7 +155,7 @@ li a {
margin-left: auto;
margin-right: auto;
background: hsl(0, 0%, 98%);
border: 1px solid hsl(0,0%,80%);
border: 1px solid var(--main-border-color);
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 */
.rowblock {
border: 1px solid hsl(0,0%,80%);
border: 1px solid var(--main-border-color);
width: 100%;
padding: 0px;
padding-top: 0px;
border-top: 1px solid hsl(0,0%,80%);
border-left: 1px solid hsl(0,0%,80%);
border-right: 1px solid hsl(0,0%,80%);
border-bottom: 1.5px inset hsl(0,0%,80%);
border-top: 1px solid var(--main-border-color);
border-left: 1px solid var(--main-border-color);
border-right: 1px solid var(--main-border-color);
border-bottom: 1.5px inset var(--main-border-color);
}
.rowblock:empty {
display: none;
@ -211,7 +216,7 @@ li a {
width: calc(70% - 13px);
}
.colstack_item {
border: 1px solid hsl(0,0%,80%);
border: 1px solid var(--main-border-color);
padding: 0px;
padding-top: 0px;
width: 100%;
@ -230,7 +235,7 @@ li a {
margin-top: 2px;
}
.grid_item {
border: 1px solid hsl(0,0%,80%);
border: 1px solid var(--main-border-color);
word-wrap: break-word;
background-color: white;
width: 100%;
@ -291,6 +296,34 @@ li a {
}
.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 {
padding-top: 10px;
padding-bottom: 10px;
@ -310,7 +343,7 @@ li a {
clear: both;
}
.formrow:not(:last-child) {
border-bottom: 1px dotted hsl(0,0%,80%);
border-bottom: 1px dotted var(--main-border-color);
}
.formitem {
@ -321,7 +354,7 @@ li a {
font-weight: normal;
}
.formitem:not(:last-child) {
border-right: 1px dotted hsl(0,0%,80%);
border-right: 1px dotted var(--main-border-color);
}
.formitem.invisible_border {
border: none;
@ -346,18 +379,18 @@ li a {
padding-bottom: 12px;/*16px;*/
/*padding-left: 15px;*/
}
.formbutton, button {
background: white;
border: 1px solid #8e8e8e;
}
.formbutton {
padding: 7px;
display: block;
margin-left: auto;
margin-right: auto;
font-size: 15px;
border-color: hsl(0,0%,80%);
}
button {
background: white;
border: 1px solid #8e8e8e;
border-color: var(--main-border-color);
}
/* 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 {
display: none;
}
@ -467,7 +533,7 @@ button {
color: #505050; /* 80,80,80 */
background-color: #FFFFFF;
border-style: solid;
border-color: hsl(0,0%,80%);
border-color: var(--main-border-color);
border-width: 1px;
font-size: 15px;
}
@ -499,7 +565,7 @@ button.username {
display: block;
padding: 5px;
margin-bottom: 10px;
border: 1px solid hsl(0,0%,80%);
border: 1px solid var(--main-border-color);
background-color: white;
}
.alert_success {
@ -580,10 +646,10 @@ button.username {
}
#profile_left_lane {
border: 1px solid hsl(0,0%,80%);
border: 1px solid var(--main-border-color);
width: 220px;
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 {
overflow: hidden;
@ -612,7 +678,7 @@ button.username {
margin-top: 20px;
}
#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 {
@ -652,7 +718,7 @@ button.username {
position: sticky;
top: 4px;
/*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 {
background-repeat: no-repeat, repeat-y;
@ -672,10 +738,12 @@ button.username {
margin-bottom: 0;
margin-right: 3px;
/*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 {
min-height: auto;
padding: 15px;
@ -689,7 +757,7 @@ button.username {
border-width: 1px;
background-color: #FFFFFF;
border-style: solid;
border-color: hsl(0,0%,80%);
border-color: var(--main-border-color);
padding: 0px;
padding-left: 5px;
padding-right: 5px;
@ -720,7 +788,7 @@ button.username {
border-bottom: none;
}
.topic_reply_form {
border-top: 1px solid hsl(0,0%,80%);
border-top: 1px solid var(--main-border-color);
}
.post_container .post_item {
background-color: #eaeaea;
@ -739,7 +807,7 @@ button.username {
}
.footer {
border: 1px solid hsl(0,0%,80%);
border: 1px solid var(--main-border-color);
margin-top: 12px;
clear: both;
height: 40px;
@ -747,7 +815,7 @@ button.username {
padding-left: 10px;
padding-right: 10px;
background-color: white;
border-bottom: 1.5px inset hsl(0,0%,80%);
border-bottom: 1.5px inset var(--main-border-color);
}
.footer select {
padding: 2px;
@ -767,7 +835,7 @@ button.username {
margin-top: -5px;
}
.pageitem {
border: 1px solid hsl(0,0%,80%);
border: 1px solid var(--main-border-color);
background-color: white;
padding: 5px;
margin-right: 5px;

View File

@ -12,7 +12,13 @@
}
}
@media (max-width: 950px) {
@media(max-width: 950px) {
.sidebar {
display: none;
}
}
@media (max-width: 700px) {
li {
height: 29px;
font-size: 15px;
@ -20,11 +26,20 @@
padding-top: 6px;
padding-bottom: 6px;
}
li, li a {
font-size: 15px;
}
ul {
height: 30px;
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 {
padding-left: 7px;
padding-right: 7px;
@ -39,9 +54,16 @@
height: 100% !important;
overflow-x: hidden;
}
.container { width: auto; }
.sidebar { display: none; }
.selectedAlert .alertList { top: 37px; right: 4px; }
.container {
width: auto;
margin-left: 5px;
margin-right: 5px;
margin-top: 5px;
}
.selectedAlert .alertList {
top: 37px;
right: 4px;
}
}
@media (max-width: 680px) {
@ -54,7 +76,7 @@
li a { font-size: 14px; }
ul { height: 26px; }
.menu_left, .menu_right { padding-right: 7px; }
.menu_create_topic, .hide_on_mobile { display: none; }
.hide_on_mobile { display: none; }
.menu_alerts {
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; }
.menu_left { padding-right: 5px; padding-top: 1px; }
.menu_right { padding-right: 5px; }
.menu_create_topic { display: none;}
.menu_alerts {
padding-left: 4px;
@ -596,8 +595,13 @@ button.username {
font-size: 16px;
padding-top: 1px;
}
.menu_alerts .alert_counter { top: -23px; left: 8px; }
.selectedAlert .alertList { top: 33px; }
.menu_alerts .alert_counter {
top: -23px;
left: 8px;
}
.selectedAlert .alertList {
top: 33px;
}
.hide_on_mobile { display: none; }
.prev_button, .next_button { top: auto; bottom: 5px; }

View File

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

View File

@ -52,7 +52,6 @@
li a { font-size: 14px; }
ul { height: 26px; }
.menu_left, .menu_right { padding-right: 7px; }
.menu_create_topic { display: none; }
.menu_alerts {
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 {
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)
tcache, ok := topics.(TopicCache)
@ -170,6 +170,7 @@ func (topic *Topic) CreateActionReply(action string, ipaddress string, user User
return err
}
// Copy gives you a non-pointer concurrency safe copy of the topic
func (topic *Topic) Copy() 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?
return copyTopicToTopicUser(topic, user), nil
} else if ucache.GetLength() < ucache.GetCapacity() {
} else if ucache.Length() < ucache.GetCapacity() {
topic, err = topics.Get(tid)
if err != nil {
return TopicUser{ID: tid}, err

View File

@ -8,7 +8,9 @@ package main
import (
"database/sql"
"errors"
"log"
"strings"
"sync"
"sync/atomic"
@ -20,17 +22,20 @@ import (
// 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
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 {
Get(id int) (*Topic, error)
BypassGet(id int) (*Topic, error)
Delete(id int) error
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
// TODO: Implement these two methods
//GetReplies() ([]*Reply, error)
//GetRepliesRange(lower int, higher int) ([]*Reply, error)
GetGlobalCount() int
//Replies(tid int) ([]*Reply, error)
//RepliesRange(tid int, lower int, higher int) ([]*Reply, error)
GlobalCount() int
}
type TopicCache interface {
@ -43,7 +48,7 @@ type TopicCache interface {
CacheRemoveUnsafe(id int) error
Flush()
Reload(id int) error
GetLength() int
Length() int
SetCapacity(capacity int)
GetCapacity() int
}
@ -176,6 +181,34 @@ func (mts *MemoryTopicStore) Exists(id int) bool {
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 {
mts.Lock()
_, ok := mts.items[item.ID]
@ -241,7 +274,9 @@ func (mts *MemoryTopicStore) Flush() {
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)
}
@ -253,8 +288,8 @@ func (mts *MemoryTopicStore) GetCapacity() int {
return mts.capacity
}
// Return the total number of topics on these forums
func (mts *MemoryTopicStore) GetGlobalCount() int {
// GlobalCount returns the total number of topics on these forums
func (mts *MemoryTopicStore) GlobalCount() int {
var tcount int
err := mts.topicCount.QueryRow().Scan(&tcount)
if err != nil {
@ -314,6 +349,34 @@ func (sts *SQLTopicStore) Exists(id int) bool {
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
func (sts *SQLTopicStore) Delete(id int) error {
topic, err := sts.Get(id)
@ -347,8 +410,8 @@ func (sts *SQLTopicStore) AddLastTopic(item *Topic, fid int) error {
return nil
}
// Return the total number of topics on these forums
func (sts *SQLTopicStore) GetGlobalCount() int {
// GlobalCount returns the total number of topics on these forums
func (sts *SQLTopicStore) GlobalCount() int {
var tcount int
err := sts.topicCount.QueryRow().Scan(&tcount)
if err != nil {

19
user.go
View File

@ -121,6 +121,24 @@ func (user *User) Activate() (err error) {
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 {
var mod int
baseScore := 1
@ -201,6 +219,7 @@ func (user *User) decreasePostStats(wcount int, topic bool) error {
return err
}
// Copy gives you a non-pointer concurrency safe copy of the user
func (user *User) Copy() User {
return *user
}

View File

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

View File

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