diff --git a/.gitignore b/.gitignore
index 13a268d5..a962e99b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/attachs/filler.txt b/attachs/filler.txt
new file mode 100644
index 00000000..20e14b1e
--- /dev/null
+++ b/attachs/filler.txt
@@ -0,0 +1 @@
+This file is here so that Git will include this folder in the repository.
\ No newline at end of file
diff --git a/auth.go b/auth.go
index 45b18224..20e8115e 100644
--- a/auth.go
+++ b/auth.go
@@ -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 {
diff --git a/cache.go b/cache.go
index 692b1652..34dd65c4 100644
--- a/cache.go
+++ b/cache.go
@@ -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
diff --git a/config.go b/config.go
index b9846e8f..20c4daea 100644
--- a/config.go
+++ b/config.go
@@ -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
diff --git a/errors.go b/errors.go
index dc501904..becb9d5b 100644
--- a/errors.go
+++ b/errors.go
@@ -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 + `"}`))
diff --git a/experimental/center-experiment.css b/experimental/center-experiment.css
deleted file mode 100644
index 22be1bae..00000000
--- a/experimental/center-experiment.css
+++ /dev/null
@@ -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;
-}
\ No newline at end of file
diff --git a/extend.go b/extend.go
index f91980a6..02b5422d 100644
--- a/extend.go
+++ b/extend.go
@@ -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,
diff --git a/forum.go b/forum.go
index bb2c2728..736124a2 100644
--- a/forum.go
+++ b/forum.go
@@ -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
diff --git a/forum_store.go b/forum_store.go
index fa1144dc..fbac8d6c 100644
--- a/forum_store.go
+++ b/forum_store.go
@@ -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)
diff --git a/gen_mysql.go b/gen_mysql.go
index 90afab99..a286023d 100644
--- a/gen_mysql.go
+++ b/gen_mysql.go
@@ -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 {
diff --git a/gen_router.go b/gen_router.go
index 8fe0464e..71fe7cf0 100644
--- a/gen_router.go
+++ b/gen_router.go
@@ -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/":
diff --git a/general_test.go b/general_test.go
index 8aa39f7c..f8fbda1b 100644
--- a/general_test.go
+++ b/general_test.go
@@ -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, "")
}
})
}
diff --git a/group.go b/group.go
index 54c40686..640f9264 100644
--- a/group.go
+++ b/group.go
@@ -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
}
diff --git a/group_store.go b/group_store.go
index ac623176..ad95ff74 100644
--- a/group_store.go
+++ b/group_store.go
@@ -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)
+}
diff --git a/images/tempra-conflux-topic-list.png b/images/tempra-conflux-topic-list.png
index b447237b..87a52cdf 100644
Binary files a/images/tempra-conflux-topic-list.png and b/images/tempra-conflux-topic-list.png differ
diff --git a/images/tempra-conflux.png b/images/tempra-conflux.png
index a01e09b5..8c7bf018 100644
Binary files a/images/tempra-conflux.png and b/images/tempra-conflux.png differ
diff --git a/install/install.go b/install/install.go
index 55cf90a8..ee640de0 100644
--- a/install/install.go
+++ b/install/install.go
@@ -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
diff --git a/langs/english.json b/langs/english.json
index 80f8e589..afc9d649 100644
--- a/langs/english.json
+++ b/langs/english.json
@@ -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",
diff --git a/main.go b/main.go
index 2079420c..aee61d2a 100644
--- a/main.go
+++ b/main.go
@@ -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)
diff --git a/member_routes.go b/member_routes.go
index 3d8b5f8a..ada8cbea 100644
--- a/member_routes.go
+++ b/member_routes.go
@@ -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(§ionID, §ionTable, &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)
+}
diff --git a/misc_test.go b/misc_test.go
index 8e70a6d5..018c2dde 100644
--- a/misc_test.go
+++ b/misc_test.go
@@ -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)
diff --git a/mod_routes.go b/mod_routes.go
index 06ca5d5e..b77c2d6b 100644
--- a/mod_routes.go
+++ b/mod_routes.go
@@ -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
diff --git a/mysql.sql b/mysql.sql
index c0c77e1f..28eff700 100644
--- a/mysql.sql
+++ b/mysql.sql
@@ -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');
diff --git a/pages.go b/pages.go
index feb273d7..acd84cee 100644
--- a/pages.go
+++ b/pages.go
@@ -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("[Invalid URL]")
var invalidTopic = []byte("[Invalid Topic]")
var invalidProfile = []byte("[Invalid Profile]")
var invalidForum = []byte("[Invalid Forum]")
+var unknownMedia = []byte("[Unknown Media]")
var urlOpen = []byte("")
var bytesSinglequote = []byte("'")
var bytesGreaterthan = []byte(">")
var urlMention = []byte(" class='mention'")
var urlClose = []byte("")
+var imageOpen = []byte("")
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)+"§ionType="+sectionType)...)
+ outbytes = append(outbytes, imageOpen2...)
+ outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"§ionType="+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
diff --git a/panel_routes.go b/panel_routes.go
index 9e2edac7..d9488c19 100644
--- a/panel_routes.go
+++ b/panel_routes.go
@@ -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 {
diff --git a/permissions.go b/permissions.go
index 95ed3a0a..6a42ec6c 100644
--- a/permissions.go
+++ b/permissions.go
@@ -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,
diff --git a/public/global.js b/public/global.js
index 96deddb6..9e034334 100644
--- a/public/global.js
+++ b/public/global.js
@@ -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
diff --git a/public/test_bg2.svg b/public/test_bg2.svg
deleted file mode 100644
index ce7769a0..00000000
--- a/public/test_bg2.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
\ No newline at end of file
diff --git a/public/test_bg3.svg b/public/test_bg3.svg
deleted file mode 100644
index 5c2ab9f9..00000000
--- a/public/test_bg3.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
\ No newline at end of file
diff --git a/query_gen/lib/mysql.go b/query_gen/lib/mysql.go
index f87bb164..7f2a69b3 100644
--- a/query_gen/lib/mysql.go
+++ b/query_gen/lib/mysql.go
@@ -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":
diff --git a/query_gen/lib/pgsql.go b/query_gen/lib/pgsql.go
index 3a2b1acf..1c74ce2a 100644
--- a/query_gen/lib/pgsql.go
+++ b/query_gen/lib/pgsql.go
@@ -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":
diff --git a/query_gen/lib/utils.go b/query_gen/lib/utils.go
index a2de942d..bab8ffbd 100644
--- a/query_gen/lib/utils.go
+++ b/query_gen/lib/utils.go
@@ -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]
diff --git a/query_gen/main.go b/query_gen/main.go
index f13cefd3..07fd689e 100644
--- a/query_gen/main.go
+++ b/query_gen/main.go
@@ -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
diff --git a/reply.go b/reply.go
index 3ac90fa5..78f42293 100644
--- a/reply.go
+++ b/reply.go
@@ -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
}
diff --git a/router_gen/main.go b/router_gen/main.go
index 8f7c77c8..ce1eda66 100644
--- a/router_gen/main.go
+++ b/router_gen/main.go
@@ -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)
diff --git a/router_gen/routes.go b/router_gen/routes.go
index f3619696..890e710d 100644
--- a/router_gen/routes.go
+++ b/router_gen/routes.go
@@ -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"}},
diff --git a/routes.go b/routes.go
index dafc9574..05a04c7f 100644
--- a/routes.go
+++ b/routes.go
@@ -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 {
diff --git a/routes_common.go b/routes_common.go
index 2891c24b..210d541d 100644
--- a/routes_common.go
+++ b/routes_common.go
@@ -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)
diff --git a/site.go b/site.go
index 9b6f5a39..d741eae1 100644
--- a/site.go
+++ b/site.go
@@ -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
diff --git a/template_forum.go b/template_forum.go
index ab115c63..2b945bf5 100644
--- a/template_forum.go
+++ b/template_forum.go
@@ -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 {
diff --git a/template_forums.go b/template_forums.go
index 00501d12..62bb9835 100644
--- a/template_forums.go
+++ b/template_forums.go
@@ -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)
diff --git a/template_init.go b/template_init.go
index eae8398b..5b536766 100644
--- a/template_init.go
+++ b/template_init.go
@@ -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
diff --git a/template_list.go b/template_list.go
index a0aaf3fa..b72b08b9 100644
--- a/template_list.go
+++ b/template_list.go
@@ -5,31 +5,36 @@ var header_0 = []byte(`
{{.Topic.ContentHTML}}
@@ -55,7 +55,7 @@ {{.ActionType}} {{else}} -{{.ContentHtml}}
diff --git a/templates/topics.html b/templates/topics.html index 0a0227af..b941e085 100644 --- a/templates/topics.html +++ b/templates/topics.html @@ -2,10 +2,48 @@