You can now manage the attachments for an opening post by hitting edit.

The update system now uses the database as the source of truth for the last version rather than lastSchema.json
Refactored several structs and bits of code, so we can avoid allocations for contexts where we never use a relative time.
Clicking on the relative times on the topic list and the forum page should now take you to the post on the last page rather than just the last page.
Added the reltime template function.
Fixed some obsolete bits of code.
Fixed some spelling mistakes.
Fixed a bug where MaxBytesReader was capped at the maxFileSize rather than r.ContentLength.
All of the client side templates should work again now.
Shortened some statement names to save some horizontal space.
accUpdateBuilder and SimpleUpdate now use updatePrebuilder behind the scenes to simplify things.
Renamed selectItem to builder in AccSelectBuilder.
Added a Total() method to accCountBuilder to reduce the amount of boilerplate used for row count queries.
The "_builder" strings have been replaced with empty strings to help save memory, to make things slightly faster and to open the door to removing the query name in many contexts down the line.
Added the open_edit and close_edit client hooks.
Removed many query name checks.
Split the attachment logic into separate functions and de-duplicated it between replies and topics.
Improved the UI for editing topics in Nox.
Used type aliases to reduce the amount of boilerplate in tables.go and patches.go
Reduced the amount of boilerplate in the action post logic.
Eliminated a map and a slice in the topic page for users who haven't given any likes. E.g. Guests.
Fixed some long out-dated parts of the update instructions.
Updated the update instructions to remove mention of the obsolete lastSchema.json
Fixed a bug in init.js where /api/me was being loaded for guests.
Added the MiniTopicGet, GlobalCount and CountInTopic methods to AttachmentStore.
Added the MiniAttachment struct.
Split the mod floaters out into their own template to reduce duplication.
Removed a couple of redundant ParseForms.

Added the common.skipUntilIfExistsOrLine function.
Added the NotFoundJS and NotFoundJSQ functions.
Added the lastReplyID and attachCount columns to the topics table.
This commit is contained in:
Azareal 2018-12-27 15:42:41 +10:00
parent 548227104c
commit 3465e4c08f
53 changed files with 1786 additions and 1333 deletions

1
.gitignore vendored
View File

