Replaced the user agent parser with a faster and more flexible one.

More progress on poll posts.

Improved the poll UI on the other themes.
De-duplicated some avatar logic.
Added Exec() to accInsertBuilder.
Moved routeOverview to routes.Overview
Moved routeCustomPage to routes.CustomPage
Moved routeTopicID to routes.ViewTopic
Moved routeLogin to routes.AccountLogin
Moved routeRegister to routes.AccountRegister
Moved routeLoginSubmit to routes.AccountLoginSubmit
Moved routeRegisterSubmit to routes.AccountRegisterSubmit
We now track the Android Chrome user agent.
We now track the UCBrowser user agent.
We now track the zgrab user agent.
Fixed a few cases where Googlebot wasn't tracked properly.
Moved routeStatic to routes.StaticFile
This commit is contained in:
Azareal 2018-02-03 05:47:14 +00:00
parent 0c26b5a8da
commit 017bce9c09
27 changed files with 1296 additions and 905 deletions

View File

@ -78,7 +78,7 @@ func init() {
isLiked: acc.Select("likes").Columns("targetItem").Where("sentBy = ? and targetItem = ? and targetType = 'replies'").Prepare(),
createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy").Fields("?,?,?,?").Prepare(),
edit: acc.Update("replies").Set("content = ?, parsed_content = ?").Where("rid = ? AND poll = 0").Prepare(),
setPoll: acc.Update("replies").Set("content = '', parsed_content = '', poll = ?").Where("rid = ? AND poll = 0").Prepare(),
setPoll: acc.Update("replies").Set("poll = ?").Where("rid = ? AND poll = 0").Prepare(),
delete: acc.Delete("replies").Where("rid = ?").Prepare(),
addLikesToReply: acc.Update("replies").Set("likeCount = likeCount + ?").Where("rid = ?").Prepare(),
removeRepliesFromTopic: acc.Update("topics").Set("postCount = postCount - ?").Where("tid = ?").Prepare(),
@ -142,6 +142,14 @@ func (reply *Reply) Topic() (*Topic, error) {
return Topics.Get(reply.ParentID)
}
func (reply *Reply) GetID() int {
return reply.ID
}
func (reply *Reply) GetTable() string {
return "replies"
}
// Copy gives you a non-pointer concurrency safe copy of the reply
func (reply *Reply) Copy() Reply {
return *reply

View File

@ -102,13 +102,7 @@ func init() {
}
func (user *User) Init() {
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
}
} else {
user.Avatar = strings.Replace(Config.Noavatar, "{id}", strconv.Itoa(user.ID), 1)
}
user.Avatar = BuildAvatar(user.ID, user.Avatar)
user.Link = BuildProfileURL(NameToSlug(user.Name), user.ID)
user.Tag = Groups.DirtyGet(user.Group).Tag
user.InitPerms()
@ -355,6 +349,17 @@ func (user *User) InitPerms() {
}
}
// ? Make this part of *User?
func BuildAvatar(uid int, avatar string) string {
if avatar != "" {
if avatar[0] == '.' {
return "/uploads/avatar_" + strconv.Itoa(uid) + avatar
}
return avatar
}
return strings.Replace(Config.Noavatar, "{id}", strconv.Itoa(uid), 1)
}
func BcryptCheckPassword(realPassword string, password string, salt string) (err error) {
return bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password+salt))
}

View File