@ -21,7 +21,6 @@ out/*
*.log
.DS_Store
.vscode/launch.json
schema/lastSchema.json
config/config.go
QueryGen
RouterGen

File diff suppressed because it is too large Load Diff

View File

@ -2,28 +2,128 @@ package common
import (
"database/sql"
"errors"
"strings"
"github.com/Azareal/Gosora/query_gen"
)
var Attachments AttachmentStore
type MiniAttachment struct {
ID int
SectionID int
OriginID int
UploadedBy int
Path string
Image bool
Ext string
}
type AttachmentStore interface {
Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) error
Get(id int) (*MiniAttachment, error)
MiniTopicGet(id int) (alist []*MiniAttachment, err error)
Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) (int, error)
GlobalCount() int
CountInTopic(tid int) int
CountInPath(path string) int
Delete(aid int) error
}
type DefaultAttachmentStore struct {
get *sql.Stmt
getByTopic *sql.Stmt
add *sql.Stmt
count *sql.Stmt
countInTopic *sql.Stmt
countInPath *sql.Stmt
delete *sql.Stmt
}
func NewDefaultAttachmentStore() (*DefaultAttachmentStore, error) {
acc := qgen.NewAcc()
return &DefaultAttachmentStore{
get: acc.Select("attachments").Columns("originID, sectionID, uploadedBy, path").Where("attachID = ?").Prepare(),
getByTopic: acc.Select("attachments").Columns("attachID, sectionID, uploadedBy, path").Where("originTable = 'topics' AND originID = ?").Prepare(),
add: acc.Insert("attachments").Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path").Fields("?,?,?,?,?,?").Prepare(),
count: acc.Count("attachments").Prepare(),
countInTopic: acc.Count("attachments").Where("originTable = 'topics' and originID = ?").Prepare(),
countInPath: acc.Count("attachments").Where("path = ?").Prepare(),
delete: acc.Delete("attachments").Where("attachID = ?").Prepare(),
}, acc.FirstError()
}
func (store *DefaultAttachmentStore) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) error {
_, err := store.add.Exec(sectionID, sectionTable, originID, originTable, uploadedBy, path)
// TODO: Make this more generic so we can use it for reply attachments too
func (store *DefaultAttachmentStore) MiniTopicGet(id int) (alist []*MiniAttachment, err error) {
rows, err := store.getByTopic.Query(id)
defer rows.Close()
for rows.Next() {
attach := &MiniAttachment{OriginID: id}
err := rows.Scan(&attach.ID, &attach.SectionID, &attach.UploadedBy, &attach.Path)
if err != nil {
return nil, err
}
extarr := strings.Split(attach.Path, ".")
if len(extarr) < 2 {
return nil, errors.New("corrupt attachment path")
}
attach.Ext = extarr[len(extarr)-1]
attach.Image = ImageFileExts.Contains(attach.Ext)
alist = append(alist, attach)
}
return alist, rows.Err()
}
func (store *DefaultAttachmentStore) Get(id int) (*MiniAttachment, error) {
attach := &MiniAttachment{ID: id}
err := store.get.QueryRow(id).Scan(&attach.OriginID, &attach.SectionID, &attach.UploadedBy, &attach.Path)
if err != nil {
return nil, err
}
extarr := strings.Split(attach.Path, ".")
if len(extarr) < 2 {
return nil, errors.New("corrupt attachment path")
}
attach.Ext = extarr[len(extarr)-1]
attach.Image = ImageFileExts.Contains(attach.Ext)
return attach, nil
}
func (store *DefaultAttachmentStore) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) (int, error) {
res, err := store.add.Exec(sectionID, sectionTable, originID, originTable, uploadedBy, path)
if err != nil {
return 0, err
}
lid, err := res.LastInsertId()
return int(lid), err
}
func (store *DefaultAttachmentStore) GlobalCount() (count int) {
err := store.count.QueryRow().Scan(&count)
if err != nil {
LogError(err)
}
return count
}
func (store *DefaultAttachmentStore) CountInTopic(tid int) (count int) {
err := store.countInTopic.QueryRow(tid).Scan(&count)
if err != nil {
LogError(err)
}
return count
}
func (store *DefaultAttachmentStore) CountInPath(path string) (count int) {
err := store.countInPath.QueryRow(path).Scan(&count)
if err != nil {
LogError(err)
}
return count
}
func (store *DefaultAttachmentStore) Delete(aid int) error {
_, err := store.delete.Exec(aid)
return err
}

View File

@ -11,6 +11,7 @@ import (
"log"
"sync/atomic"
"time"
"github.com/Azareal/Gosora/query_gen"
)
@ -33,7 +34,7 @@ var TmplPtrMap = make(map[string]interface{})
// Anti-spam token with rotated key
var JSTokenBox atomic.Value // TODO: Move this and some of these other globals somewhere else
var SessionSigningKeyBox atomic.Value // For MFA to avoid hitting the database unneccesarily
var SessionSigningKeyBox atomic.Value // For MFA to avoid hitting the database unneccessarily
var OldSessionSigningKeyBox atomic.Value // Just in case we've signed with a key that's about to go stale so we don't annoy the user too much
var IsDBDown int32 = 0 // 0 = false, 1 = true. this is value which should be manipulated with package atomic for representing whether the database is down so we don't spam the log with lots of redundant errors

View File

@ -314,12 +314,29 @@ func SecurityError(w http.ResponseWriter, r *http.Request, user User) RouteError
}
// NotFound is used when the requested page doesn't exist
// ? - Add a JSQ and JS version of this?
// ? - Add a JSQ version of this?
// ? - Add a user parameter?
func NotFound(w http.ResponseWriter, r *http.Request, header *Header) RouteError {
return CustomError(phrases.GetErrorPhrase("not_found_body"), 404, phrases.GetErrorPhrase("not_found_title"), w, r, header, GuestUser)
}
// ? - Add a user parameter?
func NotFoundJS(w http.ResponseWriter, r *http.Request) RouteError {
w.WriteHeader(401)
writeJsonError(phrases.GetErrorPhrase("not_found_body"), w)
return HandledRouteError()
}
func NotFoundJSQ(w http.ResponseWriter, r *http.Request, header *Header, js bool) RouteError {
if js {
return NotFoundJS(w, r)
}
if header == nil {
header = DefaultHeader(w, GuestUser)
}
return NotFound(w, r, header)
}
// CustomError lets us make custom error types which aren't covered by the generic functions above
func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, header *Header, user User) RouteError {
if header == nil {

View File

@ -94,7 +94,7 @@ func (list SFileList) JSTmplInit() error {
preLen := len(data)
data = replace(data, string(data[spaceIndex:endBrace]), "")
data = replace(data, "))\n", "\n")
data = replace(data, "))\n", " \n")
endBrace -= preLen - len(data) // Offset it as we've deleted portions
fmt.Println("new endBrace: ", endBrace)
fmt.Println("data: ", string(data))
@ -130,58 +130,38 @@ func (list SFileList) JSTmplInit() error {
}
}
each("strconv.Itoa(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
if hasEndBrace {
data[braceAt] = ' ' // Blank it
}
})
each("w.Write([]byte(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
if hasEndBrace {
data[braceAt] = ' ' // Blank it
}
braceAt, hasEndBrace = skipUntilIfExists(data, braceAt, ')')
if hasEndBrace {
data[braceAt] = ' ' // Blank this one too
}
})
each(" = []byte(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
each("[]byte(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
if hasEndBrace {
data[braceAt] = ' ' // Blank it
}
})
each("w.Write(StringToBytes(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
if hasEndBrace {
data[braceAt] = ' ' // Blank it
}
braceAt, hasEndBrace = skipUntilIfExists(data, braceAt, ')')
if hasEndBrace {
data[braceAt] = ' ' // Blank this one too
}
})
each(" = StringToBytes(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
each("StringToBytes(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
if hasEndBrace {
data[braceAt] = ' ' // Blank it
}
})
each("w.Write(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
if hasEndBrace {
data[braceAt] = ' ' // Blank it
}
})
each("RelativeTime(", func(index int) {
braceAt, _ := skipUntilIfExistsOrLine(data, index, 10)
if data[braceAt-1] == ' ' {
data[braceAt-1] = ')' // Blank it
}
})
each("if ", func(index int) {
//fmt.Println("if index: ", index)
braceAt, hasBrace := skipUntilIfExists(data, index, '{')
braceAt, hasBrace := skipUntilIfExistsOrLine(data, index, '{')
if hasBrace {
if data[braceAt-1] != ' ' {
panic("couldn't find space before brace, found ' " + string(data[braceAt-1]) + "' instead")
@ -210,10 +190,12 @@ func (list SFileList) JSTmplInit() error {
data = replace(data, ", 10;", "")
data = replace(data, shortName+"_tmpl_phrase_id = RegisterTmplPhraseNames([]string{", "[")
data = replace(data, "var plist = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let plist = tmplPhrases[\""+tmplName+"\"];")
//data = replace(data, "var phrases = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let phrases = tmplPhrases[\""+tmplName+"\"];\nconsole.log('tmplName:','"+tmplName+"')\nconsole.log('phrases:', phrases);")
data = replace(data, "var cached_var_", "let cached_var_")
data = replace(data, " = []byte(", " = ")
data = replace(data, " = StringToBytes(", " = ")
data = replace(data, "[]byte(", "")
data = replace(data, "StringToBytes(", "")
// TODO: Format dates properly on the client side
data = replace(data, ".Format(\"2006-01-02 15:04:05\"", "")
data = replace(data, ", 10", "")
data = replace(data, "if ", "if(")
data = replace(data, "return nil", "return out")
data = replace(data, " )", ")")

View File

@ -190,6 +190,18 @@ func skipUntilIfExists(tmplData []byte, i int, expects byte) (newI int, hasIt bo
return j, false
}
func skipUntilIfExistsOrLine(tmplData []byte, i int, expects byte) (newI int, hasIt bool) {
j := i
for ; j < len(tmplData); j++ {
if tmplData[j] == 10 {
return j, false
} else if tmplData[j] == expects {
return j, true
}
}
return j, false
}
func skipUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, hasIt bool) {
j := i
expectIndex := 0

View File

@ -17,7 +17,6 @@ type ProfileReply struct {
CreatedBy int
Group int
CreatedAt time.Time
RelativeCreatedAt string
LastEdit int
LastEditBy int
ContentLines int

View File

@ -25,7 +25,6 @@ type ReplyUser struct {
CreatedByName string
Group int
CreatedAt time.Time
RelativeCreatedAt string
LastEdit int
LastEditBy int
Avatar string
@ -51,7 +50,6 @@ type Reply struct {
CreatedBy int
Group int
CreatedAt time.Time
RelativeCreatedAt string
LastEdit int
LastEditBy int
ContentLines int

View File

@ -40,5 +40,5 @@ func (store *SQLReplyStore) Create(topic *Topic, content string, ipaddress strin
if err != nil {
return 0, err
}
return int(lastID), topic.AddReply(uid)
return int(lastID), topic.AddReply(int(lastID), uid)
}

View File

@ -341,7 +341,7 @@ func HandleUploadRoute(w http.ResponseWriter, r *http.Request, user User, maxFil
size, unit := ConvertByteUnit(float64(maxFileSize))
return CustomError("Your upload is too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, nil, user)
}
r.Body = http.MaxBytesReader(w, r.Body, int64(maxFileSize))
r.Body = http.MaxBytesReader(w, r.Body, r.ContentLength)
err := r.ParseMultipartForm(int64(Megabyte))
if err != nil {

View File

@ -226,11 +226,12 @@ func CompileTemplates() error {
PollOption{1, "Something"},
}, VoteCount: 7}
avatar, microAvatar := BuildAvatar(62, "")
topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, RelativeTime(now), now, RelativeTime(now), 0, "", "127.0.0.1", 1, 0, 1, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false}
miniAttach := []*MiniAttachment{&MiniAttachment{Path: "/"}}
topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false, miniAttach}
var replyList []ReplyUser
// TODO: Do we want the UID on this to be 0?
avatar, microAvatar = BuildAvatar(0, "")
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, RelativeTime(now), 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
var varList = make(map[string]tmpl.VarItem)
var compile = func(name string, expects string, expectsInt interface{}) (tmpl string, err error) {
@ -285,7 +286,7 @@ func CompileTemplates() error {
}
var topicsList []*TopicsRow
topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 1, 0, 1, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"})
topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 1, 0, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"})
header2.Title = "Topic List"
topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, TopicListSort{"lastupdated", false}, Paginator{[]int{1}, 1, 1}}
/*topicListTmpl, err := compile("topics", "common.TopicListPage", topicListPage)
@ -439,7 +440,7 @@ func CompileJSTemplates() error {
// TODO: Fix the import loop so we don't have to use this hack anymore
c.SetBuildTags("!no_templategen,tmplgentopic")
var topicsRow = &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 1, 0, 1, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}
var topicsRow = &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}
topicListItemTmpl, err := c.Compile("topics_topic.html", "templates/", "*common.TopicsRow", topicsRow, varList)
if err != nil {
return err
@ -450,11 +451,12 @@ func CompileJSTemplates() error {
PollOption{1, "Something"},
}, VoteCount: 7}
avatar, microAvatar := BuildAvatar(62, "")
topic := TopicUser{1, "blah", "Blah", "Hey there!", 62, false, false, now, RelativeTime(now), now, RelativeTime(now), 0, "", "127.0.0.1", 1, 0, 1, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false}
miniAttach := []*MiniAttachment{&MiniAttachment{Path: "/"}}
topic := TopicUser{1, "blah", "Blah", "Hey there!", 62, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false, miniAttach}
var replyList []ReplyUser
// TODO: Do we really want the UID here to be zero?
avatar, microAvatar = BuildAvatar(0, "")
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, RelativeTime(now), 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
varList = make(map[string]tmpl.VarItem)
header.Title = "Topic Name"
@ -639,10 +641,17 @@ func InitTemplates() error {
if !ok {
panic("timeInt is not a time.Time")
}
//return time.String()
return time.Format("2006-01-02 15:04:05")
}
fmap["reltime"] = func(timeInt interface{}) interface{} {
time, ok := timeInt.(time.Time)
if !ok {
panic("timeInt is not a time.Time")
}
return RelativeTime(time)
}
fmap["scope"] = func(name interface{}) interface{} {
return ""
}

View File

@ -91,6 +91,7 @@ func NewCTemplateSet() *CTemplateSet {
//"langf":true,
"level": true,
"abstime": true,
"reltime": true,
"scope": true,
"dyntmpl": true,
},
@ -959,6 +960,16 @@ ArgLoop:
// TODO: Refactor this
litString(leftParam+".Format(\"2006-01-02 15:04:05\")", false)
break ArgLoop
case "reltime":
// TODO: Implement level literals
leftOperand := node.Args[pos+1].String()
if len(leftOperand) == 0 {
panic("The leftoperand for function reltime cannot be left blank")
}
leftParam, _ := c.compileIfVarSub(con, leftOperand)
// TODO: Refactor this
litString("common.RelativeTime("+leftParam+")", false)
break ArgLoop
case "scope":
literal = true
break ArgLoop

View File

@ -30,16 +30,16 @@ type Topic struct {
IsClosed bool
Sticky bool
CreatedAt time.Time
RelativeCreatedAt string
LastReplyAt time.Time
RelativeLastReplyAt string
//LastReplyBy int
LastReplyBy int
LastReplyID int
ParentID int
Status string // Deprecated. Marked for removal.
IPAddress string
ViewCount int64
PostCount int
LikeCount int
AttachCount int
ClassName string // CSS Class Name
Poll int
Data string // Used for report metadata
@ -54,16 +54,16 @@ type TopicUser struct {
IsClosed bool
Sticky bool
CreatedAt time.Time
RelativeCreatedAt string
LastReplyAt time.Time
RelativeLastReplyAt string
//LastReplyBy int
LastReplyBy int
LastReplyID int
ParentID int
Status string // Deprecated. Marked for removal.
IPAddress string
ViewCount int64
PostCount int
LikeCount int
AttachCount int
ClassName string
Poll int
Data string // Used for report metadata
@ -81,6 +81,8 @@ type TopicUser struct {
URLName string
Level int
Liked bool
Attachments []*MiniAttachment
}
type TopicsRow struct {
@ -92,16 +94,16 @@ type TopicsRow struct {
IsClosed bool
Sticky bool
CreatedAt time.Time
//RelativeCreatedAt string
LastReplyAt time.Time
RelativeLastReplyAt string
LastReplyBy int
LastReplyID int
ParentID int
Status string // Deprecated. Marked for removal. -Is there anything we could use it for?
IPAddress string
ViewCount int64
PostCount int
LikeCount int
AttachCount int
LastPage int
ClassName string
Data string // Used for report metadata
@ -126,10 +128,12 @@ type WsTopicsRow struct {
LastReplyAt time.Time
RelativeLastReplyAt string
LastReplyBy int
LastReplyID int
ParentID int
ViewCount int64
PostCount int
LikeCount int
AttachCount int
ClassName string
Creator *WsJSONUser
LastUser *WsJSONUser
@ -137,12 +141,14 @@ type WsTopicsRow struct {
ForumLink string
}
// TODO: Can we get the client side to render the relative times instead?
func (row *TopicsRow) WebSockets() *WsTopicsRow {
return &WsTopicsRow{row.ID, row.Link, row.Title, row.CreatedBy, row.IsClosed, row.Sticky, row.CreatedAt, row.LastReplyAt, row.RelativeLastReplyAt, row.LastReplyBy, row.ParentID, row.ViewCount, row.PostCount, row.LikeCount, row.ClassName, row.Creator.WebSockets(), row.LastUser.WebSockets(), row.ForumName, row.ForumLink}
return &WsTopicsRow{row.ID, row.Link, row.Title, row.CreatedBy, row.IsClosed, row.Sticky, row.CreatedAt, row.LastReplyAt, RelativeTime(row.LastReplyAt), row.LastReplyBy, row.LastReplyID, row.ParentID, row.ViewCount, row.PostCount, row.LikeCount, row.AttachCount, row.ClassName, row.Creator.WebSockets(), row.LastUser.WebSockets(), row.ForumName, row.ForumLink}
}
type TopicStmts struct {
addRepliesToTopic *sql.Stmt
addReplies *sql.Stmt
updateLastReply *sql.Stmt
lock *sql.Stmt
unlock *sql.Stmt
moveTo *sql.Stmt
@ -154,7 +160,7 @@ type TopicStmts struct {
delete *sql.Stmt
edit *sql.Stmt
setPoll *sql.Stmt
createActionReply *sql.Stmt
createAction *sql.Stmt
getTopicUser *sql.Stmt // TODO: Can we get rid of this?
getByReplyID *sql.Stmt
@ -165,7 +171,8 @@ var topicStmts TopicStmts
func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
topicStmts = TopicStmts{
addRepliesToTopic: acc.Update("topics").Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").Where("tid = ?").Prepare(),
addReplies: acc.Update("topics").Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").Where("tid = ?").Prepare(),
updateLastReply: acc.Update("topics").Set("lastReplyID = ?").Where("lastReplyID > ? AND tid = ?").Prepare(),
lock: acc.Update("topics").Set("is_closed = 1").Where("tid = ?").Prepare(),
unlock: acc.Update("topics").Set("is_closed = 0").Where("tid = ?").Prepare(),
moveTo: acc.Update("topics").Set("parentID = ?").Where("tid = ?").Prepare(),
@ -177,9 +184,9 @@ func init() {
delete: acc.Delete("topics").Where("tid = ?").Prepare(),
edit: acc.Update("topics").Set("title = ?, content = ?, parsed_content = ?").Where("tid = ?").Prepare(), // TODO: Only run the content update bits on non-polls, does this matter?
setPoll: acc.Update("topics").Set("content = '', parsed_content = '', poll = ?").Where("tid = ? AND poll = 0").Prepare(),
createActionReply: acc.Insert("replies").Columns("tid, actionType, ipaddress, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(),
createAction: acc.Insert("replies").Columns("tid, actionType, ipaddress, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(),
getTopicUser: acc.SimpleLeftJoin("topics", "users", "topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.views, topics.postCount, topics.likeCount, topics.poll, users.name, users.avatar, users.group, users.url_prefix, users.url_name, users.level", "topics.createdBy = users.uid", "tid = ?", "", ""),
getTopicUser: acc.SimpleLeftJoin("topics", "users", "topics.title, topics.content, topics.createdBy, topics.createdAt, topics.lastReplyAt, topics.lastReplyBy, topics.lastReplyID, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.views, topics.postCount, topics.likeCount, topics.attachCount,topics.poll, users.name, users.avatar, users.group, users.url_prefix, users.url_name, users.level", "topics.createdBy = users.uid", "tid = ?", "", ""),
getByReplyID: acc.SimpleLeftJoin("replies", "topics", "topics.tid, topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.views, topics.postCount, topics.likeCount, topics.poll, topics.data", "replies.tid = topics.tid", "rid = ?", "", ""),
}
return acc.FirstError()
@ -197,8 +204,12 @@ func (topic *Topic) cacheRemove() {
}
// TODO: Write a test for this
func (topic *Topic) AddReply(uid int) (err error) {
_, err = topicStmts.addRepliesToTopic.Exec(1, uid, topic.ID)
func (topic *Topic) AddReply(rid int, uid int) (err error) {
_, err = topicStmts.addReplies.Exec(1, uid, topic.ID)
if err != nil {
return err
}
_, err = topicStmts.updateLastReply.Exec(rid, rid, topic.ID)
topic.cacheRemove()
return err
}
@ -314,11 +325,20 @@ func (topic *Topic) SetPoll(pollID int) error {
// TODO: Have this go through the ReplyStore?
func (topic *Topic) CreateActionReply(action string, ipaddress string, uid int) (err error) {
_, err = topicStmts.createActionReply.Exec(topic.ID, action, ipaddress, uid)
res, err := topicStmts.createAction.Exec(topic.ID, action, ipaddress, uid)
if err != nil {
return err
}
_, err = topicStmts.addRepliesToTopic.Exec(1, uid, topic.ID)
_, err = topicStmts.addReplies.Exec(1, uid, topic.ID)
if err != nil {
return err
}
lid, err := res.LastInsertId()
if err != nil {
return err
}
rid := int(lid)
_, err = topicStmts.updateLastReply.Exec(rid, rid, topic.ID)
topic.cacheRemove()
// ? - Update the last topic cache for the parent forum?
return err
@ -336,7 +356,7 @@ func (topic *Topic) Copy() Topic {
return *topic
}
// TODO: Load LastReplyAt?
// TODO: Load LastReplyAt and LastReplyID?
func TopicByReplyID(rid int) (*Topic, error) {
topic := Topic{ID: 0}
err := topicStmts.getByReplyID.QueryRow(rid).Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
@ -376,14 +396,15 @@ func GetTopicUser(user *User, tid int) (tu TopicUser, err error) {
}
tu = TopicUser{ID: tid}
err = topicStmts.getTopicUser.QueryRow(tid).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IPAddress, &tu.ViewCount, &tu.PostCount, &tu.LikeCount, &tu.Poll, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.URLPrefix, &tu.URLName, &tu.Level)
// TODO: This misses some important bits...
err = topicStmts.getTopicUser.QueryRow(tid).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.LastReplyAt, &tu.LastReplyBy, &tu.LastReplyID, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IPAddress, &tu.ViewCount, &tu.PostCount, &tu.LikeCount, &tu.AttachCount, &tu.Poll, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.URLPrefix, &tu.URLName, &tu.Level)
tu.Avatar, tu.MicroAvatar = BuildAvatar(tu.CreatedBy, tu.Avatar)
tu.Link = BuildTopicURL(NameToSlug(tu.Title), tu.ID)
tu.UserLink = BuildProfileURL(NameToSlug(tu.CreatedByName), tu.CreatedBy)
tu.Tag = Groups.DirtyGet(tu.Group).Tag
if tcache != nil {
theTopic := Topic{ID: tu.ID, Link: tu.Link, Title: tu.Title, Content: tu.Content, CreatedBy: tu.CreatedBy, IsClosed: tu.IsClosed, Sticky: tu.Sticky, CreatedAt: tu.CreatedAt, LastReplyAt: tu.LastReplyAt, ParentID: tu.ParentID, IPAddress: tu.IPAddress, ViewCount: tu.ViewCount, PostCount: tu.PostCount, LikeCount: tu.LikeCount, Poll: tu.Poll}
theTopic := Topic{ID: tu.ID, Link: tu.Link, Title: tu.Title, Content: tu.Content, CreatedBy: tu.CreatedBy, IsClosed: tu.IsClosed, Sticky: tu.Sticky, CreatedAt: tu.CreatedAt, LastReplyAt: tu.LastReplyAt, LastReplyID: tu.LastReplyID, ParentID: tu.ParentID, IPAddress: tu.IPAddress, ViewCount: tu.ViewCount, PostCount: tu.PostCount, LikeCount: tu.LikeCount, AttachCount: tu.AttachCount, Poll: tu.Poll}
//log.Printf("theTopic: %+v\n", theTopic)
_ = tcache.Add(&theTopic)
}
@ -409,11 +430,13 @@ func copyTopicToTopicUser(topic *Topic, user *User) (tu TopicUser) {
tu.Sticky = topic.Sticky
tu.CreatedAt = topic.CreatedAt
tu.LastReplyAt = topic.LastReplyAt
tu.LastReplyBy = topic.LastReplyBy
tu.ParentID = topic.ParentID
tu.IPAddress = topic.IPAddress
tu.ViewCount = topic.ViewCount
tu.PostCount = topic.PostCount
tu.LikeCount = topic.LikeCount
tu.AttachCount = topic.AttachCount
tu.Poll = topic.Poll
tu.Data = topic.Data

View File

@ -211,7 +211,7 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter
}
// TODO: Prepare common qlist lengths to speed this up in common cases, prepared statements are prepared lazily anyway, so it probably doesn't matter if we do ten or so
stmt, err := qgen.Builder.SimpleSelect("topics", "tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, views, postCount, likeCount", "parentID IN("+qlist+")", orderq, "?,?")
stmt, err := qgen.Builder.SimpleSelect("topics", "tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, lastReplyID, parentID, views, postCount, likeCount", "parentID IN("+qlist+")", orderq, "?,?")
if err != nil {
return nil, Paginator{nil, 1, 1}, err
}
@ -230,7 +230,7 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter
for rows.Next() {
// TODO: Embed Topic structs in TopicsRow to make it easier for us to reuse this work in the topic cache
topicItem := TopicsRow{ID: 0}
err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount)
err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.LastReplyID, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount)
if err != nil {
return nil, Paginator{nil, 1, 1}, err
}
@ -241,8 +241,6 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter
topicItem.ForumName = forum.Name
topicItem.ForumLink = forum.Link
//topicItem.RelativeCreatedAt = RelativeTime(topicItem.CreatedAt)
topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt)
// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
_, _, lastPage := PageOffset(topicItem.PostCount, 1, Config.ItemsPerPage)
topicItem.LastPage = lastPage

View File

@ -57,7 +57,7 @@ func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) {
}
return &DefaultTopicStore{
cache: cache,
get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, views, postCount, likeCount, poll, data").Where("tid = ?").Prepare(),
get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, lastReplyID, is_closed, sticky, parentID, ipaddress, views, postCount, likeCount, attachCount, poll, data").Where("tid = ?").Prepare(),
exists: acc.Select("topics").Columns("tid").Where("tid = ?").Prepare(),
topicCount: acc.Count("topics").Prepare(),
create: acc.Insert("topics").Columns("parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ipaddress, words, createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(),
@ -71,7 +71,7 @@ func (mts *DefaultTopicStore) DirtyGet(id int) *Topic {
}
topic = &Topic{ID: id}
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data)
if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.cache.Add(topic)
@ -88,7 +88,7 @@ func (mts *DefaultTopicStore) Get(id int) (topic *Topic, err error) {
}
topic = &Topic{ID: id}
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data)
if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.cache.Add(topic)
@ -99,14 +99,14 @@ func (mts *DefaultTopicStore) Get(id int) (topic *Topic, err error) {
// BypassGet will always bypass the cache and pull the topic directly from the database
func (mts *DefaultTopicStore) BypassGet(id int) (*Topic, error) {
topic := &Topic{ID: id}
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data)
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
return topic, err
}
func (mts *DefaultTopicStore) Reload(id int) error {
topic := &Topic{ID: id}
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data)
if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.cache.Set(topic)

View File

@ -202,6 +202,7 @@ func ConvertByteInUnit(bytes float64, unit string) (count float64) {
}
// TODO: Write a test for this
// TODO: Localise this?
func FriendlyUnitToBytes(quantity int, unit string) (bytes int, err error) {
switch unit {
case "PB":
@ -323,7 +324,7 @@ func WeakPassword(password string, username string, email string) error {
return errors.New("Your password needs to be at-least eight characters long")
}
if strings.Contains(lowPassword, "test") || /*strings.Contains(password,"123456") || */ strings.Contains(password, "123") || strings.Contains(lowPassword, "password") || strings.Contains(lowPassword, "qwerty") || strings.Contains(lowPassword, "fuck") || strings.Contains(lowPassword, "love") {
if strings.Contains(lowPassword, "test") || strings.Contains(password, "123") || strings.Contains(lowPassword, "password") || strings.Contains(lowPassword, "qwerty") || strings.Contains(lowPassword, "fuck") || strings.Contains(lowPassword, "love") {
return errors.New("You may not have 'test', '123', 'password', 'qwerty', 'love' or 'fuck' in your password")
}

View File

@ -45,7 +45,7 @@ func (hub *WsHubImpl) Start() {
AddScheduledSecondTask(hub.Tick)
}
// This Tick is seperate from the admin one, as we want to process that in parallel with this due to the blocking calls to gopsutil
// This Tick is separate from the admin one, as we want to process that in parallel with this due to the blocking calls to gopsutil
func (hub *WsHubImpl) Tick() error {
// Don't waste CPU time if nothing has happened
// TODO: Get a topic list method which strips stickies?

View File

@ -2,8 +2,6 @@ echo "Updating the dependencies"
go get
echo "Updating Gosora"
rm ./schema/lastSchema.json
cp ./schema/schema.json ./schema/lastSchema.json
git stash
git pull origin master
git stash apply

View File

@ -1,4 +1,3 @@
echo "Building the patcher"
cp ./schema/schema.json ./schema/lastSchema.json
go generate
go build -o Patcher "./patcher"

View File

@ -8,10 +8,6 @@ if %errorlevel% neq 0 (
)
echo Updating Gosora
cd schema
del /Q lastSchema.json
copy schema.json lastSchema.json
cd ..
git stash
if %errorlevel% neq 0 (
pause

View File

@ -4,7 +4,7 @@ The update system is currently under development, but you can run `dev-update.ba
If you run into any issues doing so, please open an issue: https://github.com/Azareal/Gosora/issues/new
If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first have to create a copy of `./schema/schema.json` named `./schema/lastSchema.json`, and then, you'll overwrite the files with the new ones with `git pull origin master`.
If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first want to save your changes with `git stash`, and then, you'll overwrite the files with the new ones with `git pull origin master`, and then, you can re-apply your custom changes with `git stash apply`
After that, you'll need to run `go build ./patcher`.
@ -16,14 +16,9 @@ The update system is currently under development, but you can run `dev-update-li
If you run into any issues doing so, please open an issue: https://github.com/Azareal/Gosora/issues/new
If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first have to create a copy of `./schema/schema.json` named `./schema/lastSchema.json`, and then, you'll overwrite the files with the new ones with `git pull origin master`.
If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first want to save your changes with `git stash`, and then, you'll overwrite the files with the new ones with `git pull origin master`, and then, you'll re-apply your changes with `git stash apply`.
After that, you'll need to run the following code block:
```
cd ./patcher
go build -o Patcher
mv ./Patcher ..
```
After that, you'll need to run `go build -o Patcher "./patcher"`
Once you've done that, you just need to run `./Patcher` to apply the latest patches to the database, etc.
@ -46,14 +41,9 @@ Replace that name and email with whatever you like. This name and email only app
If you get an access denied error, then you might need to run `chown -R gosora /home/gosora` and `chgrp -R www-data /home/gosora` to fix the ownership of the files.
If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first have to create a copy of `./schema/schema.json` named `./schema/lastSchema.json`, and then, you'll overwrite the files with the new ones with `git pull origin master`.
If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first want to save your changes with `git stash`, and then, you'll overwrite the files with the new ones with `git pull origin master`, and then, you'll re-apply your changes with `git stash apply`.
After that, you'll need to run:
```
cd ./patcher
go build -o Patcher
mv ./Patcher ..
```
After that, you'll need to run `go build -o Patcher "./patcher"`
Once you've done that, you just need to run `./Patcher` to apply the latest patches to the database, etc.

View File

@ -128,6 +128,8 @@ var RouteMap = map[string]interface{}{
"routes.UnlockTopicSubmit": routes.UnlockTopicSubmit,
"routes.MoveTopicSubmit": routes.MoveTopicSubmit,
"routes.LikeTopicSubmit": routes.LikeTopicSubmit,
"routes.AddAttachToTopicSubmit": routes.AddAttachToTopicSubmit,
"routes.RemoveAttachFromTopicSubmit": routes.RemoveAttachFromTopicSubmit,
"routes.ViewTopic": routes.ViewTopic,
"routes.CreateReplySubmit": routes.CreateReplySubmit,
"routes.ReplyEditSubmit": routes.ReplyEditSubmit,
@ -261,29 +263,31 @@ var routeMapEnum = map[string]int{
"routes.UnlockTopicSubmit": 103,
"routes.MoveTopicSubmit": 104,
"routes.LikeTopicSubmit": 105,
"routes.ViewTopic": 106,
"routes.CreateReplySubmit": 107,
"routes.ReplyEditSubmit": 108,
"routes.ReplyDeleteSubmit": 109,
"routes.ReplyLikeSubmit": 110,
"routes.ProfileReplyCreateSubmit": 111,
"routes.ProfileReplyEditSubmit": 112,
"routes.ProfileReplyDeleteSubmit": 113,
"routes.PollVote": 114,
"routes.PollResults": 115,
"routes.AccountLogin": 116,
"routes.AccountRegister": 117,
"routes.AccountLogout": 118,
"routes.AccountLoginSubmit": 119,
"routes.AccountLoginMFAVerify": 120,
"routes.AccountLoginMFAVerifySubmit": 121,
"routes.AccountRegisterSubmit": 122,
"routes.DynamicRoute": 123,
"routes.UploadedFile": 124,
"routes.StaticFile": 125,
"routes.RobotsTxt": 126,
"routes.SitemapXml": 127,
"routes.BadRoute": 128,
"routes.AddAttachToTopicSubmit": 106,
"routes.RemoveAttachFromTopicSubmit": 107,
"routes.ViewTopic": 108,
"routes.CreateReplySubmit": 109,
"routes.ReplyEditSubmit": 110,
"routes.ReplyDeleteSubmit": 111,
"routes.ReplyLikeSubmit": 112,
"routes.ProfileReplyCreateSubmit": 113,
"routes.ProfileReplyEditSubmit": 114,
"routes.ProfileReplyDeleteSubmit": 115,
"routes.PollVote": 116,
"routes.PollResults": 117,
"routes.AccountLogin": 118,
"routes.AccountRegister": 119,
"routes.AccountLogout": 120,
"routes.AccountLoginSubmit": 121,
"routes.AccountLoginMFAVerify": 122,
"routes.AccountLoginMFAVerifySubmit": 123,
"routes.AccountRegisterSubmit": 124,
"routes.DynamicRoute": 125,
"routes.UploadedFile": 126,
"routes.StaticFile": 127,
"routes.RobotsTxt": 128,
"routes.SitemapXml": 129,
"routes.BadRoute": 130,
}
var reverseRouteMapEnum = map[int]string{
0: "routes.Overview",
@ -392,29 +396,31 @@ var reverseRouteMapEnum = map[int]string{
103: "routes.UnlockTopicSubmit",
104: "routes.MoveTopicSubmit",
105: "routes.LikeTopicSubmit",
106: "routes.ViewTopic",
107: "routes.CreateReplySubmit",
108: "routes.ReplyEditSubmit",
109: "routes.ReplyDeleteSubmit",
110: "routes.ReplyLikeSubmit",
111: "routes.ProfileReplyCreateSubmit",
112: "routes.ProfileReplyEditSubmit",
113: "routes.ProfileReplyDeleteSubmit",
114: "routes.PollVote",
115: "routes.PollResults",
116: "routes.AccountLogin",
117: "routes.AccountRegister",
118: "routes.AccountLogout",
119: "routes.AccountLoginSubmit",
120: "routes.AccountLoginMFAVerify",
121: "routes.AccountLoginMFAVerifySubmit",
122: "routes.AccountRegisterSubmit",
123: "routes.DynamicRoute",
124: "routes.UploadedFile",
125: "routes.StaticFile",
126: "routes.RobotsTxt",
127: "routes.SitemapXml",
128: "routes.BadRoute",
106: "routes.AddAttachToTopicSubmit",
107: "routes.RemoveAttachFromTopicSubmit",
108: "routes.ViewTopic",
109: "routes.CreateReplySubmit",
110: "routes.ReplyEditSubmit",
111: "routes.ReplyDeleteSubmit",
112: "routes.ReplyLikeSubmit",
113: "routes.ProfileReplyCreateSubmit",
114: "routes.ProfileReplyEditSubmit",
115: "routes.ProfileReplyDeleteSubmit",
116: "routes.PollVote",
117: "routes.PollResults",
118: "routes.AccountLogin",
119: "routes.AccountRegister",
120: "routes.AccountLogout",
121: "routes.AccountLoginSubmit",
122: "routes.AccountLoginMFAVerify",
123: "routes.AccountLoginMFAVerifySubmit",
124: "routes.AccountRegisterSubmit",
125: "routes.DynamicRoute",
126: "routes.UploadedFile",
127: "routes.StaticFile",
128: "routes.RobotsTxt",
129: "routes.SitemapXml",
130: "routes.BadRoute",
}
var osMapEnum = map[string]int{
"unknown": 0,
@ -705,7 +711,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
counters.GlobalViewCounter.Bump()
if prefix == "/static" {
counters.RouteViewCounter.Bump(125)
counters.RouteViewCounter.Bump(127)
req.URL.Path += extraData
routes.StaticFile(w, req)
return
@ -1780,15 +1786,40 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
err = common.ParseForm(w,req,user)
counters.RouteViewCounter.Bump(105)
err = routes.LikeTopicSubmit(w,req,user,extraData)
case "/topic/attach/add/submit/":
err = common.MemberOnly(w,req,user)
if err != nil {
return err
}
err = common.HandleUploadRoute(w,req,user,int(common.Config.MaxRequestSize))
if err != nil {
return err
}
err = common.NoUploadSessionMismatch(w,req,user)
if err != nil {
return err
}
counters.RouteViewCounter.Bump(105)
err = routes.LikeTopicSubmit(w,req,user,extraData)
default:
counters.RouteViewCounter.Bump(106)
err = routes.AddAttachToTopicSubmit(w,req,user,extraData)
case "/topic/attach/remove/submit/":
err = common.NoSessionMismatch(w,req,user)
if err != nil {
return err
}
err = common.MemberOnly(w,req,user)
if err != nil {
return err
}
counters.RouteViewCounter.Bump(107)
err = routes.RemoveAttachFromTopicSubmit(w,req,user,extraData)
default:
counters.RouteViewCounter.Bump(108)
head, err := common.UserCheck(w,req,&user)
if err != nil {
return err
@ -1812,7 +1843,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
counters.RouteViewCounter.Bump(107)
counters.RouteViewCounter.Bump(109)
err = routes.CreateReplySubmit(w,req,user)
case "/reply/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1825,7 +1856,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
counters.RouteViewCounter.Bump(108)
counters.RouteViewCounter.Bump(110)
err = routes.ReplyEditSubmit(w,req,user,extraData)
case "/reply/delete/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1838,7 +1869,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
counters.RouteViewCounter.Bump(109)
counters.RouteViewCounter.Bump(111)
err = routes.ReplyDeleteSubmit(w,req,user,extraData)
case "/reply/like/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1851,12 +1882,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
err = common.ParseForm(w,req,user)
if err != nil {
return err
}
counters.RouteViewCounter.Bump(110)
counters.RouteViewCounter.Bump(112)
err = routes.ReplyLikeSubmit(w,req,user,extraData)
}
case "/profile":
@ -1872,7 +1898,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
counters.RouteViewCounter.Bump(111)
counters.RouteViewCounter.Bump(113)
err = routes.ProfileReplyCreateSubmit(w,req,user)
case "/profile/reply/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1885,7 +1911,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
counters.RouteViewCounter.Bump(112)
counters.RouteViewCounter.Bump(114)
err = routes.ProfileReplyEditSubmit(w,req,user,extraData)
case "/profile/reply/delete/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1898,7 +1924,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
counters.RouteViewCounter.Bump(113)
counters.RouteViewCounter.Bump(115)
err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData)
}
case "/poll":
@ -1914,23 +1940,23 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
counters.RouteViewCounter.Bump(114)
counters.RouteViewCounter.Bump(116)
err = routes.PollVote(w,req,user,extraData)
case "/poll/results/":
counters.RouteViewCounter.Bump(115)
counters.RouteViewCounter.Bump(117)
err = routes.PollResults(w,req,user,extraData)
}
case "/accounts":
switch(req.URL.Path) {
case "/accounts/login/":
counters.RouteViewCounter.Bump(116)
counters.RouteViewCounter.Bump(118)
head, err := common.UserCheck(w,req,&user)
if err != nil {
return err
}
err = routes.AccountLogin(w,req,user,head)
case "/accounts/create/":
counters.RouteViewCounter.Bump(117)
counters.RouteViewCounter.Bump(119)
head, err := common.UserCheck(w,req,&user)
if err != nil {
return err
@ -1947,7 +1973,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
counters.RouteViewCounter.Bump(118)
counters.RouteViewCounter.Bump(120)
err = routes.AccountLogout(w,req,user)
case "/accounts/login/submit/":
err = common.ParseForm(w,req,user)
@ -1955,10 +1981,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
counters.RouteViewCounter.Bump(119)
counters.RouteViewCounter.Bump(121)
err = routes.AccountLoginSubmit(w,req,user)
case "/accounts/mfa_verify/":
counters.RouteViewCounter.Bump(120)
counters.RouteViewCounter.Bump(122)
head, err := common.UserCheck(w,req,&user)
if err != nil {
return err
@ -1970,7 +1996,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
counters.RouteViewCounter.Bump(121)
counters.RouteViewCounter.Bump(123)
err = routes.AccountLoginMFAVerifySubmit(w,req,user)
case "/accounts/create/submit/":
err = common.ParseForm(w,req,user)
@ -1978,7 +2004,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err
}
counters.RouteViewCounter.Bump(122)
counters.RouteViewCounter.Bump(124)
err = routes.AccountRegisterSubmit(w,req,user)
}
/*case "/sitemaps": // TODO: Count these views
@ -1994,7 +2020,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
w.Header().Del("Content-Type")
w.Header().Del("Content-Encoding")
}
counters.RouteViewCounter.Bump(124)
counters.RouteViewCounter.Bump(126)
req.URL.Path += extraData
// TODO: Find a way to propagate errors up from this?
r.UploadHandler(w,req) // TODO: Count these views
@ -2004,10 +2030,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
// TODO: Add support for favicons and robots.txt files
switch(extraData) {
case "robots.txt":
counters.RouteViewCounter.Bump(126)
counters.RouteViewCounter.Bump(128)
return routes.RobotsTxt(w,req)
/*case "sitemap.xml":
counters.RouteViewCounter.Bump(127)
counters.RouteViewCounter.Bump(129)
return routes.SitemapXml(w,req)*/
}
return common.NotFound(w,req,nil)
@ -2018,7 +2044,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
r.RUnlock()
if ok {
counters.RouteViewCounter.Bump(123) // TODO: Be more specific about *which* dynamic route it is
counters.RouteViewCounter.Bump(125) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData
return handle(w,req,user)
}
@ -2029,7 +2055,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
} else {
r.DumpRequest(req,"Bad Route")
}
counters.RouteViewCounter.Bump(128)
counters.RouteViewCounter.Bump(130)
return common.NotFound(w,req,nil)
}
return err

View File

@ -92,7 +92,7 @@ func userStoreTest(t *testing.T, newUserID int) {
expect(t, cond, prefix+" "+midfix+" "+suffix)
}
// TODO: Add email checks too? Do them seperately?
// TODO: Add email checks too? Do them separately?
var expectUser = func(user *common.User, uid int, name string, group int, super bool, admin bool, mod bool, banned bool) {
expect(t, user.ID == uid, fmt.Sprintf("user.ID should be %d. Got '%d' instead.", uid, user.ID))
expect(t, user.Name == name, fmt.Sprintf("user.Name should be '%s', not '%s'", name, user.Name))

View File

@ -2,6 +2,7 @@ package main
import (
"bufio"
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
@ -89,22 +90,31 @@ type SchemaFile struct {
MinVersion string // TODO: Minimum version of Gosora to jump to this version, might be tricky as we don't store this in the schema file, maybe store it in the database
}
func patcher(scanner *bufio.Scanner) error {
func loadSchema() (schemaFile SchemaFile, err error) {
fmt.Println("Loading the schema file")
data, err := ioutil.ReadFile("./schema/lastSchema.json")
if err != nil {
return err
return schemaFile, err
}
var schemaFile SchemaFile
err = json.Unmarshal(data, &schemaFile)
return schemaFile, err
}
func patcher(scanner *bufio.Scanner) error {
var dbVersion int
err := qgen.NewAcc().Select("updates").Columns("dbVersion").QueryRow().Scan(&dbVersion)
if err == sql.ErrNoRows {
schemaFile, err := loadSchema()
if err != nil {
return err
}
dbVersion, err := strconv.Atoi(schemaFile.DBVersion)
dbVersion, err = strconv.Atoi(schemaFile.DBVersion)
if err != nil {
return err
}
} else if err != nil {
return err
}
fmt.Println("Applying the patches")
var pslice = make([]func(*bufio.Scanner) error, len(patches))
@ -113,6 +123,7 @@ func patcher(scanner *bufio.Scanner) error {
}
// Run the queued up patches
var patched int
for index, patch := range pslice {
if dbVersion > index {
continue
@ -121,6 +132,14 @@ func patcher(scanner *bufio.Scanner) error {
if err != nil {
return err
}
patched++
}
if patched > 0 {
_, err := qgen.NewAcc().Update("updates").Set("dbVersion = ?").Exec(len(pslice))
if err != nil {
return err
}
}
return nil

View File

@ -7,6 +7,9 @@ import (
"github.com/Azareal/Gosora/query_gen"
)
type tblColumn = qgen.DBTableColumn
type tblKey = qgen.DBTableKey
func init() {
addPatch(0, patch0)
addPatch(1, patch1)
@ -18,6 +21,7 @@ func init() {
addPatch(7, patch7)
addPatch(8, patch8)
addPatch(9, patch9)
addPatch(10, patch10)
}
func patch0(scanner *bufio.Scanner) (err error) {
@ -32,11 +36,11 @@ func patch0(scanner *bufio.Scanner) (err error) {
}
err = execStmt(qgen.Builder.CreateTable("menus", "", "",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"mid", "int", 0, false, true, ""},
[]tblColumn{
tblColumn{"mid", "int", 0, false, true, ""},
},
[]qgen.DBTableKey{
qgen.DBTableKey{"mid", "primary"},
[]tblKey{
tblKey{"mid", "primary"},
},
))
if err != nil {
@ -44,26 +48,26 @@ func patch0(scanner *bufio.Scanner) (err error) {
}
err = execStmt(qgen.Builder.CreateTable("menu_items", "", "",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"miid", "int", 0, false, true, ""},
qgen.DBTableColumn{"mid", "int", 0, false, false, ""},
qgen.DBTableColumn{"name", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"htmlID", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"cssClass", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"position", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"path", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"aria", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"tooltip", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"tmplName", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"order", "int", 0, false, false, "0"},
[]tblColumn{
tblColumn{"miid", "int", 0, false, true, ""},
tblColumn{"mid", "int", 0, false, false, ""},
tblColumn{"name", "varchar", 200, false, false, ""},
tblColumn{"htmlID", "varchar", 200, false, false, "''"},
tblColumn{"cssClass", "varchar", 200, false, false, "''"},
tblColumn{"position", "varchar", 100, false, false, ""},
tblColumn{"path", "varchar", 200, false, false, "''"},
tblColumn{"aria", "varchar", 200, false, false, "''"},
tblColumn{"tooltip", "varchar", 200, false, false, "''"},
tblColumn{"tmplName", "varchar", 200, false, false, "''"},
tblColumn{"order", "int", 0, false, false, "0"},
qgen.DBTableColumn{"guestOnly", "boolean", 0, false, false, "0"},
qgen.DBTableColumn{"memberOnly", "boolean", 0, false, false, "0"},
qgen.DBTableColumn{"staffOnly", "boolean", 0, false, false, "0"},
qgen.DBTableColumn{"adminOnly", "boolean", 0, false, false, "0"},
tblColumn{"guestOnly", "boolean", 0, false, false, "0"},
tblColumn{"memberOnly", "boolean", 0, false, false, "0"},
tblColumn{"staffOnly", "boolean", 0, false, false, "0"},
tblColumn{"adminOnly", "boolean", 0, false, false, "0"},
},
[]qgen.DBTableKey{
qgen.DBTableKey{"miid", "primary"},
[]tblKey{
tblKey{"miid", "primary"},
},
))
if err != nil {
@ -159,25 +163,20 @@ func patch2(scanner *bufio.Scanner) error {
}
func patch3(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.CreateTable("registration_logs", "", "",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"rlid", "int", 0, false, true, ""},
qgen.DBTableColumn{"username", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"email", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"failureReason", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed?
qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""},
return execStmt(qgen.Builder.CreateTable("registration_logs", "", "",
[]tblColumn{
tblColumn{"rlid", "int", 0, false, true, ""},
tblColumn{"username", "varchar", 100, false, false, ""},
tblColumn{"email", "varchar", 100, false, false, ""},
tblColumn{"failureReason", "varchar", 100, false, false, ""},
tblColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed?
tblColumn{"ipaddress", "varchar", 200, false, false, ""},
tblColumn{"doneAt", "createdAt", 0, false, false, ""},
},
[]qgen.DBTableKey{
qgen.DBTableKey{"rlid", "primary"},
[]tblKey{
tblKey{"rlid", "primary"},
},
))
if err != nil {
return err
}
return nil
}
func patch4(scanner *bufio.Scanner) error {
@ -229,16 +228,16 @@ func patch4(scanner *bufio.Scanner) error {
}
err = execStmt(qgen.Builder.CreateTable("pages", "utf8mb4", "utf8mb4_general_ci",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"pid", "int", 0, false, true, ""},
qgen.DBTableColumn{"name", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"title", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"body", "text", 0, false, false, ""},
qgen.DBTableColumn{"allowedGroups", "text", 0, false, false, ""},
qgen.DBTableColumn{"menuID", "int", 0, false, false, "-1"},
[]tblColumn{
tblColumn{"pid", "int", 0, false, true, ""},
tblColumn{"name", "varchar", 200, false, false, ""},
tblColumn{"title", "varchar", 200, false, false, ""},
tblColumn{"body", "text", 0, false, false, ""},
tblColumn{"allowedGroups", "text", 0, false, false, ""},
tblColumn{"menuID", "int", 0, false, false, "-1"},
},
[]qgen.DBTableKey{
qgen.DBTableKey{"pid", "primary"},
[]tblKey{
tblKey{"pid", "primary"},
},
))
if err != nil {
@ -267,21 +266,21 @@ func patch5(scanner *bufio.Scanner) error {
}
err = execStmt(qgen.Builder.CreateTable("users_2fa_keys", "utf8mb4", "utf8mb4_general_ci",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"uid", "int", 0, false, false, ""},
qgen.DBTableColumn{"secret", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"scratch1", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch2", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch3", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch4", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch5", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch6", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch7", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch8", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""},
[]tblColumn{
tblColumn{"uid", "int", 0, false, false, ""},
tblColumn{"secret", "varchar", 100, false, false, ""},
tblColumn{"scratch1", "varchar", 50, false, false, ""},
tblColumn{"scratch2", "varchar", 50, false, false, ""},
tblColumn{"scratch3", "varchar", 50, false, false, ""},
tblColumn{"scratch4", "varchar", 50, false, false, ""},
tblColumn{"scratch5", "varchar", 50, false, false, ""},
tblColumn{"scratch6", "varchar", 50, false, false, ""},
tblColumn{"scratch7", "varchar", 50, false, false, ""},
tblColumn{"scratch8", "varchar", 50, false, false, ""},
tblColumn{"createdAt", "createdAt", 0, false, false, ""},
},
[]qgen.DBTableKey{
qgen.DBTableKey{"uid", "primary"},
[]tblKey{
tblKey{"uid", "primary"},
},
))
if err != nil {
@ -292,28 +291,18 @@ func patch5(scanner *bufio.Scanner) error {
}
func patch6(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'rapid_loading','1','bool'"))
if err != nil {
return err
}
return nil
return execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'rapid_loading','1','bool'"))
}
func patch7(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.CreateTable("users_avatar_queue", "", "",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
return execStmt(qgen.Builder.CreateTable("users_avatar_queue", "", "",
[]tblColumn{
tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
},
[]qgen.DBTableKey{
qgen.DBTableKey{"uid", "primary"},
[]tblKey{
tblKey{"uid", "primary"},
},
))
if err != nil {
return err
}
return nil
}
func renameRoutes(routes map[string]string) error {
@ -369,17 +358,12 @@ func patch8(scanner *bufio.Scanner) error {
if err != nil {
return err
}
err = execStmt(qgen.Builder.CreateTable("updates", "", "",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"dbVersion", "int", 0, false, false, "0"},
return execStmt(qgen.Builder.CreateTable("updates", "", "",
[]tblColumn{
tblColumn{"dbVersion", "int", 0, false, false, "0"},
},
[]qgen.DBTableKey{},
[]tblKey{},
))
if err != nil {
return err
}
return nil
}
func patch9(scanner *bufio.Scanner) error {
@ -389,21 +373,60 @@ func patch9(scanner *bufio.Scanner) error {
return err
}
err = execStmt(qgen.Builder.CreateTable("login_logs", "", "",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"lid", "int", 0, false, true, ""},
qgen.DBTableColumn{"uid", "int", 0, false, false, ""},
qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed?
qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""},
return execStmt(qgen.Builder.CreateTable("login_logs", "", "",
[]tblColumn{
tblColumn{"lid", "int", 0, false, true, ""},
tblColumn{"uid", "int", 0, false, false, ""},
tblColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed?
tblColumn{"ipaddress", "varchar", 200, false, false, ""},
tblColumn{"doneAt", "createdAt", 0, false, false, ""},
},
[]qgen.DBTableKey{
qgen.DBTableKey{"lid", "primary"},
[]tblKey{
tblKey{"lid", "primary"},
},
))
}
var acc = qgen.NewAcc
var itoa = strconv.Itoa
func patch10(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.AddColumn("topics", tblColumn{"attachCount", "int", 0, false, false, "0"}))
if err != nil {
return err
}
err = execStmt(qgen.Builder.AddColumn("topics", tblColumn{"lastReplyID", "int", 0, false, false, "0"}))
if err != nil {
return err
}
return nil
// We could probably do something more efficient, but as there shouldn't be too many sites right now, we can probably cheat a little, otherwise it'll take forever to get things done
err = acc().Select("topics").Cols("tid").EachInt(func(tid int) error {
stid := itoa(tid)
count, err := acc().Count("attachments").Where("originTable = 'topics' and originID = " + stid).Total()
if err != nil {
return err
}
var hasReply = false
err = acc().Select("replies").Cols("rid").Where("tid = " + stid).Orderby("rid DESC").Limit("1").EachInt(func(rid int) error {
hasReply = true
_, err := acc().Update("topics").Set("lastReplyID = ?, attachCount = ?").Where("tid = "+stid).Exec(rid, count)
return err
})
if err != nil {
return err
}
if !hasReply {
_, err = acc().Update("topics").Set("attachCount = ?").Where("tid = " + stid).Exec(count)
}
return err
})
if err != nil {
return err
}
_, err = acc().Insert("updates").Columns("dbVersion").Fields("0").Exec()
return err
}

View File

@ -224,11 +224,8 @@ function runWebSockets() {
// TODO: Add support for other alert feeds like PM Alerts
var generalAlerts = document.getElementById("general_alerts");
if(alertList.length < 8) {
loadAlerts(generalAlerts);
} else {
updateAlertList(generalAlerts);
}
if(alertList.length < 8) loadAlerts(generalAlerts);
else updateAlertList(generalAlerts);
}
});
}
@ -374,6 +371,7 @@ function mainInit(){
event.preventDefault();
$('.hide_on_edit').addClass("edit_opened");
$('.show_on_edit').addClass("edit_opened");
runHook("open_edit");
});
$(".topic_item .submit_edit").click(function(event){
@ -388,6 +386,7 @@ function mainInit(){
$('.hide_on_edit').removeClass("edit_opened");
$('.show_on_edit').removeClass("edit_opened");
runHook("close_edit");
let formAction = this.form.getAttribute("action");
$.ajax({
@ -566,47 +565,106 @@ function mainInit(){
$(".topic_create_form").hide();
});
function uploadFileHandler() {
var fileList = this.files;
// Truncate the number of files to 5
function uploadFileHandler(fileList,maxFiles = 5, step1 = () => {}, step2 = () => {}) {
let files = [];
for(var i = 0; i < fileList.length && i < 5; i++) {
files[i] = fileList[i];
}
// Iterate over the files
let totalSize = 0;
for(let i = 0; i < files.length; i++) {
console.log("files[" + i + "]",files[i]);
totalSize += files[i]["size"];
let reader = new FileReader();
reader.onload = function(e) {
var fileDock = document.getElementById("upload_file_dock");
var fileItem = document.createElement("label");
console.log("fileItem",fileItem);
if(!files[i]["name"].indexOf('.' > -1)) {
// TODO: Surely, there's a prettier and more elegant way of doing this?
alert("This file doesn't have an extension");
return;
}
if(totalSize > me.Site.MaxRequestSize) {
throw("You can't upload this much at once, max: " + me.Site.MaxRequestSize);
}
var ext = files[i]["name"].split('.').pop();
for(let i = 0; i < files.length; i++) {
let reader = new FileReader();
reader.onload = (e) => {
let filename = files[i]["name"];
step1(e,filename)
let reader = new FileReader();
reader.onload = (e2) => {
crypto.subtle.digest('SHA-256',e2.target.result)
.then((hash) => {
const hashArray = Array.from(new Uint8Array(hash))
return hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('')
}).then(hash => step2(e,hash,filename));
}
reader.readAsArrayBuffer(files[i]);
}
reader.readAsDataURL(files[i]);
}
}
// TODO: Surely, there's a prettier and more elegant way of doing this?
function getExt(filename) {
if(!filename.indexOf('.' > -1)) {
throw("This file doesn't have an extension");
}
return filename.split('.').pop();
}
// Attachment Manager
function uploadAttachHandler2() {
let fileDock = this.closest(".attach_edit_bay");
try {
uploadFileHandler(this.files, 5, () => {},
(e, hash, filename) => {
console.log("hash",hash);
let formData = new FormData();
formData.append("session",me.User.Session);
for(let i = 0; i < this.files.length; i++) {
formData.append("upload_files",this.files[i]);
}
let req = new XMLHttpRequest();
req.addEventListener("load", () => {
let data = JSON.parse(req.responseText);
let fileItem = document.createElement("div");
let ext = getExt(filename);
// TODO: Check if this is actually an image, maybe push ImageFileExts to the client from the server in some sort of gen.js?
// TODO: Use client templates here
fileItem.className = "attach_item attach_image_holder";
fileItem.innerHTML = "<img src='"+e.target.result+"' height=24 width=24 /><span class='attach_item_path' aid='"+data[hash+"."+ext]+"' fullpath='//" + window.location.host + "/attachs/" + hash + "." + ext+"'>"+hash+"."+ext+"</span><button class='attach_item_select'>Select</button><button class='attach_item_copy'>Copy</button>";
fileDock.insertBefore(fileItem,fileDock.querySelector(".attach_item_buttons"));
$(".attach_item_select").unbind("click");
$(".attach_item_copy").unbind("click");
bindAttachItems()
});
req.open("POST","//"+window.location.host+"/topic/attach/add/submit/"+fileDock.getAttribute("tid"));
req.send(formData);
});
} catch(e) {
// TODO: Use a notice instead
alert(e);
}
}
// Quick Topic / Quick Reply
function uploadAttachHandler() {
try {
uploadFileHandler(this.files,5,(e,filename) => {
// TODO: Use client templates here
let fileDock = document.getElementById("upload_file_dock");
let fileItem = document.createElement("label");
console.log("fileItem",fileItem);
let ext = getExt(filename)
fileItem.innerText = "." + ext;
fileItem.className = "formbutton uploadItem";
// TODO: Check if this is actually an image
fileItem.style.backgroundImage = "url("+e.target.result+")";
fileDock.appendChild(fileItem);
let reader = new FileReader();
reader.onload = function(e) {
crypto.subtle.digest('SHA-256',e.target.result)
.then(function(hash) {
const hashArray = Array.from(new Uint8Array(hash))
return hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('')
}).then(function(hash) {
},(e,hash, filename) => {
console.log("hash",hash);
let ext = getExt(filename)
let content = document.getElementById("input_content")
console.log("content.value", content.value);
@ -619,21 +677,73 @@ function mainInit(){
// For custom / third party text editors
attachItemCallback(attachItem);
});
}
reader.readAsArrayBuffer(files[i]);
}
reader.readAsDataURL(files[i]);
}
if(totalSize > me.Site.MaxRequestSize) {
} catch(e) {
// TODO: Use a notice instead
alert("You can't upload this much data at once, max: " + me.Site.MaxRequestSize);
alert(e);
}
}
var uploadFiles = document.getElementById("upload_files");
if(uploadFiles != null) {
uploadFiles.addEventListener("change", uploadFileHandler, false);
uploadFiles.addEventListener("change", uploadAttachHandler, false);
}
var uploadFilesOp = document.getElementById("upload_files_op");
if(uploadFilesOp != null) {
uploadFilesOp.addEventListener("change", uploadAttachHandler2, false);
}
function copyToClipboard(str) {
const el = document.createElement('textarea');
el.value = str;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
}
function bindAttachItems() {
$(".attach_item_select").click(function(){
let hold = $(this).closest(".attach_item");
if(hold.hasClass("attach_item_selected")) {
hold.removeClass("attach_item_selected");
} else {
hold.addClass("attach_item_selected");
}
});
$(".attach_item_copy").click(function(){
let hold = $(this).closest(".attach_item");
let pathNode = hold.find(".attach_item_path");
copyToClipboard(pathNode.attr("fullPath"));
});
}
bindAttachItems();
$(".attach_item_delete").click(function(){
let formData = new URLSearchParams();
formData.append("session",me.User.Session);
let aidList = "";
let elems = document.getElementsByClassName("attach_item_selected");
if(elems == null) return;
for(let i = 0; i < elems.length; i++) {
let pathNode = elems[i].querySelector(".attach_item_path");
console.log("pathNode",pathNode);
aidList += pathNode.getAttribute("aid") + ",";
elems[i].remove();
}
if(aidList.length > 0) aidList = aidList.slice(0, -1);
console.log("aidList",aidList)
formData.append("aids",aidList);
let req = new XMLHttpRequest();
let fileDock = this.closest(".attach_edit_bay");
req.open("POST","//"+window.location.host+"/topic/attach/remove/submit/"+fileDock.getAttribute("tid"),true);
req.send(formData);
});
$(".moderate_link").click((event) => {
event.preventDefault();
@ -643,10 +753,11 @@ function mainInit(){
$(this).click(function(){
selectedTopics.push(parseInt($(this).attr("data-tid"),10));
if(selectedTopics.length==1) {
$(".mod_floater_head span").html("What do you want to do with this topic?");
var msg = "What do you want to do with this topic?";
} else {
$(".mod_floater_head span").html("What do you want to do with these "+selectedTopics.length+" topics?");
var msg = "What do you want to do with these "+selectedTopics.length+" topics?";
}
$(".mod_floater_head span").html(msg);
$(this).addClass("topic_selected");
$(".mod_floater").removeClass("auto_hide");
});
@ -670,7 +781,6 @@ function mainInit(){
let selectNode = this.form.querySelector(".mod_floater_options");
let optionNode = selectNode.options[selectNode.selectedIndex];
let action = optionNode.getAttribute("val");
//console.log("action", action);
// Handle these specially
switch(action) {

View File

@ -12,6 +12,8 @@ var hooks = {
"after_phrases":[],
"after_add_alert":[],
"after_update_alert_list":[],
"open_edit":[],
"close_edit":[],
};
var ranInitHooks = {}
@ -130,7 +132,7 @@ function fetchPhrases() {
(() => {
runInitHook("pre_iife");
let loggedIn = document.head.querySelector("[property='x-loggedin']").content;
if(loggedIn) {
if(loggedIn=="true") {
fetch("/api/me/")
.then((resp) => resp.json())
.then((data) => {

View File

@ -40,28 +40,41 @@ func (builder *accDeleteBuilder) Run(args ...interface{}) (int, error) {
}
type accUpdateBuilder struct {
table string
set string
where string
up *updatePrebuilder
build *Accumulator
}
func (update *accUpdateBuilder) Set(set string) *accUpdateBuilder {
update.set = set
update.up.set = set
return update
}
func (update *accUpdateBuilder) Where(where string) *accUpdateBuilder {
if update.where != "" {
update.where += " AND "
if update.up.where != "" {
update.up.where += " AND "
}
update.where += where
update.up.where += where
return update
}
func (update *accUpdateBuilder) Prepare() *sql.Stmt {
return update.build.SimpleUpdate(update.table, update.set, update.where)
func (update *accUpdateBuilder) WhereQ(sel *selectPrebuilder) *accUpdateBuilder {
update.up.whereSubQuery = sel
return update
}
func (builder *accUpdateBuilder) Prepare() *sql.Stmt {
if builder.up.whereSubQuery != nil {
return builder.build.prepare(builder.build.adapter.SimpleUpdateSelect(builder.up))
}
return builder.build.prepare(builder.build.adapter.SimpleUpdate(builder.up))
}
func (builder *accUpdateBuilder) Exec(args ...interface{}) (res sql.Result, err error) {
query, err := builder.build.adapter.SimpleUpdate(builder.up)
if err != nil {
return res, err
}
return builder.build.exec(query, args...)
}
type AccSelectBuilder struct {
@ -77,17 +90,22 @@ type AccSelectBuilder struct {
build *Accumulator
}
func (selectItem *AccSelectBuilder) Columns(columns string) *AccSelectBuilder {
selectItem.columns = columns
return selectItem
func (builder *AccSelectBuilder) Columns(columns string) *AccSelectBuilder {
builder.columns = columns
return builder
}
func (selectItem *AccSelectBuilder) Where(where string) *AccSelectBuilder {
if selectItem.where != "" {
selectItem.where += " AND "
func (builder *AccSelectBuilder) Cols(columns string) *AccSelectBuilder {
builder.columns = columns
return builder
}
func (builder *AccSelectBuilder) Where(where string) *AccSelectBuilder {
if builder.where != "" {
builder.where += " AND "
}
selectItem.where += where
return selectItem
builder.where += where
return builder
}
// TODO: Don't implement the SQL at the accumulator level but the adapter level
@ -115,28 +133,28 @@ func (selectItem *AccSelectBuilder) InQ(column string, subBuilder *AccSelectBuil
return selectItem
}
func (selectItem *AccSelectBuilder) DateCutoff(column string, quantity int, unit string) *AccSelectBuilder {
selectItem.dateCutoff = &dateCutoff{column, quantity, unit}
return selectItem
func (builder *AccSelectBuilder) DateCutoff(column string, quantity int, unit string) *AccSelectBuilder {
builder.dateCutoff = &dateCutoff{column, quantity, unit}
return builder
}
func (selectItem *AccSelectBuilder) Orderby(orderby string) *AccSelectBuilder {
selectItem.orderby = orderby
return selectItem
func (builder *AccSelectBuilder) Orderby(orderby string) *AccSelectBuilder {
builder.orderby = orderby
return builder
}
func (selectItem *AccSelectBuilder) Limit(limit string) *AccSelectBuilder {
selectItem.limit = limit
return selectItem
func (builder *AccSelectBuilder) Limit(limit string) *AccSelectBuilder {
builder.limit = limit
return builder
}
func (selectItem *AccSelectBuilder) Prepare() *sql.Stmt {
func (builder *AccSelectBuilder) Prepare() *sql.Stmt {
// TODO: Phase out the procedural API and use the adapter's OO API? The OO API might need a bit more work before we do that and it needs to be rolled out to MSSQL.
if selectItem.dateCutoff != nil || selectItem.inChain != nil {
selectBuilder := selectItem.build.GetAdapter().Builder().Select().FromAcc(selectItem)
return selectItem.build.prepare(selectItem.build.GetAdapter().ComplexSelect(selectBuilder))
if builder.dateCutoff != nil || builder.inChain != nil {
selectBuilder := builder.build.GetAdapter().Builder().Select().FromAcc(builder)
return builder.build.prepare(builder.build.GetAdapter().ComplexSelect(selectBuilder))
}
return selectItem.build.SimpleSelect(selectItem.table, selectItem.columns, selectItem.where, selectItem.orderby, selectItem.limit)
return builder.build.SimpleSelect(builder.table, builder.columns, builder.where, builder.orderby, builder.limit)
}
func (builder *AccSelectBuilder) query() (string, error) {
@ -145,15 +163,15 @@ func (builder *AccSelectBuilder) query() (string, error) {
selectBuilder := builder.build.GetAdapter().Builder().Select().FromAcc(builder)
return builder.build.GetAdapter().ComplexSelect(selectBuilder)
}
return builder.build.adapter.SimpleSelect("_builder", builder.table, builder.columns, builder.where, builder.orderby, builder.limit)
return builder.build.adapter.SimpleSelect("", builder.table, builder.columns, builder.where, builder.orderby, builder.limit)
}
func (selectItem *AccSelectBuilder) Query(args ...interface{}) (*sql.Rows, error) {
stmt := selectItem.Prepare()
func (builder *AccSelectBuilder) Query(args ...interface{}) (*sql.Rows, error) {
stmt := builder.Prepare()
if stmt != nil {
return stmt.Query(args...)
}
return nil, selectItem.build.FirstError()
return nil, builder.build.FirstError()
}
type AccRowWrap struct {
@ -245,7 +263,7 @@ func (insert *accInsertBuilder) Prepare() *sql.Stmt {
}
func (builder *accInsertBuilder) Exec(args ...interface{}) (res sql.Result, err error) {
query, err := builder.build.adapter.SimpleInsert("_builder", builder.table, builder.columns, builder.fields)
query, err := builder.build.adapter.SimpleInsert("", builder.table, builder.columns, builder.fields)
if err != nil {
return res, err
}
@ -253,7 +271,7 @@ func (builder *accInsertBuilder) Exec(args ...interface{}) (res sql.Result, err
}
func (builder *accInsertBuilder) Run(args ...interface{}) (int, error) {
query, err := builder.build.adapter.SimpleInsert("_builder", builder.table, builder.columns, builder.fields)
query, err := builder.build.adapter.SimpleInsert("", builder.table, builder.columns, builder.fields)
if err != nil {
return 0, err
}
@ -292,4 +310,13 @@ func (count *accCountBuilder) Prepare() *sql.Stmt {
return count.build.SimpleCount(count.table, count.where, count.limit)
}
func (count *accCountBuilder) Total() (total int, err error) {
stmt := count.Prepare()
if stmt == nil {
return 0, count.build.FirstError()
}
err = stmt.QueryRow().Scan(&total)
return total, err
}
// TODO: Add a Sum builder for summing viewchunks up into one number for the dashboard?

View File

@ -95,52 +95,56 @@ func (build *Accumulator) Tx(handler func(*TransactionBuilder) error) {
}
func (build *Accumulator) SimpleSelect(table string, columns string, where string, orderby string, limit string) *sql.Stmt {
return build.prepare(build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit))
return build.prepare(build.adapter.SimpleSelect("", table, columns, where, orderby, limit))
}
func (build *Accumulator) SimpleCount(table string, where string, limit string) *sql.Stmt {
return build.prepare(build.adapter.SimpleCount("_builder", table, where, limit))
return build.prepare(build.adapter.SimpleCount("", table, where, limit))
}
func (build *Accumulator) SimpleLeftJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) *sql.Stmt {
return build.prepare(build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit))
return build.prepare(build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit))
}
func (build *Accumulator) SimpleInnerJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) *sql.Stmt {
return build.prepare(build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit))
return build.prepare(build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit))
}
func (build *Accumulator) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) *sql.Stmt {
return build.prepare(build.adapter.CreateTable("_builder", table, charset, collation, columns, keys))
return build.prepare(build.adapter.CreateTable("", table, charset, collation, columns, keys))
}
func (build *Accumulator) SimpleInsert(table string, columns string, fields string) *sql.Stmt {
return build.prepare(build.adapter.SimpleInsert("_builder", table, columns, fields))
return build.prepare(build.adapter.SimpleInsert("", table, columns, fields))
}
func (build *Accumulator) SimpleInsertSelect(ins DBInsert, sel DBSelect) *sql.Stmt {
return build.prepare(build.adapter.SimpleInsertSelect("_builder", ins, sel))
return build.prepare(build.adapter.SimpleInsertSelect("", ins, sel))
}
func (build *Accumulator) SimpleInsertLeftJoin(ins DBInsert, sel DBJoin) *sql.Stmt {
return build.prepare(build.adapter.SimpleInsertLeftJoin("_builder", ins, sel))
return build.prepare(build.adapter.SimpleInsertLeftJoin("", ins, sel))
}
func (build *Accumulator) SimpleInsertInnerJoin(ins DBInsert, sel DBJoin) *sql.Stmt {
return build.prepare(build.adapter.SimpleInsertInnerJoin("_builder", ins, sel))
return build.prepare(build.adapter.SimpleInsertInnerJoin("", ins, sel))
}
func (build *Accumulator) SimpleUpdate(table string, set string, where string) *sql.Stmt {
return build.prepare(build.adapter.SimpleUpdate("_builder", table, set, where))
return build.prepare(build.adapter.SimpleUpdate(qUpdate(table, set, where)))
}
func (build *Accumulator) SimpleUpdateSelect(table string, set string, where string) *sql.Stmt {
return build.prepare(build.adapter.SimpleUpdateSelect(qUpdate(table, set, where)))
}
func (build *Accumulator) SimpleDelete(table string, where string) *sql.Stmt {
return build.prepare(build.adapter.SimpleDelete("_builder", table, where))
return build.prepare(build.adapter.SimpleDelete("", table, where))
}
// I don't know why you need this, but here it is x.x
func (build *Accumulator) Purge(table string) *sql.Stmt {
return build.prepare(build.adapter.Purge("_builder", table))
return build.prepare(build.adapter.Purge("", table))
}
func (build *Accumulator) prepareTx(tx *sql.Tx, res string, err error) (stmt *sql.Stmt) {
@ -155,63 +159,63 @@ func (build *Accumulator) prepareTx(tx *sql.Tx, res string, err error) (stmt *sq
// These ones support transactions
func (build *Accumulator) SimpleSelectTx(tx *sql.Tx, table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit)
res, err := build.adapter.SimpleSelect("", table, columns, where, orderby, limit)
return build.prepareTx(tx, res, err)
}
func (build *Accumulator) SimpleCountTx(tx *sql.Tx, table string, where string, limit string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleCount("_builder", table, where, limit)
res, err := build.adapter.SimpleCount("", table, where, limit)
return build.prepareTx(tx, res, err)
}
func (build *Accumulator) SimpleLeftJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)
res, err := build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit)
return build.prepareTx(tx, res, err)
}
func (build *Accumulator) SimpleInnerJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)
res, err := build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit)
return build.prepareTx(tx, res, err)
}
func (build *Accumulator) CreateTableTx(tx *sql.Tx, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt) {
res, err := build.adapter.CreateTable("_builder", table, charset, collation, columns, keys)
res, err := build.adapter.CreateTable("", table, charset, collation, columns, keys)
return build.prepareTx(tx, res, err)
}
func (build *Accumulator) SimpleInsertTx(tx *sql.Tx, table string, columns string, fields string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleInsert("_builder", table, columns, fields)
res, err := build.adapter.SimpleInsert("", table, columns, fields)
return build.prepareTx(tx, res, err)
}
func (build *Accumulator) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleInsertSelect("_builder", ins, sel)
res, err := build.adapter.SimpleInsertSelect("", ins, sel)
return build.prepareTx(tx, res, err)
}
func (build *Accumulator) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleInsertLeftJoin("_builder", ins, sel)
res, err := build.adapter.SimpleInsertLeftJoin("", ins, sel)
return build.prepareTx(tx, res, err)
}
func (build *Accumulator) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleInsertInnerJoin("_builder", ins, sel)
res, err := build.adapter.SimpleInsertInnerJoin("", ins, sel)
return build.prepareTx(tx, res, err)
}
func (build *Accumulator) SimpleUpdateTx(tx *sql.Tx, table string, set string, where string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleUpdate("_builder", table, set, where)
res, err := build.adapter.SimpleUpdate(qUpdate(table, set, where))
return build.prepareTx(tx, res, err)
}
func (build *Accumulator) SimpleDeleteTx(tx *sql.Tx, table string, where string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleDelete("_builder", table, where)
res, err := build.adapter.SimpleDelete("", table, where)
return build.prepareTx(tx, res, err)
}
// I don't know why you need this, but here it is x.x
func (build *Accumulator) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt) {
res, err := build.adapter.Purge("_builder", table)
res, err := build.adapter.Purge("", table)
return build.prepareTx(tx, res, err)
}
@ -220,7 +224,7 @@ func (build *Accumulator) Delete(table string) *accDeleteBuilder {
}
func (build *Accumulator) Update(table string) *accUpdateBuilder {
return &accUpdateBuilder{table, "", "", build}
return &accUpdateBuilder{qUpdate(table, "", ""), build}
}
func (build *Accumulator) Select(table string) *AccSelectBuilder {

View File

@ -85,60 +85,60 @@ func (build *builder) prepare(res string, err error) (*sql.Stmt, error) {
}
func (build *builder) SimpleSelect(table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit))
return build.prepare(build.adapter.SimpleSelect("", table, columns, where, orderby, limit))
}
func (build *builder) SimpleCount(table string, where string, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleCount("_builder", table, where, limit))
return build.prepare(build.adapter.SimpleCount("", table, where, limit))
}
func (build *builder) SimpleLeftJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit))
return build.prepare(build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit))
}
func (build *builder) SimpleInnerJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit))
return build.prepare(build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit))
}
func (build *builder) DropTable(table string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.DropTable("_builder", table))
return build.prepare(build.adapter.DropTable("", table))
}
func (build *builder) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.CreateTable("_builder", table, charset, collation, columns, keys))
return build.prepare(build.adapter.CreateTable("", table, charset, collation, columns, keys))
}
func (build *builder) AddColumn(table string, column DBTableColumn) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.AddColumn("_builder", table, column))
return build.prepare(build.adapter.AddColumn("", table, column))
}
func (build *builder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsert("_builder", table, columns, fields))
return build.prepare(build.adapter.SimpleInsert("", table, columns, fields))
}
func (build *builder) SimpleInsertSelect(ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsertSelect("_builder", ins, sel))
return build.prepare(build.adapter.SimpleInsertSelect("", ins, sel))
}
func (build *builder) SimpleInsertLeftJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsertLeftJoin("_builder", ins, sel))
return build.prepare(build.adapter.SimpleInsertLeftJoin("", ins, sel))
}
func (build *builder) SimpleInsertInnerJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsertInnerJoin("_builder", ins, sel))
return build.prepare(build.adapter.SimpleInsertInnerJoin("", ins, sel))
}
func (build *builder) SimpleUpdate(table string, set string, where string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleUpdate("_builder", table, set, where))
return build.prepare(build.adapter.SimpleUpdate(qUpdate(table, set, where)))
}
func (build *builder) SimpleDelete(table string, where string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleDelete("_builder", table, where))
return build.prepare(build.adapter.SimpleDelete("", table, where))
}
// I don't know why you need this, but here it is x.x
func (build *builder) Purge(table string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.Purge("_builder", table))
return build.prepare(build.adapter.Purge("", table))
}
func (build *builder) prepareTx(tx *sql.Tx, res string, err error) (*sql.Stmt, error) {
@ -150,62 +150,62 @@ func (build *builder) prepareTx(tx *sql.Tx, res string, err error) (*sql.Stmt, e
// These ones support transactions
func (build *builder) SimpleSelectTx(tx *sql.Tx, table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit)
res, err := build.adapter.SimpleSelect("", table, columns, where, orderby, limit)
return build.prepareTx(tx, res, err)
}
func (build *builder) SimpleCountTx(tx *sql.Tx, table string, where string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleCount("_builder", table, where, limit)
res, err := build.adapter.SimpleCount("", table, where, limit)
return build.prepareTx(tx, res, err)
}
func (build *builder) SimpleLeftJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)
res, err := build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit)
return build.prepareTx(tx, res, err)
}
func (build *builder) SimpleInnerJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)
res, err := build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit)
return build.prepareTx(tx, res, err)
}
func (build *builder) CreateTableTx(tx *sql.Tx, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) {
res, err := build.adapter.CreateTable("_builder", table, charset, collation, columns, keys)
res, err := build.adapter.CreateTable("", table, charset, collation, columns, keys)
return build.prepareTx(tx, res, err)
}
func (build *builder) SimpleInsertTx(tx *sql.Tx, table string, columns string, fields string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsert("_builder", table, columns, fields)
res, err := build.adapter.SimpleInsert("", table, columns, fields)
return build.prepareTx(tx, res, err)
}
func (build *builder) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertSelect("_builder", ins, sel)
res, err := build.adapter.SimpleInsertSelect("", ins, sel)
return build.prepareTx(tx, res, err)
}
func (build *builder) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertLeftJoin("_builder", ins, sel)
res, err := build.adapter.SimpleInsertLeftJoin("", ins, sel)
return build.prepareTx(tx, res, err)
}
func (build *builder) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertInnerJoin("_builder", ins, sel)
res, err := build.adapter.SimpleInsertInnerJoin("", ins, sel)
return build.prepareTx(tx, res, err)
}
func (build *builder) SimpleUpdateTx(tx *sql.Tx, table string, set string, where string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleUpdate("_builder", table, set, where)
res, err := build.adapter.SimpleUpdate(qUpdate(table, set, where))
return build.prepareTx(tx, res, err)
}
func (build *builder) SimpleDeleteTx(tx *sql.Tx, table string, where string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleDelete("_builder", table, where)
res, err := build.adapter.SimpleDelete("", table, where)
return build.prepareTx(tx, res, err)
}
// I don't know why you need this, but here it is x.x
func (build *builder) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.Purge("_builder", table)
res, err := build.adapter.Purge("", table)
return build.prepareTx(tx, res, err)
}

View File

@ -11,22 +11,22 @@ type prebuilder struct {
}
func (build *prebuilder) Select(nlist ...string) *selectPrebuilder {
name := optString(nlist, "_builder")
name := optString(nlist, "")
return &selectPrebuilder{name, "", "", "", "", "", nil, nil, "", build.adapter}
}
func (build *prebuilder) Insert(nlist ...string) *insertPrebuilder {
name := optString(nlist, "_builder")
name := optString(nlist, "")
return &insertPrebuilder{name, "", "", "", build.adapter}
}
func (build *prebuilder) Update(nlist ...string) *updatePrebuilder {
name := optString(nlist, "_builder")
return &updatePrebuilder{name, "", "", "", build.adapter}
name := optString(nlist, "")
return &updatePrebuilder{name, "", "", "", nil, build.adapter}
}
func (build *prebuilder) Delete(nlist ...string) *deletePrebuilder {
name := optString(nlist, "_builder")
name := optString(nlist, "")
return &deletePrebuilder{name, "", "", build.adapter}
}
@ -64,10 +64,15 @@ type updatePrebuilder struct {
table string
set string
where string
whereSubQuery *selectPrebuilder
build Adapter
}
func qUpdate(table string, set string, where string) *updatePrebuilder {
return &updatePrebuilder{table: table, set: set, where: where}
}
func (update *updatePrebuilder) Table(table string) *updatePrebuilder {
update.table = table
return update
@ -86,12 +91,17 @@ func (update *updatePrebuilder) Where(where string) *updatePrebuilder {
return update
}
func (update *updatePrebuilder) WhereQ(sel *selectPrebuilder) *updatePrebuilder {
update.whereSubQuery = sel
return update
}
func (update *updatePrebuilder) Text() (string, error) {
return update.build.SimpleUpdate(update.name, update.table, update.set, update.where)
return update.build.SimpleUpdate(update)
}
func (update *updatePrebuilder) Parse() {
update.build.SimpleUpdate(update.name, update.table, update.set, update.where)
update.build.SimpleUpdate(update)
}
type selectPrebuilder struct {
@ -151,7 +161,7 @@ func (selectItem *selectPrebuilder) FromAcc(accBuilder *AccSelectBuilder) *selec
selectItem.dateCutoff = accBuilder.dateCutoff
if accBuilder.inChain != nil {
selectItem.inChain = &selectPrebuilder{"__builder", accBuilder.inChain.table, accBuilder.inChain.columns, accBuilder.inChain.where, accBuilder.inChain.orderby, accBuilder.inChain.limit, accBuilder.inChain.dateCutoff, nil, "", selectItem.build}
selectItem.inChain = &selectPrebuilder{"", accBuilder.inChain.table, accBuilder.inChain.columns, accBuilder.inChain.where, accBuilder.inChain.orderby, accBuilder.inChain.limit, accBuilder.inChain.dateCutoff, nil, "", selectItem.build}
selectItem.inColumn = accBuilder.inColumn
}
return selectItem

View File

@ -45,9 +45,6 @@ func (adapter *MssqlAdapter) DbVersion() string {
}
func (adapter *MssqlAdapter) DropTable(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -59,9 +56,6 @@ func (adapter *MssqlAdapter) DropTable(name string, table string) (string, error
// TODO: Convert any remaining stringy types to nvarchar
// We may need to change the CreateTable API to better suit Mssql and the other database drivers which are coming up
func (adapter *MssqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -142,9 +136,6 @@ func (adapter *MssqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum
// TODO: Test this, not sure if some things work
func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -156,9 +147,6 @@ func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTable
}
func (adapter *MssqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -237,9 +225,6 @@ func (adapter *MssqlAdapter) SimpleReplace(name string, table string, columns st
}
func (adapter *MssqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -332,19 +317,16 @@ func (adapter *MssqlAdapter) SimpleUpsert(name string, table string, columns str
return querystr, nil
}
func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
func (adapter *MssqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) {
if up.table == "" {
return "", errors.New("You need a name for this table")
}
if set == "" {
if up.set == "" {
return "", errors.New("You need to set data in this update statement")
}
var querystr = "UPDATE [" + table + "] SET "
for _, item := range processSet(set) {
var querystr = "UPDATE [" + up.table + "] SET "
for _, item := range processSet(up.set) {
querystr += "[" + item.Column + "] ="
for _, token := range item.Expr {
switch token.Type {
@ -370,9 +352,9 @@ func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string,
querystr = querystr[0 : len(querystr)-1]
// Add support for BETWEEN x.x
if len(where) != 0 {
if len(up.where) != 0 {
querystr += " WHERE"
for _, loc := range processWhere(where) {
for _, loc := range processWhere(up.where) {
for _, token := range loc.Expr {
switch token.Type {
case "function", "operator", "number", "substitute", "or":
@ -394,14 +376,15 @@ func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string,
querystr = querystr[0 : len(querystr)-4]
}
adapter.pushStatement(name, "update", querystr)
adapter.pushStatement(up.name, "update", querystr)
return querystr, nil
}
func (adapter *MssqlAdapter) SimpleUpdateSelect(up *updatePrebuilder) (string, error) {
return "", errors.New("not implemented")
}
func (adapter *MssqlAdapter) SimpleDelete(name string, table string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -441,9 +424,6 @@ func (adapter *MssqlAdapter) SimpleDelete(name string, table string, where strin
// We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead
func (adapter *MssqlAdapter) Purge(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -452,9 +432,6 @@ func (adapter *MssqlAdapter) Purge(name string, table string) (string, error) {
}
func (adapter *MssqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -554,9 +531,6 @@ func (adapter *MssqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (string
}
func (adapter *MssqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" {
return "", errors.New("You need a name for the left table")
}
@ -683,9 +657,6 @@ func (adapter *MssqlAdapter) SimpleLeftJoin(name string, table1 string, table2 s
}
func (adapter *MssqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" {
return "", errors.New("You need a name for the left table")
}
@ -1067,9 +1038,6 @@ func (adapter *MssqlAdapter) SimpleInsertInnerJoin(name string, ins DBInsert, se
}
func (adapter *MssqlAdapter) SimpleCount(name string, table string, where string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -1116,7 +1084,7 @@ func (adapter *MssqlAdapter) Builder() *prebuilder {
func (adapter *MssqlAdapter) Write() error {
var stmts, body string
for _, name := range adapter.BufferOrder {
if name[0] == '_' {
if name == "" {
continue
}
stmt := adapter.Buffer[name]

View File

@ -83,21 +83,16 @@ func (adapter *MysqlAdapter) DbVersion() string {
}
func (adapter *MysqlAdapter) DropTable(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
querystr := "DROP TABLE IF EXISTS `" + table + "`;"
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "drop-table", querystr)
return querystr, nil
}
func (adapter *MysqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -133,6 +128,7 @@ func (adapter *MysqlAdapter) CreateTable(name string, table string, charset stri
querystr += " COLLATE " + collation
}
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "create-table", querystr+";")
return querystr + ";", nil
}
@ -178,23 +174,18 @@ func (adapter *MysqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum
// TODO: Support AFTER column
// TODO: Test to make sure everything works here
func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
column, size, end := adapter.parseColumn(column)
querystr := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end + ";"
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "add-column", querystr)
return querystr, nil
}
func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -218,6 +209,7 @@ func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns str
}
querystr += ")"
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "insert", querystr)
return querystr, nil
}
@ -239,9 +231,6 @@ func (adapter *MysqlAdapter) buildColumns(columns string) (querystr string) {
// ! DEPRECATED
func (adapter *MysqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -258,14 +247,12 @@ func (adapter *MysqlAdapter) SimpleReplace(name string, table string, columns st
}
querystr = querystr[0 : len(querystr)-1]
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "replace", querystr+")")
return querystr + ")", nil
}
func (adapter *MysqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -305,23 +292,21 @@ func (adapter *MysqlAdapter) SimpleUpsert(name string, table string, columns str
querystr += insertColumns + setBit
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "upsert", querystr)
return querystr, nil
}
func (adapter *MysqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
func (adapter *MysqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) {
if up.table == "" {
return "", errors.New("You need a name for this table")
}
if set == "" {
if up.set == "" {
return "", errors.New("You need to set data in this update statement")
}
var querystr = "UPDATE `" + table + "` SET "
for _, item := range processSet(set) {
var querystr = "UPDATE `" + up.table + "` SET "
for _, item := range processSet(up.set) {
querystr += "`" + item.Column + "` ="
for _, token := range item.Expr {
switch token.Type {
@ -337,20 +322,18 @@ func (adapter *MysqlAdapter) SimpleUpdate(name string, table string, set string,
}
querystr = querystr[0 : len(querystr)-1]
whereStr, err := adapter.buildWhere(where)
whereStr, err := adapter.buildWhere(up.where)
if err != nil {
return querystr, err
}
querystr += whereStr
adapter.pushStatement(name, "update", querystr)
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(up.name, "update", querystr)
return querystr, nil
}
func (adapter *MysqlAdapter) SimpleDelete(name string, table string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -378,15 +361,13 @@ func (adapter *MysqlAdapter) SimpleDelete(name string, table string, where strin
}
querystr = strings.TrimSpace(querystr[0 : len(querystr)-4])
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "delete", querystr)
return querystr, nil
}
// We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead
func (adapter *MysqlAdapter) Purge(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -459,9 +440,6 @@ func (adapter *MysqlAdapter) buildOrderby(orderby string) (querystr string) {
}
func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -490,9 +468,6 @@ func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns str
}
func (adapter *MysqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (out string, err error) {
if preBuilder.name == "" {
return "", errors.New("You need a name for this statement")
}
if preBuilder.table == "" {
return "", errors.New("You need a name for this table")
}
@ -531,9 +506,6 @@ func (adapter *MysqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (out st
}
func (adapter *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" {
return "", errors.New("You need a name for the left table")
}
@ -560,9 +532,6 @@ func (adapter *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 s
}
func (adapter *MysqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" {
return "", errors.New("You need a name for the left table")
}
@ -588,6 +557,37 @@ func (adapter *MysqlAdapter) SimpleInnerJoin(name string, table1 string, table2
return querystr, nil
}
func (adapter *MysqlAdapter) SimpleUpdateSelect(up *updatePrebuilder) (string, error) {
sel := up.whereSubQuery
whereStr, err := adapter.buildWhere(sel.where)
if err != nil {
return "", err
}
var setter string
for _, item := range processSet(up.set) {
setter += "`" + item.Column + "` ="
for _, token := range item.Expr {
switch token.Type {
case "function", "operator", "number", "substitute", "or":
setter += " " + token.Contents
case "column":
setter += " `" + token.Contents + "`"
case "string":
setter += " '" + token.Contents + "'"
}
}
setter += ","
}
setter = setter[0 : len(setter)-1]
var querystr = "UPDATE `" + up.table + "` SET " + setter + " WHERE (SELECT" + adapter.buildJoinColumns(sel.columns) + " FROM `" + sel.table + "`" + whereStr + adapter.buildOrderby(sel.orderby) + adapter.buildLimit(sel.limit) + ")"
querystr = strings.TrimSpace(querystr)
adapter.pushStatement(up.name, "update", querystr)
return querystr, nil
}
func (adapter *MysqlAdapter) SimpleInsertSelect(name string, ins DBInsert, sel DBSelect) (string, error) {
whereStr, err := adapter.buildWhere(sel.Where)
if err != nil {
@ -692,9 +692,6 @@ func (adapter *MysqlAdapter) SimpleInsertInnerJoin(name string, ins DBInsert, se
}
func (adapter *MysqlAdapter) SimpleCount(name string, table string, where string, limit string) (querystr string, err error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -778,7 +775,7 @@ func _gen_mysql() (err error) {
// Internal methods, not exposed in the interface
func (adapter *MysqlAdapter) pushStatement(name string, stype string, querystr string) {
if name[0] == '_' {
if name == "" {
return
}
adapter.Buffer[name] = DBStmt{querystr, stype}

View File

@ -43,9 +43,6 @@ func (adapter *PgsqlAdapter) DbVersion() string {
}
func (adapter *PgsqlAdapter) DropTable(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -57,9 +54,6 @@ func (adapter *PgsqlAdapter) DropTable(name string, table string) (string, error
// TODO: Implement this
// We may need to change the CreateTable API to better suit PGSQL and the other database drivers which are coming up
func (adapter *PgsqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -120,9 +114,6 @@ func (adapter *PgsqlAdapter) CreateTable(name string, table string, charset stri
// TODO: Implement this
func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -132,9 +123,6 @@ func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTable
// TODO: Test this
// ! We need to get the last ID out of this somehow, maybe add returning to every query? Might require some sort of wrapper over the sql statements
func (adapter *PgsqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -179,9 +167,6 @@ func (adapter *PgsqlAdapter) buildColumns(columns string) (querystr string) {
// TODO: Implement this
func (adapter *PgsqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -196,9 +181,6 @@ func (adapter *PgsqlAdapter) SimpleReplace(name string, table string, columns st
// TODO: Implement this
func (adapter *PgsqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -212,19 +194,16 @@ func (adapter *PgsqlAdapter) SimpleUpsert(name string, table string, columns str
}
// TODO: Implemented, but we need CreateTable and a better installer to *test* it
func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
func (adapter *PgsqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) {
if up.table == "" {
return "", errors.New("You need a name for this table")
}
if set == "" {
if up.set == "" {
return "", errors.New("You need to set data in this update statement")
}
var querystr = "UPDATE \"" + table + "\" SET "
for _, item := range processSet(set) {
var querystr = "UPDATE \"" + up.table + "\" SET "
for _, item := range processSet(up.set) {
querystr += "`" + item.Column + "` ="
for _, token := range item.Expr {
switch token.Type {
@ -248,9 +227,9 @@ func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string,
querystr = querystr[0 : len(querystr)-1]
// Add support for BETWEEN x.x
if len(where) != 0 {
if len(up.where) != 0 {
querystr += " WHERE"
for _, loc := range processWhere(where) {
for _, loc := range processWhere(up.where) {
for _, token := range loc.Expr {
switch token.Type {
case "function":
@ -274,15 +253,17 @@ func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string,
querystr = querystr[0 : len(querystr)-4]
}
adapter.pushStatement(name, "update", querystr)
adapter.pushStatement(up.name, "update", querystr)
return querystr, nil
}
// TODO: Implement this
func (adapter *PgsqlAdapter) SimpleUpdateSelect(up *updatePrebuilder) (string, error) {
return "", errors.New("not implemented")
}
// TODO: Implement this
func (adapter *PgsqlAdapter) SimpleDelete(name string, table string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -295,9 +276,6 @@ func (adapter *PgsqlAdapter) SimpleDelete(name string, table string, where strin
// TODO: Implement this
// We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead
func (adapter *PgsqlAdapter) Purge(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -306,9 +284,6 @@ func (adapter *PgsqlAdapter) Purge(name string, table string) (string, error) {
// TODO: Implement this
func (adapter *PgsqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -320,9 +295,6 @@ func (adapter *PgsqlAdapter) SimpleSelect(name string, table string, columns str
// TODO: Implement this
func (adapter *PgsqlAdapter) ComplexSelect(prebuilder *selectPrebuilder) (string, error) {
if prebuilder.name == "" {
return "", errors.New("You need a name for this statement")
}
if prebuilder.table == "" {
return "", errors.New("You need a name for this table")
}
@ -334,9 +306,6 @@ func (adapter *PgsqlAdapter) ComplexSelect(prebuilder *selectPrebuilder) (string
// TODO: Implement this
func (adapter *PgsqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" {
return "", errors.New("You need a name for the left table")
}
@ -354,9 +323,6 @@ func (adapter *PgsqlAdapter) SimpleLeftJoin(name string, table1 string, table2 s
// TODO: Implement this
func (adapter *PgsqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" {
return "", errors.New("You need a name for the left table")
}
@ -389,9 +355,6 @@ func (adapter *PgsqlAdapter) SimpleInsertInnerJoin(name string, ins DBInsert, se
// TODO: Implement this
func (adapter *PgsqlAdapter) SimpleCount(name string, table string, where string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table")
}
@ -454,7 +417,7 @@ func _gen_pgsql() (err error) {
// Internal methods, not exposed in the interface
func (adapter *PgsqlAdapter) pushStatement(name string, stype string, querystr string) {
if name[0] == '_' {
if name == "" {
return
}
adapter.Buffer[name] = DBStmt{querystr, stype}

View File

@ -110,7 +110,8 @@ type Adapter interface {
// TODO: Test this
AddColumn(name string, table string, column DBTableColumn) (string, error)
SimpleInsert(name string, table string, columns string, fields string) (string, error)
SimpleUpdate(name string, table string, set string, where string) (string, error)
SimpleUpdate(up *updatePrebuilder) (string, error)
SimpleUpdateSelect(up *updatePrebuilder) (string, error) // ! Experimental
SimpleDelete(name string, table string, where string) (string, error)
Purge(name string, table string) (string, error)
SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error)

View File

@ -25,7 +25,7 @@ type TransactionBuilder struct {
}
func (build *TransactionBuilder) SimpleDelete(table string, where string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleDelete("_builder", table, where)
res, err := build.adapter.SimpleDelete("", table, where)
if err != nil {
return stmt, err
}
@ -34,7 +34,7 @@ func (build *TransactionBuilder) SimpleDelete(table string, where string) (stmt
// Quick* versions refer to it being quick to type not the performance. For performance critical transactions, you might want to use the Simple* methods or the *Tx methods on the main builder. Alternate suggestions for names are welcome :)
func (build *TransactionBuilder) QuickDelete(table string, where string) *transactionStmt {
res, err := build.adapter.SimpleDelete("_builder", table, where)
res, err := build.adapter.SimpleDelete("", table, where)
if err != nil {
return newTransactionStmt(nil, err)
}
@ -49,7 +49,7 @@ func (build *TransactionBuilder) QuickDelete(table string, where string) *transa
}
func (build *TransactionBuilder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsert("_builder", table, columns, fields)
res, err := build.adapter.SimpleInsert("", table, columns, fields)
if err != nil {
return stmt, err
}
@ -57,7 +57,7 @@ func (build *TransactionBuilder) SimpleInsert(table string, columns string, fiel
}
func (build *TransactionBuilder) QuickInsert(table string, where string) *transactionStmt {
res, err := build.adapter.SimpleDelete("_builder", table, where)
res, err := build.adapter.SimpleDelete("", table, where)
if err != nil {
return newTransactionStmt(nil, err)
}

View File

@ -1,14 +1,8 @@
echo "Updating Gosora"
rm ./schema/lastSchema.json
cp ./schema/schema.json ./schema/lastSchema.json
git stash
git pull origin master
git stash apply
echo "Patching Gosora"
cd ./patcher
go generate
go build -o Patcher
mv ./Patcher ..
cd ..
go build -o Patcher "./patcher"
./Patcher

View File

@ -89,7 +89,9 @@ func topicRoutes() *RouteGroup {
Action("routes.LockTopicSubmit", "/topic/lock/submit/").LitBefore("req.URL.Path += extraData"),
Action("routes.UnlockTopicSubmit", "/topic/unlock/submit/", "extraData"),
Action("routes.MoveTopicSubmit", "/topic/move/submit/", "extraData"),
Action("routes.LikeTopicSubmit", "/topic/like/submit/", "extraData").Before("ParseForm"),
Action("routes.LikeTopicSubmit", "/topic/like/submit/", "extraData"),
UploadAction("routes.AddAttachToTopicSubmit", "/topic/attach/add/submit/", "extraData").MaxSizeVar("int(common.Config.MaxRequestSize)"),
Action("routes.RemoveAttachFromTopicSubmit", "/topic/attach/remove/submit/", "extraData"),
)
}
@ -99,7 +101,7 @@ func replyRoutes() *RouteGroup {
UploadAction("routes.CreateReplySubmit", "/reply/create/").MaxSizeVar("int(common.Config.MaxRequestSize)"), // TODO: Rename the route so it's /reply/create/submit/
Action("routes.ReplyEditSubmit", "/reply/edit/submit/", "extraData"),
Action("routes.ReplyDeleteSubmit", "/reply/delete/submit/", "extraData"),
Action("routes.ReplyLikeSubmit", "/reply/like/submit/", "extraData").Before("ParseForm"),
Action("routes.ReplyLikeSubmit", "/reply/like/submit/", "extraData"),
//MemberView("routes.ReplyEdit","/reply/edit/","extraData"), // No js fallback
//MemberView("routes.ReplyDelete","/reply/delete/","extraData"), // No js confirmation page? We could have a confirmation modal for the JS case
)

View File

@ -30,6 +30,7 @@ var successJSONBytes = []byte(`{"success":"1"}`)
var phraseLoginAlerts = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}],"msgCount":0}`)
// TODO: Refactor this endpoint
// TODO: Move this into the routes package
func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
// TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats
w.Header().Set("Content-Type", "application/json")
@ -44,6 +45,7 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R
}
switch r.FormValue("module") {
// TODO: Split this into it's own function
case "dismiss-alert":
asid, err := strconv.Atoi(r.FormValue("asid"))
if err != nil {
@ -61,6 +63,7 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R
if common.EnableWebsockets && count > 0 {
_ = common.WsHub.PushMessage(user.ID, `{"event":"dismiss-alert","asid":`+strconv.Itoa(asid)+`}`)
}
// TODO: Split this into it's own function
case "alerts": // A feed of events tailored for a specific user
if !user.Loggedin {
w.Write(phraseLoginAlerts)

View File

@ -21,7 +21,7 @@ var forumStmts ForumStmts
func init() {
common.DbInits.Add(func(acc *qgen.Accumulator) error {
forumStmts = ForumStmts{
getTopics: acc.Select("topics").Columns("tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, views, postCount, likeCount").Where("parentID = ?").Orderby("sticky DESC, lastReplyAt DESC, createdBy DESC").Limit("?,?").Prepare(),
getTopics: acc.Select("topics").Columns("tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, lastReplyID, parentID, views, postCount, likeCount").Where("parentID = ?").Orderby("sticky DESC, lastReplyAt DESC, createdBy DESC").Limit("?,?").Prepare(),
}
return acc.FirstError()
})
@ -68,13 +68,12 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, header
var reqUserList = make(map[int]bool)
for rows.Next() {
var topicItem = common.TopicsRow{ID: 0}
err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount)
err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.LastReplyID, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount)
if err != nil {
return common.InternalError(err, w, r)
}
topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID)
topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt)
// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
_, _, lastPage := common.PageOffset(topicItem.PostCount, 1, common.Config.ItemsPerPage)
topicItem.LastPage = lastPage

View File

@ -34,7 +34,7 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User, heade
var err error
var replyCreatedAt time.Time
var replyContent, replyCreatedByName, replyRelativeCreatedAt, replyAvatar, replyMicroAvatar, replyTag, replyClassName string
var replyContent, replyCreatedByName, replyAvatar, replyMicroAvatar, replyTag, replyClassName string
var rid, replyCreatedBy, replyLastEdit, replyLastEditBy, replyLines, replyGroup int
var replyList []common.ReplyUser
@ -98,11 +98,9 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User, heade
replyLiked := false
replyLikeCount := 0
replyRelativeCreatedAt = common.RelativeTime(replyCreatedAt)
// TODO: Add a hook here
replyList = append(replyList, common.ReplyUser{rid, puser.ID, replyContent, common.ParseMessage(replyContent, 0, ""), replyCreatedBy, common.BuildProfileURL(common.NameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyRelativeCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyMicroAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""})
replyList = append(replyList, common.ReplyUser{rid, puser.ID, replyContent, common.ParseMessage(replyContent, 0, ""), replyCreatedBy, common.BuildProfileURL(common.NameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyMicroAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""})
}
err = rows.Err()
if err != nil {

View File

@ -1,14 +1,8 @@
package routes
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"io"
"log"
"net/http"
"os"
"regexp"
"strconv"
"strings"
@ -16,7 +10,6 @@ import (
"github.com/Azareal/Gosora/common/counters"
)
// TODO: De-duplicate the upload logic
func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
tid, err := strconv.Atoi(r.PostFormValue("tid"))
if err != nil {
@ -45,70 +38,9 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
// Handle the file attachments
// TODO: Stop duplicating this code
if user.Perms.UploadFiles {
files, ok := r.MultipartForm.File["upload_files"]
if ok {
if len(files) > 5 {
return common.LocalError("You can't attach more than five files", w, r, user)
}
for _, file := range files {
if file.Filename == "" {
continue
}
log.Print("file.Filename ", file.Filename)
extarr := strings.Split(file.Filename, ".")
if len(extarr) < 2 {
return common.LocalError("Bad file", w, r, user)
}
ext := extarr[len(extarr)-1]
// TODO: Can we do this without a regex?
reg, err := regexp.Compile("[^A-Za-z0-9]+")
if err != nil {
return common.LocalError("Bad file extension", w, r, user)
}
ext = strings.ToLower(reg.ReplaceAllString(ext, ""))
if !common.AllowedFileExts.Contains(ext) {
return common.LocalError("You're not allowed to upload files with this extension", w, r, user)
}
infile, err := file.Open()
if err != nil {
return common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
hasher := sha256.New()
_, err = io.Copy(hasher, infile)
if err != nil {
return common.LocalError("Upload failed [Hashing Failed]", w, r, user)
}
infile.Close()
checksum := hex.EncodeToString(hasher.Sum(nil))
filename := checksum + "." + ext
outfile, err := os.Create("." + "/attachs/" + filename)
if err != nil {
return common.LocalError("Upload failed [File Creation Failed]", w, r, user)
}
defer outfile.Close()
infile, err = file.Open()
if err != nil {
return common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
_, err = io.Copy(outfile, infile)
if err != nil {
return common.LocalError("Upload failed [Copy Failed]", w, r, user)
}
err = common.Attachments.Add(topic.ParentID, "forums", tid, "replies", user.ID, filename)
if err != nil {
return common.InternalError(err, w, r)
}
}
_, rerr := uploadAttachment(w, r, user, topic.ParentID, "forums", tid, "replies")
if rerr != nil {
return rerr
}
}
@ -127,8 +59,8 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
var maxPollOptions = 10
var pollInputItems = make(map[int]string)
for key, values := range r.Form {
common.DebugDetail("key: ", key)
common.DebugDetailf("values: %+v\n", values)
//common.DebugDetail("key: ", key)
//common.DebugDetailf("values: %+v\n", values)
for _, value := range values {
if strings.HasPrefix(key, "pollinputitem[") {
halves := strings.Split(key, "[")

View File

@ -5,6 +5,7 @@ import (
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"io"
"log"
"net/http"
@ -22,6 +23,7 @@ import (
type TopicStmts struct {
getReplies *sql.Stmt
getLikedTopic *sql.Stmt
updateAttachs *sql.Stmt
}
var topicStmts TopicStmts
@ -32,6 +34,8 @@ func init() {
topicStmts = TopicStmts{
getReplies: acc.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", "?,?"),
getLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy = ? && targetItem = ? && targetType = 'topics'").Prepare(),
// TODO: Less race-y attachment count updates
updateAttachs: acc.Update("topics").Set("attachCount = ?").Where("tid = ?").Prepare(),
}
return acc.FirstError()
})
@ -51,7 +55,6 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
} else if err != nil {
return common.InternalError(err, w, r)
}
topic.ClassName = ""
ferr := common.ForumUserCheck(header, w, r, &user, topic.ParentID)
if ferr != nil {
@ -64,6 +67,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
header.Zone = "view_topic"
header.Path = common.BuildTopicURL(common.NameToSlug(topic.Title), topic.ID)
// TODO: Cache ContentHTML when possible?
topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums")
topic.ContentLines = strings.Count(topic.Content, "\n")
@ -76,7 +80,6 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
if postGroup.IsMod {
topic.ClassName = common.Config.StaffCSS
}
topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt)
forum, err := common.Forums.Get(topic.ParentID)
if err != nil {
@ -105,6 +108,15 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
}
}
if topic.AttachCount > 0 {
attachs, err := common.Attachments.MiniTopicGet(topic.ID)
if err != nil {
// TODO: We might want to be a little permissive here in-case of a desync?
return common.InternalError(err, w, r)
}
topic.Attachments = attachs
}
// Calculate the offset
offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage)
pageList := common.Paginate(topic.PostCount, common.Config.ItemsPerPage, 5)
@ -150,33 +162,37 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
// 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, replyItem.MicroAvatar = 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 != "" {
var action string
switch replyItem.ActionType {
case "lock":
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_lock", replyItem.UserLink, replyItem.CreatedByName)
action = "lock"
replyItem.ActionIcon = "&#x1F512;&#xFE0E"
case "unlock":
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_unlock", replyItem.UserLink, replyItem.CreatedByName)
action = "unlock"
replyItem.ActionIcon = "&#x1F513;&#xFE0E"
case "stick":
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_stick", replyItem.UserLink, replyItem.CreatedByName)
action = "stick"
replyItem.ActionIcon = "&#x1F4CC;&#xFE0E"
case "unstick":
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_unstick", replyItem.UserLink, replyItem.CreatedByName)
action = "unstick"
replyItem.ActionIcon = "&#x1F4CC;&#xFE0E"
case "move":
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_move", replyItem.UserLink, replyItem.CreatedByName)
action = "move"
replyItem.ActionIcon = ""
}
if action != "" {
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_"+action, replyItem.UserLink, replyItem.CreatedByName)
} else {
// TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry?
default:
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_default", replyItem.ActionType)
replyItem.ActionIcon = ""
}
}
if replyItem.LikeCount > 0 {
if replyItem.LikeCount > 0 && user.Liked > 0 {
likedMap[replyItem.ID] = len(tpage.ItemList)
likedQueryList = append(likedQueryList, replyItem.ID)
}
@ -192,6 +208,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
// TODO: Add a config setting to disable the liked query for a burst of extra speed
if user.Liked > 0 && len(likedQueryList) > 1 /*&& user.LastLiked <= time.Now()*/ {
// TODO: Abstract this
rows, err := qgen.NewAcc().Select("likes").Columns("targetItem").Where("sentBy = ? AND targetType = 'replies'").In("targetItem", likedQueryList[1:]).Query(user.ID)
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
@ -219,6 +236,89 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
return rerr
}
// TODO: Avoid uploading this again if the attachment already exists? They'll resolve to the same hash either way, but we could save on some IO / bandwidth here
// TODO: Enforce the max request limit on all of this topic's attachments
// TODO: Test this route
func AddAttachToTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError {
tid, err := strconv.Atoi(stid)
if err != nil {
return common.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r)
}
topic, err := common.Topics.Get(tid)
if err != nil {
return common.NotFoundJS(w, r)
}
_, ferr := common.SimpleForumUserCheck(w, r, &user, topic.ParentID)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic || !user.Perms.EditTopic || !user.Perms.UploadFiles {
return common.NoPermissionsJS(w, r, user)
}
if topic.IsClosed && !user.Perms.CloseTopic {
return common.NoPermissionsJS(w, r, user)
}
// Handle the file attachments
pathMap, rerr := uploadAttachment(w, r, user, topic.ParentID, "forums", tid, "topics")
if rerr != nil {
// TODO: This needs to be a JS error...
return rerr
}
if len(pathMap) == 0 {
return common.InternalErrorJS(errors.New("no paths for attachment add"), w, r)
}
var elemStr string
for path, aids := range pathMap {
elemStr += "\"" + path + "\":\"" + aids + "\","
}
if len(elemStr) > 1 {
elemStr = elemStr[:len(elemStr)-1]
}
w.Write([]byte(`{"success":"1","elems":[{` + elemStr + `}]}`))
return nil
}
func RemoveAttachFromTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError {
tid, err := strconv.Atoi(stid)
if err != nil {
return common.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r)
}
topic, err := common.Topics.Get(tid)
if err != nil {
return common.NotFoundJS(w, r)
}
_, ferr := common.SimpleForumUserCheck(w, r, &user, topic.ParentID)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic || !user.Perms.EditTopic {
return common.NoPermissionsJS(w, r, user)
}
if topic.IsClosed && !user.Perms.CloseTopic {
return common.NoPermissionsJS(w, r, user)
}
for _, said := range strings.Split(r.PostFormValue("aids"), ",") {
aid, err := strconv.Atoi(said)
if err != nil {
return common.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r)
}
rerr := deleteAttachment(w, r, user, aid, true)
if rerr != nil {
// TODO: This needs to be a JS error...
return rerr
}
}
w.Write(successJSONBytes)
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
@ -337,8 +437,6 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User)
var maxPollOptions = 10
var pollInputItems = make(map[int]string)
for key, values := range r.Form {
//common.DebugDetail("key: ", key)
//common.DebugDetailf("values: %+v\n", values)
for _, value := range values {
if strings.HasPrefix(key, "pollinputitem[") {
halves := strings.Split(key, "[")
@ -389,72 +487,10 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User)
}
// Handle the file attachments
// TODO: Stop duplicating this code
if user.Perms.UploadFiles {
files, ok := r.MultipartForm.File["upload_files"]
if ok {
if len(files) > 5 {
return common.LocalError("You can't attach more than five files", w, r, user)
}
for _, file := range files {
if file.Filename == "" {
continue
}
common.DebugLog("file.Filename ", file.Filename)
extarr := strings.Split(file.Filename, ".")
if len(extarr) < 2 {
return common.LocalError("Bad file", w, r, user)
}
ext := extarr[len(extarr)-1]
// TODO: Can we do this without a regex?
reg, err := regexp.Compile("[^A-Za-z0-9]+")
if err != nil {
return common.LocalError("Bad file extension", w, r, user)
}
ext = strings.ToLower(reg.ReplaceAllString(ext, ""))
if !common.AllowedFileExts.Contains(ext) {
return common.LocalError("You're not allowed to upload files with this extension", w, r, user)
}
infile, err := file.Open()
if err != nil {
return common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
hasher := sha256.New()
_, err = io.Copy(hasher, infile)
if err != nil {
return common.LocalError("Upload failed [Hashing Failed]", w, r, user)
}
infile.Close()
checksum := hex.EncodeToString(hasher.Sum(nil))
filename := checksum + "." + ext
outfile, err := os.Create("." + "/attachs/" + filename)
if err != nil {
return common.LocalError("Upload failed [File Creation Failed]", w, r, user)
}
defer outfile.Close()
infile, err = file.Open()
if err != nil {
return common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
_, err = io.Copy(outfile, infile)
if err != nil {
return common.LocalError("Upload failed [Copy Failed]", w, r, user)
}
err = common.Attachments.Add(fid, "forums", tid, "topics", user.ID, filename)
if err != nil {
return common.InternalError(err, w, r)
}
}
_, rerr := uploadAttachment(w, r, user, fid, "forums", tid, "topics")
if rerr != nil {
return rerr
}
}
@ -464,6 +500,141 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User)
return nil
}
func uploadFilesWithHash(w http.ResponseWriter, r *http.Request, user common.User, dir string) (filenames []string, rerr common.RouteError) {
files, ok := r.MultipartForm.File["upload_files"]
if !ok {
return nil, nil
}
if len(files) > 5 {
return nil, common.LocalError("You can't attach more than five files", w, r, user)
}
for _, file := range files {
if file.Filename == "" {
continue
}
//common.DebugLog("file.Filename ", file.Filename)
extarr := strings.Split(file.Filename, ".")
if len(extarr) < 2 {
return nil, common.LocalError("Bad file", w, r, user)
}
ext := extarr[len(extarr)-1]
// TODO: Can we do this without a regex?
reg, err := regexp.Compile("[^A-Za-z0-9]+")
if err != nil {
return nil, common.LocalError("Bad file extension", w, r, user)
}
ext = strings.ToLower(reg.ReplaceAllString(ext, ""))
if !common.AllowedFileExts.Contains(ext) {
return nil, common.LocalError("You're not allowed to upload files with this extension", w, r, user)
}
infile, err := file.Open()
if err != nil {
return nil, common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
hasher := sha256.New()
_, err = io.Copy(hasher, infile)
if err != nil {
return nil, common.LocalError("Upload failed [Hashing Failed]", w, r, user)
}
infile.Close()
checksum := hex.EncodeToString(hasher.Sum(nil))
filename := checksum + "." + ext
outfile, err := os.Create(dir + filename)
if err != nil {
return nil, common.LocalError("Upload failed [File Creation Failed]", w, r, user)
}
defer outfile.Close()
infile, err = file.Open()
if err != nil {
return nil, common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
_, err = io.Copy(outfile, infile)
if err != nil {
return nil, common.LocalError("Upload failed [Copy Failed]", w, r, user)
}
filenames = append(filenames, filename)
}
return filenames, nil
}
// TODO: Add a table for the files and lock the file row when performing tasks related to the file
func deleteAttachment(w http.ResponseWriter, r *http.Request, user common.User, aid int, js bool) common.RouteError {
attach, err := common.Attachments.Get(aid)
if err == sql.ErrNoRows {
return common.NotFoundJSQ(w, r, nil, js)
} else if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
err = common.Attachments.Delete(aid)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
count := common.Attachments.CountInPath(attach.Path)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
if count == 0 {
err := os.Remove("./attachs/" + attach.Path)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
}
return nil
}
// TODO: Stop duplicating this code
// TODO: Use a transaction here
func uploadAttachment(w http.ResponseWriter, r *http.Request, user common.User, sid int, sectionTable string, oid int, originTable string) (pathMap map[string]string, rerr common.RouteError) {
pathMap = make(map[string]string)
files, rerr := uploadFilesWithHash(w, r, user, "./attachs/")
if rerr != nil {
return nil, rerr
}
for _, filename := range files {
aid, err := common.Attachments.Add(sid, sectionTable, oid, originTable, user.ID, filename)
if err != nil {
return nil, common.InternalError(err, w, r)
}
_, ok := pathMap[filename]
if ok {
pathMap[filename] += "," + strconv.Itoa(aid)
} else {
pathMap[filename] = strconv.Itoa(aid)
}
switch sectionTable {
case "topics":
_, err = topicStmts.updateAttachs.Exec(common.Attachments.CountInTopic(oid), oid)
if err != nil {
return nil, common.InternalError(err, w, r)
}
err = common.Topics.Reload(oid)
if err != nil {
return nil, common.InternalError(err, w, r)
}
}
}
return pathMap, nil
}
// TODO: Update the stats after edits so that we don't under or over decrement stats during deletes
// TODO: Disable stat updates in posts handled by plugin_guilds
func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError {

View File

@ -25,21 +25,8 @@
{{end}}
</div>
{{if .CurrentUser.Loggedin}}
<div class="mod_floater auto_hide">
<form method="post">
<div class="mod_floater_head">
<span></span>
</div>
<div class="mod_floater_body">
<select class="mod_floater_options">
<option val="delete">{{lang "topic_list.moderate_delete"}}</option>
<option val="lock">{{lang "topic_list.moderate_lock"}}</option>
<option val="move">{{lang "topic_list.moderate_move"}}</option>
</select>
<button>{{lang "topic_list.moderate_run"}}</button>
</div>
</form>
</div>
{{template "topics_mod_floater.html"}}
{{if .CurrentUser.Perms.CreateTopic}}
<div id="forum_topic_create_form" class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="{{lang "quick_topic.aria"}}">
<form id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/?session={{.CurrentUser.Session}}" method="post"></form>
@ -106,7 +93,7 @@
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<a href="{{.Link}}?page={{.LastPage}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{.RelativeLastReplyAt}}</a>
<a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a>
</span>
</div>
</div>

View File

@ -77,7 +77,26 @@
</div>
<div class="content_container">
<div class="hide_on_edit topic_content user_content" itemprop="text">{{.Topic.ContentHTML}}</div>
{{if .CurrentUser.Loggedin}}{{if .CurrentUser.Perms.EditTopic}}<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>{{end}}{{end}}
{{if .CurrentUser.Loggedin}}{{if .CurrentUser.Perms.EditTopic}}<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
{{if .Topic.Attachments}}<div class="show_on_edit attach_edit_bay" tid="{{.Topic.ID}}">
{{range .Topic.Attachments}}
<div class="attach_item{{if .Image}} attach_image_holder{{end}}">
{{if .Image}}<img src="//{{$.Header.Site.URL}}/attachs/{{.Path}}?sectionID={{.SectionID}}&sectionType=forums" height="24" width="24" />{{end}}
<span class="attach_item_path" aid="{{.ID}}" fullPath="//{{$.Header.Site.URL}}/attachs/{{.Path}}">{{.Path}}</span>
<button class="attach_item_select">Select</button>
<button class="attach_item_copy">Copy</button>
</div>
{{end}}
<div class="attach_item attach_item_buttons">
{{if .CurrentUser.Perms.UploadFiles}}
<input name="upload_files" id="upload_files_op" multiple type="file" style="display: none;" />
<label for="upload_files_op" class="formbutton add_file_button">Upload</label>{{end}}
<button class="attach_item_delete">Delete</button>
</div>
</div>{{end}}
{{end}}{{end}}
<div class="controls button_container{{if .Topic.LikeCount}} has_likes{{end}}">
<div class="action_button_left">
{{if .CurrentUser.Loggedin}}
@ -97,7 +116,7 @@
</div>
<div class="action_button_right">
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.like_count_aria"}}">{{.Topic.LikeCount}}</a>
<a class="action_button created_at hide_on_mobile" title="{{abstime .Topic.CreatedAt}}">{{.Topic.RelativeCreatedAt}}</a>
<a class="action_button created_at hide_on_mobile" title="{{abstime .Topic.CreatedAt}}">{{reltime .Topic.CreatedAt}}</a>
{{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IPAddress}}" title="{{lang "topic.ip_full_tooltip"}}" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.Topic.IPAddress}}</a>{{end}}
</div>
</div>

View File

@ -28,7 +28,7 @@
</div>
<div class="action_button_right">
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.post_like_count_tooltip"}}">{{.LikeCount}}</a>
<a class="action_button created_at hide_on_mobile" title="{{abstime .CreatedAt}}">{{.RelativeCreatedAt}}</a>
<a class="action_button created_at hide_on_mobile" title="{{abstime .CreatedAt}}">{{reltime .CreatedAt}}</a>
{{if $.CurrentUser.Loggedin}}{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="IP Address" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.IPAddress}}</a>{{end}}{{end}}
</div>
</div>

View File

@ -32,22 +32,7 @@
</div>
{{if .CurrentUser.Loggedin}}
{{/** TODO: Hide these from unauthorised users? **/}}
<div class="mod_floater auto_hide">
<form method="post">
<div class="mod_floater_head">
<span></span>
</div>
<div class="mod_floater_body">
<select class="mod_floater_options">
<option val="delete">{{lang "topic_list.moderate_delete"}}</option>
<option val="lock">{{lang "topic_list.moderate_lock"}}</option>
<option val="move">{{lang "topic_list.moderate_move"}}</option>
</select>
<button class="mod_floater_submit">{{lang "topic_list.moderate_run"}}</button>
</div>
</form>
</div>
{{template "topics_mod_floater.html"}}
{{if .ForumList}}
{{/** TODO: Have a seperate forum list for moving topics? Maybe an AJAX forum search compatible with plugin_guilds? **/}}

View File

@ -0,0 +1,16 @@
{{/** TODO: Hide these from unauthorised users? **/}}
<div class="mod_floater auto_hide">
<form method="post">
<div class="mod_floater_head">
<span></span>
</div>
<div class="mod_floater_body">
<select class="mod_floater_options">
<option val="delete">{{lang "topic_list.moderate_delete"}}</option>
<option val="lock">{{lang "topic_list.moderate_lock"}}</option>
<option val="move">{{lang "topic_list.moderate_move"}}</option>
</select>
<button class="mod_floater_submit">{{lang "topic_list.moderate_run"}}</button>
</div>
</form>
</div>

View File

@ -27,7 +27,7 @@
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<a href="{{.Link}}?page={{.LastPage}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{.RelativeLastReplyAt}}</a>
<a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a>
</span>
</div>
</div>

View File

@ -1006,8 +1006,6 @@ textarea {
padding-right: 42px;
padding-bottom: 18px;
height: min-content;
/*overflow: hidden;
text-overflow: ellipsis;*/
}
.user_meta {
display: flex;
@ -1129,6 +1127,9 @@ textarea {
content: "{{lang "topic.report_button_text" .}}";
}
.attach_edit_bay {
display: none;
}
.zone_view_topic .pageset {
margin-bottom: 14px;
}

View File

@ -293,12 +293,17 @@ h2 {
.quick_create_form .topic_meta {
display: flex;
}
.quick_create_form input, .quick_create_form select {
margin-left: 0px;
margin-bottom: 0px;
}
.quick_create_form .topic_meta .topic_name_row {
margin-bottom: 8px;
width: 100%;
font-size: 14px;
}
.quick_create_form .topic_meta .topic_name_row:not(:only-child) {
margin-left: 8px;
margin-left: 6px;
}
.quick_create_form .topic_meta .topic_name_row:only-child input {
margin-left: 0px;
@ -623,12 +628,24 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
.topic_view_count:after {
content: "{{lang "topic.view_count_suffix" . }}";
}
.edithead {
margin-left: 0px;
margin-bottom: 10px;
}
.topic_name_input {
width: 100%;
margin-right: 12px;
margin-right: 10px;
margin-bottom: 0px;
margin-left: 0px;
margin-left: 0px;
}
.topic_item .submit_edit {
margin-right: 16px;
/*margin-right: 16px;*/
}
.zone_view_topic button, .zone_view_topic .formbutton {
padding: 5px;
padding-top: 4px;
padding-bottom: 4px;
}
.postImage {
width: 100%;
@ -688,7 +705,7 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
flex-direction: column;
color: #bbbbbb;
}
.action_item .content_container, .post_item .user_content {
.action_item .content_container, .post_item .user_content, .post_item .button_container {
background-color: #444444;
border-radius: 3px;
padding: 16px;
@ -698,8 +715,6 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
margin-top: 8px;
margin-bottom: auto;
padding: 14px;
background-color: #444444;
border-radius: 3px;
}
.post_item .action_button {
margin-right: 5px;
@ -713,11 +728,7 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
.post_item .action_button_right {
margin-left: auto;
}
.post_item .controls:not(.has_likes) .like_count {
display: none;
}
.action_item .userinfo, .action_item .action_icon {
.post_item .controls:not(.has_likes) .like_count, .action_item .userinfo, .action_item .action_icon {
display: none;
}
.action_item .content_container {
@ -788,6 +799,49 @@ input[type=checkbox]:checked + label .sel {
content: "{{lang "topic.like_count_suffix" . }}";
}
/*.attach_edit_bay {
display: flex;
flex-direction: row;
}*/
.attach_item {
display: flex;
background-color: #444444;
border-radius: 4px;
margin-top: 8px;
padding: 6px;
text-overflow: ellipsis;
overflow: hidden;
}
.attach_item_selected {
background-color: #446644
}
.attach_item img {
margin-right: 8px;
border-radius: 4px;
}
.attach_image_holder span {
margin-bottom: 4px;
}
.attach_edit_bay button {
margin-top: 8px;
margin-left: 8px;
}
/* New */
.attach_item {
padding: 8px;
width: 100%;
}
.attach_image_holder span {
margin-right: auto;
overflow: hidden;
text-overflow: ellipsis;
width: 300px;
}
.attach_item button {
margin-top: -1px;
}
.zone_view_topic .pageset {
margin-bottom: 14px;
}

View File

@ -12,7 +12,9 @@
$(".alerts").html(alertCount + " new alerts");
$(".user_box").addClass("has_alerts");
}
})
});
addHook("open_edit", () => $('.topic_block').addClass("edithead"));
addHook("close_edit", () => $('.topic_block').removeClass("edithead"));
})();
$(document).ready(() => {