@ -333,13 +333,7 @@ func RouteMemberList(w http.ResponseWriter, r *http.Request, user common.User) c
return common.InternalError(err, w, r)
}
guildMember.Link = common.BuildProfileURL(common.NameToSlug(guildMember.User.Name), guildMember.User.ID)
if guildMember.User.Avatar != "" {
if guildMember.User.Avatar[0] == '.' {
guildMember.User.Avatar = "/uploads/avatar_" + strconv.Itoa(guildMember.User.ID) + guildMember.User.Avatar
}
} else {
guildMember.User.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(guildMember.User.ID), 1)
}
guildMember.User.Avatar = common.BuildAvatar(guildMember.User.ID, guildMember.User.Avatar)
guildMember.JoinedAt, _ = common.RelativeTimeFromString(guildMember.JoinedAt)
if guildItem.Owner == guildMember.User.ID {
guildMember.RankString = "Owner"

View File

@ -25,7 +25,6 @@ type Stmts struct {
groupEntryExists *sql.Stmt
getForumTopicsOffset *sql.Stmt
getAttachment *sql.Stmt
getTopicRepliesOffset *sql.Stmt
getTopicList *sql.Stmt
getTopicReplies *sql.Stmt
getForumTopics *sql.Stmt
@ -34,7 +33,6 @@ type Stmts struct {
createReport *sql.Stmt
addActivity *sql.Stmt
notifyOne *sql.Stmt
addEmail *sql.Stmt
addForumPermsToForum *sql.Stmt
addPlugin *sql.Stmt
addTheme *sql.Stmt
@ -184,13 +182,6 @@ func _gen_mssql() (err error) {
return err
}
log.Print("Preparing getTopicRepliesOffset statement.")
stmts.getTopicRepliesOffset, 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 [replies].[tid] = ?1 ORDER BY replies.rid ASC OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY")
if err != nil {
log.Print("Bad Query: ","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 [replies].[tid] = ?1 ORDER BY replies.rid ASC OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY")
return err
}
log.Print("Preparing getTopicList statement.")
stmts.getTopicList, err = db.Prepare("SELECT [topics].[tid],[topics].[title],[topics].[content],[topics].[createdBy],[topics].[is_closed],[topics].[sticky],[topics].[createdAt],[topics].[parentID],[users].[name],[users].[avatar] FROM [topics] LEFT JOIN [users] ON [topics].[createdBy] = [users].[uid] ORDER BY topics.sticky DESC,topics.lastReplyAt DESC,topics.createdBy DESC")
if err != nil {
@ -247,13 +238,6 @@ func _gen_mssql() (err error) {
return err
}
log.Print("Preparing addEmail statement.")
stmts.addEmail, err = db.Prepare("INSERT INTO [emails] ([email],[uid],[validated],[token]) VALUES (?,?,?,?)")
if err != nil {
log.Print("Bad Query: ","INSERT INTO [emails] ([email],[uid],[validated],[token]) VALUES (?,?,?,?)")
return err
}
log.Print("Preparing addForumPermsToForum statement.")
stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) VALUES (?,?,?,?)")
if err != nil {

View File

@ -27,7 +27,6 @@ type Stmts struct {
groupEntryExists *sql.Stmt
getForumTopicsOffset *sql.Stmt
getAttachment *sql.Stmt
getTopicRepliesOffset *sql.Stmt
getTopicList *sql.Stmt
getTopicReplies *sql.Stmt
getForumTopics *sql.Stmt
@ -36,7 +35,6 @@ type Stmts struct {
createReport *sql.Stmt
addActivity *sql.Stmt
notifyOne *sql.Stmt
addEmail *sql.Stmt
addForumPermsToForum *sql.Stmt
addPlugin *sql.Stmt
addTheme *sql.Stmt
@ -170,12 +168,6 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing getTopicRepliesOffset statement.")
stmts.getTopicRepliesOffset, 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 `replies`.`tid` = ? ORDER BY replies.rid ASC LIMIT ?,?")
if err != nil {
return err
}
log.Print("Preparing getTopicList statement.")
stmts.getTopicList, err = db.Prepare("SELECT `topics`.`tid`, `topics`.`title`, `topics`.`content`, `topics`.`createdBy`, `topics`.`is_closed`, `topics`.`sticky`, `topics`.`createdAt`, `topics`.`parentID`, `users`.`name`, `users`.`avatar` FROM `topics` LEFT JOIN `users` ON `topics`.`createdBy` = `users`.`uid` ORDER BY topics.sticky DESC,topics.lastReplyAt DESC,topics.createdBy DESC")
if err != nil {
@ -224,12 +216,6 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing addEmail statement.")
stmts.addEmail, err = db.Prepare("INSERT INTO `emails`(`email`,`uid`,`validated`,`token`) VALUES (?,?,?,?)")
if err != nil {
return err
}
log.Print("Preparing addForumPermsToForum statement.")
stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) VALUES (?,?,?,?)")
if err != nil {

View File

@ -17,8 +17,8 @@ var ErrNoRoute = errors.New("That route doesn't exist.")
// TODO: What about the /uploads/ route? x.x
var RouteMap = map[string]interface{}{
"routeAPI": routeAPI,
"routeOverview": routeOverview,
"routeCustomPage": routeCustomPage,
"routes.Overview": routes.Overview,
"routes.CustomPage": routes.CustomPage,
"routeForums": routeForums,
"routeForum": routeForum,
"routeChangeTheme": routeChangeTheme,
@ -92,7 +92,7 @@ var RouteMap = map[string]interface{}{
"routes.UnlockTopicSubmit": routes.UnlockTopicSubmit,
"routes.MoveTopicSubmit": routes.MoveTopicSubmit,
"routeLikeTopicSubmit": routeLikeTopicSubmit,
"routeTopicID": routeTopicID,
"routes.ViewTopic": routes.ViewTopic,
"routeCreateReplySubmit": routeCreateReplySubmit,
"routes.ReplyEditSubmit": routes.ReplyEditSubmit,
"routes.ReplyDeleteSubmit": routes.ReplyDeleteSubmit,
@ -102,11 +102,11 @@ var RouteMap = map[string]interface{}{
"routes.ProfileReplyDeleteSubmit": routes.ProfileReplyDeleteSubmit,
"routes.PollVote": routes.PollVote,
"routes.PollResults": routes.PollResults,
"routeLogin": routeLogin,
"routeRegister": routeRegister,
"routes.AccountLogin": routes.AccountLogin,
"routes.AccountRegister": routes.AccountRegister,
"routeLogout": routeLogout,
"routeLoginSubmit": routeLoginSubmit,
"routeRegisterSubmit": routeRegisterSubmit,
"routes.AccountLoginSubmit": routes.AccountLoginSubmit,
"routes.AccountRegisterSubmit": routes.AccountRegisterSubmit,
"routeDynamic": routeDynamic,
"routeUploads": routeUploads,
"BadRoute": BadRoute,
@ -115,8 +115,8 @@ var RouteMap = map[string]interface{}{
// ! NEVER RELY ON THESE REMAINING THE SAME BETWEEN COMMITS
var routeMapEnum = map[string]int{
"routeAPI": 0,
"routeOverview": 1,
"routeCustomPage": 2,
"routes.Overview": 1,
"routes.CustomPage": 2,
"routeForums": 3,
"routeForum": 4,
"routeChangeTheme": 5,
@ -190,7 +190,7 @@ var routeMapEnum = map[string]int{
"routes.UnlockTopicSubmit": 73,
"routes.MoveTopicSubmit": 74,
"routeLikeTopicSubmit": 75,
"routeTopicID": 76,
"routes.ViewTopic": 76,
"routeCreateReplySubmit": 77,
"routes.ReplyEditSubmit": 78,
"routes.ReplyDeleteSubmit": 79,
@ -200,19 +200,19 @@ var routeMapEnum = map[string]int{
"routes.ProfileReplyDeleteSubmit": 83,
"routes.PollVote": 84,
"routes.PollResults": 85,
"routeLogin": 86,
"routeRegister": 87,
"routes.AccountLogin": 86,
"routes.AccountRegister": 87,
"routeLogout": 88,
"routeLoginSubmit": 89,
"routeRegisterSubmit": 90,
"routes.AccountLoginSubmit": 89,
"routes.AccountRegisterSubmit": 90,
"routeDynamic": 91,
"routeUploads": 92,
"BadRoute": 93,
}
var reverseRouteMapEnum = map[int]string{
0: "routeAPI",
1: "routeOverview",
2: "routeCustomPage",
1: "routes.Overview",
2: "routes.CustomPage",
3: "routeForums",
4: "routeForum",
5: "routeChangeTheme",
@ -286,7 +286,7 @@ var reverseRouteMapEnum = map[int]string{
73: "routes.UnlockTopicSubmit",
74: "routes.MoveTopicSubmit",
75: "routeLikeTopicSubmit",
76: "routeTopicID",
76: "routes.ViewTopic",
77: "routeCreateReplySubmit",
78: "routes.ReplyEditSubmit",
79: "routes.ReplyDeleteSubmit",
@ -296,11 +296,11 @@ var reverseRouteMapEnum = map[int]string{
83: "routes.ProfileReplyDeleteSubmit",
84: "routes.PollVote",
85: "routes.PollResults",
86: "routeLogin",
87: "routeRegister",
86: "routes.AccountLogin",
87: "routes.AccountRegister",
88: "routeLogout",
89: "routeLoginSubmit",
90: "routeRegisterSubmit",
89: "routes.AccountLoginSubmit",
90: "routes.AccountRegisterSubmit",
91: "routeDynamic",
92: "routeUploads",
93: "BadRoute",
@ -313,18 +313,22 @@ var agentMapEnum = map[string]int{
"safari": 4,
"edge": 5,
"internetexplorer": 6,
"googlebot": 7,
"yandex": 8,
"bing": 9,
"baidu": 10,
"duckduckgo": 11,
"discord": 12,
"cloudflarealwayson": 13,
"uptimebot": 14,
"lynx": 15,
"blank": 16,
"malformed": 17,
"suspicious": 18,
"androidchrome": 7,
"mobilesafari": 8,
"ucbrowser": 9,
"googlebot": 10,
"yandex": 11,
"bing": 12,
"baidu": 13,
"duckduckgo": 14,
"discord": 15,
"cloudflare": 16,
"uptimebot": 17,
"lynx": 18,
"blank": 19,
"malformed": 20,
"suspicious": 21,
"zgrab": 22,
}
var reverseAgentMapEnum = map[int]string{
0: "unknown",
@ -334,19 +338,51 @@ var reverseAgentMapEnum = map[int]string{
4: "safari",
5: "edge",
6: "internetexplorer",
7: "googlebot",
8: "yandex",
9: "bing",
10: "baidu",
11: "duckduckgo",
12: "discord",
13: "cloudflarealwayson",
14: "uptimebot",
15: "lynx",
16: "blank",
17: "malformed",
18: "suspicious",
7: "androidchrome",
8: "mobilesafari",
9: "ucbrowser",
10: "googlebot",
11: "yandex",
12: "bing",
13: "baidu",
14: "duckduckgo",
15: "discord",
16: "cloudflare",
17: "uptimebot",
18: "lynx",
19: "blank",
20: "malformed",
21: "suspicious",
22: "zgrab",
}
var markToAgent = map[string]string{
"OPR":"opera",
"Chrome":"chrome",
"Firefox":"firefox",
"MSIE":"internetexplorer",
//"Trident":"internetexplorer",
"Edge":"edge",
"Lynx":"lynx", // There's a rare android variant of lynx which isn't covered by this
"UCBrowser":"ucbrowser",
"Google":"googlebot",
"Googlebot":"googlebot",
"yandex": "yandex", // from the URL
"DuckDuckBot":"duckduckgo",
"Baiduspider":"baidu",
"bingbot":"bing",
"BingPreview":"bing",
"CloudFlare":"cloudflare", // Track alwayson specifically in case there are other bots?
"Uptimebot":"uptimebot",
"Discordbot":"discord",
"zgrab":"zgrab",
}
/*var agentRank = map[string]int{
"opera":9,
"chrome":8,
"safari":1,
}*/
// TODO: Stop spilling these into the package scope?
func init() {
@ -420,7 +456,7 @@ func (router *GenRouter) DumpRequest(req *http.Request) {
func (router *GenRouter) SuspiciousRequest(req *http.Request) {
log.Print("Suspicious Request")
router.DumpRequest(req)
common.AgentViewCounter.Bump(18)
common.AgentViewCounter.Bump(21)
}
// TODO: Pass the default route or config struct to the router rather than accessing it via a package global
@ -448,7 +484,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Write([]byte(""))
log.Print("Malformed Request")
router.DumpRequest(req)
common.AgentViewCounter.Bump(17)
common.AgentViewCounter.Bump(20)
return
}
@ -475,7 +511,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
if common.Dev.SuperDebug {
log.Print("before routeStatic")
log.Print("before routes.StaticFile")
log.Print("Method: ", req.Method)
for key, value := range req.Header {
for _, vvalue := range value {
@ -493,7 +529,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if prefix == "/static" {
req.URL.Path += extraData
routeStatic(w, req)
routes.StaticFile(w, req)
return
}
if common.Dev.SuperDebug {
@ -506,49 +542,65 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Track the user agents. Unfortunately, everyone pretends to be Mozilla, so this'll be a little less efficient than I would like.
// TODO: Add a setting to disable this?
// TODO: Use a more efficient detector instead of smashing every possible combination in
ua := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(req.UserAgent(),"Mozilla/5.0 ")," Safari/537.36")) // Noise, no one's going to be running this and it complicates implementing an efficient UA parser, particularly the more efficient right-to-left one I have in mind
switch {
case strings.Contains(ua,"Google"):
common.AgentViewCounter.Bump(7)
case strings.Contains(ua,"Yandex"):
common.AgentViewCounter.Bump(8)
case strings.Contains(ua,"bingbot"), strings.Contains(ua,"BingPreview"):
common.AgentViewCounter.Bump(9)
case strings.Contains(ua,"OPR"): // Pretends to be Chrome, needs to run before that
common.AgentViewCounter.Bump(3)
case strings.Contains(ua,"Edge"):
common.AgentViewCounter.Bump(5)
case strings.Contains(ua,"Chrome"):
common.AgentViewCounter.Bump(2)
case strings.Contains(ua,"Firefox"):
common.AgentViewCounter.Bump(1)
case strings.Contains(ua,"Safari"):
common.AgentViewCounter.Bump(4)
case strings.Contains(ua,"MSIE"):
common.AgentViewCounter.Bump(6)
case strings.Contains(ua,"Baiduspider"):
common.AgentViewCounter.Bump(10)
case strings.Contains(ua,"DuckDuckBot"):
common.AgentViewCounter.Bump(11)
case strings.Contains(ua,"Discordbot"):
common.AgentViewCounter.Bump(12)
case strings.Contains(ua,"Lynx"):
common.AgentViewCounter.Bump(15)
case strings.Contains(ua,"CloudFlare-AlwaysOnline"):
common.AgentViewCounter.Bump(13)
case strings.Contains(ua,"Uptimebot"):
common.AgentViewCounter.Bump(14)
case ua == "":
common.AgentViewCounter.Bump(16)
ua := strings.TrimSpace(strings.Replace(strings.TrimPrefix(req.UserAgent(),"Mozilla/5.0 ")," Safari/537.36","",-1)) // Noise, no one's going to be running this and it would require some sort of agent ranking system to determine which identifier should be prioritised over another
if ua == "" {
common.AgentViewCounter.Bump(19)
if common.Dev.DebugMode {
log.Print("Blank UA: ", req.UserAgent())
router.DumpRequest(req)
}
default:
common.AgentViewCounter.Bump(0)
} else {
// WIP UA Parser
var indices []int
var items []string
var buffer []rune
for index, item := range ua {
if (item > 64 && item < 91) || (item > 96 && item < 123) {
buffer = append(buffer, item)
} else if len(buffer) != 0 {
items = append(items, string(buffer))
indices = append(indices, index - 1)
buffer = buffer[:0]
}
}
// Iterate over this in reverse as the real UA tends to be on the right side
var agent string
for i := len(items) - 1; i >= 0; i-- {
fAgent, ok := markToAgent[items[i]]
if ok {
agent = fAgent
if agent != "safari" {
break
}
}
}
if common.Dev.DebugMode {
log.Print("Unknown UA: ", req.UserAgent())
router.DumpRequest(req)
log.Print("parsed agent: ",agent)
}
// Special handling
switch(agent) {
case "chrome":
for _, mark := range items {
if mark == "Android" {
agent = "androidchrome"
break
}
}
case "zgrab":
router.SuspiciousRequest(req)
}
if agent == "" {
common.AgentViewCounter.Bump(0)
if common.Dev.DebugMode {
log.Print("Unknown UA: ", req.UserAgent())
router.DumpRequest(req)
}
} else {
common.AgentViewCounter.Bump(agentMapEnum[agent])
}
}
@ -572,13 +624,13 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
case "/overview":
common.RouteViewCounter.Bump(1)
err = routeOverview(w,req,user)
err = routes.Overview(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
}
case "/pages":
common.RouteViewCounter.Bump(2)
err = routeCustomPage(w,req,user,extraData)
err = routes.CustomPage(w,req,user,extraData)
if err != nil {
router.handleError(err,w,req,user)
}
@ -1278,7 +1330,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
err = routeLikeTopicSubmit(w,req,user,extraData)
default:
common.RouteViewCounter.Bump(76)
err = routeTopicID(w,req,user, extraData)
err = routes.ViewTopic(w,req,user, extraData)
}
if err != nil {
router.handleError(err,w,req,user)
@ -1433,10 +1485,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch(req.URL.Path) {
case "/accounts/login/":
common.RouteViewCounter.Bump(86)
err = routeLogin(w,req,user)
err = routes.AccountLogin(w,req,user)
case "/accounts/create/":
common.RouteViewCounter.Bump(87)
err = routeRegister(w,req,user)
err = routes.AccountRegister(w,req,user)
case "/accounts/logout/":
err = common.NoSessionMismatch(w,req,user)
if err != nil {
@ -1460,7 +1512,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
common.RouteViewCounter.Bump(89)
err = routeLoginSubmit(w,req,user)
err = routes.AccountLoginSubmit(w,req,user)
case "/accounts/create/submit/":
err = common.ParseForm(w,req,user)
if err != nil {
@ -1469,7 +1521,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
common.RouteViewCounter.Bump(90)
err = routeRegisterSubmit(w,req,user)
err = routes.AccountRegisterSubmit(w,req,user)
}
if err != nil {
router.handleError(err,w,req,user)
@ -1537,8 +1589,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
// TODO: Log all bad routes for the admin to figure out where users are going wrong?
lowerPath := strings.ToLower(req.URL.Path)
if strings.Contains(lowerPath,"admin") || strings.Contains(lowerPath,"sql") || strings.Contains(lowerPath,"manage") {
if strings.Contains(lowerPath,"admin") || strings.Contains(lowerPath,"sql") || strings.Contains(lowerPath,"manage") || strings.Contains(lowerPath,"//") || strings.Contains(lowerPath,"\\\\") {
router.SuspiciousRequest(req)
}
common.RouteViewCounter.Bump(93)

View File

@ -107,11 +107,61 @@ func routeCreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.
content := common.PreparseMessage(r.PostFormValue("reply-content"))
// TODO: Fully parse the post and put that in the parsed column
_, err = common.Rstore.Create(topic, content, user.LastIP, user.ID)
rid, err := common.Rstore.Create(topic, content, user.LastIP, user.ID)
if err != nil {
return common.InternalError(err, w, r)
}
reply, err := common.Rstore.Get(rid)
if err != nil {
return common.LocalError("Unable to load the reply", w, r, user)
}
if r.PostFormValue("has_poll") == "1" {
var maxPollOptions = 10
var pollInputItems = make(map[int]string)
for key, values := range r.Form {
//if common.Dev.SuperDebug {
log.Print("key: ", key)
log.Printf("values: %+v\n", values)
//}
for _, value := range values {
if strings.HasPrefix(key, "pollinputitem[") {
halves := strings.Split(key, "[")
if len(halves) != 2 {
return common.LocalError("Malformed pollinputitem", w, r, user)
}
halves[1] = strings.TrimSuffix(halves[1], "]")
index, err := strconv.Atoi(halves[1])
if err != nil {
return common.LocalError("Malformed pollinputitem", w, r, user)
}
// If there are duplicates, then something has gone horribly wrong, so let's ignore them, this'll likely happen during an attack
_, exists := pollInputItems[index]
if !exists && len(html.EscapeString(value)) != 0 {
pollInputItems[index] = html.EscapeString(value)
if len(pollInputItems) >= maxPollOptions {
break
}
}
}
}
}
// Make sure the indices are sequential to avoid out of bounds issues
var seqPollInputItems = make(map[int]string)
for i := 0; i < len(pollInputItems); i++ {
seqPollInputItems[i] = pollInputItems[i]
}
pollType := 0 // Basic single choice
_, err := common.Polls.Create(reply, pollType, seqPollInputItems)
if err != nil {
return common.LocalError("Failed to add poll to reply", w, r, user) // TODO: Might need to be an internal error as it could leave phantom polls?
}
}
err = common.Forums.UpdateLastTopic(tid, user.ID, topic.ParentID)
if err != nil && err != ErrNoRows {
return common.InternalError(err, w, r)

View File

@ -1501,14 +1501,7 @@ func routePanelUsers(w http.ResponseWriter, r *http.Request, user common.User) c
}
puser.InitPerms()
if puser.Avatar != "" {
if puser.Avatar[0] == '.' {
puser.Avatar = "/uploads/avatar_" + strconv.Itoa(puser.ID) + puser.Avatar
}
} else {
puser.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(puser.ID), 1)
}
puser.Avatar = common.BuildAvatar(puser.ID, puser.Avatar)
if common.Groups.DirtyGet(puser.Group).Tag != "" {
puser.Tag = common.Groups.DirtyGet(puser.Group).Tag
} else {

View File

@ -590,7 +590,7 @@ $(document).ready(function(){
if(dataPollInput == undefined) return;
if(dataPollInput != (pollInputIndex-1)) return;
$(".poll_content_row .formitem").append("<div class='pollinput' data-pollinput='"+pollInputIndex+"'><input type='checkbox' disabled /><label class='pollinputlabel'></label><input form='topic_create_form_form' name='pollinputitem["+pollInputIndex+"]' class='pollinputinput' type='text' placeholder='Add new poll option' /></div>");
$(".poll_content_row .formitem").append("<div class='pollinput' data-pollinput='"+pollInputIndex+"'><input type='checkbox' disabled /><label class='pollinputlabel'></label><input form='quick_post_form' name='pollinputitem["+pollInputIndex+"]' class='pollinputinput' type='text' placeholder='Add new poll option' /></div>");
pollInputIndex++;
console.log("new pollInputIndex: ", pollInputIndex);
$(".pollinputinput").off("click");
@ -605,9 +605,11 @@ $(document).ready(function(){
$(".pollinputinput").click(addPollInput);
});
//id="poll_results_{{.Poll.ID}}" class="poll_results auto_hide"
$(".poll_results_button").click(function(){
let pollID = $(this).attr("data-poll-id");
$("#poll_results_" + pollID + " .user_content").html("<div id='poll_results_chart_"+pollID+"'></div>");
$("#poll_results_" + pollID).removeClass("auto_hide");
fetch("/poll/results/" + pollID, {
credentials: 'same-origin'
}).then((response) => response.text()).catch((error) => console.error("Error:",error)).then((rawData) => {
@ -618,7 +620,7 @@ $(document).ready(function(){
Chartist.Pie('#poll_results_chart_' + pollID, {
series: data,
}, {
height: '100%',
height: '120px',
});
})
});

View File

@ -123,6 +123,14 @@ func (insert *accInsertBuilder) Prepare() *sql.Stmt {
return insert.build.SimpleInsert(insert.table, insert.columns, insert.fields)
}
func (insert *accInsertBuilder) Exec(args ...interface{}) (res sql.Result, err error) {
stmt := insert.Prepare()
if stmt != nil {
return stmt.Exec(args...)
}
return res, insert.build.FirstError()
}
type accCountBuilder struct {
table string
where string

View File

@ -255,8 +255,6 @@ func writeSelects(adapter qgen.Adapter) error {
}
func writeLeftJoins(adapter qgen.Adapter) error {
adapter.SimpleLeftJoin("getTopicRepliesOffset", "replies", "users", "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", "replies.createdBy = users.uid", "replies.tid = ?", "replies.rid ASC", "?,?")
adapter.SimpleLeftJoin("getTopicList", "topics", "users", "topics.tid, topics.title, topics.content, topics.createdBy, topics.is_closed, topics.sticky, topics.createdAt, topics.parentID, users.name, users.avatar", "topics.createdBy = users.uid", "", "topics.sticky DESC, topics.lastReplyAt DESC, topics.createdBy DESC", "")
adapter.SimpleLeftJoin("getTopicReplies", "replies", "users", "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.createdBy = users.uid", "tid = ?", "", "")
@ -286,8 +284,6 @@ func writeInserts(adapter qgen.Adapter) error {
build.Insert("notifyOne").Table("activity_stream_matches").Columns("watcher, asid").Fields("?,?").Parse()
build.Insert("addEmail").Table("emails").Columns("email, uid, validated, token").Fields("?,?,?,?").Parse()
build.Insert("addForumPermsToForum").Table("forums_permissions").Columns("gid,fid,preset,permissions").Fields("?,?,?,?").Parse()
build.Insert("addPlugin").Table("plugins").Columns("uname, active, installed").Fields("?,?,?").Parse()

View File

@ -167,18 +167,23 @@ func main() {
"edge",
"internetexplorer",
"androidchrome",
"mobilesafari", // Coming soon
"ucbrowser",
"googlebot",
"yandex",
"bing",
"baidu",
"duckduckgo",
"discord",
"cloudflarealwayson",
"cloudflare",
"uptimebot",
"lynx",
"blank",
"malformed",
"suspicious",
"zgrab",
}
tmplVars.AllAgentMap = make(map[string]int)
@ -220,6 +225,34 @@ var agentMapEnum = map[string]int{ {{range $index, $element := .AllAgentNames}}
var reverseAgentMapEnum = map[int]string{ {{range $index, $element := .AllAgentNames}}
{{$index}}: "{{$element}}",{{end}}
}
var markToAgent = map[string]string{
"OPR":"opera",
"Chrome":"chrome",
"Firefox":"firefox",
"MSIE":"internetexplorer",
//"Trident":"internetexplorer",
"Edge":"edge",
"Lynx":"lynx", // There's a rare android variant of lynx which isn't covered by this
"UCBrowser":"ucbrowser",
"Google":"googlebot",
"Googlebot":"googlebot",
"yandex": "yandex", // from the URL
"DuckDuckBot":"duckduckgo",
"Baiduspider":"baidu",
"bingbot":"bing",
"BingPreview":"bing",
"CloudFlare":"cloudflare", // Track alwayson specifically in case there are other bots?
"Uptimebot":"uptimebot",
"Discordbot":"discord",
"zgrab":"zgrab",
}
/*var agentRank = map[string]int{
"opera":9,
"chrome":8,
"safari":1,
}*/
// TODO: Stop spilling these into the package scope?
func init() {
@ -348,7 +381,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
if common.Dev.SuperDebug {
log.Print("before routeStatic")
log.Print("before routes.StaticFile")
log.Print("Method: ", req.Method)
for key, value := range req.Header {
for _, vvalue := range value {
@ -366,7 +399,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if prefix == "/static" {
req.URL.Path += extraData
routeStatic(w, req)
routes.StaticFile(w, req)
return
}
if common.Dev.SuperDebug {
@ -379,49 +412,65 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Track the user agents. Unfortunately, everyone pretends to be Mozilla, so this'll be a little less efficient than I would like.
// TODO: Add a setting to disable this?
// TODO: Use a more efficient detector instead of smashing every possible combination in
ua := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(req.UserAgent(),"Mozilla/5.0 ")," Safari/537.36")) // Noise, no one's going to be running this and it complicates implementing an efficient UA parser, particularly the more efficient right-to-left one I have in mind
switch {
case strings.Contains(ua,"Google"):
common.AgentViewCounter.Bump({{.AllAgentMap.googlebot}})
case strings.Contains(ua,"Yandex"):
common.AgentViewCounter.Bump({{.AllAgentMap.yandex}})
case strings.Contains(ua,"bingbot"), strings.Contains(ua,"BingPreview"):
common.AgentViewCounter.Bump({{.AllAgentMap.bing}})
case strings.Contains(ua,"OPR"): // Pretends to be Chrome, needs to run before that
common.AgentViewCounter.Bump({{.AllAgentMap.opera}})
case strings.Contains(ua,"Edge"):
common.AgentViewCounter.Bump({{.AllAgentMap.edge}})
case strings.Contains(ua,"Chrome"):
common.AgentViewCounter.Bump({{.AllAgentMap.chrome}})
case strings.Contains(ua,"Firefox"):
common.AgentViewCounter.Bump({{.AllAgentMap.firefox}})
case strings.Contains(ua,"Safari"):
common.AgentViewCounter.Bump({{.AllAgentMap.safari}})
case strings.Contains(ua,"MSIE"):
common.AgentViewCounter.Bump({{.AllAgentMap.internetexplorer}})
case strings.Contains(ua,"Baiduspider"):
common.AgentViewCounter.Bump({{.AllAgentMap.baidu}})
case strings.Contains(ua,"DuckDuckBot"):
common.AgentViewCounter.Bump({{.AllAgentMap.duckduckgo}})
case strings.Contains(ua,"Discordbot"):
common.AgentViewCounter.Bump({{.AllAgentMap.discord}})
case strings.Contains(ua,"Lynx"):
common.AgentViewCounter.Bump({{.AllAgentMap.lynx}})
case strings.Contains(ua,"CloudFlare-AlwaysOnline"):
common.AgentViewCounter.Bump({{.AllAgentMap.cloudflarealwayson}})
case strings.Contains(ua,"Uptimebot"):
common.AgentViewCounter.Bump({{.AllAgentMap.uptimebot}})
case ua == "":
ua := strings.TrimSpace(strings.Replace(strings.TrimPrefix(req.UserAgent(),"Mozilla/5.0 ")," Safari/537.36","",-1)) // Noise, no one's going to be running this and it would require some sort of agent ranking system to determine which identifier should be prioritised over another
if ua == "" {
common.AgentViewCounter.Bump({{.AllAgentMap.blank}})
if common.Dev.DebugMode {
log.Print("Blank UA: ", req.UserAgent())
router.DumpRequest(req)
}
default:
common.AgentViewCounter.Bump({{.AllAgentMap.unknown}})
} else {
// WIP UA Parser
var indices []int
var items []string
var buffer []rune
for index, item := range ua {
if (item > 64 && item < 91) || (item > 96 && item < 123) {
buffer = append(buffer, item)
} else if len(buffer) != 0 {
items = append(items, string(buffer))
indices = append(indices, index - 1)
buffer = buffer[:0]
}
}
// Iterate over this in reverse as the real UA tends to be on the right side
var agent string
for i := len(items) - 1; i >= 0; i-- {
fAgent, ok := markToAgent[items[i]]
if ok {
agent = fAgent
if agent != "safari" {
break
}
}
}
if common.Dev.DebugMode {
log.Print("Unknown UA: ", req.UserAgent())
router.DumpRequest(req)
log.Print("parsed agent: ",agent)
}
// Special handling
switch(agent) {
case "chrome":
for _, mark := range items {
if mark == "Android" {
agent = "androidchrome"
break
}
}
case "zgrab":
router.SuspiciousRequest(req)
}
if agent == "" {
common.AgentViewCounter.Bump({{.AllAgentMap.unknown}})
if common.Dev.DebugMode {
log.Print("Unknown UA: ", req.UserAgent())
router.DumpRequest(req)
}
} else {
common.AgentViewCounter.Bump(agentMapEnum[agent])
}
}
@ -500,8 +549,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
// TODO: Log all bad routes for the admin to figure out where users are going wrong?
lowerPath := strings.ToLower(req.URL.Path)
if strings.Contains(lowerPath,"admin") || strings.Contains(lowerPath,"sql") || strings.Contains(lowerPath,"manage") {
if strings.Contains(lowerPath,"admin") || strings.Contains(lowerPath,"sql") || strings.Contains(lowerPath,"manage") || strings.Contains(lowerPath,"//") || strings.Contains(lowerPath,"\\\\") {
router.SuspiciousRequest(req)
}
common.RouteViewCounter.Bump({{.AllRouteMap.BadRoute}})

View File

@ -3,8 +3,8 @@ package main
// TODO: How should we handle headerLite and headerVar?
func routes() {
addRoute(View("routeAPI", "/api/"))
addRoute(View("routeOverview", "/overview/"))
addRoute(View("routeCustomPage", "/pages/", "extraData"))
addRoute(View("routes.Overview", "/overview/"))
addRoute(View("routes.CustomPage", "/pages/", "extraData"))
addRoute(View("routeForums", "/forums/" /*,"&forums"*/))
addRoute(View("routeForum", "/forum/", "extraData"))
addRoute(AnonAction("routeChangeTheme", "/theme/"))
@ -65,7 +65,7 @@ func buildUserRoutes() {
func buildTopicRoutes() {
topicGroup := newRouteGroup("/topic/")
topicGroup.Routes(
View("routeTopicID", "/topic/", "extraData"),
View("routes.ViewTopic", "/topic/", "extraData"),
UploadAction("routes.CreateTopicSubmit", "/topic/create/submit/").MaxSizeVar("common.Config.MaxRequestSize"),
Action("routes.EditTopicSubmit", "/topic/edit/submit/", "extraData"),
Action("routes.DeleteTopicSubmit", "/topic/delete/submit/").LitBefore("req.URL.Path += extraData"),
@ -118,11 +118,11 @@ func buildAccountRoutes() {
//router.HandleFunc("/accounts/list/", routeLogin) // Redirect /accounts/ and /user/ to here.. // Get a list of all of the accounts on the forum
accReplyGroup := newRouteGroup("/accounts/")
accReplyGroup.Routes(
View("routeLogin", "/accounts/login/"),
View("routeRegister", "/accounts/create/"),
View("routes.AccountLogin", "/accounts/login/"),
View("routes.AccountRegister", "/accounts/create/"),
Action("routeLogout", "/accounts/logout/"),
AnonAction("routeLoginSubmit", "/accounts/login/submit/"), // TODO: Guard this with a token, maybe the IP hashed with a rotated key?
AnonAction("routeRegisterSubmit", "/accounts/create/submit/"),
AnonAction("routes.AccountLoginSubmit", "/accounts/login/submit/"), // TODO: Guard this with a token, maybe the IP hashed with a rotated key?
AnonAction("routes.AccountRegisterSubmit", "/accounts/create/submit/"),
)
addRouteGroup(accReplyGroup)
}

431
routes.go
View File

@ -7,9 +7,7 @@
package main
import (
"bytes"
"html"
"io"
"log"
"net/http"
"strconv"
@ -25,7 +23,6 @@ var tList []interface{}
//var nList []string
var successJSONBytes = []byte(`{"success":"1"}`)
var cacheControlMaxAge = "max-age=" + strconv.Itoa(common.Day) // TODO: Make this a common.Config value
// HTTPSRedirect is a connection handler which redirects all HTTP requests to HTTPS
type HTTPSRedirect struct {
@ -48,87 +45,6 @@ func routeUploads() {
func BadRoute() {
}
// GET functions
func routeStatic(w http.ResponseWriter, r *http.Request) {
file, ok := common.StaticFiles.Get(r.URL.Path)
if !ok {
if common.Dev.DebugMode {
log.Printf("Failed to find '%s'", r.URL.Path)
}
w.WriteHeader(http.StatusNotFound)
return
}
h := w.Header()
// Surely, there's a more efficient way of doing this?
t, err := time.Parse(http.TimeFormat, h.Get("If-Modified-Since"))
if err == nil && file.Info.ModTime().Before(t.Add(1*time.Second)) {
w.WriteHeader(http.StatusNotModified)
return
}
h.Set("Last-Modified", file.FormattedModTime)
h.Set("Content-Type", file.Mimetype)
h.Set("Cache-Control", cacheControlMaxAge) //Cache-Control: max-age=31536000
h.Set("Vary", "Accept-Encoding")
if strings.Contains(h.Get("Accept-Encoding"), "gzip") {
h.Set("Content-Encoding", "gzip")
h.Set("Content-Length", strconv.FormatInt(file.GzipLength, 10))
io.Copy(w, bytes.NewReader(file.GzipData)) // Use w.Write instead?
} else {
h.Set("Content-Length", strconv.FormatInt(file.Length, 10)) // Avoid doing a type conversion every time?
io.Copy(w, bytes.NewReader(file.Data))
}
// Other options instead of io.Copy: io.CopyN(), w.Write(), http.ServeContent()
}
func routeOverview(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
return ferr
}
headerVars.Zone = "overview"
pi := common.Page{common.GetTitlePhrase("overview"), user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_overview"] != nil {
if common.RunPreRenderHook("pre_render_overview", w, r, &user, &pi) {
return nil
}
}
err := common.Templates.ExecuteTemplate(w, "overview.html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func routeCustomPage(w http.ResponseWriter, r *http.Request, user common.User, name string) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
return ferr
}
// ! Is this safe?
if common.Templates.Lookup("page_"+name+".html") == nil {
return common.NotFound(w, r)
}
headerVars.Zone = "custom_page"
pi := common.Page{common.GetTitlePhrase("page"), user, headerVars, tList, nil}
// TODO: Pass the page name to the pre-render hook?
if common.PreRenderHooks["pre_render_custom_page"] != nil {
if common.RunPreRenderHook("pre_render_custom_page", w, r, &user, &pi) {
return nil
}
}
err := common.Templates.ExecuteTemplate(w, "page_"+name+".html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
@ -420,181 +336,6 @@ func routeForums(w http.ResponseWriter, r *http.Request, user common.User) commo
return nil
}
func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User, urlBit string) common.RouteError {
var err error
var replyList []common.ReplyUser
page, _ := strconv.Atoi(r.FormValue("page"))
// SEO URLs...
// TODO: Make a shared function for this
halves := strings.Split(urlBit, ".")
if len(halves) < 2 {
halves = append(halves, halves[0])
}
tid, err := strconv.Atoi(halves[1])
if err != nil {
return common.PreError("The provided TopicID is not a valid number.", w, r)
}
// Get the topic...
topic, err := common.GetTopicUser(tid)
if err == ErrNoRows {
return common.NotFound(w, r)
} else if err != nil {
return common.InternalError(err, w, r)
}
topic.ClassName = ""
//log.Printf("topic: %+v\n", topic)
headerVars, ferr := common.ForumUserCheck(w, r, &user, topic.ParentID)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic {
//log.Printf("user.Perms: %+v\n", user.Perms)
return common.NoPermissions(w, r, user)
}
headerVars.Zone = "view_topic"
// TODO: Only include these on pages with polls
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums")
topic.ContentLines = strings.Count(topic.Content, "\n")
// We don't want users posting in locked topics...
if topic.IsClosed && !user.IsMod {
user.Perms.CreateReply = false
}
postGroup, err := common.Groups.Get(topic.Group)
if err != nil {
return common.InternalError(err, w, r)
}
topic.Tag = postGroup.Tag
if postGroup.IsMod || postGroup.IsAdmin {
topic.ClassName = common.Config.StaffCSS
}
topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt)
// TODO: Make a function for this? Build a more sophisticated noavatar handling system?
if topic.Avatar != "" {
if topic.Avatar[0] == '.' {
topic.Avatar = "/uploads/avatar_" + strconv.Itoa(topic.CreatedBy) + topic.Avatar
}
} else {
topic.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(topic.CreatedBy), 1)
}
var poll common.Poll
if topic.Poll != 0 {
pPoll, err := common.Polls.Get(topic.Poll)
if err != nil {
log.Print("Couldn't find the attached poll for topic " + strconv.Itoa(topic.ID))
return common.InternalError(err, w, r)
}
poll = pPoll.Copy()
}
// Calculate the offset
offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage)
tpage := common.TopicPage{topic.Title, user, headerVars, replyList, topic, poll, page, lastPage}
// Get the replies..
rows, err := stmts.getTopicRepliesOffset.Query(topic.ID, offset, common.Config.ItemsPerPage)
if err == ErrNoRows {
return common.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
defer rows.Close()
replyItem := common.ReplyUser{ClassName: ""}
for rows.Next() {
err := rows.Scan(&replyItem.ID, &replyItem.Content, &replyItem.CreatedBy, &replyItem.CreatedAt, &replyItem.LastEdit, &replyItem.LastEditBy, &replyItem.Avatar, &replyItem.CreatedByName, &replyItem.Group, &replyItem.URLPrefix, &replyItem.URLName, &replyItem.Level, &replyItem.IPAddress, &replyItem.LikeCount, &replyItem.ActionType)
if err != nil {
return common.InternalError(err, w, r)
}
replyItem.UserLink = common.BuildProfileURL(common.NameToSlug(replyItem.CreatedByName), replyItem.CreatedBy)
replyItem.ParentID = topic.ID
replyItem.ContentHtml = common.ParseMessage(replyItem.Content, topic.ParentID, "forums")
replyItem.ContentLines = strings.Count(replyItem.Content, "\n")
postGroup, err = common.Groups.Get(replyItem.Group)
if err != nil {
return common.InternalError(err, w, r)
}
if postGroup.IsMod || postGroup.IsAdmin {
replyItem.ClassName = common.Config.StaffCSS
} else {
replyItem.ClassName = ""
}
// TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the common.UserStore initialise this?
if replyItem.Avatar != "" {
if replyItem.Avatar[0] == '.' {
replyItem.Avatar = "/uploads/avatar_" + strconv.Itoa(replyItem.CreatedBy) + replyItem.Avatar
}
} else {
replyItem.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(replyItem.CreatedBy), 1)
}
replyItem.Tag = postGroup.Tag
replyItem.RelativeCreatedAt = common.RelativeTime(replyItem.CreatedAt)
// We really shouldn't have inline HTML, we should do something about this...
if replyItem.ActionType != "" {
switch replyItem.ActionType {
case "lock":
replyItem.ActionType = "This topic has been locked by <a href='" + replyItem.UserLink + "'>" + replyItem.CreatedByName + "</a>"
replyItem.ActionIcon = "&#x1F512;&#xFE0E"
case "unlock":
replyItem.ActionType = "This topic has been reopened by <a href='" + replyItem.UserLink + "'>" + replyItem.CreatedByName + "</a>"
replyItem.ActionIcon = "&#x1F513;&#xFE0E"
case "stick":
replyItem.ActionType = "This topic has been pinned by <a href='" + replyItem.UserLink + "'>" + replyItem.CreatedByName + "</a>"
replyItem.ActionIcon = "&#x1F4CC;&#xFE0E"
case "unstick":
replyItem.ActionType = "This topic has been unpinned by <a href='" + replyItem.UserLink + "'>" + replyItem.CreatedByName + "</a>"
replyItem.ActionIcon = "&#x1F4CC;&#xFE0E"
case "move":
replyItem.ActionType = "This topic has been moved by <a href='" + replyItem.UserLink + "'>" + replyItem.CreatedByName + "</a>"
default:
replyItem.ActionType = replyItem.ActionType + " has happened"
replyItem.ActionIcon = ""
}
}
replyItem.Liked = false
if common.Vhooks["topic_reply_row_assign"] != nil {
common.RunVhook("topic_reply_row_assign", &tpage, &replyItem)
}
replyList = append(replyList, replyItem)
}
err = rows.Err()
if err != nil {
return common.InternalError(err, w, r)
}
tpage.ItemList = replyList
if common.PreRenderHooks["pre_render_view_topic"] != nil {
if common.RunPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) {
return nil
}
}
err = common.RunThemeTemplate(headerVars.Theme.Name, "topic", tpage, w)
if err != nil {
return common.InternalError(err, w, r)
}
common.TopicViewCounter.Bump(topic.ID) // TODO Move this into the router?
return nil
}
func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
@ -658,14 +399,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm
} else {
replyClassName = ""
}
if replyAvatar != "" {
if replyAvatar[0] == '.' {
replyAvatar = "/uploads/avatar_" + strconv.Itoa(replyCreatedBy) + replyAvatar
}
} else {
replyAvatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(replyCreatedBy), 1)
}
replyAvatar = common.BuildAvatar(replyCreatedBy, replyAvatar)
if group.Tag != "" {
replyTag = group.Tag
@ -703,169 +437,6 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm
return nil
}
func routeLogin(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
return ferr
}
if user.Loggedin {
return common.LocalError("You're already logged in.", w, r, user)
}
pi := common.Page{common.GetTitlePhrase("login"), user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_login"] != nil {
if common.RunPreRenderHook("pre_render_login", w, r, &user, &pi) {
return nil
}
}
err := common.Templates.ExecuteTemplate(w, "login.html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
// TODO: Log failed attempted logins?
// TODO: Lock IPS out if they have too many failed attempts?
// TODO: Log unusual countries in comparison to the country a user usually logs in from? Alert the user about this?
func routeLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
if user.Loggedin {
return common.LocalError("You're already logged in.", w, r, user)
}
username := html.EscapeString(strings.Replace(r.PostFormValue("username"), "\n", "", -1))
uid, err := common.Auth.Authenticate(username, r.PostFormValue("password"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
userPtr, err := common.Users.Get(uid)
if err != nil {
return common.LocalError("Bad account", w, r, user)
}
user = *userPtr
var session string
if user.Session == "" {
session, err = common.Auth.CreateSession(uid)
if err != nil {
return common.InternalError(err, w, r)
}
} else {
session = user.Session
}
common.Auth.SetCookies(w, uid, session)
if user.IsAdmin {
// Is this error check redundant? We already check for the error in PreRoute for the same IP
// TODO: Should we be logging this?
log.Printf("#%d has logged in with IP %s", uid, user.LastIP)
}
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}
func routeRegister(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
return ferr
}
if user.Loggedin {
return common.LocalError("You're already logged in.", w, r, user)
}
pi := common.Page{common.GetTitlePhrase("register"), user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_register"] != nil {
if common.RunPreRenderHook("pre_render_register", w, r, &user, &pi) {
return nil
}
}
err := common.Templates.ExecuteTemplate(w, "register.html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func routeRegisterSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerLite, _ := common.SimpleUserCheck(w, r, &user)
username := html.EscapeString(strings.Replace(r.PostFormValue("username"), "\n", "", -1))
if username == "" {
return common.LocalError("You didn't put in a username.", w, r, user)
}
email := html.EscapeString(strings.Replace(r.PostFormValue("email"), "\n", "", -1))
if email == "" {
return common.LocalError("You didn't put in an email.", w, r, user)
}
password := r.PostFormValue("password")
switch password {
case "":
return common.LocalError("You didn't put in a password.", w, r, user)
case username:
return common.LocalError("You can't use your username as your password.", w, r, user)
case email:
return common.LocalError("You can't use your email as your password.", w, r, user)
}
// ? Move this into Create()? What if we want to programatically set weak passwords for tests?
err := common.WeakPassword(password)
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
confirmPassword := r.PostFormValue("confirm_password")
if common.Dev.DebugMode {
log.Print("Registration Attempt! Username: " + username) // TODO: Add more controls over what is logged when?
}
// Do the two inputted passwords match..?
if password != confirmPassword {
return common.LocalError("The two passwords don't match.", w, r, user)
}
var active bool
var group int
switch headerLite.Settings["activation_type"] {
case 1: // Activate All
active = true
group = common.Config.DefaultGroup
default: // Anything else. E.g. Admin Activation or Email Activation.
group = common.Config.ActivationGroup
}
uid, err := common.Users.Create(username, password, email, group, active)
if err == common.ErrAccountExists {
return common.LocalError("This username isn't available. Try another.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
// Check if this user actually owns this email, if email activation is on, automatically flip their account to active when the email is validated. Validation is also useful for determining whether this user should receive any alerts, etc. via email
if common.Site.EnableEmails {
token, err := common.GenerateSafeString(80)
if err != nil {
return common.InternalError(err, w, r)
}
_, err = stmts.addEmail.Exec(email, uid, 0, token)
if err != nil {
return common.InternalError(err, w, r)
}
if !common.SendValidationEmail(username, email, token) {
return common.LocalError("We were unable to send the email for you to confirm that this email address belongs to you. You may not have access to some functionality until you do so. Please ask an administrator for assistance.", w, r, user)
}
}
session, err := common.Auth.CreateSession(uid)
if err != nil {
return common.InternalError(err, w, r)
}
common.Auth.SetCookies(w, uid, session)
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}
// TODO: Set the cookie domain
func routeChangeTheme(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
//headerLite, _ := SimpleUserCheck(w, r, &user)

View File

@ -1,14 +1,185 @@
package routes
import (
"html"
"log"
"net/http"
"strings"
"../common"
"../query_gen/lib"
)
// A blank list to fill out that parameter in Page for routes which don't use it
var tList []interface{}
func AccountLogin(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
return ferr
}
if user.Loggedin {
return common.LocalError("You're already logged in.", w, r, user)
}
pi := common.Page{common.GetTitlePhrase("login"), user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_login"] != nil {
if common.RunPreRenderHook("pre_render_login", w, r, &user, &pi) {
return nil
}
}
err := common.Templates.ExecuteTemplate(w, "login.html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
// TODO: Log failed attempted logins?
// TODO: Lock IPS out if they have too many failed attempts?
// TODO: Log unusual countries in comparison to the country a user usually logs in from? Alert the user about this?
func AccountLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
if user.Loggedin {
return common.LocalError("You're already logged in.", w, r, user)
}
username := html.EscapeString(strings.Replace(r.PostFormValue("username"), "\n", "", -1))
uid, err := common.Auth.Authenticate(username, r.PostFormValue("password"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
userPtr, err := common.Users.Get(uid)
if err != nil {
return common.LocalError("Bad account", w, r, user)
}
user = *userPtr
var session string
if user.Session == "" {
session, err = common.Auth.CreateSession(uid)
if err != nil {
return common.InternalError(err, w, r)
}
} else {
session = user.Session
}
common.Auth.SetCookies(w, uid, session)
if user.IsAdmin {
// Is this error check redundant? We already check for the error in PreRoute for the same IP
// TODO: Should we be logging this?
log.Printf("#%d has logged in with IP %s", uid, user.LastIP)
}
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}
func AccountRegister(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
return ferr
}
if user.Loggedin {
return common.LocalError("You're already logged in.", w, r, user)
}
pi := common.Page{common.GetTitlePhrase("register"), user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_register"] != nil {
if common.RunPreRenderHook("pre_render_register", w, r, &user, &pi) {
return nil
}
}
err := common.Templates.ExecuteTemplate(w, "register.html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func AccountRegisterSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerLite, _ := common.SimpleUserCheck(w, r, &user)
username := html.EscapeString(strings.Replace(r.PostFormValue("username"), "\n", "", -1))
if username == "" {
return common.LocalError("You didn't put in a username.", w, r, user)
}
email := html.EscapeString(strings.Replace(r.PostFormValue("email"), "\n", "", -1))
if email == "" {
return common.LocalError("You didn't put in an email.", w, r, user)
}
password := r.PostFormValue("password")
switch password {
case "":
return common.LocalError("You didn't put in a password.", w, r, user)
case username:
return common.LocalError("You can't use your username as your password.", w, r, user)
case email:
return common.LocalError("You can't use your email as your password.", w, r, user)
}
// ? Move this into Create()? What if we want to programatically set weak passwords for tests?
err := common.WeakPassword(password)
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
confirmPassword := r.PostFormValue("confirm_password")
if common.Dev.DebugMode {
log.Print("Registration Attempt! Username: " + username) // TODO: Add more controls over what is logged when?
}
// Do the two inputted passwords match..?
if password != confirmPassword {
return common.LocalError("The two passwords don't match.", w, r, user)
}
var active bool
var group int
switch headerLite.Settings["activation_type"] {
case 1: // Activate All
active = true
group = common.Config.DefaultGroup
default: // Anything else. E.g. Admin Activation or Email Activation.
group = common.Config.ActivationGroup
}
uid, err := common.Users.Create(username, password, email, group, active)
if err == common.ErrAccountExists {
return common.LocalError("This username isn't available. Try another.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
// Check if this user actually owns this email, if email activation is on, automatically flip their account to active when the email is validated. Validation is also useful for determining whether this user should receive any alerts, etc. via email
if common.Site.EnableEmails {
token, err := common.GenerateSafeString(80)
if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Add an EmailStore and move this there
acc := qgen.Builder.Accumulator()
_, err = acc.Insert("emails").Columns("email, uid, validated, token").Fields("?,?,?,?").Exec(email, uid, 0, token)
//_, err = stmts.addEmail.Exec(email, uid, 0, token)
if err != nil {
return common.InternalError(err, w, r)
}
if !common.SendValidationEmail(username, email, token) {
return common.LocalError("We were unable to send the email for you to confirm that this email address belongs to you. You may not have access to some functionality until you do so. Please ask an administrator for assistance.", w, r, user)
}
}
session, err := common.Auth.CreateSession(uid)
if err != nil {
return common.InternalError(err, w, r)
}
common.Auth.SetCookies(w, uid, session)
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}
func AccountEditCritical(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {

96
routes/misc.go Normal file
View File

@ -0,0 +1,96 @@
package routes
import (
"bytes"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
"../common"
)
var cacheControlMaxAge = "max-age=" + strconv.Itoa(common.Day) // TODO: Make this a common.Config value
// GET functions
func StaticFile(w http.ResponseWriter, r *http.Request) {
file, ok := common.StaticFiles.Get(r.URL.Path)
if !ok {
if common.Dev.DebugMode {
log.Printf("Failed to find '%s'", r.URL.Path)
}
w.WriteHeader(http.StatusNotFound)
return
}
h := w.Header()
// Surely, there's a more efficient way of doing this?
t, err := time.Parse(http.TimeFormat, h.Get("If-Modified-Since"))
if err == nil && file.Info.ModTime().Before(t.Add(1*time.Second)) {
w.WriteHeader(http.StatusNotModified)
return
}
h.Set("Last-Modified", file.FormattedModTime)
h.Set("Content-Type", file.Mimetype)
h.Set("Cache-Control", cacheControlMaxAge) //Cache-Control: max-age=31536000
h.Set("Vary", "Accept-Encoding")
if strings.Contains(h.Get("Accept-Encoding"), "gzip") {
h.Set("Content-Encoding", "gzip")
h.Set("Content-Length", strconv.FormatInt(file.GzipLength, 10))
io.Copy(w, bytes.NewReader(file.GzipData)) // Use w.Write instead?
} else {
h.Set("Content-Length", strconv.FormatInt(file.Length, 10)) // Avoid doing a type conversion every time?
io.Copy(w, bytes.NewReader(file.Data))
}
// Other options instead of io.Copy: io.CopyN(), w.Write(), http.ServeContent()
}
func Overview(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
return ferr
}
headerVars.Zone = "overview"
pi := common.Page{common.GetTitlePhrase("overview"), user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_overview"] != nil {
if common.RunPreRenderHook("pre_render_overview", w, r, &user, &pi) {
return nil
}
}
err := common.Templates.ExecuteTemplate(w, "overview.html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func CustomPage(w http.ResponseWriter, r *http.Request, user common.User, name string) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
return ferr
}
// ! Is this safe?
if common.Templates.Lookup("page_"+name+".html") == nil {
return common.NotFound(w, r)
}
headerVars.Zone = "custom_page"
pi := common.Page{common.GetTitlePhrase("page"), user, headerVars, tList, nil}
// TODO: Pass the page name to the pre-render hook?
if common.PreRenderHooks["pre_render_custom_page"] != nil {
if common.RunPreRenderHook("pre_render_custom_page", w, r, &user, &pi) {
return nil
}
}
err := common.Templates.ExecuteTemplate(w, "page_"+name+".html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}

View File

@ -15,10 +15,177 @@ import (
"strings"
"../common"
"../query_gen/lib"
)
var successJSONBytes = []byte(`{"success":"1"}`)
func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit string) common.RouteError {
var err error
var replyList []common.ReplyUser
page, _ := strconv.Atoi(r.FormValue("page"))
// SEO URLs...
// TODO: Make a shared function for this
halves := strings.Split(urlBit, ".")
if len(halves) < 2 {
halves = append(halves, halves[0])
}
tid, err := strconv.Atoi(halves[1])
if err != nil {
return common.PreError("The provided TopicID is not a valid number.", w, r)
}
// Get the topic...
topic, err := common.GetTopicUser(tid)
if err == sql.ErrNoRows {
return common.NotFound(w, r)
} else if err != nil {
return common.InternalError(err, w, r)
}
topic.ClassName = ""
//log.Printf("topic: %+v\n", topic)
headerVars, ferr := common.ForumUserCheck(w, r, &user, topic.ParentID)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic {
//log.Printf("user.Perms: %+v\n", user.Perms)
return common.NoPermissions(w, r, user)
}
headerVars.Zone = "view_topic"
// TODO: Only include these on pages with polls
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums")
topic.ContentLines = strings.Count(topic.Content, "\n")
// We don't want users posting in locked topics...
if topic.IsClosed && !user.IsMod {
user.Perms.CreateReply = false
}
postGroup, err := common.Groups.Get(topic.Group)
if err != nil {
return common.InternalError(err, w, r)
}
topic.Tag = postGroup.Tag
if postGroup.IsMod || postGroup.IsAdmin {
topic.ClassName = common.Config.StaffCSS
}
topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt)
// TODO: Make a function for this? Build a more sophisticated noavatar handling system?
topic.Avatar = common.BuildAvatar(topic.CreatedBy, topic.Avatar)
var poll common.Poll
if topic.Poll != 0 {
pPoll, err := common.Polls.Get(topic.Poll)
if err != nil {
log.Print("Couldn't find the attached poll for topic " + strconv.Itoa(topic.ID))
return common.InternalError(err, w, r)
}
poll = pPoll.Copy()
}
// Calculate the offset
offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage)
tpage := common.TopicPage{topic.Title, user, headerVars, replyList, topic, poll, page, lastPage}
// Get the replies..
// TODO: Reuse this statement rather than preparing it on the spot, maybe via a TopicList abstraction
stmt, err := qgen.Builder.SimpleLeftJoin("replies", "users", "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", "replies.createdBy = users.uid", "replies.tid = ?", "replies.rid ASC", "?,?")
if err != nil {
return common.InternalError(err, w, r)
}
rows, err := stmt.Query(topic.ID, offset, common.Config.ItemsPerPage)
if err == sql.ErrNoRows {
return common.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
defer rows.Close()
replyItem := common.ReplyUser{ClassName: ""}
for rows.Next() {
err := rows.Scan(&replyItem.ID, &replyItem.Content, &replyItem.CreatedBy, &replyItem.CreatedAt, &replyItem.LastEdit, &replyItem.LastEditBy, &replyItem.Avatar, &replyItem.CreatedByName, &replyItem.Group, &replyItem.URLPrefix, &replyItem.URLName, &replyItem.Level, &replyItem.IPAddress, &replyItem.LikeCount, &replyItem.ActionType)
if err != nil {
return common.InternalError(err, w, r)
}
replyItem.UserLink = common.BuildProfileURL(common.NameToSlug(replyItem.CreatedByName), replyItem.CreatedBy)
replyItem.ParentID = topic.ID
replyItem.ContentHtml = common.ParseMessage(replyItem.Content, topic.ParentID, "forums")
replyItem.ContentLines = strings.Count(replyItem.Content, "\n")
postGroup, err = common.Groups.Get(replyItem.Group)
if err != nil {
return common.InternalError(err, w, r)
}
if postGroup.IsMod || postGroup.IsAdmin {
replyItem.ClassName = common.Config.StaffCSS
} else {
replyItem.ClassName = ""
}
// TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the common.UserStore initialise this?
replyItem.Avatar = common.BuildAvatar(replyItem.CreatedBy, replyItem.Avatar)
replyItem.Tag = postGroup.Tag
replyItem.RelativeCreatedAt = common.RelativeTime(replyItem.CreatedAt)
// We really shouldn't have inline HTML, we should do something about this...
if replyItem.ActionType != "" {
switch replyItem.ActionType {
case "lock":
replyItem.ActionType = "This topic has been locked by <a href='" + replyItem.UserLink + "'>" + replyItem.CreatedByName + "</a>"
replyItem.ActionIcon = "&#x1F512;&#xFE0E"
case "unlock":
replyItem.ActionType = "This topic has been reopened by <a href='" + replyItem.UserLink + "'>" + replyItem.CreatedByName + "</a>"
replyItem.ActionIcon = "&#x1F513;&#xFE0E"
case "stick":
replyItem.ActionType = "This topic has been pinned by <a href='" + replyItem.UserLink + "'>" + replyItem.CreatedByName + "</a>"
replyItem.ActionIcon = "&#x1F4CC;&#xFE0E"
case "unstick":
replyItem.ActionType = "This topic has been unpinned by <a href='" + replyItem.UserLink + "'>" + replyItem.CreatedByName + "</a>"
replyItem.ActionIcon = "&#x1F4CC;&#xFE0E"
case "move":
replyItem.ActionType = "This topic has been moved by <a href='" + replyItem.UserLink + "'>" + replyItem.CreatedByName + "</a>"
default:
replyItem.ActionType = replyItem.ActionType + " has happened"
replyItem.ActionIcon = ""
}
}
replyItem.Liked = false
if common.Vhooks["topic_reply_row_assign"] != nil {
common.RunVhook("topic_reply_row_assign", &tpage, &replyItem)
}
replyList = append(replyList, replyItem)
}
err = rows.Err()
if err != nil {
return common.InternalError(err, w, r)
}
tpage.ItemList = replyList
if common.PreRenderHooks["pre_render_view_topic"] != nil {
if common.RunPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) {
return nil
}
}
err = common.RunThemeTemplate(headerVars.Theme.Name, "topic", tpage, w)
if err != nil {
return common.InternalError(err, w, r)
}
common.TopicViewCounter.Bump(topic.ID) // TODO Move this into the router?
return nil
}
// ? - Should we add a new permission or permission zone (like per-forum permissions) specifically for profile comment creation
// ? - Should we allow banned users to make reports? How should we handle report abuse?
// TODO: Add a permission to stop certain users from using custom avatars

View File

@ -126,157 +126,207 @@ var topic_20 = []byte(`' type="text" />
var topic_21 = []byte(`
</div>
</div>
<article itemscope itemtype="http://schema.org/CreativeWork" class="rowblock post_container top_post" aria-label="The opening post for this topic">
<div class="rowitem passive editable_parent post_item `)
<article class="rowblock post_container poll">
<div class="rowitem passive editable_parent post_item poll_item `)
var topic_22 = []byte(`" style="background-image: url(`)
var topic_23 = []byte(`), url(/static/`)
var topic_24 = []byte(`/post-avatar-bg.jpg);background-position: 0px `)
var topic_25 = []byte(`-1`)
var topic_26 = []byte(`0px;background-repeat:no-repeat, repeat-y;">
<div class="topic_content user_content" style="margin:0;padding:0;">
`)
var topic_27 = []byte(`
<div class="poll_option">
<input form="poll_`)
var topic_28 = []byte(`_form" id="poll_option_`)
var topic_29 = []byte(`" name="poll_option_input" type="checkbox" value="`)
var topic_30 = []byte(`" />
<label class="poll_option_label" for="poll_option_`)
var topic_31 = []byte(`">
<div class="sel"></div>
</label>
<span id="poll_option_text_`)
var topic_32 = []byte(`" class="poll_option_text">`)
var topic_33 = []byte(`</span>
</div>
`)
var topic_34 = []byte(`
<div class="poll_buttons">
<button form="poll_`)
var topic_35 = []byte(`_form" class="poll_vote_button">Vote</button>
<button class="poll_results_button" data-poll-id="`)
var topic_36 = []byte(`">Results</button>
<a href="#"><button class="poll_cancel_button">Cancel</button></a>
</div>
</div>
<div id="poll_results_`)
var topic_37 = []byte(`" class="poll_results auto_hide">
<div class="topic_content user_content"></div>
</div>
</div>
</article>
<article itemscope itemtype="http://schema.org/CreativeWork" class="rowblock post_container top_post" aria-label="The opening post for this topic">
<div class="rowitem passive editable_parent post_item `)
var topic_38 = []byte(`" style="background-image: url(`)
var topic_39 = []byte(`), url(/static/`)
var topic_40 = []byte(`/post-avatar-bg.jpg);background-position: 0px `)
var topic_41 = []byte(`-1`)
var topic_42 = []byte(`0px;background-repeat:no-repeat, repeat-y;">
<p class="hide_on_edit topic_content user_content" itemprop="text" style="margin:0;padding:0;">`)
var topic_27 = []byte(`</p>
var topic_43 = []byte(`</p>
<textarea name="topic_content" class="show_on_edit topic_content_input">`)
var topic_28 = []byte(`</textarea>
var topic_44 = []byte(`</textarea>
<span class="controls" aria-label="Controls and Author Information">
<a href="`)
var topic_29 = []byte(`" class="username real_username" rel="author">`)
var topic_30 = []byte(`</a>&nbsp;&nbsp;
var topic_45 = []byte(`" class="username real_username" rel="author">`)
var topic_46 = []byte(`</a>&nbsp;&nbsp;
`)
var topic_31 = []byte(`<a href="/topic/like/submit/`)
var topic_32 = []byte(`?session=`)
var topic_33 = []byte(`" class="mod_button" title="Love it" `)
var topic_34 = []byte(`aria-label="Unlike this topic"`)
var topic_35 = []byte(`aria-label="Like this topic"`)
var topic_36 = []byte(` style="color:#202020;">
<button class="username like_label"`)
var topic_37 = []byte(` style="background-color:#D6FFD6;"`)
var topic_38 = []byte(`></button></a>`)
var topic_39 = []byte(`<a href='/topic/edit/`)
var topic_40 = []byte(`' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic" aria-label="Edit this topic"><button class="username edit_label"></button></a>`)
var topic_41 = []byte(`<a href='/topic/delete/submit/`)
var topic_42 = []byte(`?session=`)
var topic_43 = []byte(`' class="mod_button" style="font-weight:normal;" title="Delete Topic" aria-label="Delete this topic"><button class="username trash_label"></button></a>`)
var topic_44 = []byte(`<a class="mod_button" href='/topic/unlock/submit/`)
var topic_45 = []byte(`?session=`)
var topic_46 = []byte(`' style="font-weight:normal;" title="Unlock Topic" aria-label="Unlock this topic"><button class="username unlock_label"></button></a>`)
var topic_47 = []byte(`<a href='/topic/lock/submit/`)
var topic_47 = []byte(`<a href="/topic/like/submit/`)
var topic_48 = []byte(`?session=`)
var topic_49 = []byte(`' class="mod_button" style="font-weight:normal;" title="Lock Topic" aria-label="Lock this topic"><button class="username lock_label"></button></a>`)
var topic_50 = []byte(`<a class="mod_button" href='/topic/unstick/submit/`)
var topic_51 = []byte(`?session=`)
var topic_52 = []byte(`' style="font-weight:normal;" title="Unpin Topic" aria-label="Unpin this topic"><button class="username unpin_label"></button></a>`)
var topic_53 = []byte(`<a href='/topic/stick/submit/`)
var topic_54 = []byte(`?session=`)
var topic_55 = []byte(`' class="mod_button" style="font-weight:normal;" title="Pin Topic" aria-label="Pin this topic"><button class="username pin_label"></button></a>`)
var topic_56 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_57 = []byte(`' style="font-weight:normal;" title="View IP" aria-label="The poster's IP is `)
var topic_58 = []byte(`"><button class="username ip_label"></button></a>`)
var topic_59 = []byte(`
var topic_49 = []byte(`" class="mod_button" title="Love it" `)
var topic_50 = []byte(`aria-label="Unlike this topic"`)
var topic_51 = []byte(`aria-label="Like this topic"`)
var topic_52 = []byte(` style="color:#202020;">
<button class="username like_label"`)
var topic_53 = []byte(` style="background-color:#D6FFD6;"`)
var topic_54 = []byte(`></button></a>`)
var topic_55 = []byte(`<a href='/topic/edit/`)
var topic_56 = []byte(`' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic" aria-label="Edit this topic"><button class="username edit_label"></button></a>`)
var topic_57 = []byte(`<a href='/topic/delete/submit/`)
var topic_58 = []byte(`?session=`)
var topic_59 = []byte(`' class="mod_button" style="font-weight:normal;" title="Delete Topic" aria-label="Delete this topic"><button class="username trash_label"></button></a>`)
var topic_60 = []byte(`<a class="mod_button" href='/topic/unlock/submit/`)
var topic_61 = []byte(`?session=`)
var topic_62 = []byte(`' style="font-weight:normal;" title="Unlock Topic" aria-label="Unlock this topic"><button class="username unlock_label"></button></a>`)
var topic_63 = []byte(`<a href='/topic/lock/submit/`)
var topic_64 = []byte(`?session=`)
var topic_65 = []byte(`' class="mod_button" style="font-weight:normal;" title="Lock Topic" aria-label="Lock this topic"><button class="username lock_label"></button></a>`)
var topic_66 = []byte(`<a class="mod_button" href='/topic/unstick/submit/`)
var topic_67 = []byte(`?session=`)
var topic_68 = []byte(`' style="font-weight:normal;" title="Unpin Topic" aria-label="Unpin this topic"><button class="username unpin_label"></button></a>`)
var topic_69 = []byte(`<a href='/topic/stick/submit/`)
var topic_70 = []byte(`?session=`)
var topic_71 = []byte(`' class="mod_button" style="font-weight:normal;" title="Pin Topic" aria-label="Pin this topic"><button class="username pin_label"></button></a>`)
var topic_72 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_73 = []byte(`' style="font-weight:normal;" title="View IP" aria-label="The poster's IP is `)
var topic_74 = []byte(`"><button class="username ip_label"></button></a>`)
var topic_75 = []byte(`
<a href="/report/submit/`)
var topic_60 = []byte(`?session=`)
var topic_61 = []byte(`&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag this topic" aria-label="Flag this topic" rel="nofollow"><button class="username flag_label"></button></a>
var topic_76 = []byte(`?session=`)
var topic_77 = []byte(`&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag this topic" aria-label="Flag this topic" rel="nofollow"><button class="username flag_label"></button></a>
`)
var topic_62 = []byte(`<a class="username hide_on_micro like_count" aria-label="The number of likes on this topic">`)
var topic_63 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_64 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_65 = []byte(`</a>`)
var topic_66 = []byte(`<a class="username hide_on_micro level" aria-label="The poster's level">`)
var topic_67 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_68 = []byte(`
var topic_78 = []byte(`<a class="username hide_on_micro like_count" aria-label="The number of likes on this topic">`)
var topic_79 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_80 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_81 = []byte(`</a>`)
var topic_82 = []byte(`<a class="username hide_on_micro level" aria-label="The poster's level">`)
var topic_83 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_84 = []byte(`
</span>
</div>
</article>
<div class="rowblock post_container" aria-label="The current page for this topic" style="overflow: hidden;">`)
var topic_69 = []byte(`
var topic_85 = []byte(`
<article itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item action_item">
<span class="action_icon" style="font-size: 18px;padding-right: 5px;">`)
var topic_70 = []byte(`</span>
var topic_86 = []byte(`</span>
<span itemprop="text">`)
var topic_71 = []byte(`</span>
var topic_87 = []byte(`</span>
</article>
`)
var topic_72 = []byte(`
var topic_88 = []byte(`
<article itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item `)
var topic_73 = []byte(`" style="background-image: url(`)
var topic_74 = []byte(`), url(/static/`)
var topic_75 = []byte(`/post-avatar-bg.jpg);background-position: 0px `)
var topic_76 = []byte(`-1`)
var topic_77 = []byte(`0px;background-repeat:no-repeat, repeat-y;">
var topic_89 = []byte(`" style="background-image: url(`)
var topic_90 = []byte(`), url(/static/`)
var topic_91 = []byte(`/post-avatar-bg.jpg);background-position: 0px `)
var topic_92 = []byte(`-1`)
var topic_93 = []byte(`0px;background-repeat:no-repeat, repeat-y;">
`)
var topic_78 = []byte(`
var topic_94 = []byte(`
<p class="editable_block user_content" itemprop="text" style="margin:0;padding:0;">`)
var topic_79 = []byte(`</p>
var topic_95 = []byte(`</p>
<span class="controls">
<a href="`)
var topic_80 = []byte(`" class="username real_username" rel="author">`)
var topic_81 = []byte(`</a>&nbsp;&nbsp;
var topic_96 = []byte(`" class="username real_username" rel="author">`)
var topic_97 = []byte(`</a>&nbsp;&nbsp;
`)
var topic_82 = []byte(`<a href="/reply/like/submit/`)
var topic_83 = []byte(`?session=`)
var topic_84 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`)
var topic_85 = []byte(` style="background-color:#D6FFD6;"`)
var topic_86 = []byte(`></button></a>`)
var topic_87 = []byte(`<a href="/reply/edit/submit/`)
var topic_88 = []byte(`?session=`)
var topic_89 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`)
var topic_90 = []byte(`<a href="/reply/delete/submit/`)
var topic_91 = []byte(`?session=`)
var topic_92 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`)
var topic_93 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_94 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_95 = []byte(`
var topic_98 = []byte(`<a href="/reply/like/submit/`)
var topic_99 = []byte(`?session=`)
var topic_100 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`)
var topic_101 = []byte(` style="background-color:#D6FFD6;"`)
var topic_102 = []byte(`></button></a>`)
var topic_103 = []byte(`<a href="/reply/edit/submit/`)
var topic_104 = []byte(`?session=`)
var topic_105 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`)
var topic_106 = []byte(`<a href="/reply/delete/submit/`)
var topic_107 = []byte(`?session=`)
var topic_108 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`)
var topic_109 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_110 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_111 = []byte(`
<a href="/report/submit/`)
var topic_96 = []byte(`?session=`)
var topic_97 = []byte(`&type=reply" class="mod_button report_item" title="Flag this reply" aria-label="Flag this reply" rel="nofollow"><button class="username report_item flag_label"></button></a>
var topic_112 = []byte(`?session=`)
var topic_113 = []byte(`&type=reply" class="mod_button report_item" title="Flag this reply" aria-label="Flag this reply" rel="nofollow"><button class="username report_item flag_label"></button></a>
`)
var topic_98 = []byte(`<a class="username hide_on_micro like_count">`)
var topic_99 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_100 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_101 = []byte(`</a>`)
var topic_102 = []byte(`<a class="username hide_on_micro level">`)
var topic_103 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_104 = []byte(`
var topic_114 = []byte(`<a class="username hide_on_micro like_count">`)
var topic_115 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_116 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_117 = []byte(`</a>`)
var topic_118 = []byte(`<a class="username hide_on_micro level">`)
var topic_119 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_120 = []byte(`
</span>
</article>
`)
var topic_105 = []byte(`</div>
var topic_121 = []byte(`</div>
`)
var topic_106 = []byte(`
var topic_122 = []byte(`
<div class="rowblock topic_reply_form quick_create_form">
<form id="reply_form" enctype="multipart/form-data" action="/reply/create/?session=`)
var topic_107 = []byte(`" method="post"></form>
<input form="reply_form" name="tid" value='`)
var topic_108 = []byte(`' type="hidden" />
<form id="quick_post_form" enctype="multipart/form-data" action="/reply/create/?session=`)
var topic_123 = []byte(`" method="post"></form>
<input form="quick_post_form" name="tid" value='`)
var topic_124 = []byte(`' type="hidden" />
<input form="quick_post_form" id="has_poll_input" name="has_poll" value="0" type="hidden" />
<div class="formrow real_first_child">
<div class="formitem">
<textarea id="input_content" form="reply_form" name="reply-content" placeholder="Insert reply here" required></textarea>
<textarea id="input_content" form="quick_post_form" name="reply-content" placeholder="Insert reply here" required></textarea>
</div>
</div>
<div class="formrow poll_content_row auto_hide">
<div class="formitem">
<div class="pollinput" data-pollinput="0">
<input type="checkbox" disabled />
<label class="pollinputlabel"></label>
<input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="Add new poll option" />
</div>
</div>
</div>
<div class="formrow quick_button_row">
<div class="formitem">
<button form="reply_form" name="reply-button" class="formbutton">Create Reply</button>
<button form="quick_post_form" name="reply-button" class="formbutton">Create Reply</button>
<button form="quick_post_form" class="formbutton" id="add_poll_button">Add Poll</button>
`)
var topic_109 = []byte(`
<input name="upload_files" form="reply_form" id="upload_files" multiple type="file" style="display: none;" />
var topic_125 = []byte(`
<input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>`)
var topic_110 = []byte(`
var topic_126 = []byte(`
</div>
</div>
</div>
`)
var topic_111 = []byte(`
var topic_127 = []byte(`
</main>
@ -357,7 +407,7 @@ var topic_alt_21 = []byte(`
var topic_alt_22 = []byte(`_form" action="/poll/vote/`)
var topic_alt_23 = []byte(`?session=`)
var topic_alt_24 = []byte(`" method="post"></form>
<article class="rowitem passive deletable_block editable_parent post_item top_post hide_on_edit">
<article class="rowitem passive deletable_block editable_parent post_item poll_item top_post hide_on_edit">
<div class="userinfo" aria-label="The information on the poster">
<div class="avatar_item" style="background-image: url(`)
var topic_alt_25 = []byte(`), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div>
@ -568,21 +618,32 @@ var topic_alt_144 = []byte(`</div><div class="tag_post"></div></div>`)
var topic_alt_145 = []byte(`
</div>
<div class="rowblock topic_reply_form quick_create_form">
<form id="reply_form" enctype="multipart/form-data" action="/reply/create/?session=`)
<form id="quick_post_form" enctype="multipart/form-data" action="/reply/create/?session=`)
var topic_alt_146 = []byte(`" method="post"></form>
<input form="reply_form" name="tid" value='`)
<input form="quick_post_form" name="tid" value='`)
var topic_alt_147 = []byte(`' type="hidden" />
<input form="quick_post_form" id="has_poll_input" name="has_poll" value="0" type="hidden" />
<div class="formrow real_first_child">
<div class="formitem">
<textarea id="input_content" form="reply_form" name="reply-content" placeholder="What do you think?" required></textarea>
<textarea id="input_content" form="quick_post_form" name="reply-content" placeholder="What do you think?" required></textarea>
</div>
</div>
<div class="formrow poll_content_row auto_hide">
<div class="formitem">
<div class="pollinput" data-pollinput="0">
<input type="checkbox" disabled />
<label class="pollinputlabel"></label>
<input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="Add new poll option" />
</div>
</div>
</div>
<div class="formrow quick_button_row">
<div class="formitem">
<button form="reply_form" name="reply-button" class="formbutton">Create Reply</button>
<button form="quick_post_form" name="reply-button" class="formbutton">Create Reply</button>
<button form="quick_post_form" class="formbutton" id="add_poll_button">Add Poll</button>
`)
var topic_alt_148 = []byte(`
<input name="upload_files" form="reply_form" id="upload_files" multiple type="file" style="display: none;" />
<input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>`)
var topic_alt_149 = []byte(`
@ -929,15 +990,15 @@ var topics_15 = []byte(`
</form>
</div>
<div class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="Quick Topic Form">
<form name="topic_create_form_form" id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/?session=`)
<form name="topic_create_form_form" id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/?session=`)
var topics_16 = []byte(`" method="post"></form>
<input form="topic_create_form_form" id="has_poll_input" name="has_poll" value="0" type="hidden" />
<input form="quick_post_form" id="has_poll_input" name="has_poll" value="0" type="hidden" />
<img class="little_row_avatar" src="`)
var topics_17 = []byte(`" height="64" alt="Your Avatar" title="Your Avatar" />
<div class="main_form">
<div class="topic_meta">
<div class="formrow topic_board_row real_first_child">
<div class="formitem"><select form="topic_create_form_form" id="topic_board_input" name="topic-board">
<div class="formitem"><select form="quick_post_form" id="topic_board_input" name="topic-board">
`)
var topics_18 = []byte(`<option `)
var topics_19 = []byte(`selected`)
@ -949,13 +1010,13 @@ var topics_23 = []byte(`
</div>
<div class="formrow topic_name_row">
<div class="formitem">
<input form="topic_create_form_form" name="topic-name" placeholder="What's up?" required>
<input form="quick_post_form" name="topic-name" placeholder="What's up?" required>
</div>
</div>
</div>
<div class="formrow topic_content_row">
<div class="formitem">
<textarea form="topic_create_form_form" id="input_content" name="topic-content" placeholder="Insert post here" required></textarea>
<textarea form="quick_post_form" id="input_content" name="topic-content" placeholder="Insert post here" required></textarea>
</div>
</div>
<div class="formrow poll_content_row auto_hide">
@ -963,17 +1024,17 @@ var topics_23 = []byte(`
<div class="pollinput" data-pollinput="0">
<input type="checkbox" disabled />
<label class="pollinputlabel"></label>
<input form="topic_create_form_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="Add new poll option" />
<input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="Add new poll option" />
</div>
</div>
</div>
<div class="formrow quick_button_row">
<div class="formitem">
<button form="topic_create_form_form" class="formbutton">Create Topic</button>
<button form="topic_create_form_form" class="formbutton" id="add_poll_button">Add Poll</button>
<button form="quick_post_form" class="formbutton">Create Topic</button>
<button form="quick_post_form" class="formbutton" id="add_poll_button">Add Poll</button>
`)
var topics_24 = []byte(`
<input name="upload_files" form="topic_create_form_form" id="upload_files" multiple type="file" style="display: none;" />
<input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>`)
var topics_25 = []byte(`
@ -1127,22 +1188,22 @@ var forum_18 = []byte(`
`)
var forum_19 = []byte(`
<div id="forum_topic_create_form" class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="Quick Topic Form">
<form id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<form id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<img class="little_row_avatar" src="`)
var forum_20 = []byte(`" height="64" alt="Your Avatar" title="Your Avatar" />
<input form="topic_create_form_form" id="topic_board_input" name="topic-board" value="`)
<input form="quick_post_form" id="topic_board_input" name="topic-board" value="`)
var forum_21 = []byte(`" type="hidden">
<div class="main_form">
<div class="topic_meta">
<div class="formrow topic_name_row real_first_child">
<div class="formitem">
<input form="topic_create_form_form" name="topic-name" placeholder="What's up?" required>
<input form="quick_post_form" name="topic-name" placeholder="What's up?" required>
</div>
</div>
</div>
<div class="formrow topic_content_row">
<div class="formitem">
<textarea form="topic_create_form_form" id="input_content" name="topic-content" placeholder="Insert post here" required></textarea>
<textarea form="quick_post_form" id="input_content" name="topic-content" placeholder="Insert post here" required></textarea>
</div>
</div>
<div class="formrow poll_content_row auto_hide">
@ -1152,11 +1213,11 @@ var forum_21 = []byte(`" type="hidden">
</div>
<div class="formrow quick_button_row">
<div class="formitem">
<button form="topic_create_form_form" name="topic-button" class="formbutton">Create Topic</button>
<button form="topic_create_form_form" class="formbutton" id="add_poll_button">Add Poll</button>
<button form="quick_post_form" name="topic-button" class="formbutton">Create Topic</button>
<button form="quick_post_form" class="formbutton" id="add_poll_button">Add Poll</button>
`)
var forum_22 = []byte(`
<input name="upload_files" form="topic_create_form_form" id="upload_files" multiple type="file" style="display: none;" />
<input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>`)
var forum_23 = []byte(`

View File

@ -137,193 +137,227 @@ if tmpl_topic_vars.Topic.ContentLines <= 5 {
w.Write(topic_25)
}
w.Write(topic_26)
w.Write([]byte(tmpl_topic_vars.Topic.ContentHTML))
if len(tmpl_topic_vars.Poll.QuickOptions) != 0 {
for _, item := range tmpl_topic_vars.Poll.QuickOptions {
w.Write(topic_27)
w.Write([]byte(tmpl_topic_vars.Topic.Content))
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Poll.ID)))
w.Write(topic_28)
w.Write([]byte(tmpl_topic_vars.Topic.UserLink))
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_29)
w.Write([]byte(tmpl_topic_vars.Topic.CreatedByName))
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_30)
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_31)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_32)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write([]byte(item.Value))
w.Write(topic_33)
if tmpl_topic_vars.Topic.Liked {
}
}
w.Write(topic_34)
} else {
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Poll.ID)))
w.Write(topic_35)
}
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Poll.ID)))
w.Write(topic_36)
if tmpl_topic_vars.Topic.Liked {
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Poll.ID)))
w.Write(topic_37)
}
w.Write([]byte(tmpl_topic_vars.Topic.ClassName))
w.Write(topic_38)
}
if tmpl_topic_vars.CurrentUser.Perms.EditTopic {
w.Write([]byte(tmpl_topic_vars.Topic.Avatar))
w.Write(topic_39)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write([]byte(tmpl_topic_vars.Header.Theme.Name))
w.Write(topic_40)
}
if tmpl_topic_vars.CurrentUser.Perms.DeleteTopic {
if tmpl_topic_vars.Topic.ContentLines <= 5 {
w.Write(topic_41)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_42)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_43)
}
if tmpl_topic_vars.CurrentUser.Perms.CloseTopic {
if tmpl_topic_vars.Topic.IsClosed {
w.Write(topic_42)
w.Write([]byte(tmpl_topic_vars.Topic.ContentHTML))
w.Write(topic_43)
w.Write([]byte(tmpl_topic_vars.Topic.Content))
w.Write(topic_44)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write([]byte(tmpl_topic_vars.Topic.UserLink))
w.Write(topic_45)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write([]byte(tmpl_topic_vars.Topic.CreatedByName))
w.Write(topic_46)
} else {
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write(topic_47)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_48)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_49)
if tmpl_topic_vars.Topic.Liked {
w.Write(topic_50)
} else {
w.Write(topic_51)
}
w.Write(topic_52)
if tmpl_topic_vars.Topic.Liked {
w.Write(topic_53)
}
w.Write(topic_54)
}
if tmpl_topic_vars.CurrentUser.Perms.EditTopic {
w.Write(topic_55)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_56)
}
if tmpl_topic_vars.CurrentUser.Perms.DeleteTopic {
w.Write(topic_57)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_58)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_59)
}
if tmpl_topic_vars.CurrentUser.Perms.CloseTopic {
if tmpl_topic_vars.Topic.IsClosed {
w.Write(topic_60)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_61)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_62)
} else {
w.Write(topic_63)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_64)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_65)
}
}
if tmpl_topic_vars.CurrentUser.Perms.PinTopic {
if tmpl_topic_vars.Topic.Sticky {
w.Write(topic_50)
w.Write(topic_66)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_51)
w.Write(topic_67)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_52)
w.Write(topic_68)
} else {
w.Write(topic_53)
w.Write(topic_69)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_54)
w.Write(topic_70)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_55)
w.Write(topic_71)
}
}
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
w.Write(topic_56)
w.Write(topic_72)
w.Write([]byte(tmpl_topic_vars.Topic.IPAddress))
w.Write(topic_57)
w.Write(topic_73)
w.Write([]byte(tmpl_topic_vars.Topic.IPAddress))
w.Write(topic_58)
w.Write(topic_74)
}
w.Write(topic_59)
w.Write(topic_75)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_60)
w.Write(topic_76)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_61)
w.Write(topic_77)
if tmpl_topic_vars.Topic.LikeCount > 0 {
w.Write(topic_62)
w.Write(topic_78)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.LikeCount)))
w.Write(topic_63)
w.Write(topic_79)
}
if tmpl_topic_vars.Topic.Tag != "" {
w.Write(topic_64)
w.Write(topic_80)
w.Write([]byte(tmpl_topic_vars.Topic.Tag))
w.Write(topic_65)
w.Write(topic_81)
} else {
w.Write(topic_66)
w.Write(topic_82)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.Level)))
w.Write(topic_67)
w.Write(topic_83)
}
w.Write(topic_68)
w.Write(topic_84)
if len(tmpl_topic_vars.ItemList) != 0 {
for _, item := range tmpl_topic_vars.ItemList {
if item.ActionType != "" {
w.Write(topic_69)
w.Write([]byte(item.ActionIcon))
w.Write(topic_70)
w.Write([]byte(item.ActionType))
w.Write(topic_71)
} else {
w.Write(topic_72)
w.Write([]byte(item.ClassName))
w.Write(topic_73)
w.Write([]byte(item.Avatar))
w.Write(topic_74)
w.Write([]byte(tmpl_topic_vars.Header.Theme.Name))
w.Write(topic_75)
if item.ContentLines <= 5 {
w.Write(topic_76)
}
w.Write(topic_77)
w.Write(topic_78)
w.Write([]byte(item.ContentHtml))
w.Write(topic_79)
w.Write([]byte(item.UserLink))
w.Write(topic_80)
w.Write([]byte(item.CreatedByName))
w.Write(topic_81)
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write(topic_82)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_83)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_84)
if item.Liked {
w.Write(topic_85)
}
w.Write([]byte(item.ActionIcon))
w.Write(topic_86)
}
if tmpl_topic_vars.CurrentUser.Perms.EditReply {
w.Write([]byte(item.ActionType))
w.Write(topic_87)
w.Write([]byte(strconv.Itoa(item.ID)))
} else {
w.Write(topic_88)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write([]byte(item.ClassName))
w.Write(topic_89)
}
if tmpl_topic_vars.CurrentUser.Perms.DeleteReply {
w.Write([]byte(item.Avatar))
w.Write(topic_90)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write([]byte(tmpl_topic_vars.Header.Theme.Name))
w.Write(topic_91)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
if item.ContentLines <= 5 {
w.Write(topic_92)
}
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
w.Write(topic_93)
w.Write([]byte(item.IPAddress))
w.Write(topic_94)
}
w.Write([]byte(item.ContentHtml))
w.Write(topic_95)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write([]byte(item.UserLink))
w.Write(topic_96)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write([]byte(item.CreatedByName))
w.Write(topic_97)
if item.LikeCount > 0 {
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write(topic_98)
w.Write([]byte(strconv.Itoa(item.LikeCount)))
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_99)
}
if item.Tag != "" {
w.Write(topic_100)
w.Write([]byte(item.Tag))
w.Write(topic_101)
} else {
w.Write(topic_102)
w.Write([]byte(strconv.Itoa(item.Level)))
w.Write(topic_103)
}
w.Write(topic_104)
}
}
}
w.Write(topic_105)
if tmpl_topic_vars.CurrentUser.Perms.CreateReply {
w.Write(topic_106)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_107)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_108)
if tmpl_topic_vars.CurrentUser.Perms.UploadFiles {
w.Write(topic_109)
w.Write(topic_100)
if item.Liked {
w.Write(topic_101)
}
w.Write(topic_102)
}
if tmpl_topic_vars.CurrentUser.Perms.EditReply {
w.Write(topic_103)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_104)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_105)
}
if tmpl_topic_vars.CurrentUser.Perms.DeleteReply {
w.Write(topic_106)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_107)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_108)
}
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
w.Write(topic_109)
w.Write([]byte(item.IPAddress))
w.Write(topic_110)
}
w.Write(topic_111)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_112)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_113)
if item.LikeCount > 0 {
w.Write(topic_114)
w.Write([]byte(strconv.Itoa(item.LikeCount)))
w.Write(topic_115)
}
if item.Tag != "" {
w.Write(topic_116)
w.Write([]byte(item.Tag))
w.Write(topic_117)
} else {
w.Write(topic_118)
w.Write([]byte(strconv.Itoa(item.Level)))
w.Write(topic_119)
}
w.Write(topic_120)
}
}
}
w.Write(topic_121)
if tmpl_topic_vars.CurrentUser.Perms.CreateReply {
w.Write(topic_122)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_123)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_124)
if tmpl_topic_vars.CurrentUser.Perms.UploadFiles {
w.Write(topic_125)
}
w.Write(topic_126)
}
w.Write(topic_127)
w.Write(footer_0)
w.Write([]byte(common.BuildWidget("footer",tmpl_topic_vars.Header)))
w.Write(footer_1)

View File

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

View File

@ -39,20 +39,20 @@
</div>
{{if .CurrentUser.Perms.CreateTopic}}
<div id="forum_topic_create_form" class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="Quick Topic Form">
<form id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<form id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<img class="little_row_avatar" src="{{.CurrentUser.Avatar}}" height="64" alt="Your Avatar" title="Your Avatar" />
<input form="topic_create_form_form" id="topic_board_input" name="topic-board" value="{{.Forum.ID}}" type="hidden">
<input form="quick_post_form" id="topic_board_input" name="topic-board" value="{{.Forum.ID}}" type="hidden">
<div class="main_form">
<div class="topic_meta">
<div class="formrow topic_name_row real_first_child">
<div class="formitem">
<input form="topic_create_form_form" name="topic-name" placeholder="What's up?" required>
<input form="quick_post_form" name="topic-name" placeholder="What's up?" required>
</div>
</div>
</div>
<div class="formrow topic_content_row">
<div class="formitem">
<textarea form="topic_create_form_form" id="input_content" name="topic-content" placeholder="Insert post here" required></textarea>
<textarea form="quick_post_form" id="input_content" name="topic-content" placeholder="Insert post here" required></textarea>
</div>
</div>
<div class="formrow poll_content_row auto_hide">
@ -62,10 +62,10 @@
</div>
<div class="formrow quick_button_row">
<div class="formitem">
<button form="topic_create_form_form" name="topic-button" class="formbutton">Create Topic</button>
<button form="topic_create_form_form" class="formbutton" id="add_poll_button">Add Poll</button>
<button form="quick_post_form" name="topic-button" class="formbutton">Create Topic</button>
<button form="quick_post_form" class="formbutton" id="add_poll_button">Add Poll</button>
{{if .CurrentUser.Perms.UploadFiles}}
<input name="upload_files" form="topic_create_form_form" id="upload_files" multiple type="file" style="display: none;" />
<input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>{{end}}
<button class="formbutton close_form">Cancel</button>

View File

@ -21,6 +21,29 @@
{{end}}
</div>
</div>
<article class="rowblock post_container poll">
<div class="rowitem passive editable_parent post_item poll_item {{.Topic.ClassName}}" style="background-image: url({{.Topic.Avatar}}), url(/static/{{.Header.Theme.Name}}/post-avatar-bg.jpg);background-position: 0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;">
<div class="topic_content user_content" style="margin:0;padding:0;">
{{range .Poll.QuickOptions}}
<div class="poll_option">
<input form="poll_{{$.Poll.ID}}_form" id="poll_option_{{.ID}}" name="poll_option_input" type="checkbox" value="{{.ID}}" />
<label class="poll_option_label" for="poll_option_{{.ID}}">
<div class="sel"></div>
</label>
<span id="poll_option_text_{{.ID}}" class="poll_option_text">{{.Value}}</span>
</div>
{{end}}
<div class="poll_buttons">
<button form="poll_{{.Poll.ID}}_form" class="poll_vote_button">Vote</button>
<button class="poll_results_button" data-poll-id="{{.Poll.ID}}">Results</button>
<a href="#"><button class="poll_cancel_button">Cancel</button></a>
</div>
</div>
<div id="poll_results_{{.Poll.ID}}" class="poll_results auto_hide">
<div class="topic_content user_content"></div>
</div>
</div>
</article>
<article itemscope itemtype="http://schema.org/CreativeWork" class="rowblock post_container top_post" aria-label="The opening post for this topic">
<div class="rowitem passive editable_parent post_item {{.Topic.ClassName}}" style="background-image: url({{.Topic.Avatar}}), url(/static/{{.Header.Theme.Name}}/post-avatar-bg.jpg);background-position: 0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;">
@ -82,18 +105,29 @@
{{if .CurrentUser.Perms.CreateReply}}
<div class="rowblock topic_reply_form quick_create_form">
<form id="reply_form" enctype="multipart/form-data" action="/reply/create/?session={{.CurrentUser.Session}}" method="post"></form>
<input form="reply_form" name="tid" value='{{.Topic.ID}}' type="hidden" />
<form id="quick_post_form" enctype="multipart/form-data" action="/reply/create/?session={{.CurrentUser.Session}}" method="post"></form>
<input form="quick_post_form" name="tid" value='{{.Topic.ID}}' type="hidden" />
<input form="quick_post_form" id="has_poll_input" name="has_poll" value="0" type="hidden" />
<div class="formrow real_first_child">
<div class="formitem">
<textarea id="input_content" form="reply_form" name="reply-content" placeholder="Insert reply here" required></textarea>
<textarea id="input_content" form="quick_post_form" name="reply-content" placeholder="Insert reply here" required></textarea>
</div>
</div>
<div class="formrow poll_content_row auto_hide">
<div class="formitem">
<div class="pollinput" data-pollinput="0">
<input type="checkbox" disabled />
<label class="pollinputlabel"></label>
<input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="Add new poll option" />
</div>
</div>
</div>
<div class="formrow quick_button_row">
<div class="formitem">
<button form="reply_form" name="reply-button" class="formbutton">Create Reply</button>
<button form="quick_post_form" name="reply-button" class="formbutton">Create Reply</button>
<button form="quick_post_form" class="formbutton" id="add_poll_button">Add Poll</button>
{{if .CurrentUser.Perms.UploadFiles}}
<input name="upload_files" form="reply_form" id="upload_files" multiple type="file" style="display: none;" />
<input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>{{end}}
</div>

View File

@ -25,7 +25,7 @@
<div class="rowblock post_container">
{{if .Poll.ID}}
<form id="poll_{{.Poll.ID}}_form" action="/poll/vote/{{.Poll.ID}}?session={{.CurrentUser.Session}}" method="post"></form>
<article class="rowitem passive deletable_block editable_parent post_item top_post hide_on_edit">
<article class="rowitem passive deletable_block editable_parent post_item poll_item top_post hide_on_edit">
<div class="userinfo" aria-label="The information on the poster">
<div class="avatar_item" style="background-image: url({{.Topic.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div>
<a href="{{.Topic.UserLink}}" class="the_name" rel="author">{{.Topic.CreatedByName}}</a>
@ -128,18 +128,29 @@
{{if .CurrentUser.Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.CurrentUser.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">Level {{.CurrentUser.Level}}</div><div class="tag_post"></div></div>{{end}}
</div>
<div class="rowblock topic_reply_form quick_create_form">
<form id="reply_form" enctype="multipart/form-data" action="/reply/create/?session={{.CurrentUser.Session}}" method="post"></form>
<input form="reply_form" name="tid" value='{{.Topic.ID}}' type="hidden" />
<form id="quick_post_form" enctype="multipart/form-data" action="/reply/create/?session={{.CurrentUser.Session}}" method="post"></form>
<input form="quick_post_form" name="tid" value='{{.Topic.ID}}' type="hidden" />
<input form="quick_post_form" id="has_poll_input" name="has_poll" value="0" type="hidden" />
<div class="formrow real_first_child">
<div class="formitem">
<textarea id="input_content" form="reply_form" name="reply-content" placeholder="What do you think?" required></textarea>
<textarea id="input_content" form="quick_post_form" name="reply-content" placeholder="What do you think?" required></textarea>
</div>
</div>
<div class="formrow poll_content_row auto_hide">
<div class="formitem">
<div class="pollinput" data-pollinput="0">
<input type="checkbox" disabled />
<label class="pollinputlabel"></label>
<input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="Add new poll option" />
</div>
</div>
</div>
<div class="formrow quick_button_row">
<div class="formitem">
<button form="reply_form" name="reply-button" class="formbutton">Create Reply</button>
<button form="quick_post_form" name="reply-button" class="formbutton">Create Reply</button>
<button form="quick_post_form" class="formbutton" id="add_poll_button">Add Poll</button>
{{if .CurrentUser.Perms.UploadFiles}}
<input name="upload_files" form="reply_form" id="upload_files" multiple type="file" style="display: none;" />
<input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>{{end}}
</div>

View File

@ -54,25 +54,25 @@
</form>
</div>
<div class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="Quick Topic Form">
<form name="topic_create_form_form" id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/?session={{.CurrentUser.Session}}" method="post"></form>
<input form="topic_create_form_form" id="has_poll_input" name="has_poll" value="0" type="hidden" />
<form name="topic_create_form_form" id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/?session={{.CurrentUser.Session}}" method="post"></form>
<input form="quick_post_form" id="has_poll_input" name="has_poll" value="0" type="hidden" />
<img class="little_row_avatar" src="{{.CurrentUser.Avatar}}" height="64" alt="Your Avatar" title="Your Avatar" />
<div class="main_form">
<div class="topic_meta">
<div class="formrow topic_board_row real_first_child">
<div class="formitem"><select form="topic_create_form_form" id="topic_board_input" name="topic-board">
<div class="formitem"><select form="quick_post_form" id="topic_board_input" name="topic-board">
{{range .ForumList}}<option {{if eq .ID $.DefaultForum}}selected{{end}} value="{{.ID}}">{{.Name}}</option>{{end}}
</select></div>
</div>
<div class="formrow topic_name_row">
<div class="formitem">
<input form="topic_create_form_form" name="topic-name" placeholder="What's up?" required>
<input form="quick_post_form" name="topic-name" placeholder="What's up?" required>
</div>
</div>
</div>
<div class="formrow topic_content_row">
<div class="formitem">
<textarea form="topic_create_form_form" id="input_content" name="topic-content" placeholder="Insert post here" required></textarea>
<textarea form="quick_post_form" id="input_content" name="topic-content" placeholder="Insert post here" required></textarea>
</div>
</div>
<div class="formrow poll_content_row auto_hide">
@ -80,16 +80,16 @@
<div class="pollinput" data-pollinput="0">
<input type="checkbox" disabled />
<label class="pollinputlabel"></label>
<input form="topic_create_form_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="Add new poll option" />
<input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="Add new poll option" />
</div>
</div>
</div>
<div class="formrow quick_button_row">
<div class="formitem">
<button form="topic_create_form_form" class="formbutton">Create Topic</button>
<button form="topic_create_form_form" class="formbutton" id="add_poll_button">Add Poll</button>
<button form="quick_post_form" class="formbutton">Create Topic</button>
<button form="quick_post_form" class="formbutton" id="add_poll_button">Add Poll</button>
{{if .CurrentUser.Perms.UploadFiles}}
<input name="upload_files" form="topic_create_form_form" id="upload_files" multiple type="file" style="display: none;" />
<input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>{{end}}
<button class="formbutton close_form">Cancel</button>

View File

@ -568,6 +568,10 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
.poll_buttons button {
margin-right: 5px;
}
.topic_reply_form .pollinput {
margin-left: 16px;
margin-top: 4px;
}
.formbutton {
margin-top: 12px;

View File

@ -10,6 +10,9 @@
--input-background-color: #444444;
--input-border-color: #555555;
--input-text-color: #999999;
--bright-input-background-color: #555555;
--bright-input-border-color: #666666;
--input-text-color: #a3a3a3;
}
body {
@ -122,9 +125,12 @@ li {
content: "Alerts";
}
.menu_alerts .alertList, .auto_hide, .hide_on_big, .show_on_mobile {
.menu_alerts .alertList, .hide_on_big, .show_on_mobile {
display: none;
}
.auto_hide {
display: none !important;
}
.selectedAlert .alertList {
display: block;
position: absolute;
@ -478,7 +484,6 @@ textarea.large {
padding: 5px;
width: calc(100% - 16px);
}
.formitem select {
background-color: var(--input-background-color);
border: 1px solid var(--input-border-color);
@ -486,7 +491,6 @@ textarea.large {
font-size: 13px;
padding: 4px;
}
.rowlist .formitem select {
padding: 2px;
font-size: 11px;
@ -532,7 +536,7 @@ input, select, textarea {
.quick_button_row .formitem, .quick_create_form .upload_file_dock {
display: flex;
}
.quick_create_form .add_file_button {
.quick_create_form .add_file_button, .quick_create_form #add_poll_button {
margin-left: 8px;
}
.quick_create_form .close_form {
@ -571,6 +575,64 @@ input, select, textarea {
font-size: 12px;
}
.poll_item {
display: flex;
}
.poll_option {
margin-bottom: 3px;
}
input[type=checkbox] {
display: none;
}
input[type=checkbox] + label {
display: inline-block;
width: 12px;
height: 12px;
margin-bottom: -2px;
border: 1px solid var(--bright-input-border-color);
background-color: var(--bright-input-background-color);
}
input[type=checkbox]:checked + label .sel {
display: inline-block;
width: 5px;
height: 5px;
background-color: var(--bright-input-background-color);
}
input[type=checkbox] + label.poll_option_label {
width: 14px;
height: 14px;
margin-right: 3px;
background-color: var(--bright-input-background-color);
border: 1px solid var(--bright-input-border-color);
color: var(--bright-input-text-color);
}
input[type=checkbox]:checked + label.poll_option_label .sel {
display: inline-block;
width: 10px;
height: 10px;
margin-left: 3px;
background: var(--bright-input-border-color);
}
/*#poll_option_text_0 {
color: hsl(359,98%,43%);
}*/
.poll_buttons {
margin-top: 12px;
}
.poll_buttons button {
background-color: var(--bright-input-background-color);
border: 1px solid var(--bright-input-border-color);
color: var(--bright-input-text-color);
padding: 7px;
padding-bottom: 6px;
font-size: 13px;
}
.poll_results {
margin-left: auto;
max-height: 120px;
}
/* Forum View */
.rowhead, .opthead, .colstack_head, .rowhead .rowitem {
display: flex;

View File

@ -475,24 +475,24 @@ li a {
.little_row_avatar {
display: none;
}
.topic_create_form .topic_button_row .formitem {
.quick_create_form .quick_button_row .formitem {
display: flex;
}
.topic_create_form .formbutton:first-child {
.quick_create_form .formbutton:first-child {
margin-left: 0px;
margin-right: 5px;
}
.topic_create_form .formbutton:not(:first-child) {
.quick_create_form .formbutton:not(:first-child) {
margin-left: 0px;
margin-right: 5px;
}
.topic_create_form .formbutton:last-child {
.quick_create_form .formbutton:last-child {
margin-left: auto;
}
.topic_create_form .upload_file_dock {
.quick_create_form .upload_file_dock {
display: flex;
}
.topic_create_form .uploadItem {
.quick_create_form .uploadItem {
display: inline-block;
margin-left: 8px;
margin-right: 8px;
@ -686,6 +686,57 @@ button.username {
display: none;
}
input[type=checkbox] {
display: none;
}
input[type=checkbox] + label {
display: inline-block;
width: 12px;
height: 12px;
margin-bottom: -2px;
border: 1px solid hsl(0, 0%, 80%);
background-color: white;
}
input[type=checkbox]:checked + label .sel {
display: inline-block;
width: 5px;
height: 5px;
background-color: white;
}
input[type=checkbox] + label.poll_option_label {
width: 18px;
height: 18px;
margin-right: 2px;
background-color: white;
border: 1px solid hsl(0, 0%, 70%);
color: #505050;
}
input[type=checkbox]:checked + label.poll_option_label .sel {
display: inline-block;
width: 10px;
height: 10px;
margin-left: 3px;
background: hsl(0,0%,70%);
}
.poll_option {
margin-bottom: 1px;
}
.poll_item {
display: flex;
padding-left: 8px;
background: none !important;
}
.poll_buttons button {
margin-top: 8px;
padding: 5px;
padding-top: 3px;
padding-bottom: 3px;
border: 1px solid hsl(0, 0%, 70%);
}
.poll_results {
margin-left: auto;
}
.alert {
display: block;
padding: 5px;