mirror of
https://github.com/Eggbertx/gochan.git
synced 2025-08-01 22:26:24 -07:00
Add de-deprecation stuff (will not build yet)
This commit is contained in:
parent
d23d8ccd8a
commit
6567da3300
29 changed files with 1094 additions and 2671 deletions
|
@ -21,10 +21,8 @@ See [`docker/README.md`](docker/README.md)
|
|||
## Configuration
|
||||
See [config.md](config.md)
|
||||
|
||||
## Migration
|
||||
<s>If you run gochan and get a message telling you your database is out of data, please run gochan-migration. If this does not work, please contact the developers.</s>
|
||||
|
||||
gochan-migration has been a gargantuan time sink and has wasted a lot of time that would be much better spent working on other features, so I am putting its development on indefinite hiatus as of 12/18/2021 so I can focus on gochan's development. It may or may not come back, but for the time being, RIP gochan-migration, we hardly knew ya.
|
||||
<!-- ## Migration
|
||||
If you run gochan and get a message telling you your database is out of data, please run gochan-migration. If this does not work, please contact the developers. -->
|
||||
|
||||
## For developers (using Vagrant)
|
||||
1. Install Vagrant and Virtualbox. Vagrant lets you create a virtual machine and run a custom setup/installation script to make installation easier and faster.
|
||||
|
|
|
@ -14,9 +14,9 @@ func (m *Pre2021Migrator) MigrateBoards() error {
|
|||
m.newBoards = map[int]string{}
|
||||
}
|
||||
// get all boards from new db
|
||||
boards, err := gcsql.GetAllBoards()
|
||||
err := gcsql.ResetBoardSectionArrays()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// get boards from old db
|
||||
|
@ -27,8 +27,6 @@ func (m *Pre2021Migrator) MigrateBoards() error {
|
|||
for rows.Next() {
|
||||
var id int
|
||||
var dir string
|
||||
var board_type int
|
||||
var upload_type int
|
||||
var title string
|
||||
var subtitle string
|
||||
var description string
|
||||
|
@ -50,8 +48,6 @@ func (m *Pre2021Migrator) MigrateBoards() error {
|
|||
if err = rows.Scan(
|
||||
&id,
|
||||
&dir,
|
||||
&board_type,
|
||||
&upload_type,
|
||||
&title,
|
||||
&subtitle,
|
||||
&description,
|
||||
|
@ -73,11 +69,11 @@ func (m *Pre2021Migrator) MigrateBoards() error {
|
|||
return err
|
||||
}
|
||||
found := false
|
||||
for b := range boards {
|
||||
for b := range gcsql.AllBoards {
|
||||
if _, ok := m.oldBoards[id]; !ok {
|
||||
m.oldBoards[id] = dir
|
||||
}
|
||||
if boards[b].Dir == dir {
|
||||
if gcsql.AllBoards[b].Dir == dir {
|
||||
log.Printf("Board /%s/ already exists in new db, moving on\n", dir)
|
||||
found = true
|
||||
break
|
||||
|
@ -90,27 +86,25 @@ func (m *Pre2021Migrator) MigrateBoards() error {
|
|||
// omitting things like ID and creation date since we don't really care
|
||||
if err = gcsql.CreateBoard(&gcsql.Board{
|
||||
Dir: dir,
|
||||
Type: board_type, // ??
|
||||
UploadType: upload_type, // ??
|
||||
Title: title,
|
||||
Subtitle: subtitle,
|
||||
Description: description,
|
||||
Section: section,
|
||||
SectionID: section,
|
||||
MaxFilesize: max_file_size,
|
||||
MaxPages: max_pages,
|
||||
DefaultStyle: default_style,
|
||||
Locked: locked,
|
||||
Anonymous: anonymous,
|
||||
ForcedAnon: forced_anon,
|
||||
AnonymousName: anonymous,
|
||||
ForceAnonymous: forced_anon,
|
||||
MaxAge: max_age,
|
||||
AutosageAfter: autosage_after,
|
||||
NoImagesAfter: no_images_after,
|
||||
MaxMessageLength: max_message_length,
|
||||
EmbedsAllowed: embeds_allowed,
|
||||
AllowEmbeds: embeds_allowed,
|
||||
RedirectToThread: redirect_to_thread,
|
||||
RequireFile: require_file,
|
||||
EnableCatalog: enable_catalog,
|
||||
}); err != nil {
|
||||
}, false); err != nil {
|
||||
return err
|
||||
}
|
||||
m.newBoards[id] = dir
|
||||
|
|
|
@ -55,15 +55,11 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R
|
|||
}
|
||||
|
||||
for _, checkedPostID := range checkedPosts {
|
||||
var post gcsql.Post
|
||||
var err error
|
||||
post.ID = checkedPostID
|
||||
post.BoardID = boardid
|
||||
post, err = gcsql.GetSpecificPost(post.ID, true)
|
||||
post, err := gcsql.GetPostFromID(checkedPostID, true)
|
||||
if err == sql.ErrNoRows {
|
||||
serverutil.ServeError(writer, "Post does not exist", wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
"boardid": post.BoardID,
|
||||
"boardid": board.ID,
|
||||
})
|
||||
return
|
||||
} else if err != nil {
|
||||
|
@ -71,11 +67,11 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R
|
|||
Str("requestType", "deletePost").
|
||||
Err(err).
|
||||
Int("postid", post.ID).
|
||||
Int("boardid", post.BoardID).
|
||||
Int("boardid", board.ID).
|
||||
Msg("Error deleting post")
|
||||
serverutil.ServeError(writer, "Error deleting post: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
"boardid": post.BoardID,
|
||||
"boardid": board.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -83,93 +79,135 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R
|
|||
if passwordMD5 != post.Password && rank == 0 {
|
||||
serverutil.ServeError(writer, fmt.Sprintf("Incorrect password for #%d", post.ID), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
"boardid": post.BoardID,
|
||||
"boardid": board.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if fileOnly {
|
||||
fileName := post.Filename
|
||||
if fileName != "" && fileName != "deleted" {
|
||||
var files []string
|
||||
if files, err = post.GetFilePaths(); err != nil {
|
||||
gcutil.Logger().Error().
|
||||
Str("requestType", "deleteFile").
|
||||
upload, err := post.GetUpload()
|
||||
if err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("IP", gcutil.GetRealIP(request)).
|
||||
Int("postid", post.ID).
|
||||
Msg("Unable to get file upload info")
|
||||
serverutil.ServeError(writer, "Error getting file uplaod info: "+err.Error(),
|
||||
wantsJSON, map[string]interface{}{"postid": post.ID})
|
||||
return
|
||||
}
|
||||
documentRoot := config.GetSystemCriticalConfig().DocumentRoot
|
||||
if upload != nil && upload.Filename != "deleted" {
|
||||
filePath := path.Join(documentRoot, board.Dir, "src", upload.Filename)
|
||||
if err = os.Remove(filePath); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("IP", gcutil.GetRealIP(request)).
|
||||
Int("postid", post.ID).
|
||||
Err(err).
|
||||
Msg("Error getting file upload info")
|
||||
serverutil.ServeError(writer, "Error getting file upload info: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
})
|
||||
Str("filename", upload.Filename).
|
||||
Msg("Unable to delete file")
|
||||
serverutil.ServeError(writer, "Unable to delete file: "+err.Error(),
|
||||
wantsJSON, map[string]interface{}{"postid": post.ID})
|
||||
return
|
||||
}
|
||||
|
||||
if err = post.UnlinkUploads(true); err != nil {
|
||||
gcutil.Logger().Error().
|
||||
Str("requestType", "deleteFile").
|
||||
// delete the file's thumbnail
|
||||
thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("thumb"))
|
||||
if err = os.Remove(thumbPath); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("IP", gcutil.GetRealIP(request)).
|
||||
Int("postid", post.ID).
|
||||
Err(err).
|
||||
Msg("Error unlinking post uploads")
|
||||
serverutil.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
})
|
||||
Str("thumbnail", upload.ThumbnailPath("thumb")).
|
||||
Msg("Unable to delete thumbnail")
|
||||
serverutil.ServeError(writer, "Unable to delete thumbnail: "+err.Error(),
|
||||
wantsJSON, map[string]interface{}{"postid": post.ID})
|
||||
return
|
||||
}
|
||||
|
||||
for _, filePath := range files {
|
||||
if err = os.Remove(filePath); err != nil {
|
||||
fileBase := path.Base(filePath)
|
||||
gcutil.Logger().Error().
|
||||
Str("requestType", "deleteFile").
|
||||
// delete the catalog thumbnail
|
||||
if post.IsTopPost {
|
||||
thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("catalog"))
|
||||
if err = os.Remove(thumbPath); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("IP", gcutil.GetRealIP(request)).
|
||||
Int("postid", post.ID).
|
||||
Str("file", filePath).
|
||||
Err(err).
|
||||
Msg("Error unlinking post uploads")
|
||||
serverutil.ServeError(writer, fmt.Sprintf("Error deleting %s: %s", fileBase, err.Error()), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
"file": fileBase,
|
||||
})
|
||||
Str("catalogThumb", upload.ThumbnailPath("catalog")).
|
||||
Msg("Unable to delete catalog thumbnail")
|
||||
serverutil.ServeError(writer, "Unable to delete catalog thumbnail: "+err.Error(),
|
||||
wantsJSON, map[string]interface{}{"postid": post.ID})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = post.UnlinkUploads(true); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("requestType", "deleteFile").
|
||||
Int("postid", post.ID).
|
||||
Msg("Error unlinking post uploads")
|
||||
serverutil.ServeError(writer, "Unable to unlink post uploads"+err.Error(),
|
||||
wantsJSON, map[string]interface{}{"postid": post.ID})
|
||||
return
|
||||
}
|
||||
}
|
||||
_board, _ := gcsql.GetBoardFromID(post.BoardID)
|
||||
building.BuildBoardPages(&_board)
|
||||
// _board, err := post.GetBoard()
|
||||
// if err != nil {
|
||||
// gcutil.LogError(err).
|
||||
// Int("postid", post.ID).
|
||||
// Str("IP", post.IP).
|
||||
// Msg("Unable to get board info from post")
|
||||
// serverutil.ServeError(writer, "Unable to get board info from post: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
// "postid": post.ID,
|
||||
// })
|
||||
// }
|
||||
// building.BuildBoardPages(_board)
|
||||
building.BuildBoardPages(board)
|
||||
|
||||
var opPost gcsql.Post
|
||||
if post.ParentID > 0 {
|
||||
// post is a reply, get the OP
|
||||
opPost, _ = gcsql.GetSpecificPost(post.ParentID, true)
|
||||
} else {
|
||||
var opPost *gcsql.Post
|
||||
if post.IsTopPost {
|
||||
opPost = post
|
||||
} else {
|
||||
if opPost, err = post.GetTopPost(); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Int("postid", post.ID).
|
||||
Str("IP", post.IP).
|
||||
Msg("Unable to get thread information from post")
|
||||
serverutil.ServeError(writer, "Unable to get thread info from post: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if building.BuildThreadPages(opPost); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Int("postid", post.ID).
|
||||
Str("IP", post.IP).
|
||||
Msg("Unable to build thread pages")
|
||||
serverutil.ServeError(writer, "Unable to get board info from post: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
building.BuildThreadPages(&opPost)
|
||||
} else {
|
||||
// delete the post
|
||||
if err = gcsql.DeletePost(post.ID, true); err != nil {
|
||||
gcutil.Logger().Error().
|
||||
Str("requestType", "deleteFile").
|
||||
if err = post.Delete(); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("requestType", "deletePost").
|
||||
Int("postid", post.ID).
|
||||
Err(err).
|
||||
Msg("Error deleting post")
|
||||
serverutil.ServeError(writer, "Error deleting post: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
if post.ParentID == 0 {
|
||||
if post.IsTopPost {
|
||||
threadIndexPath := path.Join(config.GetSystemCriticalConfig().DocumentRoot, board.WebPath(strconv.Itoa(post.ID), "threadPage"))
|
||||
os.Remove(threadIndexPath + ".html")
|
||||
os.Remove(threadIndexPath + ".json")
|
||||
} else {
|
||||
_board, _ := gcsql.GetBoardFromID(post.BoardID)
|
||||
building.BuildBoardPages(&_board)
|
||||
building.BuildBoardPages(board)
|
||||
// _board, _ := gcsql.GetBoardFromID(post.BoardID)
|
||||
// building.BuildBoardPages(&_board)
|
||||
}
|
||||
building.BuildBoards(false, post.BoardID)
|
||||
building.BuildBoards(false, boardid)
|
||||
}
|
||||
gcutil.LogAccess(request).
|
||||
Str("requestType", "deletePost").
|
||||
Int("boardid", post.BoardID).
|
||||
Int("boardid", boardid).
|
||||
Int("postid", post.ID).
|
||||
Bool("fileOnly", fileOnly).
|
||||
Msg("Post deleted")
|
||||
|
@ -177,16 +215,16 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R
|
|||
serverutil.ServeJSON(writer, map[string]interface{}{
|
||||
"success": "post deleted",
|
||||
"postid": post.ID,
|
||||
"boardid": post.BoardID,
|
||||
"boardid": boardid,
|
||||
"fileOnly": fileOnly,
|
||||
})
|
||||
} else {
|
||||
if post.ParentID == 0 {
|
||||
if post.IsTopPost {
|
||||
// deleted thread
|
||||
http.Redirect(writer, request, board.WebPath("/", "boardPage"), http.StatusFound)
|
||||
} else {
|
||||
// deleted a post in the thread
|
||||
http.Redirect(writer, request, post.GetURL(false), http.StatusFound)
|
||||
http.Redirect(writer, request, post.WebPath(), http.StatusFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,8 +35,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
}
|
||||
passwordMD5 := gcutil.Md5Sum(password)
|
||||
|
||||
var post gcsql.Post
|
||||
post, err = gcsql.GetSpecificPost(checkedPosts[0], true)
|
||||
post, err := gcsql.GetPostFromID(checkedPosts[0], true)
|
||||
if err != nil {
|
||||
gcutil.Logger().Error().
|
||||
Err(err).
|
||||
|
@ -69,11 +68,24 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
var password string
|
||||
postid, err := strconv.Atoi(request.FormValue("postid"))
|
||||
if err != nil {
|
||||
gcutil.Logger().Error().
|
||||
Err(err).
|
||||
gcutil.LogError(err).
|
||||
Str("postid", request.FormValue("postid")).
|
||||
Str("IP", gcutil.GetRealIP(request)).
|
||||
Msg("Invalid form data")
|
||||
serverutil.ServeErrorPage(writer, "Invalid form data: "+err.Error())
|
||||
serverutil.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": postid,
|
||||
})
|
||||
return
|
||||
}
|
||||
post, err := gcsql.GetPostFromID(postid, true)
|
||||
if err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("IP", gcutil.GetRealIP(request)).
|
||||
Int("postid", postid).
|
||||
Msg("Unable to find post")
|
||||
serverutil.ServeError(writer, "Unable to find post: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": postid,
|
||||
})
|
||||
return
|
||||
}
|
||||
boardid, err := strconv.Atoi(request.FormValue("boardid"))
|
||||
|
@ -82,41 +94,47 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
Err(err).
|
||||
Str("IP", gcutil.GetRealIP(request)).
|
||||
Msg("Invalid form data")
|
||||
serverutil.ServeErrorPage(writer, "Invalid form data: "+err.Error())
|
||||
return
|
||||
}
|
||||
password, err = gcsql.GetPostPassword(postid)
|
||||
if err != nil {
|
||||
gcutil.Logger().Error().
|
||||
Err(err).
|
||||
Str("IP", gcutil.GetRealIP(request)).
|
||||
Msg("Invalid form data")
|
||||
serverutil.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
// password, err = gcsql.GetPostPassword(postid)
|
||||
// if err != nil {
|
||||
// gcutil.LogError(err).
|
||||
// Str("IP", gcutil.GetRealIP(request)).
|
||||
// Msg("Invalid form data")
|
||||
// return
|
||||
// }
|
||||
|
||||
rank := manage.GetStaffRank(request)
|
||||
if request.FormValue("password") != password && rank == 0 {
|
||||
serverutil.ServeErrorPage(writer, "Wrong password")
|
||||
serverutil.ServeError(writer, "Wrong password", wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var board gcsql.Board
|
||||
if err = board.PopulateData(boardid); err != nil {
|
||||
serverutil.ServeErrorPage(writer, "Invalid form data: "+err.Error())
|
||||
gcutil.Logger().Error().
|
||||
Err(err).
|
||||
board, err := gcsql.GetBoardFromID(boardid)
|
||||
if err != nil {
|
||||
serverutil.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"boardid": boardid,
|
||||
})
|
||||
gcutil.LogError(err).
|
||||
Str("IP", gcutil.GetRealIP(request)).
|
||||
Msg("Invalid form data")
|
||||
return
|
||||
}
|
||||
|
||||
if err = gcsql.UpdatePost(postid, request.FormValue("editemail"), request.FormValue("editsubject"),
|
||||
posting.FormatMessage(request.FormValue("editmsg"), board.Dir), request.FormValue("editmsg")); err != nil {
|
||||
gcutil.Logger().Error().
|
||||
Err(err).
|
||||
if err = post.UpdateContents(
|
||||
request.FormValue("editemail"),
|
||||
request.FormValue("editsubject"),
|
||||
posting.FormatMessage(request.FormValue("editmsg"), board.Dir),
|
||||
request.FormValue("editmsg"),
|
||||
); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Int("postid", post.ID).
|
||||
Str("IP", gcutil.GetRealIP(request)).
|
||||
Msg("Unable to edit post")
|
||||
serverutil.ServeErrorPage(writer, "Unable to edit post: "+err.Error())
|
||||
serverutil.ServeError(writer, "Unable to edit post: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ func parseCommandLine() {
|
|||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Creating new staff: %q, with password: %q and rank: %d from command line", arr[0], arr[1], rank)
|
||||
if err = gcsql.NewStaff(arr[0], arr[1], rank); err != nil {
|
||||
if _, err = gcsql.NewStaff(arr[0], arr[1], rank); err != nil {
|
||||
fmt.Printf("Failed creating new staff account for %q: %s\n", arr[0], err.Error())
|
||||
gcutil.LogFatal().
|
||||
Str("staff", "add").
|
||||
|
@ -132,7 +132,7 @@ func parseCommandLine() {
|
|||
fmt.Scanln(&answer)
|
||||
answer = strings.ToLower(answer)
|
||||
if answer == "y" || answer == "yes" {
|
||||
if err = gcsql.DeleteStaff(delstaff); err != nil {
|
||||
if err = gcsql.DeactivateStaff(delstaff); err != nil {
|
||||
fmt.Printf("Error deleting %q: %s", delstaff, err.Error())
|
||||
gcutil.LogFatal().Str("staff", "delete").Err(err).Send()
|
||||
}
|
||||
|
|
|
@ -40,10 +40,21 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
Msg("Error getting post from ID")
|
||||
return
|
||||
}
|
||||
if post.ParentID != post.ID {
|
||||
if !post.IsTopPost {
|
||||
topPostID, err := post.TopPostID()
|
||||
if err != nil {
|
||||
serverutil.ServeError(writer, "Unable to get top post ID: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
})
|
||||
gcutil.LogError(err).
|
||||
Str("IP", gcutil.GetRealIP(request)).
|
||||
Int("postid", post.ID).
|
||||
Msg("Unable to get top post ID")
|
||||
return
|
||||
}
|
||||
serverutil.ServeError(writer, "You appear to be trying to move a post that is not the top post in the thread", wantsJSON, map[string]interface{}{
|
||||
"postid": checkedPosts[0],
|
||||
"parentid": post.ParentID,
|
||||
"postid": checkedPosts[0],
|
||||
"toppost": topPostID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -160,7 +171,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
})
|
||||
return
|
||||
}
|
||||
threadUploads, err := getThreadFiles(post)
|
||||
threadUploads, err := gcsql.GetThreadFiles(post)
|
||||
if err != nil {
|
||||
gcutil.LogError(err).Int("postid", post.ID).Send()
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
|
@ -172,11 +183,11 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
for _, upload := range threadUploads {
|
||||
// move the upload itself
|
||||
tmpErr := moveFileIfExists(
|
||||
path.Join(documentRoot, srcBoard.Dir, "src", upload.filename),
|
||||
path.Join(documentRoot, destBoard.Dir, "src", upload.filename))
|
||||
path.Join(documentRoot, srcBoard.Dir, "src", upload.Filename),
|
||||
path.Join(documentRoot, destBoard.Dir, "src", upload.Filename))
|
||||
if tmpErr != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("filename", upload.filename).
|
||||
Str("filename", upload.Filename).
|
||||
Str("srcBoard", srcBoard.Dir).
|
||||
Str("destBoard", destBoard.Dir).
|
||||
Msg("Unable to move file from source board to destination board")
|
||||
|
@ -188,11 +199,11 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
|
||||
// move the upload thumbnail
|
||||
if tmpErr = moveFileIfExists(
|
||||
path.Join(documentRoot, srcBoard.Dir, "thumb", upload.thumbnail),
|
||||
path.Join(documentRoot, destBoard.Dir, "thumb", upload.thumbnail),
|
||||
path.Join(documentRoot, srcBoard.Dir, "thumb", upload.ThumbnailPath("upload")),
|
||||
path.Join(documentRoot, destBoard.Dir, "thumb", upload.ThumbnailPath("upload")),
|
||||
); tmpErr != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("thumbnail", upload.thumbnail).
|
||||
Str("thumbnail", upload.ThumbnailPath("upload")).
|
||||
Str("srcBoard", srcBoard.Dir).
|
||||
Str("destBoard", destBoard.Dir).
|
||||
Msg("Unable to move thumbnail from source board to destination board")
|
||||
|
@ -200,14 +211,14 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
err = tmpErr
|
||||
}
|
||||
}
|
||||
if upload.postID == post.ID {
|
||||
if upload.PostID == post.ID {
|
||||
// move the upload catalog thumbnail
|
||||
if tmpErr = moveFileIfExists(
|
||||
path.Join(documentRoot, srcBoard.Dir, "thumb", upload.catalogThumbnail),
|
||||
path.Join(documentRoot, destBoard.Dir, "thumb", upload.catalogThumbnail),
|
||||
path.Join(documentRoot, srcBoard.Dir, "thumb", upload.ThumbnailPath("catalog")),
|
||||
path.Join(documentRoot, destBoard.Dir, "thumb", upload.ThumbnailPath("catalog")),
|
||||
); tmpErr != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("catalogThumbnail", upload.catalogThumbnail).
|
||||
Str("catalogThumbnail", upload.ThumbnailPath("catalog")).
|
||||
Str("srcBoard", srcBoard.Dir).
|
||||
Str("destBoard", destBoard.Dir).
|
||||
Msg("Unable to move catalog thumbnail from source board to destination board")
|
||||
|
@ -222,7 +233,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
Int("movedFileForPost", post.ID).
|
||||
Str("srcBoard", srcBoard.Dir).
|
||||
Str("destBoard", destBoard.Dir).
|
||||
Str("filename", upload.filename).Send()
|
||||
Str("filename", upload.Filename).Send()
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -263,8 +274,10 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
return
|
||||
}
|
||||
|
||||
oldParentID := post.ParentID // hacky, this will likely be fixed when gcsql's handling of ParentID struct properties is changed
|
||||
post.ParentID = 0
|
||||
// oldThreadID := post.ThreadID
|
||||
|
||||
// oldParentID := post.ParentID // hacky, this will likely be fixed when gcsql's handling of ParentID struct properties is changed
|
||||
// post.ParentID = 0
|
||||
if err = building.BuildThreadPages(post); err != nil {
|
||||
gcutil.LogError(err).Int("postID", postID).Msg("Failed moved thread page")
|
||||
writer.WriteHeader(500)
|
||||
|
@ -273,8 +286,8 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
})
|
||||
return
|
||||
}
|
||||
post.ParentID = oldParentID
|
||||
if err = building.BuildBoardPages(&srcBoard); err != nil {
|
||||
// post.ParentID = oldParentID
|
||||
if err = building.BuildBoardPages(srcBoard); err != nil {
|
||||
gcutil.LogError(err).Int("srcBoardID", srcBoardID).Send()
|
||||
writer.WriteHeader(500)
|
||||
serverutil.ServeError(writer, "Failed building board page: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
|
@ -282,7 +295,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
})
|
||||
return
|
||||
}
|
||||
if err = building.BuildBoardPages(&destBoard); err != nil {
|
||||
if err = building.BuildBoardPages(destBoard); err != nil {
|
||||
gcutil.LogError(err).Int("destBoardID", destBoardID).Send()
|
||||
writer.WriteHeader(500)
|
||||
serverutil.ServeError(writer, "Failed building destination board page: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
|
@ -312,40 +325,3 @@ func moveFileIfExists(src string, dest string) error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type postUpload struct {
|
||||
filename string
|
||||
thumbnail string
|
||||
catalogThumbnail string
|
||||
postID int
|
||||
}
|
||||
|
||||
// getThreadFiles gets a list of the files owned by posts in the thread, including thumbnails for convenience.
|
||||
// TODO: move this to gcsql when the package is de-deprecated
|
||||
func getThreadFiles(post *gcsql.Post) ([]postUpload, error) {
|
||||
query := `SELECT filename,post_id FROM DBPREFIXfiles WHERE post_id IN (
|
||||
SELECT id FROM DBPREFIXposts WHERE thread_id = (
|
||||
SELECT thread_id FROM DBPREFIXposts WHERE id = ?)) AND filename != 'deleted'`
|
||||
rows, err := gcsql.QuerySQL(query, post.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var uploads []postUpload
|
||||
for rows.Next() {
|
||||
var upload postUpload
|
||||
if err = rows.Scan(&upload.filename, &upload.postID); err != nil {
|
||||
return uploads, err
|
||||
}
|
||||
upload.thumbnail = gcutil.GetThumbnailPath("thumb", upload.filename)
|
||||
|
||||
var parentID int
|
||||
if parentID, err = gcsql.GetThreadIDZeroIfTopPost(upload.postID); err != nil {
|
||||
return uploads, err
|
||||
}
|
||||
if parentID == 0 {
|
||||
upload.catalogThumbnail = gcutil.GetThumbnailPath("catalog", upload.filename)
|
||||
}
|
||||
uploads = append(uploads, upload)
|
||||
}
|
||||
return uploads, nil
|
||||
}
|
||||
|
|
|
@ -36,13 +36,20 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
return err
|
||||
}
|
||||
var currentPageFile *os.File
|
||||
var threads []interface{}
|
||||
var threadPages [][]interface{}
|
||||
var stickiedThreads []interface{}
|
||||
var nonStickiedThreads []interface{}
|
||||
var opPosts []gcsql.Post
|
||||
|
||||
// Get all top level posts for the board.
|
||||
threads, err := gcsql.GetThreadsWithBoardID(board.ID, true)
|
||||
if err != nil {
|
||||
gcutil.LogError(err).
|
||||
Int("boardID", board.ID).
|
||||
Msg("Failed getting OP posts")
|
||||
return fmt.Errorf("error getting OP posts for /%s/: %s", board.Dir, err.Error())
|
||||
}
|
||||
|
||||
// Get all top level posts for the board
|
||||
if opPosts, err = gcsql.GetTopPosts(board.ID); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("boardDir", board.Dir).
|
||||
|
@ -172,7 +179,7 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
board.NumPages = len(threadPages)
|
||||
|
||||
// Create array of page wrapper objects, and open the file.
|
||||
pagesArr := make([]map[string]interface{}, board.NumPages)
|
||||
var pagesArr boardCatalog
|
||||
|
||||
catalogJSONFile, err := os.OpenFile(path.Join(criticalCfg.DocumentRoot, board.Dir, "catalog.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
|
|
18
pkg/building/catalog.go
Normal file
18
pkg/building/catalog.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package building
|
||||
|
||||
type catalogThreadData struct {
|
||||
Replies int `json:"replies"`
|
||||
Images int `json:"images"`
|
||||
OmittedPosts int `json:"omitted_posts"`
|
||||
OmittedImages int `json:"omitted_images"`
|
||||
Sticky int `json:"sticky"`
|
||||
Locked int `json:"locked"`
|
||||
numPages int
|
||||
}
|
||||
|
||||
type catalogPage struct {
|
||||
PageNum int `json:"page"`
|
||||
Threads []catalogThreadData `json:"threads"`
|
||||
}
|
||||
|
||||
type boardCatalog []catalogPage
|
|
@ -316,6 +316,12 @@ type SiteConfig struct {
|
|||
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
|
||||
}
|
||||
|
||||
type BoardCooldowns struct {
|
||||
NewThread int `json:"threads"`
|
||||
Reply int `json:"replies"`
|
||||
ImageReply int `json:"images"`
|
||||
}
|
||||
|
||||
// BoardConfig contains information about a specific board to be stored in /path/to/board/board.json
|
||||
// If a board doesn't have board.json, the site's default board config (with values set in gochan.json) will be used
|
||||
type BoardConfig struct {
|
||||
|
@ -328,14 +334,21 @@ type BoardConfig struct {
|
|||
PostConfig
|
||||
UploadConfig
|
||||
|
||||
DateTimeFormat string `description:"The format used for dates. See <a href=\"https://golang.org/pkg/time/#Time.Format\">here</a> for more info."`
|
||||
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
|
||||
UseCaptcha bool
|
||||
CaptchaWidth int
|
||||
CaptchaHeight int
|
||||
CaptchaMinutesTimeout int
|
||||
|
||||
EnableGeoIP bool
|
||||
DateTimeFormat string `description:"The format used for dates. See <a href=\"https://golang.org/pkg/time/#Time.Format\">here</a> for more info."`
|
||||
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
|
||||
UseCaptcha bool
|
||||
CaptchaWidth int
|
||||
CaptchaHeight int
|
||||
CaptchaMinutesTimeout int
|
||||
MaxBoardPages int
|
||||
ShowPosterID bool
|
||||
EnableSpoileredImages bool
|
||||
EnableSpoileredThreads bool
|
||||
Worksafe bool
|
||||
ThreadPage int
|
||||
Cooldowns BoardCooldowns
|
||||
ThreadsPerPage int
|
||||
EnableGeoIP bool
|
||||
}
|
||||
|
||||
type BoardListConfig struct {
|
||||
|
|
|
@ -40,18 +40,18 @@ func TestVersionFunction(t *testing.T) {
|
|||
func TestStructPassing(t *testing.T) {
|
||||
initPluginTests()
|
||||
p := &gcsql.Post{
|
||||
Name: "Joe Poster",
|
||||
Email: "joeposter@gmail.com",
|
||||
MessageHTML: "Message test<br />",
|
||||
MessageText: "Message text\n",
|
||||
Name: "Joe Poster",
|
||||
Email: "joeposter@gmail.com",
|
||||
Message: "Message test<br />",
|
||||
MessageRaw: "Message text\n",
|
||||
}
|
||||
lState.SetGlobal("post", luar.New(lState, p))
|
||||
err := lState.DoString(structPassingStr)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
t.Logf("Modified message text after Lua: %q", p.MessageText)
|
||||
if p.MessageText != "Message modified by a plugin\n" || p.MessageHTML != "Message modified by a plugin<br />" {
|
||||
t.Logf("Modified message text after Lua: %q", p.MessageRaw)
|
||||
if p.MessageRaw != "Message modified by a plugin\n" || p.Message != "Message modified by a plugin<br />" {
|
||||
t.Fatal("message was not properly modified by plugin")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,114 +1,29 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"errors"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
)
|
||||
|
||||
// UpdateID takes a board struct and sets the database id according to the dir that is already set
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design. (Just bad design in general, try to avoid directly mutating state like this)
|
||||
func (board *Board) UpdateID() error {
|
||||
const query = `SELECT id FROM DBPREFIXboards WHERE dir = ?`
|
||||
return QueryRowSQL(query, interfaceSlice(board.Dir), interfaceSlice(&board.ID))
|
||||
}
|
||||
const (
|
||||
// selects all columns from DBPREFIXboards
|
||||
selectBoardsBaseSQL = `SELECT
|
||||
id, section_id, dir, navbar_position, title, subtitle, description,
|
||||
max_file_size, default_style, locked, created_at, anonymous_name, force_anonymous,
|
||||
autosage_after, no_images_after, max_message_length, allow_embeds, redirect_to_thread,
|
||||
require_file, enable_catalog
|
||||
FROM DBPREFIXboards `
|
||||
)
|
||||
|
||||
// ChangeFromRequest takes values from a HTTP request
|
||||
func (board *Board) ChangeFromRequest(request *http.Request, dbUpdate bool) error {
|
||||
if request.FormValue("docreate") != "" {
|
||||
// prevent directory changes if the board already exists
|
||||
board.Dir = request.FormValue("dir")
|
||||
}
|
||||
board.Title = request.FormValue("title")
|
||||
board.Subtitle = request.FormValue("subtitle")
|
||||
board.Description = request.FormValue("description")
|
||||
board.Type, _ = strconv.Atoi(request.FormValue("boardtype"))
|
||||
board.UploadType, _ = strconv.Atoi(request.FormValue("uploadtype"))
|
||||
board.Section, _ = strconv.Atoi(request.FormValue("section"))
|
||||
board.MaxFilesize, _ = strconv.Atoi(request.FormValue("maxfilesize"))
|
||||
board.MaxPages, _ = strconv.Atoi(request.FormValue("maxpages"))
|
||||
board.DefaultStyle = request.FormValue("defaultstyle")
|
||||
board.Locked = len(request.Form["locked"]) > 0
|
||||
board.Anonymous = request.FormValue("anonname")
|
||||
board.ForcedAnon = len(request.Form["forcedanon"]) > 0
|
||||
board.MaxAge, _ = strconv.Atoi(request.FormValue("maxage"))
|
||||
board.AutosageAfter, _ = strconv.Atoi(request.FormValue("autosageafter"))
|
||||
board.NoImagesAfter, _ = strconv.Atoi(request.FormValue("nouploadsafter"))
|
||||
board.MaxMessageLength, _ = strconv.Atoi(request.FormValue("maxmessagelength"))
|
||||
board.EmbedsAllowed = len(request.Form["embedsallowed"]) > 0
|
||||
board.RedirectToThread = len(request.Form["redirecttothread"]) > 0
|
||||
board.ShowID = len(request.Form["showid"]) > 0
|
||||
board.RequireFile = len(request.Form["requirefile"]) > 0
|
||||
board.EnableCatalog = len(request.Form["enablecatalog"]) > 0
|
||||
board.EnableSpoileredImages = len(request.Form["enablefilespoilers"]) > 0
|
||||
board.EnableSpoileredThreads = len(request.Form["enablethreadspoilers"]) > 0
|
||||
board.Worksafe = len(request.Form["worksafe"]) > 0
|
||||
board.Cooldowns.NewThread, _ = strconv.Atoi(request.FormValue("threadcooldown"))
|
||||
board.Cooldowns.Reply, _ = strconv.Atoi(request.FormValue("replycooldown"))
|
||||
board.Cooldowns.ImageReply, _ = strconv.Atoi(request.FormValue("imagecooldown"))
|
||||
board.ThreadsPerPage, _ = strconv.Atoi(request.FormValue("threadsperpage"))
|
||||
if !dbUpdate {
|
||||
return nil
|
||||
}
|
||||
id, err := getBoardIDFromURI(board.Dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
const query = `UPDATE DBPREFIXboards SET
|
||||
section_id = ?,navbar_position = ?,
|
||||
title = ?,subtitle = ?,description = ?,max_file_size = ?,default_style = ?,
|
||||
locked = ?,anonymous_name = ?,force_anonymous = ?,autosage_after = ?,no_images_after = ?,
|
||||
max_message_length = ?,allow_embeds = ?,redirect_to_thread = ?,require_file = ?,
|
||||
enable_catalog = ? WHERE id = ?`
|
||||
|
||||
_, err = ExecSQL(query,
|
||||
board.Section, board.ListOrder,
|
||||
board.Title, board.Subtitle, board.Description, board.MaxFilesize, board.DefaultStyle,
|
||||
board.Locked, board.Anonymous, board.ForcedAnon, board.AutosageAfter, board.NoImagesAfter,
|
||||
board.MaxMessageLength, board.EmbedsAllowed, board.RedirectToThread, board.RequireFile,
|
||||
board.EnableCatalog, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// PopulateData gets the board data from the database, according to its id, and sets the respective properties.
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func (board *Board) PopulateData(id int) error {
|
||||
const sql = "SELECT id, section_id, dir, navbar_position, title, subtitle, description, max_file_size, default_style, locked, created_at, anonymous_name, force_anonymous, autosage_after, no_images_after, max_message_length, allow_embeds, redirect_to_thread, require_file, enable_catalog FROM DBPREFIXboards WHERE id = ?"
|
||||
return QueryRowSQL(sql, interfaceSlice(id), interfaceSlice(&board.ID, &board.Section, &board.Dir, &board.ListOrder, &board.Title, &board.Subtitle, &board.Description, &board.MaxFilesize, &board.DefaultStyle, &board.Locked, &board.CreatedOn, &board.Anonymous, &board.ForcedAnon, &board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength, &board.EmbedsAllowed, &board.RedirectToThread, &board.RequireFile, &board.EnableCatalog))
|
||||
}
|
||||
|
||||
// Delete deletes the board from the database (if a row with the struct's ID exists) and
|
||||
// returns any errors. It does not remove the board directory or its files
|
||||
func (board *Board) Delete() error {
|
||||
exists := DoesBoardExistByID(board.ID)
|
||||
if !exists {
|
||||
return ErrBoardDoesNotExist
|
||||
}
|
||||
if board.ID == 0 {
|
||||
return ErrNilBoard
|
||||
}
|
||||
const delSql = `DELETE FROM DBPREFIXboards WHERE id = ?`
|
||||
_, err := ExecSQL(delSql, board.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// WordFilters gets an array of wordfilters that should be applied to new posts on
|
||||
// this board
|
||||
func (board *Board) WordFilters() ([]WordFilter, error) {
|
||||
wfs, err := GetWordFilters()
|
||||
if err != nil {
|
||||
return wfs, err
|
||||
}
|
||||
var applicable []WordFilter
|
||||
for _, filter := range wfs {
|
||||
if filter.OnBoard(board.Dir) {
|
||||
applicable = append(applicable, filter)
|
||||
}
|
||||
}
|
||||
return applicable, nil
|
||||
}
|
||||
var (
|
||||
AllBoards []Board
|
||||
ErrNilBoard = errors.New("board is nil")
|
||||
ErrBoardExists = errors.New("board already exists")
|
||||
ErrBoardDoesNotExist = errors.New("board does not exist")
|
||||
)
|
||||
|
||||
// DoesBoardExistByID returns a bool indicating whether a board with a given id exists
|
||||
func DoesBoardExistByID(ID int) bool {
|
||||
|
@ -126,20 +41,21 @@ func DoesBoardExistByDir(dir string) bool {
|
|||
return count > 0
|
||||
}
|
||||
|
||||
// GetAllBoards gets a list of all existing boards
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetAllBoards() ([]Board, error) {
|
||||
const sql = `SELECT id, section_id, dir, navbar_position, title, subtitle, description, max_file_size, default_style, locked, created_at, anonymous_name, force_anonymous, autosage_after, no_images_after, max_message_length, allow_embeds, redirect_to_thread, require_file, enable_catalog FROM DBPREFIXboards
|
||||
ORDER BY navbar_position ASC, dir ASC`
|
||||
rows, err := QuerySQL(sql)
|
||||
// getAllBoards gets a list of all existing boards
|
||||
func getAllBoards() ([]Board, error) {
|
||||
const query = selectBoardsBaseSQL + "ORDER BY navbar_position ASC, id ASC"
|
||||
rows, err := QuerySQL(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var boards []Board
|
||||
for rows.Next() {
|
||||
var board Board
|
||||
err = rows.Scan(&board.ID, &board.Section, &board.Dir, &board.ListOrder, &board.Title, &board.Subtitle, &board.Description, &board.MaxFilesize, &board.DefaultStyle, &board.Locked, &board.CreatedOn, &board.Anonymous, &board.ForcedAnon, &board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength, &board.EmbedsAllowed, &board.RedirectToThread, &board.RequireFile, &board.EnableCatalog)
|
||||
err = rows.Scan(
|
||||
&board.ID, &board.SectionID, &board.Dir, &board.NavbarPosition, &board.Title, &board.Subtitle, &board.Description,
|
||||
&board.MaxFilesize, &board.DefaultStyle, &board.Locked, &board.CreatedAt, &board.AnonymousName, &board.ForceAnonymous,
|
||||
&board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength, &board.AllowEmbeds, &board.RedirectToThread,
|
||||
&board.RequireFile, &board.EnableCatalog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -148,101 +64,161 @@ func GetAllBoards() ([]Board, error) {
|
|||
return boards, nil
|
||||
}
|
||||
|
||||
func GetBoardDir(id int) (string, error) {
|
||||
const query = `SELECT dir FROM DBPREFIXboards WHERE id = ?`
|
||||
var dir string
|
||||
err := QueryRowSQL(query, interfaceSlice(id), interfaceSlice(&dir))
|
||||
return dir, err
|
||||
}
|
||||
|
||||
// GetBoardFromID returns the board corresponding to a given id
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetBoardFromID(boardID int) (Board, error) {
|
||||
var board Board
|
||||
err := board.PopulateData(boardID)
|
||||
func GetBoardFromID(id int) (*Board, error) {
|
||||
const query = selectBoardsBaseSQL + "WHERE id = ?"
|
||||
board := new(Board)
|
||||
err := QueryRowSQL(query, interfaceSlice(id), interfaceSlice(
|
||||
&board.ID, &board.SectionID, &board.URI, &board.Dir, &board.NavbarPosition, &board.Title, &board.Subtitle,
|
||||
&board.Description, &board.MaxFilesize, &board.MaxThreads, &board.DefaultStyle, &board.Locked,
|
||||
&board.CreatedAt, &board.AnonymousName, &board.ForceAnonymous, &board.AutosageAfter, &board.NoImagesAfter,
|
||||
&board.MaxMessageLength, &board.MinMessageLength, &board.AllowEmbeds, &board.RedirectToThread, &board.RequireFile,
|
||||
&board.EnableCatalog,
|
||||
))
|
||||
return board, err
|
||||
}
|
||||
|
||||
// GetBoardFromPostID gets the boardURI that a given postid exists on
|
||||
func GetBoardFromPostID(postID int) (boardURI string, wasFound bool, err error) {
|
||||
const query = `SELECT board.uri FROM DBPREFIXboards as board
|
||||
JOIN (
|
||||
SELECT threads.board_id FROM DBPREFIXthreads as threads
|
||||
JOIN DBPREFIXposts as posts ON posts.thread_id = threads.id
|
||||
WHERE posts.id = ?
|
||||
) as threads ON threads.board_id = board.id`
|
||||
err = QueryRowSQL(query, interfaceSlice(postID), interfaceSlice(&boardURI))
|
||||
if err == sql.ErrNoRows {
|
||||
return "", false, nil
|
||||
}
|
||||
return boardURI, true, err
|
||||
}
|
||||
// ResetBoardSectionArrays is run when the board list needs to be changed
|
||||
// (board/section is added, deleted, etc)
|
||||
func ResetBoardSectionArrays() error {
|
||||
AllBoards = nil
|
||||
AllSections = nil
|
||||
|
||||
func getBoardIDFromURI(URI string) (id int, err error) {
|
||||
const sql = `SELECT id FROM DBPREFIXboards WHERE uri = ?`
|
||||
err = QueryRowSQL(sql, interfaceSlice(URI), interfaceSlice(&id))
|
||||
return id, err
|
||||
}
|
||||
|
||||
// CreateDefaultBoardIfNoneExist creates a default board if no boards exist yet
|
||||
func CreateDefaultBoardIfNoneExist() error {
|
||||
const sqlStr = `SELECT COUNT(id) FROM DBPREFIXboards`
|
||||
var count int
|
||||
QueryRowSQL(sqlStr, interfaceSlice(), interfaceSlice(&count))
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
defaultSectionID, err := GetOrCreateDefaultSectionID()
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
allBoardsArr, err := getAllBoards()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
board := Board{}
|
||||
board.SetDefaults("", "", "")
|
||||
board.Section = defaultSectionID
|
||||
if err = CreateBoard(&board); err != nil {
|
||||
AllBoards = append(AllBoards, allBoardsArr...)
|
||||
|
||||
allSectionsArr, err := getAllSections()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
AllSections = append(AllSections, allSectionsArr...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewBoardSimple creates a new board in the database given the directory, title, subtitle, and description.
|
||||
// Generic values are used for the other columns to be optionally changed later
|
||||
func NewBoardSimple(dir string, title string, subtitle string, description string, appendToAllBoards bool) (*Board, error) {
|
||||
sectionID, err := getOrCreateDefaultSectionID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
board := &Board{
|
||||
SectionID: sectionID,
|
||||
URI: dir,
|
||||
Dir: dir,
|
||||
NavbarPosition: 3,
|
||||
Title: title,
|
||||
Subtitle: subtitle,
|
||||
Description: description,
|
||||
MaxFilesize: 15000,
|
||||
MaxThreads: 300,
|
||||
DefaultStyle: config.GetBoardConfig("").DefaultStyle,
|
||||
Locked: false,
|
||||
AnonymousName: "Anonymous",
|
||||
ForceAnonymous: false,
|
||||
AutosageAfter: 500,
|
||||
NoImagesAfter: -1,
|
||||
MaxMessageLength: 1500,
|
||||
MinMessageLength: 0,
|
||||
AllowEmbeds: false,
|
||||
RedirectToThread: false,
|
||||
RequireFile: false,
|
||||
EnableCatalog: true,
|
||||
}
|
||||
// board.ShowID = false
|
||||
// board.EnableSpoileredImages = true
|
||||
// board.Worksafe = true
|
||||
// board.ThreadsPerPage = 20
|
||||
// board.Cooldowns = BoardCooldowns{
|
||||
// NewThread: 30,
|
||||
// Reply: 7,
|
||||
// ImageReply: 7,
|
||||
// }
|
||||
return board, CreateBoard(board, appendToAllBoards)
|
||||
}
|
||||
|
||||
// CreateBoard inserts a new board into the database, using the fields from the given Board pointer.
|
||||
// It sets board.ID and board.CreatedAt if it is successfull
|
||||
func CreateBoard(board *Board, appendToAllBoards bool) error {
|
||||
const sqlINSERT = `INSERT INTO DBPREFIXboards
|
||||
(section_id, uri, dir, navbar_position, title, suttitle,
|
||||
description, max_file_size, max_threads, default_style, locked,
|
||||
anonymous_name, force_anonymous, autosage_after, no_images_after, max_message_length,
|
||||
min_message_length, allow_embeds, redirect_to_thread, require_file, enable_catalog)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
||||
if board == nil {
|
||||
return ErrNilBoard
|
||||
}
|
||||
id, err := getNextFreeID("DBPREFIXboards")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = ExecSQL(sqlINSERT,
|
||||
&board.SectionID, &board.URI, &board.Dir, &board.NavbarPosition, &board.Title, &board.Subtitle,
|
||||
&board.Description, &board.MaxFilesize, &board.MaxThreads, &board.DefaultStyle, &board.Locked,
|
||||
&board.AnonymousName, &board.ForceAnonymous, &board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength,
|
||||
&board.MinMessageLength, &board.AllowEmbeds, &board.RedirectToThread, &board.RequireFile, &board.EnableCatalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
board.ID = id
|
||||
board.CreatedAt = time.Now()
|
||||
if appendToAllBoards {
|
||||
AllBoards = append(AllBoards, *board)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateBoard creates this board in the database if it doesnt exist already, also sets ID to correct value
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func CreateBoard(values *Board) error {
|
||||
exists := DoesBoardExistByDir(values.Dir)
|
||||
if exists {
|
||||
return ErrBoardExists
|
||||
// createDefaultBoardIfNoneExist creates a default board if no boards exist yet
|
||||
func createDefaultBoardIfNoneExist() error {
|
||||
const query = `SELECT COUNT(id) FROM DBPREFIXboards`
|
||||
var count int
|
||||
QueryRowSQL(query, interfaceSlice(), interfaceSlice(&count))
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
const maxThreads = 300
|
||||
const sqlINSERT = `INSERT INTO DBPREFIXboards (
|
||||
navbar_position, dir, uri, title, subtitle, description, max_file_size, max_threads, default_style, locked, anonymous_name, force_anonymous, autosage_after, no_images_after, max_message_length, min_message_length, allow_embeds, redirect_to_thread, require_file, enable_catalog, section_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
const sqlSELECT = "SELECT id FROM DBPREFIXboards WHERE dir = ?"
|
||||
//Excecuted in two steps this way because last row id functions arent thread safe, dir and uri is unique
|
||||
|
||||
if values == nil {
|
||||
return ErrNilBoard
|
||||
}
|
||||
_, err := ExecSQL(sqlINSERT,
|
||||
values.ListOrder, values.Dir, values.Dir, values.Title, values.Subtitle,
|
||||
values.Description, values.MaxFilesize, maxThreads, values.DefaultStyle,
|
||||
values.Locked, values.Anonymous, values.ForcedAnon, values.AutosageAfter,
|
||||
values.NoImagesAfter, values.MaxMessageLength, 1, values.EmbedsAllowed,
|
||||
values.RedirectToThread, values.RequireFile, values.EnableCatalog, values.Section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return QueryRowSQL(sqlSELECT, interfaceSlice(values.Dir), interfaceSlice(&values.ID))
|
||||
// create a default generic /test/ board
|
||||
_, err := NewBoardSimple("test", "Testing Board", "Board for testing stuff", "Board for testing stuff", true)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetBoardUris gets a list of all existing board URIs
|
||||
func GetBoardUris() (URIS []string, err error) {
|
||||
const sql = `SELECT uri FROM DBPREFIXboards`
|
||||
rows, err := QuerySQL(sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var uris []string
|
||||
for rows.Next() {
|
||||
var uri string
|
||||
if err = rows.Scan(&uri); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uris = append(uris, uri)
|
||||
}
|
||||
return uris, nil
|
||||
func getBoardIDFromURI(uri string) (int, error) {
|
||||
const sql = `SELECT id FROM DBPREFIXboards WHERE uri = ?`
|
||||
var id int
|
||||
err := QueryRowSQL(sql, interfaceSlice(uri), interfaceSlice(&id))
|
||||
return id, err
|
||||
}
|
||||
|
||||
// WebPath returns a string that represents the file's path as accessible by a browser
|
||||
// fileType should be "boardPage", "threadPage", "upload", or "thumb"
|
||||
func (board *Board) WebPath(fileName, fileType string) string {
|
||||
var filePath string
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
|
||||
switch fileType {
|
||||
case "":
|
||||
fallthrough
|
||||
case "boardPage":
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, fileName)
|
||||
case "threadPage":
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, "res", fileName)
|
||||
case "upload":
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, "src", fileName)
|
||||
case "thumb":
|
||||
fallthrough
|
||||
case "thumbnail":
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, "thumb", fileName)
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateBoard(t *testing.T) {
|
||||
err := CreateDefaultBoardIfNoneExist()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed creating default board if none exists: %s", err.Error())
|
||||
}
|
||||
}
|
|
@ -1,16 +1,12 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
var (
|
||||
gcdb *GCDB
|
||||
tcpHostIsolator = regexp.MustCompile(`\b(tcp\()?([^\(\)]*)\b`)
|
||||
)
|
||||
|
||||
|
@ -21,17 +17,6 @@ func ConnectToDB(host, driver, dbName, username, password, prefix string) error
|
|||
return err
|
||||
}
|
||||
|
||||
func initDB(initFile string) error {
|
||||
filePath := gcutil.FindResource(initFile,
|
||||
"/usr/local/share/gochan/"+initFile,
|
||||
"/usr/share/gochan/"+initFile)
|
||||
if filePath == "" {
|
||||
return fmt.Errorf(
|
||||
"SQL database initialization file (%s) missing. Please reinstall gochan", initFile)
|
||||
}
|
||||
return RunSQLFile(filePath)
|
||||
}
|
||||
|
||||
// RunSQLFile cuts a given sql file into individual statements and runs it.
|
||||
func RunSQLFile(path string) error {
|
||||
sqlBytes, err := os.ReadFile(path)
|
||||
|
|
|
@ -10,6 +10,8 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// GochanVersionKeyConstant is the key value used in the version table of the database to store and receive the (database) version of base gochan
|
||||
gochanVersionKeyConstant = "gochan"
|
||||
UnsupportedSQLVersionMsg = `Received syntax error while preparing a SQL string.
|
||||
This means that either there is a bug in gochan's code (hopefully not) or that you are using an unsupported MySQL/PostgreSQL version.
|
||||
Before reporting an error, make sure that you are using the up to date version of your selected SQL server.
|
||||
|
@ -19,12 +21,13 @@ const (
|
|||
sqlite3ConnStr = "file:%s?_auth&_auth_user=%s&_auth_pass=%s&_auth_crypt=sha1"
|
||||
)
|
||||
|
||||
var gcdb *GCDB
|
||||
|
||||
type GCDB struct {
|
||||
db *sql.DB
|
||||
connStr string
|
||||
driver string
|
||||
replacer *strings.Replacer
|
||||
// nilTimestamp string
|
||||
}
|
||||
|
||||
func (db *GCDB) ConnectionString() string {
|
||||
|
@ -39,10 +42,6 @@ func (db *GCDB) SQLDriver() string {
|
|||
return db.driver
|
||||
}
|
||||
|
||||
/* func (db *GCDB) NilSQLTimestamp() string {
|
||||
return db.nilTimestamp
|
||||
} */
|
||||
|
||||
func (db *GCDB) Close() error {
|
||||
if db.db != nil {
|
||||
return db.db.Close()
|
||||
|
@ -112,7 +111,7 @@ Example:
|
|||
var intVal int
|
||||
var stringVal string
|
||||
err := db.QueryRowSQL("SELECT intval,stringval FROM table WHERE id = ?",
|
||||
[]interface{}{&id},
|
||||
[]interface{}{id},
|
||||
[]interface{}{&intVal, &stringVal})
|
||||
*/
|
||||
func (db *GCDB) QueryRowSQL(query string, values, out []interface{}) error {
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
package gcsql
|
||||
|
||||
import "database/sql"
|
||||
|
||||
// GetFilenameBans returns an array of filename bans. If matchFilename is a blank string, it returns all of them.
|
||||
// If exactMatch is true, it returns an array of bans that = matchFilename, otherwise it treats matchFilename
|
||||
// as a SQL wildcard query using LIKE
|
||||
func GetFilenameBans(matchFilename string, exactMatch bool) ([]FilenameBan, error) {
|
||||
query := `SELECT id,board_id,staff_id,staff_note,issued_at,filename,is_regex FROM DBPREFIXfilename_ban`
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
if matchFilename != "" {
|
||||
if exactMatch {
|
||||
rows, err = QuerySQL(query+" WHERE filename = ?", matchFilename)
|
||||
} else {
|
||||
rows, err = QuerySQL(query+" WHERE filename LIKE ?", matchFilename)
|
||||
}
|
||||
} else {
|
||||
rows, err = QuerySQL(query)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var fnBans []FilenameBan
|
||||
for rows.Next() {
|
||||
var ban FilenameBan
|
||||
if err = rows.Scan(
|
||||
&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote,
|
||||
&ban.IssuedAt, &ban.Filename, &ban.IsRegex,
|
||||
); err != nil {
|
||||
return fnBans, err
|
||||
}
|
||||
fnBans = append(fnBans, ban)
|
||||
}
|
||||
return fnBans, err
|
||||
}
|
||||
|
||||
// CreateFileNameBan creates a new ban on a filename. If boards is an empty string
|
||||
// or the resulting query = nil, the ban is global, whether or not allBoards is set
|
||||
func CreateFileNameBan(fileName string, isRegex bool, staffName string, staffNote, boardURI string) error {
|
||||
const sql = `INSERT INTO DBPREFIXfilename_ban (board_id, staff_id, staff_note, filename, is_regex) VALUES(?,?,?,?,?)`
|
||||
staffID, err := getStaffID(staffName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var boardID *int = nil
|
||||
if boardURI != "" {
|
||||
boardID = getBoardIDFromURIOrNil(boardURI)
|
||||
}
|
||||
_, err = ExecSQL(sql, boardID, staffID, staffNote, fileName, isRegex)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteFilenameBanByID deletes the ban, given the id column value
|
||||
func DeleteFilenameBanByID(id int) error {
|
||||
_, err := ExecSQL("DELETE FROM DBPREFIXfilename_ban WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetFileChecksumBans(matchChecksum string) ([]FileBan, error) {
|
||||
query := `SELECT id,board_id,staff_id,staff_note,issued_at,checksum FROM DBPREFIXfile_ban`
|
||||
if matchChecksum != "" {
|
||||
query += " WHERE checksum = ?"
|
||||
}
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
if matchChecksum == "" {
|
||||
rows, err = QuerySQL(query)
|
||||
} else {
|
||||
rows, err = QuerySQL(query, matchChecksum)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var fileBans []FileBan
|
||||
for rows.Next() {
|
||||
var fileBan FileBan
|
||||
if err = rows.Scan(
|
||||
&fileBan.ID, &fileBan.BoardID, &fileBan.StaffID, &fileBan.StaffNote,
|
||||
&fileBan.IssuedAt, &fileBan.Checksum,
|
||||
); err != nil {
|
||||
return fileBans, err
|
||||
}
|
||||
fileBans = append(fileBans, fileBan)
|
||||
}
|
||||
return fileBans, nil
|
||||
}
|
||||
|
||||
// CreateFileBan creates a new ban on a file. If boards = nil, the ban is global.
|
||||
func CreateFileBan(fileChecksum, staffName string, staffNote, boardURI string) error {
|
||||
const sql = `INSERT INTO DBPREFIXfile_ban (board_id, staff_id, staff_note, checksum) VALUES(?,?,?,?)`
|
||||
staffID, err := getStaffID(staffName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
boardID := getBoardIDFromURIOrNil(boardURI)
|
||||
_, err = ExecSQL(sql, boardID, staffID, staffNote, fileChecksum)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteFileBanByID deletes the ban, given the id column value
|
||||
func DeleteFileBanByID(id int) error {
|
||||
_, err := ExecSQL("DELETE FROM DBPREFIXfile_ban WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
func checkFilenameBan(filename string) (*FilenameBan, error) {
|
||||
const sql = `SELECT id, board_id, staff_id, staff_note, issued_at, filename, is_regex
|
||||
FROM DBPREFIXfilename_ban WHERE filename = ?`
|
||||
var ban = new(FilenameBan)
|
||||
err := QueryRowSQL(sql, interfaceSlice(filename), interfaceSlice(&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt, &ban.Filename, &ban.IsRegex))
|
||||
return ban, err
|
||||
}
|
||||
|
||||
func checkFileBan(checksum string) (*FileBan, error) {
|
||||
const sql = `SELECT id, board_id, staff_id, staff_note, issued_at, checksum
|
||||
FROM DBPREFIXfile_ban WHERE checksum = ?`
|
||||
var ban = new(FileBan)
|
||||
err := QueryRowSQL(sql, interfaceSlice(checksum), interfaceSlice(&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt, &ban.Checksum))
|
||||
return ban, err
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func populateTestDB() error {
|
||||
var err error
|
||||
if err = initDB("../../initdb_sqlite3.sql"); err != nil {
|
||||
return err
|
||||
}
|
||||
err = CreateDefaultBoardIfNoneExist()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = CreateDefaultAdminIfNoStaff()
|
||||
return err
|
||||
}
|
||||
|
||||
func TestDatabaseVersion(t *testing.T) {
|
||||
dbVersion, dbFlag, err := GetCompleteDatabaseVersion()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
if dbVersion < 1 {
|
||||
t.Fatalf("Database version should be > 0 (got %d)", dbVersion)
|
||||
}
|
||||
if dbFlag != DBClean && dbFlag != DBUpToDate {
|
||||
t.Fatalf("Got an unexpected DB flag (%#x), should be either clean or up to date", dbFlag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log.SetFlags(0)
|
||||
config.InitConfig("3.2.0")
|
||||
|
||||
os.Remove("./testdata/gochantest.db")
|
||||
var err error
|
||||
gcdb, err = Open("./testdata/gochantest.db", "sqlite3", "gochan", "gochan", "gochan", "gc_")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer gcdb.Close()
|
||||
|
||||
if err = populateTestDB(); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
exitCode := m.Run()
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
|
@ -2,211 +2,147 @@ package gcsql
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"path"
|
||||
"time"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
// SinceLastPost returns the number of seconds since the given IP address created a post
|
||||
// (used for checking against the new thread/new reply cooldown)
|
||||
func SinceLastPost(postIP string) (int, error) {
|
||||
const sql = `SELECT MAX(created_on) FROM DBPREFIXposts WHERE ip = ?`
|
||||
var when time.Time
|
||||
err := QueryRowSQL(sql, interfaceSlice(postIP), interfaceSlice(&when))
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return int(time.Since(when).Seconds()), nil
|
||||
}
|
||||
const (
|
||||
// should be appended when selecting info from DBPREFIXboards, requires a post ID
|
||||
boardFromPostIdSuffixSQL = ` WHERE id = (
|
||||
SELECT board_id FROM DBPREFIXthreads WHERE id = (
|
||||
SELECT thread_id FROM DBPREFIXposts WHERE id = ?))`
|
||||
selectPostsBaseSQL = `SELECT
|
||||
id, thread_id, is_top_post, ip, created_on, name, tripcode, is_role_signature,
|
||||
email, subject, message, message_raw, password, deleted_at, is_deleted, banned_message
|
||||
FROM DBPREFIXposts `
|
||||
)
|
||||
|
||||
// InsertPost insersts prepared post object into the SQL table so that it can be rendered
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func InsertPost(post *Post, bump bool) error {
|
||||
const sql = `INSERT INTO DBPREFIXposts (id, thread_id, name, tripcode, is_role_signature, email, subject, ip, is_top_post, message, message_raw, banned_message, password)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
isNewThread := post.ParentID == 0
|
||||
var threadID int
|
||||
var err error
|
||||
if isNewThread {
|
||||
threadID, err = createThread(post.BoardID, post.Locked, post.Stickied, post.Autosage, false)
|
||||
} else {
|
||||
threadID, err = getThreadID(post.ParentID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
ErrNotTopPost = errors.New("not the top post in the thread")
|
||||
ErrPostDoesNotExist = errors.New("post does not exist")
|
||||
ErrPostDeleted = errors.New("post is deleted")
|
||||
)
|
||||
|
||||
//Retrieves next free ID, explicitly inserts it, keeps retrying until succesfull insert or until a non-pk error is encountered.
|
||||
//This is done because mysql doesnt support RETURNING and both LAST_INSERT_ID() and last_row_id() are not thread-safe
|
||||
isPrimaryKeyError := true
|
||||
for isPrimaryKeyError {
|
||||
nextFreeID, err := getNextFreeID("DBPREFIXposts")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = ExecSQL(sql, nextFreeID, threadID, post.Name, post.Tripcode, false, post.Email, post.Subject, post.IP, isNewThread, string(post.MessageHTML), post.MessageText, "", post.Password)
|
||||
|
||||
isPrimaryKeyError, err = errFilterDuplicatePrimaryKey(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isPrimaryKeyError {
|
||||
post.ID = nextFreeID
|
||||
}
|
||||
}
|
||||
|
||||
if post.Filename != "" {
|
||||
err = appendFile(post.ID, post.FilenameOriginal, post.Filename, post.FileChecksum, post.Filesize, false, post.ImageW, post.ImageH, post.ThumbW, post.ThumbH)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bump {
|
||||
return bumpThread(threadID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetReplyCount gets the total amount non-deleted of replies in a thread
|
||||
func GetReplyCount(postID int) (int, error) {
|
||||
const sql = `SELECT COUNT(posts.id) FROM DBPREFIXposts as posts
|
||||
JOIN (
|
||||
SELECT threads.id FROM DBPREFIXthreads as threads
|
||||
JOIN DBPREFIXposts as posts
|
||||
ON posts.thread_id = threads.id
|
||||
WHERE posts.id = ?
|
||||
) as thread
|
||||
ON posts.thread_id = thread.id
|
||||
WHERE posts.is_deleted = FALSE`
|
||||
var count int
|
||||
err := QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&count))
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetReplyFileCount gets the amount of files non-deleted posted in total in a thread
|
||||
func GetReplyFileCount(postID int) (int, error) {
|
||||
const sql = `SELECT COUNT(files.id) from DBPREFIXfiles as files
|
||||
JOIN (SELECT posts.id FROM DBPREFIXposts as posts
|
||||
JOIN (
|
||||
SELECT threads.id FROM DBPREFIXthreads as threads
|
||||
JOIN DBPREFIXposts as posts
|
||||
ON posts.thread_id = threads.id
|
||||
WHERE posts.id = ?
|
||||
) as thread
|
||||
ON posts.thread_id = thread.id
|
||||
WHERE posts.is_deleted = FALSE) as posts
|
||||
ON posts.id = files.post_id`
|
||||
var count int
|
||||
err := QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&count))
|
||||
return count, err
|
||||
}
|
||||
|
||||
const selectPostsQuery = `SELECT
|
||||
DBPREFIXposts.id,
|
||||
thread_id AS threadid,
|
||||
(SELECT id FROM DBPREFIXposts WHERE is_top_post = TRUE AND thread_id = threadid LIMIT 1),
|
||||
(SELECT board_id FROM DBPREFIXthreads WHERE id = DBPREFIXposts.thread_id) as board_id,
|
||||
ip,created_on,name,tripcode,email,subject,message,message_raw,password,
|
||||
|
||||
COALESCE(files.filename,''),
|
||||
COALESCE(files.original_filename,''),
|
||||
COALESCE(files.file_size, 0),
|
||||
COALESCE(files.checksum, ''),
|
||||
COALESCE(files.thumbnail_width, 0),
|
||||
COALESCE(files.thumbnail_height, 0),
|
||||
COALESCE(files.width, 0),
|
||||
COALESCE(files.height, 0)
|
||||
FROM DBPREFIXposts LEFT OUTER JOIN DBPREFIXfiles AS files ON files.post_id = DBPREFIXposts.id`
|
||||
|
||||
// GetPostFromID gets the post from the database with a matching ID,
|
||||
// optionally requiring it to not be deleted
|
||||
func GetPostFromID(id int, onlyNotDeleted bool) (*Post, error) {
|
||||
sql := selectPostsQuery + ` WHERE DBPREFIXposts.id = ?`
|
||||
query := selectPostsBaseSQL + "WHERE id = ?"
|
||||
if onlyNotDeleted {
|
||||
sql += " AND is_deleted = 0"
|
||||
query += " AND is_deleted = 0"
|
||||
}
|
||||
var drop int
|
||||
post := new(Post)
|
||||
err := QueryRowSQL(sql, []interface{}{id}, []interface{}{
|
||||
&post.ID, &drop, &post.ParentID,
|
||||
&post.BoardID,
|
||||
&post.IP, &post.Timestamp, &post.Name, &post.Tripcode, &post.Email, &post.Subject, &post.MessageHTML, &post.MessageText, &post.Password,
|
||||
&post.Filename, &post.FilenameOriginal, &post.Filesize, &post.FileChecksum, &post.ThumbW, &post.ThumbH, &post.ImageW, &post.ImageH,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
post.ID = id
|
||||
err := QueryRowSQL(query, interfaceSlice(id), interfaceSlice(
|
||||
&post.ID, &post.ThreadID, &post.IsTopPost, &post.IP, &post.CreatedOn, &post.Name, &post.Tripcode, &post.IsRoleSignature,
|
||||
&post.Email, &post.Subject, &post.Message, &post.MessageRaw, &post.Password, &post.DeletedAt, &post.IsDeleted, &post.BannedMessage,
|
||||
))
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrPostDoesNotExist
|
||||
|
||||
}
|
||||
return post, err
|
||||
}
|
||||
|
||||
// GetPostsFromIP gets the posts from the database with a matching IP address, specifying
|
||||
// optionally requiring them to not be deleted
|
||||
func GetPostsFromIP(ip string, limit int, onlyNotDeleted bool) ([]Post, error) {
|
||||
sql := selectPostsQuery + ` WHERE DBPREFIXposts.ip = ?`
|
||||
if onlyNotDeleted {
|
||||
sql += " AND is_deleted = 0"
|
||||
}
|
||||
|
||||
sql += " ORDER BY id DESC LIMIT ?"
|
||||
rows, err := QuerySQL(sql, ip, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var posts []Post
|
||||
for rows.Next() {
|
||||
var post Post
|
||||
var drop int
|
||||
if err = rows.Scan(
|
||||
&post.ID, &drop, &post.ParentID,
|
||||
&post.BoardID,
|
||||
&post.IP, &post.Timestamp, &post.Name, &post.Tripcode, &post.Email, &post.Subject, &post.MessageHTML, &post.MessageText, &post.Password,
|
||||
&post.Filename, &post.FilenameOriginal, &post.Filesize, &post.FileChecksum, &post.ThumbW, &post.ThumbH, &post.ImageW, &post.ImageH,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
posts = append(posts, post)
|
||||
}
|
||||
return posts, nil
|
||||
// GetPostPassword returns the password checksum of the post with the given ID
|
||||
func GetPostPassword(id int) (string, error) {
|
||||
const query = `SELECT password FROM DBPREFIXposts WHERE id = ?`
|
||||
var passwordChecksum string
|
||||
err := QueryRowSQL(query, interfaceSlice(id), interfaceSlice(&passwordChecksum))
|
||||
return passwordChecksum, err
|
||||
}
|
||||
|
||||
// GetFilePaths returns an array of absolute paths to uploaded files and thumbnails associated
|
||||
// with this post, and any errors that occurred
|
||||
func (p *Post) GetFilePaths() ([]string, error) {
|
||||
boardDir, err := p.GetBoardDir()
|
||||
// UpdateContents updates the email, subject, and message text of the post
|
||||
func (p *Post) UpdateContents(email string, subject string, message template.HTML, messageRaw string) error {
|
||||
const sqlUpdate = `UPDATE DBPREFIXposts SET email = ?, subject = ?, message = ?, message_raw = ? WHERE ID = ?`
|
||||
_, err := ExecSQL(sqlUpdate, email, subject, message, messageRaw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Email = email
|
||||
p.Subject = subject
|
||||
p.Message = message
|
||||
p.MessageRaw = messageRaw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Post) GetBoardID() (int, error) {
|
||||
const query = `SELECT board_id FROM DBPREFIXthreads where id = ?`
|
||||
var boardID int
|
||||
err := QueryRowSQL(query, interfaceSlice(p.ThreadID), interfaceSlice(&boardID))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = ErrBoardDoesNotExist
|
||||
}
|
||||
return boardID, err
|
||||
}
|
||||
|
||||
func (p *Post) GetBoardDir() (string, error) {
|
||||
const query = "SELECT dir FROM DBPREFIXboards" + boardFromPostIdSuffixSQL
|
||||
var dir string
|
||||
err := QueryRowSQL(query, interfaceSlice(p.ID), interfaceSlice(&dir))
|
||||
return dir, err
|
||||
}
|
||||
|
||||
func (p *Post) GetBoard() (*Board, error) {
|
||||
const query = selectBoardsBaseSQL + boardFromPostIdSuffixSQL
|
||||
|
||||
board := new(Board)
|
||||
err := QueryRowSQL(query, interfaceSlice(), interfaceSlice(
|
||||
&board.ID, &board.SectionID, &board.URI, &board.Dir, &board.NavbarPosition, &board.Title, &board.Subtitle,
|
||||
&board.Description, &board.MaxFilesize, &board.MaxThreads, &board.DefaultStyle, &board.Locked,
|
||||
&board.CreatedAt, &board.AnonymousName, &board.ForceAnonymous, &board.AutosageAfter, &board.NoImagesAfter,
|
||||
&board.MaxMessageLength, &board.MinMessageLength, &board.AllowEmbeds, &board.RedirectToThread, &board.RequireFile,
|
||||
&board.EnableCatalog,
|
||||
))
|
||||
return board, err
|
||||
}
|
||||
|
||||
// ChangeBoardID updates the post with the new board ID if it is a top post. It returns an error if it is not
|
||||
// an OP or if ChangeThreadBoardID returned any errors
|
||||
func (p *Post) ChangeBoardID(newBoardID int) error {
|
||||
if !p.IsTopPost {
|
||||
return ErrNotTopPost
|
||||
}
|
||||
return ChangeThreadBoardID(p.ThreadID, newBoardID)
|
||||
}
|
||||
|
||||
// TopPostID returns the OP post ID of the thread that p is in
|
||||
func (p *Post) TopPostID() (int, error) {
|
||||
if p.IsTopPost {
|
||||
return p.ID, nil
|
||||
}
|
||||
const query = `SELECT id FROM DBPREFIXposts WHERE thread_id = ? and is_top_post = TRUE ORDER BY id ASC LIMIT 1`
|
||||
var topPostID int
|
||||
err := QueryRowSQL(query, interfaceSlice(p.ThreadID), interfaceSlice(&topPostID))
|
||||
return topPostID, err
|
||||
}
|
||||
|
||||
// GetTopPost returns the OP of the thread that p is in
|
||||
func (p *Post) GetTopPost() (*Post, error) {
|
||||
opID, err := p.TopPostID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
const filenameSQL = `SELECT filename FROM DBPREFIXfiles WHERE post_id = ?`
|
||||
rows, err := QuerySQL(filenameSQL, p.ID)
|
||||
var paths []string
|
||||
if err == sql.ErrNoRows {
|
||||
return paths, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
return GetPostFromID(opID, true)
|
||||
}
|
||||
|
||||
// GetPostUpload returns the upload info associated with the file as well as any errors encountered.
|
||||
// If the file has no uploads, then *Upload is nil. If the file was removed from the post, then Filename
|
||||
// and OriginalFilename = "deleted"
|
||||
func (p *Post) GetUpload() (*Upload, error) {
|
||||
const query = `SELECT
|
||||
id, post_id, file_order, original_filename, filename, checksum,
|
||||
file_size, is_spoilered, thumbnail_width, thumbnail_height, width, height
|
||||
FROM DBPREFIXfiles WHERE post_id = ?`
|
||||
upload := new(Upload)
|
||||
err := QueryRowSQL(query, interfaceSlice(p.ID), interfaceSlice(
|
||||
&upload.ID, &upload.PostID, &upload.FileOrder, &upload.OriginalFilename, &upload.Filename, &upload.Checksum,
|
||||
&upload.FileSize, &upload.IsSpoilered, &upload.ThumbnailWidth, &upload.ThumbnailHeight, &upload.Width, &upload.Height,
|
||||
))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
documentRoot := config.GetSystemCriticalConfig().DocumentRoot
|
||||
for rows.Next() {
|
||||
var filename string
|
||||
if err = rows.Scan(&filename); err != nil {
|
||||
return paths, err
|
||||
}
|
||||
_, filenameBase, fileExt := gcutil.GetFileParts(filename)
|
||||
thumbExt := fileExt
|
||||
if thumbExt == "gif" || thumbExt == "webm" || thumbExt == "mp4" {
|
||||
thumbExt = "jpg"
|
||||
}
|
||||
paths = append(paths,
|
||||
path.Join(documentRoot, boardDir, "/src/", filenameBase+"."+fileExt),
|
||||
path.Join(documentRoot, boardDir, "/thumb/", filenameBase+"t."+thumbExt), // thumbnail path
|
||||
)
|
||||
if p.ParentID == 0 {
|
||||
paths = append(paths, path.Join(documentRoot, boardDir, "/thumb/", filenameBase+"c."+thumbExt)) // catalog thumbnail path
|
||||
}
|
||||
}
|
||||
return paths, nil
|
||||
return upload, err
|
||||
}
|
||||
|
||||
// UnlinkUploads disassociates the post with any uploads in DBPREFIXfiles
|
||||
|
@ -223,3 +159,29 @@ func (p *Post) UnlinkUploads(leaveDeletedBox bool) error {
|
|||
_, err := ExecSQL(sqlStr, p.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete sets the post as deleted and sets the deleted_at timestamp to the current time
|
||||
func (p *Post) Delete() error {
|
||||
if p.IsTopPost {
|
||||
return deleteThread(p.ThreadID)
|
||||
}
|
||||
const deleteSQL = `UPDATE DBPREFIXposts SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE id = ?`
|
||||
_, err := ExecSQL(deleteSQL, p.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Post) WebPath() string {
|
||||
webRoot := config.GetSystemCriticalConfig().WebRoot
|
||||
var threadID, opID, boardID int
|
||||
var boardDir string
|
||||
const query = `SELECT thread_id as threadid,
|
||||
(SELECT id from DBPREFIXposts WHERE thread_id = threadid AND is_top_post = TRUE LIMIT 1) as op,
|
||||
(SELECT board_id from DBPREFIXthreads WHERE id = threadid) AS boardid,
|
||||
(SELECT dir FROM DBPREFIXboards WHERE id = boardid) AS dir
|
||||
FROM DBPREFIXposts WHERE id = ?`
|
||||
err := QueryRowSQL(query, interfaceSlice(p.ID), interfaceSlice(&threadID, &opID, &boardID, &boardDir))
|
||||
if err != nil {
|
||||
return webRoot
|
||||
}
|
||||
return webRoot + boardDir + fmt.Sprintf("/res/%d.html#%d", opID, p.ID)
|
||||
}
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInsertPosts(t *testing.T) {
|
||||
err := InsertPost(&Post{
|
||||
ParentID: 0,
|
||||
BoardID: 1,
|
||||
Name: "Joe Poster",
|
||||
Tripcode: "Blah",
|
||||
Email: "any@example.com",
|
||||
Subject: "First thread",
|
||||
MessageHTML: "First post best post",
|
||||
MessageText: "First post best post",
|
||||
Password: "12345",
|
||||
Filename: "12345.png",
|
||||
FilenameOriginal: "somefile.png",
|
||||
FileChecksum: "abcd1234",
|
||||
FileExt: ".png",
|
||||
Filesize: 1000,
|
||||
ImageW: 2000,
|
||||
ImageH: 2000,
|
||||
ThumbW: 250,
|
||||
ThumbH: 250,
|
||||
IP: "192.168.56.1",
|
||||
}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
|
@ -1,313 +0,0 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"html/template"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
/*
|
||||
Left join singlefiles on recentposts where recentposts.selfid = singlefiles.post_id
|
||||
Coalesce filenames to "" (if filename = null -> "" else filename)
|
||||
*/
|
||||
var abstractSelectPosts = `
|
||||
SELECT
|
||||
recentposts.selfid AS id,
|
||||
recentposts.toppostid AS parentid,
|
||||
recentposts.boardid,
|
||||
recentposts.name,
|
||||
recentposts.tripcode,
|
||||
recentposts.email,
|
||||
recentposts.subject,
|
||||
recentposts.message,
|
||||
recentposts.message_raw,
|
||||
recentposts.password,
|
||||
COALESCE(singlefiles.filename, '') as filename,
|
||||
COALESCE(singlefiles.original_filename, '') as original_filename,
|
||||
COALESCE(singlefiles.checksum, ''),
|
||||
COALESCE(singlefiles.file_size, 0),
|
||||
COALESCE(singlefiles.width, 0),
|
||||
COALESCE(singlefiles.height, 0),
|
||||
COALESCE(singlefiles.thumbnail_width, 0),
|
||||
COALESCE(singlefiles.thumbnail_height, 0),
|
||||
recentposts.ip,
|
||||
recentposts.created_on,
|
||||
recentposts.anchored,
|
||||
recentposts.last_bump,
|
||||
recentposts.stickied,
|
||||
recentposts.locked
|
||||
FROM
|
||||
(SELECT
|
||||
posts.id AS selfid,
|
||||
COALESCE(NULLIF(topposts.id, posts.id), 0) AS toppostid,
|
||||
boards.id AS boardid,
|
||||
posts.name,
|
||||
posts.ip,
|
||||
posts.tripcode,
|
||||
posts.message,
|
||||
posts.email,
|
||||
posts.subject,
|
||||
posts.message_raw,
|
||||
posts.password,
|
||||
posts.created_on,
|
||||
posts.is_top_post,
|
||||
threads.anchored,
|
||||
threads.last_bump,
|
||||
threads.stickied,
|
||||
threads.locked
|
||||
FROM
|
||||
DBPREFIXposts AS posts
|
||||
JOIN DBPREFIXthreads AS threads
|
||||
ON threads.id = posts.thread_id
|
||||
JOIN DBPREFIXposts AS topposts
|
||||
ON threads.id = topposts.thread_id
|
||||
JOIN DBPREFIXboards AS boards
|
||||
ON threads.board_id = boards.id
|
||||
WHERE
|
||||
topposts.is_top_post = TRUE AND posts.is_deleted = FALSE
|
||||
|
||||
) as recentposts
|
||||
LEFT JOIN
|
||||
(SELECT files.*
|
||||
FROM DBPREFIXfiles as files
|
||||
JOIN
|
||||
(SELECT post_id, min(file_order) as file_order
|
||||
FROM DBPREFIXfiles
|
||||
GROUP BY post_id) as topfiles
|
||||
ON files.post_id = topfiles.post_id AND files.file_order = topfiles.file_order
|
||||
) AS singlefiles
|
||||
|
||||
ON recentposts.selfid = singlefiles.post_id`
|
||||
|
||||
// getPostsExcecution excecutes a given variation on abstractSelectPosts with parameters and loads the result into an array of posts
|
||||
func getPostsExcecution(sql string, arguments ...interface{}) ([]Post, error) {
|
||||
rows, err := QuerySQL(sql, arguments...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var posts []Post
|
||||
for rows.Next() {
|
||||
post := new(Post)
|
||||
var messageHTML string
|
||||
if err = rows.Scan(&post.ID, &post.ParentID, &post.BoardID, &post.Name, &post.Tripcode, &post.Email,
|
||||
&post.Subject, &messageHTML, &post.MessageText, &post.Password, &post.Filename,
|
||||
&post.FilenameOriginal, &post.FileChecksum, &post.Filesize, &post.ImageW, &post.ImageH,
|
||||
&post.ThumbW, &post.ThumbH, &post.IP, &post.Timestamp, &post.Autosage, &post.Bumped, &post.Stickied, &post.Locked,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
post.MessageHTML = template.HTML(messageHTML)
|
||||
post.FileExt = "placeholder"
|
||||
posts = append(posts, *post)
|
||||
}
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
var onlyTopPosts = abstractSelectPosts + "\nWHERE recentposts.is_top_post AND recentposts.boardid = ?"
|
||||
var sortedTopPosts = onlyTopPosts + "\nORDER BY recentposts.last_bump DESC"
|
||||
|
||||
// GetTopPostsNoSort gets the thread ops for a given board.
|
||||
// Results are unsorted
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetTopPostsNoSort(boardID int) (posts []Post, err error) {
|
||||
return getPostsExcecution(onlyTopPosts, boardID)
|
||||
}
|
||||
|
||||
// GetTopPosts gets the thread ops for a given board.
|
||||
// newestFirst sorts the ops by the newest first if true, by newest last if false
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetTopPosts(boardID int) (posts []Post, err error) {
|
||||
return getPostsExcecution(sortedTopPosts, boardID)
|
||||
}
|
||||
|
||||
var repliesToX = abstractSelectPosts + "\nWHERE recentposts.toppostid = ?"
|
||||
var oldestRepliesFirst = repliesToX + "\nORDER BY recentposts.created_on ASC"
|
||||
var newestFirstLimited = repliesToX + "\nORDER BY recentposts.created_on DESC\nLIMIT ?"
|
||||
|
||||
// GetExistingReplies gets all the reply posts to a given thread, ordered by oldest first.
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetExistingReplies(topPost int) (posts []Post, err error) {
|
||||
return getPostsExcecution(oldestRepliesFirst, topPost)
|
||||
}
|
||||
|
||||
// GetExistingRepliesLimitedRev gets N amount of reply posts to a given thread, ordered by newest first.
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetExistingRepliesLimitedRev(topPost, limit int) (posts []Post, err error) {
|
||||
return getPostsExcecution(newestFirstLimited, topPost, limit)
|
||||
}
|
||||
|
||||
//Toppost: where a post with a given id has this as their top post
|
||||
|
||||
// GetSpecificTopPost gets the information for the top post for a given id.
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetSpecificTopPost(ID int) (Post, error) {
|
||||
const topPostIDQuery = `SELECT posts.id from DBPREFIXposts as posts
|
||||
JOIN (
|
||||
SELECT threads.id from DBPREFIXthreads as threads
|
||||
JOIN DBPREFIXposts as posts
|
||||
ON posts.thread_id = threads.id
|
||||
WHERE posts.id = ?
|
||||
) as thread
|
||||
ON posts.thread_id = thread.id
|
||||
WHERE posts.is_top_post`
|
||||
//get top post of item with given id
|
||||
var FoundID int
|
||||
err := QueryRowSQL(topPostIDQuery, interfaceSlice(ID), interfaceSlice(&FoundID))
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
return GetSpecificPost(FoundID, false)
|
||||
}
|
||||
|
||||
// GetSpecificPostByString gets a specific post for a given string id.
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetSpecificPostByString(ID string, onlyNotDeleted bool) (Post, error) {
|
||||
return getSpecificPostStringDecorated(ID, onlyNotDeleted)
|
||||
}
|
||||
|
||||
// GetSpecificPost gets a specific post for a given id.
|
||||
// returns SQL.ErrNoRows if no post could be found
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetSpecificPost(ID int, onlyNotDeleted bool) (Post, error) {
|
||||
return getSpecificPostStringDecorated(strconv.Itoa(ID), onlyNotDeleted)
|
||||
}
|
||||
|
||||
var specificPostSQL = abstractSelectPosts + "\nWHERE recentposts.selfid = ?"
|
||||
var specificPostSQLNotDeleted = specificPostSQL + "\nAND recentposts.is_deleted = FALSE"
|
||||
|
||||
func getSpecificPostStringDecorated(ID string, onlyNotDeleted bool) (Post, error) {
|
||||
var sql string
|
||||
if onlyNotDeleted {
|
||||
sql = specificPostSQL
|
||||
} else {
|
||||
sql = specificPostSQLNotDeleted
|
||||
}
|
||||
posts, err := getPostsExcecution(sql, ID)
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
if len(posts) == 0 {
|
||||
return Post{}, errors.New("Could not find a post with the ID: " + ID)
|
||||
}
|
||||
return posts[0], nil
|
||||
}
|
||||
|
||||
// getRecentPostsInternal returns the most recent N posts, on a specific board if specified, only with files if specified
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
// TODO: rework so it uses all features/better sql
|
||||
// get recent posts
|
||||
func getRecentPostsInternal(amount int, onlyWithFile bool, boardID int, onSpecificBoard bool) ([]RecentPost, error) {
|
||||
// recentposts = join all non-deleted posts with the post id of their thread and the board it belongs on, sort by date and grab top x posts
|
||||
// singlefiles = the top file per post id
|
||||
|
||||
// Left join singlefiles on recentposts where recentposts.selfid = singlefiles.post_id
|
||||
// Coalesce filenames to "" (if filename = null -> "" else filename)
|
||||
|
||||
// Query might benefit from [filter on posts with at least one file -> ] filter N most recent -> manually loop N results for file/board/parentthreadid
|
||||
recentQueryStr := `SELECT
|
||||
recentposts.selfid AS id,
|
||||
recentposts.toppostid AS parentid,
|
||||
recentposts.boardname,
|
||||
recentposts.boardid,
|
||||
recentposts.ip,
|
||||
recentposts.name,
|
||||
recentposts.tripcode,
|
||||
recentposts.message,
|
||||
COALESCE(singlefiles.filename, '') as filename,
|
||||
COALESCE(singlefiles.thumbnail_width, 0) as thumb_w,
|
||||
COALESCE(singlefiles.thumbnail_height, 0) as thumb_h
|
||||
FROM
|
||||
(SELECT
|
||||
posts.id AS selfid,
|
||||
topposts.id AS toppostid,
|
||||
boards.dir AS boardname,
|
||||
boards.id AS boardid,
|
||||
posts.ip,
|
||||
posts.name,
|
||||
posts.tripcode,
|
||||
posts.message,
|
||||
posts.email,
|
||||
posts.created_on
|
||||
FROM
|
||||
DBPREFIXposts AS posts
|
||||
JOIN DBPREFIXthreads AS threads
|
||||
ON threads.id = posts.thread_id
|
||||
JOIN DBPREFIXposts AS topposts
|
||||
ON threads.id = topposts.thread_id
|
||||
JOIN DBPREFIXboards AS boards
|
||||
ON threads.board_id = boards.id
|
||||
WHERE
|
||||
topposts.is_top_post = TRUE AND posts.is_deleted = FALSE
|
||||
) as recentposts
|
||||
LEFT JOIN
|
||||
(SELECT files.post_id, filename, files.thumbnail_width, files.thumbnail_height
|
||||
FROM DBPREFIXfiles as files
|
||||
JOIN
|
||||
(SELECT post_id, min(file_order) as file_order
|
||||
FROM DBPREFIXfiles
|
||||
GROUP BY post_id) as topfiles
|
||||
ON files.post_id = topfiles.post_id AND files.file_order = topfiles.file_order
|
||||
) AS singlefiles
|
||||
|
||||
ON recentposts.selfid = singlefiles.post_id`
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
|
||||
if onlyWithFile && onSpecificBoard {
|
||||
recentQueryStr += "\n" + `WHERE singlefiles.filename IS NOT NULL AND recentposts.boardid = ?
|
||||
ORDER BY recentposts.created_on DESC LIMIT ?`
|
||||
rows, err = QuerySQL(recentQueryStr, boardID, amount)
|
||||
}
|
||||
if onlyWithFile && !onSpecificBoard {
|
||||
recentQueryStr += "\n" + `WHERE singlefiles.filename IS NOT NULL
|
||||
ORDER BY recentposts.created_on DESC LIMIT ?`
|
||||
rows, err = QuerySQL(recentQueryStr, amount)
|
||||
}
|
||||
if !onlyWithFile && onSpecificBoard {
|
||||
recentQueryStr += "\n" + `WHERE recentposts.boardid = ?
|
||||
ORDER BY recentposts.created_on DESC LIMIT ?`
|
||||
rows, err = QuerySQL(recentQueryStr, boardID, amount)
|
||||
}
|
||||
if !onlyWithFile && !onSpecificBoard {
|
||||
recentQueryStr += "\nORDER BY recentposts.created_on DESC LIMIT ?"
|
||||
rows, err = QuerySQL(recentQueryStr, amount)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var recentPostsArr []RecentPost
|
||||
|
||||
for rows.Next() {
|
||||
recentPost := new(RecentPost)
|
||||
var formattedHTML template.HTML
|
||||
if err = rows.Scan(
|
||||
&recentPost.PostID, &recentPost.ParentID, &recentPost.BoardName, &recentPost.BoardID,
|
||||
&recentPost.IP, &recentPost.Name, &recentPost.Tripcode, &formattedHTML, &recentPost.Filename,
|
||||
&recentPost.ThumbW, &recentPost.ThumbH,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recentPost.Message = formattedHTML
|
||||
recentPostsArr = append(recentPostsArr, *recentPost)
|
||||
}
|
||||
|
||||
return recentPostsArr, nil
|
||||
}
|
||||
|
||||
// GetRecentPostsGlobal returns the global N most recent posts from the database.
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetRecentPostsGlobal(amount int, onlyWithFile bool) ([]RecentPost, error) {
|
||||
return getRecentPostsInternal(amount, onlyWithFile, 0, false)
|
||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -27,6 +28,16 @@ var (
|
|||
ErrInvalidDBVersion = errors.New("invalid version flag returned by GetCompleteDatabaseVersion()")
|
||||
)
|
||||
|
||||
func initDB(initFile string) error {
|
||||
filePath := gcutil.FindResource(initFile,
|
||||
"/usr/local/share/gochan/"+initFile,
|
||||
"/usr/share/gochan/"+initFile)
|
||||
if filePath == "" {
|
||||
return fmt.Errorf("missing SQL database initialization file (%s), please reinstall gochan", initFile)
|
||||
}
|
||||
return RunSQLFile(filePath)
|
||||
}
|
||||
|
||||
// GetCompleteDatabaseVersion checks the database for any versions and errors that may exist.
|
||||
// If a version is found, execute the version check. Otherwise check for deprecated info
|
||||
// If no deprecated info is found, check if any databases exist prefixed with config.DBprefix
|
||||
|
@ -37,7 +48,7 @@ func GetCompleteDatabaseVersion() (dbVersion, dbFlag int, err error) {
|
|||
return 0, 0, err
|
||||
}
|
||||
if versionTableExists {
|
||||
databaseVersion, versionError := getDatabaseVersion(GochanVersionKeyConstant)
|
||||
databaseVersion, versionError := getDatabaseVersion(gochanVersionKeyConstant)
|
||||
if versionError != nil {
|
||||
return 0, 0, ErrInvalidVersion
|
||||
}
|
||||
|
@ -102,12 +113,12 @@ func CheckAndInitializeDatabase(dbType string) error {
|
|||
func buildNewDatabase(dbType string) error {
|
||||
var err error
|
||||
if err = initDB("initdb_" + dbType + ".sql"); err != nil {
|
||||
return err
|
||||
return errors.New("database initialization failed: " + err.Error())
|
||||
}
|
||||
if err = CreateDefaultBoardIfNoneExist(); err != nil {
|
||||
return errors.New("Failed creating default board if non already exists: " + err.Error())
|
||||
if err = createDefaultBoardIfNoneExist(); err != nil {
|
||||
return errors.New("failed creating default board if non already exists: " + err.Error())
|
||||
}
|
||||
if err = CreateDefaultAdminIfNoStaff(); err != nil {
|
||||
if err = createDefaultAdminIfNoStaff(); err != nil {
|
||||
return errors.New("failed creating default admin account: " + err.Error())
|
||||
}
|
||||
return nil
|
|
@ -1,613 +0,0 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
// GochanVersionKeyConstant is the key value used in the version table of the database to store and receive the (database) version of base gochan
|
||||
const GochanVersionKeyConstant = "gochan"
|
||||
|
||||
var (
|
||||
ErrNilBoard = errors.New("board is nil")
|
||||
ErrBoardExists = errors.New("board already exists")
|
||||
ErrBoardDoesNotExist = errors.New("board does not exist")
|
||||
ErrThreadExists = errors.New("thread already exists")
|
||||
ErrThreadDoesNotExist = errors.New("thread does not exist")
|
||||
)
|
||||
|
||||
// GetAllNondeletedMessageRaw gets all the raw message texts from the database, saved per id
|
||||
func GetAllNondeletedMessageRaw() ([]MessagePostContainer, error) {
|
||||
const sql = `SELECT posts.id, posts.message, posts.message_raw, DBPREFIXboards.dir as dir
|
||||
FROM DBPREFIXposts as posts, DBPREFIXboards
|
||||
where posts.is_deleted = FALSE`
|
||||
rows, err := QuerySQL(sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var messages []MessagePostContainer
|
||||
for rows.Next() {
|
||||
var message MessagePostContainer
|
||||
var formattedHTML template.HTML
|
||||
if err = rows.Scan(&message.ID, &formattedHTML, &message.MessageRaw, &message.Board); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message.Message = template.HTML(formattedHTML)
|
||||
messages = append(messages, message)
|
||||
}
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// SetFormattedInDatabase sets all the non-raw text for a given array of items.
|
||||
func SetFormattedInDatabase(messages []MessagePostContainer) error {
|
||||
const sql = `UPDATE DBPREFIXposts
|
||||
SET message = ?
|
||||
WHERE id = ?`
|
||||
stmt, err := PrepareSQL(sql, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for _, message := range messages {
|
||||
if _, err = stmt.Exec(string(message.Message), message.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateReport inserts a new report into the database and returns a Report pointer and any
|
||||
// errors encountered
|
||||
func CreateReport(postID int, ip string, reason string) (*Report, error) {
|
||||
currentTime := time.Now()
|
||||
sql := `INSERT INTO DBPREFIXreports (post_id, ip, reason, is_cleared) VALUES(?, ?, ?, FALSE)`
|
||||
result, err := ExecSQL(sql, postID, ip, reason)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reportID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sql = `INSERT INTO DBPREFIXreports_audit (report_id, timestamp, is_cleared) VALUES(?, ?, FALSE)`
|
||||
if _, err = ExecSQL(sql, reportID, currentTime); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Report{
|
||||
ID: int(reportID),
|
||||
HandledByStaffID: -1,
|
||||
PostID: postID,
|
||||
IP: ip,
|
||||
Reason: reason,
|
||||
IsCleared: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ClearReport dismisses the report with the given `id`. If `block` is true, future reports of the post will
|
||||
// be ignored. It returns a boolean value representing whether or not any reports matched,
|
||||
// as well as any errors encountered
|
||||
func ClearReport(id int, staffID int, block bool) (bool, error) {
|
||||
sql := `UPDATE DBPREFIXreports SET is_cleared = ?, handled_by_staff_id = ? WHERE id = ?`
|
||||
isCleared := 1
|
||||
if block {
|
||||
isCleared = 2
|
||||
}
|
||||
result, err := ExecSQL(sql, isCleared, staffID, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return affected > 0, err
|
||||
}
|
||||
sql = `UPDATE DBPREFIXreports_audit SET is_cleared = ?, handled_by_staff_id = ? WHERE report_id = ?`
|
||||
_, err = ExecSQL(sql, isCleared, staffID, id)
|
||||
return affected > 0, err
|
||||
}
|
||||
|
||||
// CheckPostReports checks to see if the given post ID has already been reported, and if a report of the post has been
|
||||
// dismissed with prejudice (so that more reports of that post can't be made)
|
||||
func CheckPostReports(postID int, reason string) (bool, bool, error) {
|
||||
sql := `SELECT COUNT(*), MAX(is_cleared) FROM DBPREFIXreports
|
||||
WHERE post_id = ? AND (reason = ? OR is_cleared = 2)`
|
||||
var num int
|
||||
var isCleared interface{}
|
||||
err := QueryRowSQL(sql, interfaceSlice(postID, reason), interfaceSlice(&num, &isCleared))
|
||||
isClearedInt, _ := isCleared.(int64)
|
||||
return num > 0, isClearedInt == 2, err
|
||||
}
|
||||
|
||||
// GetReports returns a Report array and any errors encountered. If `includeCleared` is true,
|
||||
// the array will include reports that have already been dismissed
|
||||
func GetReports(includeCleared bool) ([]Report, error) {
|
||||
sql := `SELECT id,handled_by_staff_id,post_id,ip,reason,is_cleared FROM DBPREFIXreports`
|
||||
if !includeCleared {
|
||||
sql += ` WHERE is_cleared = FALSE`
|
||||
}
|
||||
rows, err := QuerySQL(sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var reports []Report
|
||||
for rows.Next() {
|
||||
var report Report
|
||||
var staffID interface{}
|
||||
err = rows.Scan(&report.ID, &staffID, &report.PostID, &report.IP, &report.Reason, &report.IsCleared)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
staffID64, _ := (staffID.(int64))
|
||||
report.HandledByStaffID = int(staffID64)
|
||||
reports = append(reports, report)
|
||||
}
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// PermanentlyRemoveDeletedPosts removes all posts and files marked as deleted from the database
|
||||
func PermanentlyRemoveDeletedPosts() error {
|
||||
const sql1 = `DELETE FROM DBPREFIXposts WHERE is_deleted`
|
||||
const sql2 = `DELETE FROM DBPREFIXthreads WHERE is_deleted`
|
||||
_, err := ExecSQL(sql1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = ExecSQL(sql2)
|
||||
return err
|
||||
}
|
||||
|
||||
// OptimizeDatabase peforms a database optimisation
|
||||
func OptimizeDatabase() error {
|
||||
tableRows, tablesErr := QuerySQL("SHOW TABLES")
|
||||
if tablesErr != nil {
|
||||
return tablesErr
|
||||
}
|
||||
for tableRows.Next() {
|
||||
var table string
|
||||
tableRows.Scan(&table)
|
||||
if _, err := ExecSQL("OPTIMIZE TABLE " + table); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Post) ChangeBoardID(newBoardID int) error {
|
||||
err := ChangeThreadBoardID(p.ID, newBoardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.BoardID = newBoardID
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangeThreadBoardID updates the given thread's post ID and the destination board ID
|
||||
func ChangeThreadBoardID(postID int, newBoardID int) error {
|
||||
if !DoesBoardExistByID(newBoardID) {
|
||||
return ErrBoardDoesNotExist
|
||||
}
|
||||
var threadRowID int
|
||||
err := QueryRowSQL(`SELECT thread_id FROM DBPREFIXposts WHERE id = ?`,
|
||||
interfaceSlice(postID),
|
||||
interfaceSlice(&threadRowID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if threadRowID < 1 {
|
||||
return ErrThreadDoesNotExist
|
||||
}
|
||||
_, err = ExecSQL(`UPDATE DBPREFIXthreads SET board_id = ? WHERE id = ?`, newBoardID, threadRowID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ChangeThreadBoardByURI updates a thread's board ID, given the thread's post ID and
|
||||
// the destination board's uri
|
||||
func ChangeThreadBoardByURI(postID int, uri string) error {
|
||||
boardID, err := getBoardIDFromURI(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ChangeThreadBoardID(postID, boardID)
|
||||
}
|
||||
|
||||
func getBoardIDFromURIOrNil(URI string) *int {
|
||||
ID, err := getBoardIDFromURI(URI)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &ID
|
||||
}
|
||||
|
||||
// CreateUserNameBan creates a new ban on a username. If boards = nil, the ban is global.
|
||||
func CreateUserNameBan(userName string, isRegex bool, staffName string, permaban bool, staffNote, boardURI string) error {
|
||||
const sql = `INSERT INTO DBPREFIXusername_ban (board_id, staff_id, staff_note, username, is_regex) VALUES board_id = ?, staff_id = ?, staff_note = ?, username = ?, is_regex = ?`
|
||||
staffID, err := getStaffID(staffName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
boardID := getBoardIDFromURIOrNil(boardURI)
|
||||
_, err = ExecSQL(sql, boardID, staffID, staffNote, userName, isRegex)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateUserBan creates either a full ip ban, or an ip ban for threads only, for a given IP.
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func CreateUserBan(IP string, threadBan bool, staffName, boardURI string, expires time.Time, permaban bool,
|
||||
staffNote, message string, canAppeal bool, appealAt time.Time) error {
|
||||
const sql = `INSERT INTO DBPREFIXip_ban (board_id, staff_id, staff_note, is_thread_ban, ip, appeal_at, expires_at, permanent, message, can_appeal, issued_at, copy_posted_text, is_active)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP,'OLD SYSTEM BAN, NO TEXT AVAILABLE',TRUE)`
|
||||
staffID, err := getStaffID(staffName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
boardID := getBoardIDFromURIOrNil(boardURI)
|
||||
_, err = ExecSQL(sql, boardID, staffID, staffNote, threadBan, IP, appealAt, expires, permaban, message, canAppeal)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAllAccouncements gets all announcements, newest first
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetAllAccouncements() ([]Announcement, error) {
|
||||
const sql = `SELECT s.username, a.timestamp, a.subject, a.message FROM DBPREFIXannouncements AS a
|
||||
JOIN DBPREFIXstaff AS s
|
||||
ON a.staff_id = s.id
|
||||
ORDER BY a.id DESC`
|
||||
rows, err := QuerySQL(sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
announcements := []Announcement{}
|
||||
for rows.Next() {
|
||||
var announcement Announcement
|
||||
err = rows.Scan(&announcement.Poster, &announcement.Timestamp, &announcement.Subject, &announcement.Message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
announcements = append(announcements, announcement)
|
||||
}
|
||||
return announcements, nil
|
||||
}
|
||||
|
||||
// GetAllBans gets a list of all bans
|
||||
// Warning, currently only gets ip bans, not other types of bans, as the ban functionality needs a major revamp anyway
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetAllBans() ([]BanInfo, error) {
|
||||
const sql = `SELECT
|
||||
ban.id,
|
||||
ban.ip,
|
||||
COALESCE(board.title, '') as boardname,
|
||||
staff.username as staff,
|
||||
ban.issued_at,
|
||||
ban.expires_at,
|
||||
ban.permanent,
|
||||
ban.message,
|
||||
ban.staff_note,
|
||||
ban.appeal_at,
|
||||
ban.can_appeal
|
||||
FROM DBPREFIXip_ban as ban
|
||||
JOIN DBPREFIXstaff as staff
|
||||
ON ban.staff_id = staff.id
|
||||
JOIN DBPREFIXboards as board
|
||||
ON ban.board_id = board.id`
|
||||
rows, err := QuerySQL(sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var bans []BanInfo
|
||||
for rows.Next() {
|
||||
var ban BanInfo
|
||||
err = rows.Scan(&ban.ID, &ban.IP, &ban.Boards, &ban.Staff, &ban.Timestamp, &ban.Expires, &ban.Permaban, &ban.Reason, &ban.StaffNote, &ban.AppealAt, &ban.CanAppeal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bans = append(bans, ban)
|
||||
}
|
||||
return bans, nil
|
||||
}
|
||||
|
||||
// CheckBan returns banentry if a ban was found or a sql.ErrNoRows if not banned
|
||||
// name, filename and checksum may be empty strings and will be treated as not requested if done so
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func CheckBan(ip, name, filename, checksum string) (*BanInfo, error) {
|
||||
ban := new(BanInfo)
|
||||
ipban, err1 := checkIPBan(ip)
|
||||
err1NoRows := (err1 == sql.ErrNoRows)
|
||||
_, err2 := checkFileBan(checksum)
|
||||
err2NoRows := (err2 == sql.ErrNoRows)
|
||||
_, err3 := checkFilenameBan(filename)
|
||||
err3NoRows := (err3 == sql.ErrNoRows)
|
||||
_, err4 := checkUsernameBan(name)
|
||||
err4NoRows := (err4 == sql.ErrNoRows)
|
||||
|
||||
if err1NoRows && err2NoRows && err3NoRows && err4NoRows {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
|
||||
if err1NoRows {
|
||||
return nil, err1
|
||||
}
|
||||
if err2NoRows {
|
||||
return nil, err2
|
||||
}
|
||||
if err3NoRows {
|
||||
return nil, err3
|
||||
}
|
||||
if err4NoRows {
|
||||
return nil, err4
|
||||
}
|
||||
|
||||
if ipban != nil {
|
||||
ban.ID = 0
|
||||
ban.IP = string(ipban.IP)
|
||||
staff, _ := getStaffByID(ipban.StaffID)
|
||||
ban.Staff = staff.Username
|
||||
ban.Timestamp = ipban.IssuedAt
|
||||
ban.Expires = ipban.ExpiresAt
|
||||
ban.Permaban = ipban.Permanent
|
||||
ban.Reason = ipban.Message
|
||||
ban.StaffNote = ipban.StaffNote
|
||||
ban.AppealAt = ipban.AppealAt
|
||||
ban.CanAppeal = ipban.CanAppeal
|
||||
return ban, nil
|
||||
}
|
||||
|
||||
// TODO implement other types of bans or refactor banning code
|
||||
return nil, gcutil.ErrNotImplemented
|
||||
}
|
||||
|
||||
func checkIPBan(ip string) (*IPBan, error) {
|
||||
const sql = `SELECT id, staff_id, board_id, banned_for_post_id, copy_post_text, is_thread_ban, is_active, ip, issued_at, appeal_at, expires_at, permanent, staff_note, message, can_appeal
|
||||
FROM DBPREFIXip_ban WHERE ip = ?`
|
||||
var ban = new(IPBan)
|
||||
var formattedHTMLcopyposttest template.HTML
|
||||
err := QueryRowSQL(sql, interfaceSlice(ip), interfaceSlice(&ban.ID, &ban.StaffID, &ban.BoardID, &ban.BannedForPostID, &formattedHTMLcopyposttest, &ban.IsThreadBan, &ban.IsActive, &ban.IP, &ban.IssuedAt, &ban.AppealAt, &ban.ExpiresAt, &ban.Permanent, &ban.StaffNote, &ban.Message, &ban.CanAppeal))
|
||||
ban.CopyPostText = formattedHTMLcopyposttest
|
||||
return ban, err
|
||||
}
|
||||
|
||||
func checkUsernameBan(name string) (*UsernameBan, error) {
|
||||
const sql = `SELECT id, board_id, staff_id, staff_note, issued_at, username, is_regex
|
||||
FROM DBPREFIXusername_ban WHERE username = ?`
|
||||
var ban = new(UsernameBan)
|
||||
err := QueryRowSQL(sql, interfaceSlice(name), interfaceSlice(&ban.ID, &ban.BoardID, &ban.StaffID, &ban.StaffNote, &ban.IssuedAt, &ban.Username, &ban.IsRegex))
|
||||
return ban, err
|
||||
}
|
||||
|
||||
// GetMaxMessageLength returns the max message length on a board
|
||||
func GetMaxMessageLength(boardID int) (length int, err error) {
|
||||
const sql = `SELECT max_message_length FROM DBPREFIXboards
|
||||
WHERE id = ?`
|
||||
err = QueryRowSQL(sql, interfaceSlice(boardID), interfaceSlice(&length))
|
||||
return length, err
|
||||
}
|
||||
|
||||
// GetEmbedsAllowed returns if embeds are allowed on a given board
|
||||
func GetEmbedsAllowed(boardID int) (allowed bool, err error) {
|
||||
const sql = `SELECT allow_embeds FROM DBPREFIXboards
|
||||
WHERE id = ?`
|
||||
err = QueryRowSQL(sql, interfaceSlice(boardID), interfaceSlice(&allowed))
|
||||
return allowed, err
|
||||
}
|
||||
|
||||
// AddBanAppeal adds a given appeal to a given ban
|
||||
func AddBanAppeal(banID uint, message string) error {
|
||||
// copy old to audit
|
||||
const sql1 = `
|
||||
INSERT INTO DBPREFIXip_ban_appeals_audit (appeal_id, staff_id, appeal_text, staff_response, is_denied)
|
||||
SELECT id, staff_id, appeal_text, staff_response, is_denied
|
||||
FROM DBPREFIXip_ban_appeals
|
||||
WHERE DBPREFIXip_ban_appeals.ip_ban_id = ?`
|
||||
// update old values to new values
|
||||
const sql2 = `UPDATE DBPREFIXip_ban_appeals SET appeal_text = ? WHERE ip_ban_id = ?`
|
||||
_, err := ExecSQL(sql1, banID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = ExecSQL(sql2, message, banID)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateDefaultAdminIfNoStaff creates a new default admin account if no accounts exist
|
||||
func CreateDefaultAdminIfNoStaff() error {
|
||||
const sql = `SELECT COUNT(id) FROM DBPREFIXstaff`
|
||||
var count int
|
||||
QueryRowSQL(sql, interfaceSlice(), interfaceSlice(&count))
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := createUser("admin", gcutil.BcryptSum("password"), 3)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateWordFilter inserts the given wordfilter data into the database and returns a pointer to a new WordFilter struct
|
||||
func CreateWordFilter(from string, to string, isRegex bool, boards []string, staffID int, staffNote string) (*WordFilter, error) {
|
||||
var err error
|
||||
if isRegex {
|
||||
_, err = regexp.Compile(from)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = ExecSQL(`INSERT INTO DBPREFIXwordfilters
|
||||
(board_dirs,staff_id,staff_note,search,is_regex,change_to)
|
||||
VALUES(?,?,?,?,?,?)`, strings.Join(boards, ","), staffID, staffNote, from, isRegex, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WordFilter{
|
||||
BoardDirs: boards,
|
||||
StaffID: staffID,
|
||||
StaffNote: staffNote,
|
||||
IssuedAt: time.Now(),
|
||||
Search: from,
|
||||
IsRegex: isRegex,
|
||||
ChangeTo: to,
|
||||
}, err
|
||||
}
|
||||
|
||||
// GetWordFilters gets a list of wordfilters from the database and returns an array of them and any errors
|
||||
// encountered
|
||||
func GetWordFilters() ([]WordFilter, error) {
|
||||
var wfs []WordFilter
|
||||
query := `SELECT id,board_dirs,staff_id,staff_note,issued_at,search,is_regex,change_to FROM DBPREFIXwordfilters`
|
||||
rows, err := QuerySQL(query)
|
||||
if err != nil {
|
||||
return wfs, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var dirsStr string
|
||||
var wf WordFilter
|
||||
if err = rows.Scan(
|
||||
&wf.ID,
|
||||
&dirsStr,
|
||||
&wf.StaffID,
|
||||
&wf.StaffNote,
|
||||
&wf.IssuedAt,
|
||||
&wf.Search,
|
||||
&wf.IsRegex,
|
||||
&wf.ChangeTo,
|
||||
); err != nil {
|
||||
return wfs, err
|
||||
}
|
||||
wf.BoardDirs = strings.Split(dirsStr, ",")
|
||||
wfs = append(wfs, wf)
|
||||
}
|
||||
return wfs, err
|
||||
}
|
||||
|
||||
func GetBoardWordFilters(board string) ([]WordFilter, error) {
|
||||
wfs, err := GetWordFilters()
|
||||
if err != nil {
|
||||
return wfs, err
|
||||
}
|
||||
var boardFilters []WordFilter
|
||||
for _, wf := range wfs {
|
||||
if wf.OnBoard(board) {
|
||||
boardFilters = append(boardFilters, wf)
|
||||
}
|
||||
}
|
||||
return boardFilters, nil
|
||||
}
|
||||
|
||||
// BoardString returns a string representing the boards that this wordfilter applies to,
|
||||
// or "*" if the filter should be applied to posts on all boards
|
||||
func (wf *WordFilter) BoardsString() string {
|
||||
if len(wf.BoardDirs) == 0 {
|
||||
return "*"
|
||||
}
|
||||
return strings.Join(wf.BoardDirs, ",")
|
||||
}
|
||||
|
||||
func (wf *WordFilter) OnBoard(dir string) bool {
|
||||
if dir == "*" {
|
||||
return true
|
||||
}
|
||||
for _, board := range wf.BoardDirs {
|
||||
if board == "*" || dir == board {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (wf *WordFilter) StaffName() string {
|
||||
staff, err := getStaffByID(wf.StaffID)
|
||||
if err != nil {
|
||||
return "?"
|
||||
}
|
||||
return staff.Username
|
||||
}
|
||||
|
||||
// Apply runs the current wordfilter on the given string, without checking the board or (re)building the post
|
||||
// It returns an error if it is a regular expression and regexp.Compile failed to parse it
|
||||
func (wf *WordFilter) Apply(message string) (string, error) {
|
||||
if wf.IsRegex {
|
||||
re, err := regexp.Compile(wf.Search)
|
||||
if err != nil {
|
||||
return message, err
|
||||
}
|
||||
message = re.ReplaceAllString(message, wf.ChangeTo)
|
||||
} else {
|
||||
message = strings.Replace(message, wf.Search, wf.ChangeTo, -1)
|
||||
}
|
||||
return message, nil
|
||||
}
|
||||
|
||||
// getDatabaseVersion gets the version of the database, or an error if none or multiple exist
|
||||
func getDatabaseVersion(componentKey string) (int, error) {
|
||||
const sql = `SELECT version FROM DBPREFIXdatabase_version WHERE component = ?`
|
||||
var version int
|
||||
err := QueryRowSQL(sql, []interface{}{componentKey}, []interface{}{&version})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return version, err
|
||||
}
|
||||
|
||||
func getNextFreeID(tableName string) (ID int, err error) {
|
||||
var sql = `SELECT COALESCE(MAX(id), 0) + 1 FROM ` + tableName
|
||||
err = QueryRowSQL(sql, interfaceSlice(), interfaceSlice(&ID))
|
||||
return ID, err
|
||||
}
|
||||
|
||||
func doesTableExist(tableName string) (bool, error) {
|
||||
var existQuery string
|
||||
|
||||
switch config.GetSystemCriticalConfig().DBtype {
|
||||
case "mysql":
|
||||
fallthrough
|
||||
case "postgresql":
|
||||
existQuery = `SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ?`
|
||||
case "sqlite3":
|
||||
existQuery = `SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?`
|
||||
default:
|
||||
return false, ErrUnsupportedDB
|
||||
}
|
||||
|
||||
var count int
|
||||
err := QueryRowSQL(existQuery, []interface{}{config.GetSystemCriticalConfig().DBprefix + tableName}, []interface{}{&count})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count == 1, nil
|
||||
}
|
||||
|
||||
// doesGochanPrefixTableExist returns true if any table with a gochan prefix was found.
|
||||
// Returns false if the prefix is an empty string
|
||||
func doesGochanPrefixTableExist() (bool, error) {
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
if systemCritical.DBprefix == "" {
|
||||
return false, nil
|
||||
}
|
||||
var prefixTableExist string
|
||||
switch systemCritical.DBtype {
|
||||
case "mysql":
|
||||
fallthrough
|
||||
case "postgresql":
|
||||
prefixTableExist = `SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'DBPREFIX%'`
|
||||
case "sqlite3":
|
||||
prefixTableExist = `SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name LIKE 'DBPREFIX%'`
|
||||
}
|
||||
|
||||
var count int
|
||||
err := QueryRowSQL(prefixTableExist, []interface{}{}, []interface{}{&count})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
|
@ -1,25 +1,22 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
)
|
||||
import "database/sql"
|
||||
|
||||
var (
|
||||
ErrCannotDeleteOnlySection = errors.New("cannot delete the only remaining section")
|
||||
AllSections []Section
|
||||
)
|
||||
|
||||
// GetAllSections gets a list of all existing sections
|
||||
func GetAllSections() ([]BoardSection, error) {
|
||||
const sql = `SELECT id, name, abbreviation, position, hidden FROM DBPREFIXsections ORDER BY position ASC, name ASC`
|
||||
rows, err := QuerySQL(sql)
|
||||
// getAllSections gets a list of all existing sections
|
||||
func getAllSections() ([]Section, error) {
|
||||
const query = `SELECT id, name, abbreviation, position, hidden FROM DBPREFIXsections ORDER BY position ASC, name ASC`
|
||||
rows, err := QuerySQL(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sections []BoardSection
|
||||
var sections []Section
|
||||
for rows.Next() {
|
||||
var section BoardSection
|
||||
err = rows.Scan(§ion.ID, §ion.Name, §ion.Abbreviation, §ion.ListOrder, §ion.Hidden)
|
||||
var section Section
|
||||
err = rows.Scan(§ion.ID, §ion.Name, §ion.Abbreviation, §ion.Position, §ion.Hidden)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -28,95 +25,69 @@ func GetAllSections() ([]BoardSection, error) {
|
|||
return sections, nil
|
||||
}
|
||||
|
||||
// GetAllSectionsOrCreateDefault gets all sections in the database, creates default if none exist
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetAllSectionsOrCreateDefault() ([]BoardSection, error) {
|
||||
_, err := GetOrCreateDefaultSectionID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return GetAllSections()
|
||||
}
|
||||
|
||||
func getNextSectionListOrder() (int, error) {
|
||||
const sql = `SELECT COALESCE(MAX(position) + 1, 0) FROM DBPREFIXsections`
|
||||
var ID int
|
||||
err := QueryRowSQL(sql, interfaceSlice(), interfaceSlice(&ID))
|
||||
return ID, err
|
||||
const query = `SELECT COALESCE(MAX(position) + 1, 0) FROM DBPREFIXsections`
|
||||
var id int
|
||||
err := QueryRowSQL(query, interfaceSlice(), interfaceSlice(&id))
|
||||
return id, err
|
||||
}
|
||||
|
||||
// GetOrCreateDefaultSectionID creates the default section if it does not exist yet, returns default section ID if it exists
|
||||
func GetOrCreateDefaultSectionID() (sectionID int, err error) {
|
||||
const SQL = `SELECT id FROM DBPREFIXsections WHERE name = 'Main'`
|
||||
var ID int
|
||||
err = QueryRowSQL(SQL, interfaceSlice(), interfaceSlice(&ID))
|
||||
// getOrCreateDefaultSectionID creates the default section if no sections have been created yet,
|
||||
// returns default section ID if it exists
|
||||
func getOrCreateDefaultSectionID() (sectionID int, err error) {
|
||||
const query = `SELECT id FROM DBPREFIXsections WHERE name = 'Main'`
|
||||
var id int
|
||||
err = QueryRowSQL(query, interfaceSlice(), interfaceSlice(&id))
|
||||
if err == sql.ErrNoRows {
|
||||
//create it
|
||||
ID, err := getNextSectionListOrder()
|
||||
if err != nil {
|
||||
var section *Section
|
||||
if section, err = NewSection("Main", "main", false, -1); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
section := BoardSection{Name: "Main", Abbreviation: "Main", Hidden: false, ListOrder: ID}
|
||||
err = CreateSection(§ion)
|
||||
return section.ID, err
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err //other error
|
||||
}
|
||||
return ID, nil
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// CreateSection creates a section, setting the newly created id in the given struct
|
||||
func CreateSection(section *BoardSection) error {
|
||||
// NewSection creates a new board section in the database and returns a *Section struct pointer.
|
||||
// If position < 0, it will use the ID
|
||||
func NewSection(name string, abbreviation string, hidden bool, position int) (*Section, error) {
|
||||
const sqlINSERT = `INSERT INTO DBPREFIXsections (name, abbreviation, hidden, position) VALUES (?,?,?,?)`
|
||||
const sqlSELECT = `SELECT id FROM DBPREFIXsections WHERE position = ?`
|
||||
//Excecuted in two steps this way because last row id functions arent thread safe, position is unique
|
||||
_, err := ExecSQL(sqlINSERT, section.Name, section.Abbreviation, section.Hidden, section.ListOrder)
|
||||
|
||||
id, err := getNextFreeID("DBPREFIXsections")
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return QueryRowSQL(
|
||||
sqlSELECT,
|
||||
interfaceSlice(section.ListOrder),
|
||||
interfaceSlice(§ion.ID))
|
||||
if position < 0 {
|
||||
// position not specified, use the ID
|
||||
position = id
|
||||
}
|
||||
if _, err = ExecSQL(sqlINSERT, name, abbreviation, hidden, position); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Section{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Abbreviation: abbreviation,
|
||||
Position: position,
|
||||
Hidden: hidden,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSectionFromID queries the database for a section with the given ID and returns the section
|
||||
// (or nil if it doesn't exist) and any errors
|
||||
func GetSectionFromID(id int) (*BoardSection, error) {
|
||||
sql := `SELECT name,abbreviation,position,hidden FROM DBPREFIXsections WHERE id = ?`
|
||||
section := &BoardSection{
|
||||
ID: id,
|
||||
}
|
||||
err := QueryRowSQL(sql, []interface{}{id}, []interface{}{§ion.Name, §ion.Abbreviation, §ion.ListOrder, §ion.Hidden})
|
||||
return section, err
|
||||
}
|
||||
// // CreateSection creates a section, setting the newly created id in the given struct
|
||||
// func CreateSection(section *Section) error {
|
||||
// const sqlINSERT = `INSERT INTO DBPREFIXsections (name, abbreviation, hidden, position) VALUES (?,?,?,?)`
|
||||
// const sqlSELECT = `SELECT id FROM DBPREFIXsections WHERE position = ?`
|
||||
|
||||
// DeleteSection deletes the section with the given ID from the database and returns any errors
|
||||
func DeleteSection(id int) error {
|
||||
sqlCount := `SELECT COUNT(*) FROM DBPREFIXsections`
|
||||
var numRows int
|
||||
err := QueryRowSQL(sqlCount, interfaceSlice(), interfaceSlice(&numRows))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if numRows <= 1 {
|
||||
return ErrCannotDeleteOnlySection
|
||||
}
|
||||
sqlDelete := `DELETE FROM DBPREFIXsections WHERE id = ?`
|
||||
_, err = ExecSQL(sqlDelete, id)
|
||||
if err == nil {
|
||||
ResetBoardSectionArrays()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *BoardSection) UpdateValues() error {
|
||||
sql := `UPDATE DBPREFIXsections SET name = ?, abbreviation = ?, position = ?, hidden = ? where id = ?`
|
||||
_, err := ExecSQL(sql, s.Name, s.Abbreviation, s.ListOrder, s.Hidden, s.ID)
|
||||
if err == nil {
|
||||
ResetBoardSectionArrays()
|
||||
}
|
||||
return err
|
||||
}
|
||||
// //Excecuted in two steps this way because last row id functions arent thread safe, position is unique
|
||||
// _, err := ExecSQL(sqlINSERT, section.Name, section.Abbreviation, section.Hidden, section.Position)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return QueryRowSQL(
|
||||
// sqlSELECT,
|
||||
// interfaceSlice(section.Position),
|
||||
// interfaceSlice(§ion.ID))
|
||||
// }
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSectionCreation(t *testing.T) {
|
||||
section := &BoardSection{
|
||||
Name: "Staff",
|
||||
Abbreviation: "hidden1",
|
||||
Hidden: true,
|
||||
ListOrder: 2,
|
||||
}
|
||||
err := CreateSection(section)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed creating section 'Staff': %s", err.Error())
|
||||
}
|
||||
|
||||
if err = section.UpdateValues(); err != nil {
|
||||
t.Fatalf("Error updating section: %s", err.Error())
|
||||
}
|
||||
bs, err := GetSectionFromID(section.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting section #%d: %s", section.ID, err.Error())
|
||||
}
|
||||
if bs.Name != section.Name {
|
||||
t.Fatalf("Got unexpected section when requesting section with ID %d: %s", section.ID, bs.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteSections(t *testing.T) {
|
||||
section := &BoardSection{
|
||||
Name: "Temp",
|
||||
Abbreviation: "temp",
|
||||
Hidden: false,
|
||||
ListOrder: 3,
|
||||
}
|
||||
err := CreateSection(section)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed creating temporary section for deletion testing: %s", err.Error())
|
||||
}
|
||||
if err = section.UpdateValues(); err != nil {
|
||||
t.Fatalf("Failed updating temp section data: %s", err.Error())
|
||||
}
|
||||
if err = DeleteSection(section.ID); err != nil {
|
||||
t.Fatalf("Failed deleting temp section: %s", err.Error())
|
||||
}
|
||||
}
|
|
@ -4,42 +4,68 @@ import (
|
|||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
// GetStaffName returns the name associated with a session
|
||||
func GetStaffName(session string) (string, error) {
|
||||
const sql = `SELECT staff.username from DBPREFIXstaff as staff
|
||||
JOIN DBPREFIXsessions as sessions
|
||||
ON sessions.staff_id = staff.id
|
||||
WHERE sessions.data = ?`
|
||||
var username string
|
||||
err := QueryRowSQL(sql, interfaceSlice(session), interfaceSlice(&username))
|
||||
return username, err
|
||||
// createDefaultAdminIfNoStaff creates a new default admin account if no accounts exist
|
||||
func createDefaultAdminIfNoStaff() error {
|
||||
const sql = `SELECT COUNT(id) FROM DBPREFIXstaff`
|
||||
var count int
|
||||
QueryRowSQL(sql, interfaceSlice(), interfaceSlice(&count))
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := NewStaff("admin", "password", 3)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetStaffBySession gets the staff that is logged in in the given session
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetStaffBySession(session string) (*Staff, error) {
|
||||
const sql = `SELECT
|
||||
staff.id,
|
||||
staff.username,
|
||||
staff.password_checksum,
|
||||
staff.global_rank,
|
||||
staff.added_on,
|
||||
staff.last_login
|
||||
FROM DBPREFIXstaff as staff
|
||||
JOIN DBPREFIXsessions as sessions
|
||||
ON sessions.staff_id = staff.id
|
||||
WHERE sessions.data = ?`
|
||||
staff := new(Staff)
|
||||
err := QueryRowSQL(sql, interfaceSlice(session), interfaceSlice(&staff.ID, &staff.Username, &staff.PasswordChecksum, &staff.Rank, &staff.AddedOn, &staff.LastActive))
|
||||
return staff, err
|
||||
func NewStaff(username string, password string, rank int) (*Staff, error) {
|
||||
const sqlINSERT = `INSERT INTO DBPREFIXstaff
|
||||
(username, password_checksum, global_rank)
|
||||
VALUES(?,?,?)`
|
||||
passwordChecksum := gcutil.BcryptSum(password)
|
||||
_, err := ExecSQL(sqlINSERT, username, passwordChecksum, rank)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Staff{
|
||||
Username: username,
|
||||
PasswordChecksum: passwordChecksum,
|
||||
Rank: rank,
|
||||
AddedOn: time.Now(),
|
||||
IsActive: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetActive changes the active status of the staff member. If `active` is false, the login sessions are cleared
|
||||
func (s *Staff) SetActive(active bool) error {
|
||||
const updateActive = `UPDATE DBPREFIXstaff SET is_active = 0 WHERE username = ?`
|
||||
_, err := ExecSQL(updateActive, s.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if active {
|
||||
return nil
|
||||
}
|
||||
return s.ClearSessions()
|
||||
}
|
||||
|
||||
// ClearSessions clears all login sessions for the user, requiring them to login again
|
||||
func (s *Staff) ClearSessions() error {
|
||||
const query = `SELECT id FROM DBPREFIXstaff WHERE username = ?`
|
||||
const deleteSessions = `DELETE FROM DBPREFIXsessions WHERE staff_id = ?`
|
||||
var err error
|
||||
if s.ID == 0 {
|
||||
// ID field not set, get it from the DB
|
||||
if err = QueryRowSQL(query, interfaceSlice(s.Username), interfaceSlice(&s.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = ExecSQL(deleteSessions, s.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// EndStaffSession deletes any session rows associated with the requests session cookie and then
|
||||
|
@ -52,8 +78,7 @@ func EndStaffSession(writer http.ResponseWriter, request *http.Request) error {
|
|||
}
|
||||
// make it so that the next time the page is loaded, the browser will delete it
|
||||
sessionVal := session.Value
|
||||
session.MaxAge = 0
|
||||
session.Expires = time.Now().Add(-7 * 24 * time.Hour)
|
||||
session.MaxAge = -1
|
||||
http.SetCookie(writer, session)
|
||||
|
||||
staffID := 0
|
||||
|
@ -70,10 +95,13 @@ func EndStaffSession(writer http.ResponseWriter, request *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetStaffByName gets the staff with a given name
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetStaffByName(name string) (*Staff, error) {
|
||||
func DeactivateStaff(username string) error {
|
||||
s := Staff{Username: username}
|
||||
return s.SetActive(false)
|
||||
}
|
||||
|
||||
// GetStaffBySession gets the staff that is logged in in the given session
|
||||
func GetStaffBySession(session string) (*Staff, error) {
|
||||
const sql = `SELECT
|
||||
staff.id,
|
||||
staff.username,
|
||||
|
@ -82,241 +110,10 @@ func GetStaffByName(name string) (*Staff, error) {
|
|||
staff.added_on,
|
||||
staff.last_login
|
||||
FROM DBPREFIXstaff as staff
|
||||
WHERE staff.username = ?`
|
||||
JOIN DBPREFIXsessions as sessions
|
||||
ON sessions.staff_id = staff.id
|
||||
WHERE sessions.data = ?`
|
||||
staff := new(Staff)
|
||||
err := QueryRowSQL(sql, interfaceSlice(name), interfaceSlice(&staff.ID, &staff.Username, &staff.PasswordChecksum, &staff.Rank, &staff.AddedOn, &staff.LastActive))
|
||||
err := QueryRowSQL(sql, interfaceSlice(session), interfaceSlice(&staff.ID, &staff.Username, &staff.PasswordChecksum, &staff.Rank, &staff.AddedOn, &staff.LastLogin))
|
||||
return staff, err
|
||||
}
|
||||
|
||||
func getStaffByID(id int) (*Staff, error) {
|
||||
const sql = `SELECT
|
||||
staff.id,
|
||||
staff.username,
|
||||
staff.password_checksum,
|
||||
staff.global_rank,
|
||||
staff.added_on,
|
||||
staff.last_login
|
||||
FROM DBPREFIXstaff as staff
|
||||
WHERE staff.id = ?`
|
||||
staff := new(Staff)
|
||||
err := QueryRowSQL(sql, interfaceSlice(id), interfaceSlice(&staff.ID, &staff.Username, &staff.PasswordChecksum, &staff.Rank, &staff.AddedOn, &staff.LastActive))
|
||||
return staff, err
|
||||
}
|
||||
|
||||
// NewStaff creates a new staff account from a given username, password and rank
|
||||
func NewStaff(username, password string, rank int) error {
|
||||
const sql = `INSERT INTO DBPREFIXstaff (username, password_checksum, global_rank)
|
||||
VALUES (?, ?, ?)`
|
||||
_, err := ExecSQL(sql, username, gcutil.BcryptSum(password), rank)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteStaff deletes the staff with a given name.
|
||||
// Implemented to change the account name to a random string and set it to inactive
|
||||
func DeleteStaff(username string) error {
|
||||
const sql = `UPDATE DBPREFIXstaff SET username = ?, is_active = FALSE WHERE username = ?`
|
||||
_, err := ExecSQL(sql, gcutil.RandomString(45), username)
|
||||
return err
|
||||
}
|
||||
|
||||
func getStaffID(username string) (int, error) {
|
||||
staff, err := GetStaffByName(username)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return staff.ID, nil
|
||||
}
|
||||
|
||||
// CreateSession inserts a session for a given key and username into the database
|
||||
func CreateSession(key, username string) error {
|
||||
const sql1 = `INSERT INTO DBPREFIXsessions (staff_id,data,expires) VALUES(?,?,?)`
|
||||
const sql2 = `UPDATE DBPREFIXstaff SET last_login = CURRENT_TIMESTAMP WHERE id = ?`
|
||||
staffID, err := getStaffID(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = ExecSQL(sql1, staffID, key, time.Now().Add(time.Duration(time.Hour*730))) //TODO move amount of time to config file
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = ExecSQL(sql2, staffID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAllStaffNopass gets all staff accounts without their password
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func GetAllStaffNopass(onlyactive bool) ([]Staff, error) {
|
||||
sql := `SELECT id, username, global_rank, added_on, last_login FROM DBPREFIXstaff`
|
||||
if onlyactive {
|
||||
sql += " where is_active = 1"
|
||||
}
|
||||
rows, err := QuerySQL(sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var staffs []Staff
|
||||
for rows.Next() {
|
||||
var staff Staff
|
||||
err = rows.Scan(&staff.ID, &staff.Username, &staff.Rank, &staff.AddedOn, &staff.LastActive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
staffs = append(staffs, staff)
|
||||
}
|
||||
return staffs, nil
|
||||
}
|
||||
|
||||
func createThread(boardID int, locked, stickied, anchored, cyclical bool) (threadID int, err error) {
|
||||
const sql = `INSERT INTO DBPREFIXthreads (board_id, locked, stickied, anchored, cyclical) VALUES (?,?,?,?,?)`
|
||||
//Retrieves next free ID, explicitly inserts it, keeps retrying until succesfull insert or until a non-pk error is encountered.
|
||||
//This is done because mysql doesnt support RETURNING and both LAST_INSERT_ID() and last_row_id() are not thread-safe
|
||||
isPrimaryKeyError := true
|
||||
for isPrimaryKeyError {
|
||||
threadID, err = getNextFreeID("DBPREFIXthreads")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_, err = ExecSQL(sql, boardID, locked, stickied, anchored, cyclical)
|
||||
|
||||
isPrimaryKeyError, err = errFilterDuplicatePrimaryKey(err)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return threadID, nil
|
||||
}
|
||||
|
||||
func bumpThreadOfPost(postID int) error {
|
||||
id, err := getThreadID(postID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bumpThread(id)
|
||||
}
|
||||
|
||||
func bumpThread(threadID int) error {
|
||||
const sql = "UPDATE DBPREFIXthreads SET last_bump = CURRENT_TIMESTAMP WHERE id = ?"
|
||||
_, err := ExecSQL(sql, threadID)
|
||||
return err
|
||||
}
|
||||
|
||||
func appendFile(postID int, originalFilename, filename, checksum string, fileSize int, isSpoilered bool, width, height, thumbnailWidth, thumbnailHeight int) error {
|
||||
const nextIDSQL = `SELECT COALESCE(MAX(file_order) + 1, 0) FROM DBPREFIXfiles WHERE post_id = ?`
|
||||
var nextID int
|
||||
err := QueryRowSQL(nextIDSQL, interfaceSlice(postID), interfaceSlice(&nextID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
const insertSQL = `INSERT INTO DBPREFIXfiles (file_order, post_id, original_filename, filename, checksum, file_size, is_spoilered, width, height, thumbnail_width, thumbnail_height)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)`
|
||||
_, err = ExecSQL(insertSQL, nextID, postID, originalFilename, filename, checksum, fileSize, isSpoilered, width, height, thumbnailWidth, thumbnailHeight)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetThreadIDZeroIfTopPost gets the post id of the top post of the thread a post belongs to, zero if the post itself is the top post
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design. Posts do not directly reference their post post anymore.
|
||||
func GetThreadIDZeroIfTopPost(postID int) (ID int, err error) {
|
||||
const sql = `SELECT t1.id FROM DBPREFIXposts as t1
|
||||
JOIN (SELECT thread_id FROM DBPREFIXposts where id = ?) as t2 ON t1.thread_id = t2.thread_id
|
||||
WHERE t1.is_top_post`
|
||||
err = QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&ID))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if ID == postID {
|
||||
return 0, nil
|
||||
}
|
||||
return ID, nil
|
||||
}
|
||||
|
||||
func getThreadID(postID int) (ID int, err error) {
|
||||
const sql = `SELECT thread_id FROM DBPREFIXposts WHERE id = ?`
|
||||
err = QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&ID))
|
||||
return ID, err
|
||||
}
|
||||
|
||||
// GetPostPassword gets the password associated with a given post
|
||||
func GetPostPassword(postID int) (password string, err error) {
|
||||
const sql = `SELECT password FROM DBPREFIXposts WHERE id = ?`
|
||||
err = QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&password))
|
||||
return password, err
|
||||
}
|
||||
|
||||
// UpdatePost updates a post with new information
|
||||
// Deprecated: This method was created to support old functionality during the database refactor of april 2020
|
||||
// The code should be changed to reflect the new database design
|
||||
func UpdatePost(postID int, email, subject string, message template.HTML, messageRaw string) error {
|
||||
const sql = `UPDATE DBPREFIXposts SET email = ?, subject = ?, message = ?, message_raw = ? WHERE id = ?`
|
||||
_, err := ExecSQL(sql, email, subject, string(message), messageRaw, postID)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeletePost deletes a post with a given ID
|
||||
func DeletePost(postID int, checkIfTopPost bool) error {
|
||||
if checkIfTopPost {
|
||||
isTopPost, err := isTopPost(postID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isTopPost {
|
||||
threadID, err := getThreadID(postID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteThread(threadID)
|
||||
}
|
||||
}
|
||||
|
||||
const sql = `UPDATE DBPREFIXposts SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE id = ?`
|
||||
_, err := ExecSQL(sql, postID)
|
||||
return err
|
||||
}
|
||||
|
||||
func isTopPost(postID int) (val bool, err error) {
|
||||
const sql = `SELECT is_top_post FROM DBPREFIXposts WHERE id = ?`
|
||||
err = QueryRowSQL(sql, interfaceSlice(postID), interfaceSlice(&val))
|
||||
return val, err
|
||||
}
|
||||
|
||||
func deleteThread(threadID int) error {
|
||||
const sql1 = `UPDATE DBPREFIXthreads SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE id = ?`
|
||||
const sql2 = `SELECT id FROM DBPREFIXposts WHERE thread_id = ?`
|
||||
|
||||
_, err := QuerySQL(sql1, threadID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := QuerySQL(sql2, threadID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ids []int
|
||||
for rows.Next() {
|
||||
var id int
|
||||
if err = rows.Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if err = DeletePost(id, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createUser(username, passwordEncrypted string, globalRank int) (userID int, err error) {
|
||||
const sqlInsert = `INSERT INTO DBPREFIXstaff (username, password_checksum, global_rank) VALUES (?,?,?)`
|
||||
const sqlSelect = `SELECT id FROM DBPREFIXstaff WHERE username = ?`
|
||||
//Excecuted in two steps this way because last row id functions arent thread safe, username is unique
|
||||
_, err = ExecSQL(sqlInsert, username, passwordEncrypted, globalRank)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = QueryRowSQL(sqlSelect, interfaceSlice(username), interfaceSlice(&userID))
|
||||
return userID, err
|
||||
}
|
||||
|
|
|
@ -1,549 +1,246 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
threadBan
|
||||
imageBan
|
||||
fullBan
|
||||
)
|
||||
|
||||
var (
|
||||
// AllSections is a cached list of all of the board sections
|
||||
AllSections []BoardSection
|
||||
// AllBoards is a cached list of all of the boards
|
||||
AllBoards []Board
|
||||
// TempPosts is a cached list of all of the posts in the temporary posts table
|
||||
TempPosts []Post
|
||||
)
|
||||
|
||||
// table: DBPREFIXannouncements
|
||||
type Announcement struct {
|
||||
ID uint `json:"no"`
|
||||
Subject string `json:"sub"`
|
||||
Message string `json:"com"`
|
||||
Poster string `json:"name"`
|
||||
Timestamp time.Time
|
||||
ID uint `json:"no"` // sql: `id`
|
||||
StaffID string `json:"name"` // sql: `staff_id`
|
||||
Subject string `json:"sub"` // sql: `subject`
|
||||
Message string `json:"com"` // sql: `message`
|
||||
Timestamp time.Time `json:"-"` // sql: `timestamp`
|
||||
}
|
||||
|
||||
type BanAppeal struct {
|
||||
ID int
|
||||
Ban int
|
||||
Message string
|
||||
Denied bool
|
||||
StaffResponse string
|
||||
}
|
||||
|
||||
type BanInfo struct {
|
||||
ID uint
|
||||
IP string
|
||||
Name string
|
||||
NameIsRegex bool
|
||||
Boards string
|
||||
Staff string
|
||||
Timestamp time.Time
|
||||
Expires time.Time
|
||||
Permaban bool
|
||||
Reason string
|
||||
Type int
|
||||
StaffNote string
|
||||
AppealAt time.Time
|
||||
CanAppeal bool
|
||||
}
|
||||
|
||||
// BannedForever returns true if the ban is an unappealable permaban
|
||||
func (ban *BanInfo) BannedForever() bool {
|
||||
return ban.Permaban && !ban.CanAppeal && ban.Type == fullBan && ban.Boards == ""
|
||||
}
|
||||
|
||||
// IsActive returns true if the ban is still active (unexpired or a permaban)
|
||||
func (ban *BanInfo) IsActive(board string) bool {
|
||||
if ban.Boards == "" && (ban.Expires.After(time.Now()) || ban.Permaban) {
|
||||
return true
|
||||
}
|
||||
boardsArr := strings.Split(ban.Boards, ",")
|
||||
for _, b := range boardsArr {
|
||||
if b == board && (ban.Expires.After(time.Now()) || ban.Permaban) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsBanned checks to see if the ban applies to the given board
|
||||
func (ban *BanInfo) IsBanned(board string) bool {
|
||||
if ban.Boards == "" && (ban.Expires.After(time.Now()) || ban.Permaban) {
|
||||
return true
|
||||
}
|
||||
boardsArr := strings.Split(ban.Boards, ",")
|
||||
for _, b := range boardsArr {
|
||||
if b == board && (ban.Expires.After(time.Now()) || ban.Permaban) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type BannedHash struct {
|
||||
ID uint
|
||||
Checksum string
|
||||
Description string
|
||||
// table: DBPREFIXboard_staff
|
||||
type BoardStaff struct {
|
||||
BoardID uint // sql: `board_id`
|
||||
StaffID uint // sql: `staff_id`
|
||||
}
|
||||
|
||||
// table: DBPREFIXboards
|
||||
type Board struct {
|
||||
ID int `json:"-"`
|
||||
CurrentPage int `json:"-"`
|
||||
NumPages int `json:"pages"`
|
||||
ListOrder int `json:"-"`
|
||||
Dir string `json:"board"`
|
||||
Type int `json:"-"`
|
||||
UploadType int `json:"-"`
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"meta_description"`
|
||||
Description string `json:"-"`
|
||||
Section int `json:"-"`
|
||||
MaxFilesize int `json:"max_filesize"`
|
||||
MaxPages int `json:"max_pages"`
|
||||
DefaultStyle string `json:"-"`
|
||||
Locked bool `json:"is_archived"`
|
||||
CreatedOn time.Time `json:"-"`
|
||||
Anonymous string `json:"-"`
|
||||
ForcedAnon bool `json:"-"`
|
||||
MaxAge int `json:"-"`
|
||||
AutosageAfter int `json:"bump_limit"`
|
||||
NoImagesAfter int `json:"image_limit"`
|
||||
MaxMessageLength int `json:"max_comment_chars"`
|
||||
EmbedsAllowed bool `json:"-"`
|
||||
RedirectToThread bool `json:"-"`
|
||||
ShowID bool `json:"-"`
|
||||
RequireFile bool `json:"-"`
|
||||
EnableCatalog bool `json:"-"`
|
||||
EnableSpoileredImages bool `json:"-"`
|
||||
EnableSpoileredThreads bool `json:"-"`
|
||||
Worksafe bool `json:"ws_board"`
|
||||
ThreadPage int `json:"-"`
|
||||
Cooldowns BoardCooldowns `json:"cooldowns"`
|
||||
ThreadsPerPage int `json:"per_page"`
|
||||
ID int `json:"-"` // sql: `id`
|
||||
SectionID int `json:"-"` // sql: `section_id`
|
||||
URI string `json:"-"` // sql: `uri`
|
||||
Dir string `json:"board"` // sql: `dir`
|
||||
NavbarPosition int `json:"-"` // sql: `navbar_position`
|
||||
Title string `json:"title"` // sql: `title`
|
||||
Subtitle string `json:"meta_description"` // sql: `suttitle`
|
||||
Description string `json:"-"` // sql: `description`
|
||||
MaxFilesize int `json:"max_filesize"` // sql: `max_file_size`
|
||||
MaxThreads int `json:"-"` // sql: `max_threads`
|
||||
DefaultStyle string `json:"-"` // sql: `default_style`
|
||||
Locked bool `json:"is_archived"` // sql: `locked`
|
||||
CreatedAt time.Time `json:"-"` // sql: `created_at`
|
||||
AnonymousName string `json:"-"` // sql: `anonymous_name`
|
||||
ForceAnonymous bool `json:"-"` // sql: `force_anonymous`
|
||||
AutosageAfter int `json:"bump_limit"` // sql: `autosage_after`
|
||||
NoImagesAfter int `json:"image_limit"` // sql: `no_images_after`
|
||||
MaxMessageLength int `json:"max_comment_chars"` // sql: `max_message_length`
|
||||
MinMessageLength int `json:"min_comment_chars"` // sql: `min_message_length`
|
||||
AllowEmbeds bool `json:"-"` // sql: `allow_embeds`
|
||||
RedirectToThread bool `json:"-"` // sql: `redirect_to_thread`
|
||||
RequireFile bool `json:"-"` // sql: `require_file`
|
||||
EnableCatalog bool `json:"-"` // sql: `enable_catalog`
|
||||
|
||||
// // not in the database, may end up moving this to a separate struct, possibly in a separate pkg
|
||||
}
|
||||
|
||||
// AbsolutePath returns the full filepath of the board directory
|
||||
func (board *Board) AbsolutePath(subpath ...string) string {
|
||||
return path.Join(config.GetSystemCriticalConfig().DocumentRoot, board.Dir, path.Join(subpath...))
|
||||
}
|
||||
|
||||
// WebPath returns a string that represents the file's path as accessible by a browser
|
||||
// fileType should be "boardPage", "threadPage", "upload", or "thumb"
|
||||
func (board *Board) WebPath(fileName, fileType string) string {
|
||||
var filePath string
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
|
||||
switch fileType {
|
||||
case "":
|
||||
fallthrough
|
||||
case "boardPage":
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, fileName)
|
||||
case "threadPage":
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, "res", fileName)
|
||||
case "upload":
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, "src", fileName)
|
||||
case "thumb":
|
||||
filePath = path.Join(systemCritical.WebRoot, board.Dir, "thumb", fileName)
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
||||
func (board *Board) PagePath(pageNum interface{}) string {
|
||||
var page string
|
||||
pageNumStr := fmt.Sprintf("%v", pageNum)
|
||||
if pageNumStr == "prev" {
|
||||
if board.CurrentPage < 2 {
|
||||
page = "1"
|
||||
} else {
|
||||
page = strconv.Itoa(board.CurrentPage - 1)
|
||||
}
|
||||
} else if pageNumStr == "next" {
|
||||
if board.CurrentPage >= board.NumPages {
|
||||
page = strconv.Itoa(board.NumPages)
|
||||
} else {
|
||||
page = strconv.Itoa(board.CurrentPage + 1)
|
||||
}
|
||||
} else {
|
||||
page = pageNumStr
|
||||
}
|
||||
return board.WebPath(page+".html", "boardPage")
|
||||
}
|
||||
|
||||
func (board *Board) SetDefaults(title string, subtitle string, description string) {
|
||||
board.CurrentPage = 1
|
||||
board.NumPages = 15
|
||||
board.ListOrder = 0
|
||||
board.Dir = "test"
|
||||
board.Type = 0
|
||||
board.UploadType = 0
|
||||
if title == "" {
|
||||
board.Title = "Testing board"
|
||||
} else {
|
||||
board.Title = title
|
||||
}
|
||||
if subtitle == "" {
|
||||
board.Subtitle = "Board for testing stuff"
|
||||
} else {
|
||||
board.Subtitle = subtitle
|
||||
}
|
||||
if description == "" {
|
||||
board.Description = "/test/ board description"
|
||||
} else {
|
||||
board.Description = description
|
||||
}
|
||||
board.Section = 1
|
||||
board.MaxFilesize = 10000
|
||||
board.MaxPages = 21
|
||||
board.DefaultStyle = config.GetBoardConfig("").DefaultStyle
|
||||
board.Locked = false
|
||||
board.CreatedOn = time.Now()
|
||||
board.Anonymous = "Anonymous"
|
||||
board.ForcedAnon = false
|
||||
board.AutosageAfter = 200
|
||||
board.NoImagesAfter = 500
|
||||
board.MaxMessageLength = 8192
|
||||
board.EmbedsAllowed = false
|
||||
board.RedirectToThread = false
|
||||
board.ShowID = false
|
||||
board.RequireFile = false
|
||||
board.EnableCatalog = true
|
||||
board.EnableSpoileredImages = true
|
||||
board.EnableSpoileredThreads = true
|
||||
board.Worksafe = true
|
||||
board.Cooldowns = BoardCooldowns{
|
||||
NewThread: 30,
|
||||
Reply: 7,
|
||||
ImageReply: 7,
|
||||
}
|
||||
board.ThreadsPerPage = 20
|
||||
}
|
||||
|
||||
func (board *Board) Create() error {
|
||||
return CreateBoard(board)
|
||||
}
|
||||
|
||||
type BoardSection struct {
|
||||
ID int
|
||||
ListOrder int
|
||||
Hidden bool
|
||||
Name string
|
||||
Abbreviation string
|
||||
}
|
||||
|
||||
// Post represents each post in the database
|
||||
// Deprecated. Struct was made for use with old database, deprecated since refactor of april 2020.
|
||||
// Please refactor all code that uses this struct to use a struct that alligns with the new database structure and functions.
|
||||
type Post struct {
|
||||
ID int `json:"no"`
|
||||
ParentID int `json:"resto"`
|
||||
CurrentPage int `json:"-"`
|
||||
BoardID int `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Tripcode string `json:"trip"`
|
||||
Email string `json:"email"`
|
||||
Subject string `json:"sub"`
|
||||
MessageHTML template.HTML `json:"com"`
|
||||
MessageText string `json:"-"`
|
||||
Password string `json:"-"`
|
||||
Filename string `json:"tim"`
|
||||
FilenameOriginal string `json:"filename"`
|
||||
FileChecksum string `json:"md5"`
|
||||
FileExt string `json:"extension"`
|
||||
Filesize int `json:"fsize"`
|
||||
ImageW int `json:"w"`
|
||||
ImageH int `json:"h"`
|
||||
ThumbW int `json:"tn_w"`
|
||||
ThumbH int `json:"tn_h"`
|
||||
IP string `json:"-"`
|
||||
Capcode string `json:"capcode"`
|
||||
Timestamp time.Time `json:"time"`
|
||||
Autosage bool `json:"-"`
|
||||
Bumped time.Time `json:"last_modified"`
|
||||
Stickied bool `json:"-"`
|
||||
Locked bool `json:"-"`
|
||||
Reviewed bool `json:"-"`
|
||||
|
||||
boardDir string
|
||||
}
|
||||
|
||||
func (p *Post) GetBoardDir() (string, error) {
|
||||
if p.boardDir != "" {
|
||||
return p.boardDir, nil
|
||||
}
|
||||
const sql = `SELECT dir FROM DBPREFIXboards WHERE id = (
|
||||
SELECT board_id FROM DBPREFIXthreads WHERE id = (
|
||||
SELECT thread_id FROM DBPREFIXposts WHERE id = ? LIMIT 1) LIMIT 1)`
|
||||
|
||||
err := QueryRowSQL(sql, []interface{}{p.ID}, interfaceSlice(&p.boardDir))
|
||||
return p.boardDir, err
|
||||
}
|
||||
|
||||
func (p *Post) GetURL(includeDomain bool) string {
|
||||
postURL := ""
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
if includeDomain {
|
||||
postURL += systemCritical.SiteDomain
|
||||
}
|
||||
var board Board
|
||||
if err := board.PopulateData(p.BoardID); err != nil {
|
||||
return postURL
|
||||
}
|
||||
|
||||
postURL += systemCritical.WebRoot + board.Dir + "/res/"
|
||||
if p.ParentID == 0 {
|
||||
postURL += fmt.Sprintf("%d.html#%d", p.ID, p.ID)
|
||||
} else {
|
||||
postURL += fmt.Sprintf("%d.html#%d", p.ParentID, p.ID)
|
||||
}
|
||||
return postURL
|
||||
}
|
||||
|
||||
// Sanitize escapes HTML strings in a post. This should be run immediately before
|
||||
// the post is inserted into the database
|
||||
func (p *Post) Sanitize() {
|
||||
p.Name = html.EscapeString(p.Name)
|
||||
p.Email = html.EscapeString(p.Email)
|
||||
p.Subject = html.EscapeString(p.Subject)
|
||||
p.Password = html.EscapeString(p.Password)
|
||||
if p.ParentID < 0 {
|
||||
p.ParentID = 0
|
||||
}
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
ID int
|
||||
HandledByStaffID int
|
||||
PostID int
|
||||
IP string
|
||||
Reason string
|
||||
IsCleared bool
|
||||
}
|
||||
|
||||
func (r *Report) Timestamp() (*time.Time, error) {
|
||||
sql := `SELECT timestamp FROM DBPREFIXreports_audit WHERE report_id = ?`
|
||||
timestamp := new(time.Time)
|
||||
err := QueryRowSQL(sql, interfaceSlice(r.ID), interfaceSlice(timestamp))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return timestamp, nil
|
||||
}
|
||||
|
||||
type LoginSession struct {
|
||||
ID uint
|
||||
Name string
|
||||
Data string
|
||||
Expires string
|
||||
}
|
||||
|
||||
// Staff represents a single staff member's info stored in the database
|
||||
type Staff struct {
|
||||
ID int
|
||||
Username string
|
||||
PasswordChecksum string `json:"-"`
|
||||
Rank int
|
||||
AddedOn time.Time
|
||||
LastActive time.Time
|
||||
}
|
||||
|
||||
// CleanSessions clears out all of the sessions with this
|
||||
// staff ID, regardless of cookie data
|
||||
func (s *Staff) CleanSessions() (int64, error) {
|
||||
var err error
|
||||
query := `DELETE FROM DBPREFIXsessions WHERE staff_id = ?`
|
||||
result, err := ExecSQL(query, s.ID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
func (s *Staff) RankString() string {
|
||||
switch s.Rank {
|
||||
case 3:
|
||||
return "Administrator"
|
||||
case 2:
|
||||
return "Moderator"
|
||||
case 1:
|
||||
return "Janitor"
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
type BoardCooldowns struct {
|
||||
NewThread int `json:"threads"`
|
||||
Reply int `json:"replies"`
|
||||
ImageReply int `json:"images"`
|
||||
}
|
||||
|
||||
type MessagePostContainer struct {
|
||||
ID int
|
||||
MessageRaw string
|
||||
Message template.HTML
|
||||
Board string
|
||||
}
|
||||
|
||||
// Deprecated. Struct was made for use with old database, deprecated since refactor of april 2020.
|
||||
// Please refactor all code that uses this struct to use a struct that alligns with the new database structure and functions.
|
||||
type RecentPost struct {
|
||||
BoardName string
|
||||
BoardID int
|
||||
PostID int
|
||||
ParentID int
|
||||
Name string
|
||||
Tripcode string
|
||||
Message template.HTML
|
||||
Filename string
|
||||
ThumbW int
|
||||
ThumbH int
|
||||
IP string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// GetURL returns the full URL of the recent post, or the full path if includeDomain is false
|
||||
func (p *RecentPost) GetURL(includeDomain bool) string {
|
||||
postURL := ""
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
if includeDomain {
|
||||
postURL += systemCritical.SiteDomain
|
||||
}
|
||||
idStr := strconv.Itoa(p.PostID)
|
||||
postURL += systemCritical.WebRoot + p.BoardName + "/res/"
|
||||
if p.ParentID == 0 {
|
||||
postURL += idStr + ".html#" + idStr
|
||||
} else {
|
||||
postURL += strconv.Itoa(p.ParentID) + ".html#" + idStr
|
||||
}
|
||||
return postURL
|
||||
}
|
||||
|
||||
type Thread struct {
|
||||
OP Post `json:"-"`
|
||||
NumReplies int `json:"replies"`
|
||||
NumImages int `json:"images"`
|
||||
OmittedPosts int `json:"omitted_posts"`
|
||||
OmittedImages int `json:"omitted_images"`
|
||||
BoardReplies []Post `json:"-"`
|
||||
Sticky int `json:"sticky"`
|
||||
Locked int `json:"locked"`
|
||||
ThreadPage int `json:"-"`
|
||||
}
|
||||
|
||||
// FileBan contains the information associated with a specific file ban
|
||||
// FileBan contains the information associated with a specific file ban.
|
||||
// table: DBPREFIXfile_ban
|
||||
type FileBan struct {
|
||||
ID int `json:"id"`
|
||||
BoardID *int `json:"board"`
|
||||
StaffID int `json:"staff_id"`
|
||||
StaffNote string `json:"staff_note"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
Checksum string `json:"checksum"`
|
||||
ID int `json:"id"` // sql: `id`
|
||||
BoardID int `json:"board_id"` // sql: `board_id`
|
||||
StaffID int `json:"staff_id"` // sql: `staff_id`
|
||||
StaffNote string `json:"staff_note"` // sql: `staff_note`
|
||||
IssuedAt time.Time `json:"issued_at"` // sql: `issued_at`
|
||||
Checksum string `json:"checksum"` // sql: `checksum`
|
||||
}
|
||||
|
||||
func (fb *FileBan) BoardURI() string {
|
||||
if fb.BoardID == nil {
|
||||
return ""
|
||||
}
|
||||
var uri string
|
||||
err := QueryRowSQL(`SELECT uri FROM DBPREFIXboards WHERE id = ?`, interfaceSlice(fb.BoardID), interfaceSlice(&uri))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
func (fb *FileBan) StaffName() string {
|
||||
staff, err := getStaffByID(fb.StaffID)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return staff.Username
|
||||
}
|
||||
|
||||
// FilenameBan contains the information associated with a specific filename ban
|
||||
// FilenameBan represents a ban on a specific filename or filename regular expression.
|
||||
// table: DBPREFIXfilename_ban
|
||||
type FilenameBan struct {
|
||||
ID int `json:"id"`
|
||||
BoardID *int `json:"board"`
|
||||
StaffID int `json:"staff_id"`
|
||||
StaffNote string `json:"staff_note"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
Filename string `json:"filename"`
|
||||
IsRegex bool `json:"is_regex"`
|
||||
ID int `json:"id"` // sql: `id`
|
||||
BoardID int `json:"board_id"` // sql: `board_id`
|
||||
StaffID int `json:"staff_id"` // sql: `staff_id`
|
||||
StaffNote string `json:"staff_note"` // sql: `staff_note`
|
||||
IssuedAt time.Time `json:"issued_at"` // sql: `issued_at`
|
||||
Filename string `json:"filename"` // sql: `filename`
|
||||
IsRegex bool `json:"is_regex"` // sql: `is_regex`
|
||||
}
|
||||
|
||||
func (fnb *FilenameBan) BoardURI() string {
|
||||
if fnb.BoardID == nil {
|
||||
return ""
|
||||
}
|
||||
var uri string
|
||||
err := QueryRowSQL(`SELECT uri FROM DBPREFIXboards WHERE id = ?`, interfaceSlice(fnb.BoardID), interfaceSlice(&uri))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return uri
|
||||
// Upload represents a file attached to a post.
|
||||
// table: DBPREFIXfiles
|
||||
type Upload struct {
|
||||
ID int // sql: `id`
|
||||
PostID int // sql: `post_id`
|
||||
FileOrder int // sql: `file_order`
|
||||
OriginalFilename string // sql: `original_filename`
|
||||
Filename string // sql: `filename`
|
||||
Checksum string // sql: `checksum`
|
||||
FileSize int // sql: `file_size`
|
||||
IsSpoilered bool // sql: `is_spoilered`
|
||||
ThumbnailWidth int // sql: `thumbnail_width`
|
||||
ThumbnailHeight int // sql: `thumbnail_height`
|
||||
Width int // sql: `width`
|
||||
Height int // sql: `height`
|
||||
}
|
||||
|
||||
func (fnb *FilenameBan) StaffName() string {
|
||||
staff, err := getStaffByID(fnb.StaffID)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return staff.Username
|
||||
// used to composition IPBan and IPBanAudit
|
||||
type ipBanBase struct {
|
||||
IsActive bool `json:"is_active"`
|
||||
IsThreadBan bool `json:"is_thread_ban"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
StaffID int `json:"staff_id"`
|
||||
AppealAt time.Time `json:"appeal_at"`
|
||||
Permanent bool `json:"permanent"`
|
||||
StaffNote string `json:"staff_note"`
|
||||
Message string `json:"message"`
|
||||
CanAppeal bool `json:"can_appeal"`
|
||||
}
|
||||
|
||||
// UsernameBan contains the information associated with a specific username ban
|
||||
type UsernameBan struct {
|
||||
ID int `json:"id"`
|
||||
BoardID *int `json:"board"`
|
||||
StaffID int `json:"staff_id"`
|
||||
StaffNote string `json:"staff_note"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
Username string `json:"username"`
|
||||
IsRegex bool `json:"is_regex"`
|
||||
}
|
||||
|
||||
// WordFilter contains the information associated with a specific wordfilter
|
||||
type WordFilter struct {
|
||||
ID int `json:"id"`
|
||||
BoardDirs []string `json:"boards"`
|
||||
StaffID int `json:"staff_id"`
|
||||
StaffNote string `json:"staff_note"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
Search string `json:"search"`
|
||||
IsRegex bool `json:"is_regex"`
|
||||
ChangeTo string `json:"change_to"`
|
||||
}
|
||||
|
||||
// IPBan contains the information association with a specific ip ban
|
||||
// IPBan contains the information association with a specific ip ban.
|
||||
// table: DBPREFIXip_ban
|
||||
type IPBan struct {
|
||||
ID int `json:"id"`
|
||||
BoardID *int `json:"board"`
|
||||
StaffID int `json:"staff_id"`
|
||||
BannedForPostID *int `json:"banned_for_post_id"`
|
||||
CopyPostText template.HTML `json:"copy_post_text"`
|
||||
IsThreadBan bool `json:"is_thread_ban"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IP string `json:"ip"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
AppealAt time.Time `json:"appeal_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Permanent bool `json:"permanent"`
|
||||
StaffNote string `json:"staff_note"`
|
||||
Message string `json:"message"`
|
||||
CanAppeal bool `json:"can_appeal"`
|
||||
ipBanBase
|
||||
}
|
||||
|
||||
// table: DBPREFIXip_ban_audit
|
||||
type IPBanAudit struct {
|
||||
IPBanID int // sql: `ip_ban_id`
|
||||
Timestamp time.Time // sql: `timestamp`
|
||||
ipBanBase
|
||||
}
|
||||
|
||||
// used to composition IPBanAppeal and IPBanAppealAudit
|
||||
type ipBanAppealBase struct {
|
||||
StaffID int // sql: `staff_id`
|
||||
AppealText string // sql: `appeal_text`
|
||||
StaffResponse string // sql: `staff_response`
|
||||
IsDenied bool // sql: `is_denied`
|
||||
}
|
||||
|
||||
// table: DBPREFIXip_ban_appeals
|
||||
type IPBanAppeal struct {
|
||||
ID int // sql: `id`
|
||||
IPBanID int // sql: `ip_ban_id`
|
||||
ipBanAppealBase
|
||||
}
|
||||
|
||||
// table: DBPREFIXip_ban_appeals_audit
|
||||
type IPBanAppealAudit struct {
|
||||
AppealID int // sql: `appeal_id`
|
||||
Timestamp time.Time // sql: `timestamp`
|
||||
ipBanAppealBase
|
||||
}
|
||||
|
||||
// table: DBPREFIXposts
|
||||
type Post struct {
|
||||
ID int // sql: `id`
|
||||
ThreadID int // sql: `thread_id`
|
||||
IsTopPost bool // sql: `is_top_post`
|
||||
IP string // sql: `ip`
|
||||
CreatedOn time.Time // sql: `created_on`
|
||||
Name string // sql: `name`
|
||||
Tripcode string // sql: `tripcode`
|
||||
IsRoleSignature bool // sql: `is_role_signature`
|
||||
Email string // sql: `email`
|
||||
Subject string // sql: `subject`
|
||||
Message template.HTML // sql: `message`
|
||||
MessageRaw string // sql: `message_raw`
|
||||
Password string // sql: `password`
|
||||
DeletedAt time.Time // sql: `deleted_at`
|
||||
IsDeleted bool // sql: `is_deleted`
|
||||
BannedMessage string // sql: `banned_message`
|
||||
}
|
||||
|
||||
// table: DBPREFIXreports
|
||||
type Report struct {
|
||||
ID int // sql: `id`
|
||||
HandledByStaffID int // sql: `handled_by_staff_id`
|
||||
PostID int // sql: `post_id`
|
||||
IP string // sql: `ip`
|
||||
Reason string // sql: `reason`
|
||||
IsCleared bool // sql: `is_cleared`
|
||||
}
|
||||
|
||||
// table: DBPREFIXreports_audit
|
||||
type ReportAudit struct {
|
||||
Report int // sql: `report_id`
|
||||
Timestamp time.Time // sql: `timestamp`
|
||||
HandledByStaffID int // sql: `handled_by_staff_id`
|
||||
IsCleared bool // sql: `is_cleared`
|
||||
}
|
||||
|
||||
// table: DBPREFIXsections
|
||||
type Section struct {
|
||||
ID int // sql: `id`
|
||||
Name string // sql: `name`
|
||||
Abbreviation string // sql: `abbreviation`
|
||||
Position int // sql: `position`
|
||||
Hidden bool // sql: `hidden`
|
||||
}
|
||||
|
||||
// table: DBPREFIXsessions
|
||||
type LoginSession struct {
|
||||
ID int // sql: `id`
|
||||
StaffID int // sql: `staff_id`
|
||||
Expires time.Time // sql: `expires`
|
||||
Data string // sql: `data`
|
||||
}
|
||||
|
||||
// DBPREFIXstaff
|
||||
type Staff struct {
|
||||
ID int // sql: `id`
|
||||
Username string // sql: `username`
|
||||
PasswordChecksum string // sql: `password_checksum`
|
||||
Rank int // sql: `global_rank`
|
||||
AddedOn time.Time // sql: `added_on`
|
||||
LastLogin time.Time // sql: `last_login`
|
||||
IsActive bool // sql: `is_active`
|
||||
}
|
||||
|
||||
// table: DBPREFIXthreads
|
||||
type Thread struct {
|
||||
ID int // sql: `id`
|
||||
BoardID int // sql: `board_id`
|
||||
Locked bool // sql: `locked`
|
||||
Stickied bool // sql: `stickied`
|
||||
Anchored bool // sql: `anchored`
|
||||
Cyclical bool // sql: `cyclical`
|
||||
LastBump time.Time // sql: `last_bump`
|
||||
DeletedAt time.Time // sql: `deleted_at`
|
||||
IsDeleted bool // sql: `is_deleted`
|
||||
}
|
||||
|
||||
// table: DBPREFIXusername_ban
|
||||
type UsernameBan struct {
|
||||
ID int `json:"id"` // sql: `id`
|
||||
BoardID *int `json:"board"` // sql: `board_id`
|
||||
StaffID int `json:"staff_id"` // sql: `staff_id`
|
||||
StaffNote string `json:"staff_note"` // sql: `staff_note`
|
||||
IssuedAt time.Time `json:"issued_at"` // sql: `issued_at`
|
||||
Username string `json:"username"` // sql: `username`
|
||||
IsRegex bool `json:"is_regex"` // sql: `is_regex`
|
||||
}
|
||||
|
||||
// table DBPREFIXwordfilters
|
||||
type Wordfilter struct {
|
||||
ID int `json:"id"` // sql: `id`
|
||||
BoardDirs *string `json:"boards"` // sql: `board_dirs`
|
||||
StaffID int `json:"staff_id"` // sql: `staff_id`
|
||||
StaffNote string `json:"staff_note"` // sql: `staff_note`
|
||||
IssuedAt time.Time `json:"issued_at"` // sql: `issued_at`
|
||||
Search string `json:"search"` // sql: `search`
|
||||
IsRegex bool `json:"is_regex"` // sql: `is_regex`
|
||||
ChangeTo string `json:"change_to"` // sql: `change_to`
|
||||
}
|
||||
|
|
74
pkg/gcsql/threads.go
Normal file
74
pkg/gcsql/threads.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package gcsql
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrThreadExists = errors.New("thread already exists")
|
||||
ErrThreadDoesNotExist = errors.New("thread does not exist")
|
||||
)
|
||||
|
||||
// GetThreadsWithBoardID queries the database for the threads with the given board ID from the database.
|
||||
// If onlyNotDeleted is true, it omits deleted threads and threads that were removed because the max
|
||||
// thread limit was reached
|
||||
func GetThreadsWithBoardID(boardID int, onlyNotDeleted bool) ([]Thread, error) {
|
||||
query := `SELECT
|
||||
id, board_id, locked, stickied, anchored, cyclical, last_bump, deleted_at, is_deleted
|
||||
FROM DBPREFIXthreads WHERE board_id = ?`
|
||||
if onlyNotDeleted {
|
||||
query += " AND is_deleted = FALSE"
|
||||
}
|
||||
rows, err := QuerySQL(query, boardID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var threads []Thread
|
||||
for rows.Next() {
|
||||
var thread Thread
|
||||
if err = rows.Scan(
|
||||
&thread.ID, &thread.BoardID, &thread.Locked, &thread.Stickied, &thread.Anchored,
|
||||
&thread.Cyclical, &thread.LastBump,
|
||||
); err != nil {
|
||||
return threads, err
|
||||
}
|
||||
}
|
||||
return threads, nil
|
||||
}
|
||||
|
||||
func GetThreadReplyCountFromOP(opID int) (int, error) {
|
||||
const query = `SELECT COUNT(*) FROM DBPREFIXposts WHERE thread_id = (
|
||||
SELECT thread_id FROM DBPREFIXposts WHERE id = ?) AND is_deleted = 0`
|
||||
var num int
|
||||
err := QueryRowSQL(query, interfaceSlice(opID), interfaceSlice(&num))
|
||||
return num, err
|
||||
}
|
||||
|
||||
// ChangeThreadBoardID updates the given thread's post ID and the destination board ID
|
||||
func ChangeThreadBoardID(threadID int, newBoardID int) error {
|
||||
if !DoesBoardExistByID(newBoardID) {
|
||||
return ErrBoardDoesNotExist
|
||||
}
|
||||
_, err := ExecSQL(`UPDATE DBPREFIXthreads SET board_id = ? WHERE id = ?`, newBoardID, threadID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ChangeThreadBoardByURI updates a thread's board ID, given the thread's post ID and
|
||||
// the destination board's uri
|
||||
func ChangeThreadBoardByURI(postID int, uri string) error {
|
||||
boardID, err := getBoardIDFromURI(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ChangeThreadBoardID(postID, boardID)
|
||||
}
|
||||
|
||||
// deleteThread updates the thread and sets it as deleted, as well as the posts where thread_id = threadID
|
||||
func deleteThread(threadID int) error {
|
||||
const deletePostsSQL = `UPDATE DBPREFIXposts SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE thread_id = ?`
|
||||
const deleteThreadSQL = `UPDATE DBPREFIXthreads SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE id = ?`
|
||||
_, err := ExecSQL(deletePostsSQL, threadID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = ExecSQL(deleteThreadSQL, threadID)
|
||||
return err
|
||||
}
|
37
pkg/gcsql/uploads.go
Normal file
37
pkg/gcsql/uploads.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"github.com/gochan-org/gochan/pkg/gcsql.bak"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
// ThumbnailPath returns the thumbnail path of the upload, given an thumbnail type ("thumbnail" or "catalog")
|
||||
func (u *Upload) ThumbnailPath(thumbType string) string {
|
||||
return gcutil.GetThumbnailPath(thumbType, u.Filename)
|
||||
}
|
||||
|
||||
// GetThreadFiles gets a list of the files owned by posts in the thread, including thumbnails for convenience.
|
||||
func GetThreadFiles(post *Post) ([]Upload, error) {
|
||||
query := `SELECT
|
||||
id, post_id, file_order, original_filename, filename, checksum,
|
||||
file_size, is_spoilered, thumbnail_width, thumbnail_height, width, height
|
||||
FROM DBPREFIXfiles WHERE post_id IN (
|
||||
SELECT id FROM DBPREFIXposts WHERE thread_id = (
|
||||
SELECT thread_id FROM DBPREFIXposts WHERE id = ?)) AND filename != 'deleted'`
|
||||
rows, err := gcsql.QuerySQL(query, post.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var uploads []Upload
|
||||
for rows.Next() {
|
||||
var upload Upload
|
||||
if err = rows.Scan(
|
||||
&upload.ID, &upload.PostID, &upload.FileOrder, &upload.OriginalFilename, &upload.Filename, &upload.Checksum,
|
||||
&upload.FileSize, &upload.IsSpoilered, &upload.ThumbnailWidth, &upload.ThumbnailHeight, &upload.Width, &upload.Height,
|
||||
); err != nil {
|
||||
return uploads, err
|
||||
}
|
||||
uploads = append(uploads, upload)
|
||||
}
|
||||
return uploads, nil
|
||||
}
|
|
@ -95,7 +95,7 @@ Example:
|
|||
var intVal int
|
||||
var stringVal string
|
||||
err := QueryRowSQL("SELECT intval,stringval FROM table WHERE id = ?",
|
||||
[]interface{}{&id},
|
||||
[]interface{}{id},
|
||||
[]interface{}{&intVal, &stringVal})
|
||||
*/
|
||||
func QueryRowSQL(query string, values, out []interface{}) error {
|
||||
|
@ -138,17 +138,68 @@ func BeginTx() (*sql.Tx, error) {
|
|||
})
|
||||
}
|
||||
|
||||
// ResetBoardSectionArrays is run when the board list needs to be changed
|
||||
// (board/section is added, deleted, etc)
|
||||
func ResetBoardSectionArrays() {
|
||||
AllBoards = nil
|
||||
AllSections = nil
|
||||
func getNextFreeID(tableName string) (ID int, err error) {
|
||||
var sql = `SELECT COALESCE(MAX(id), 0) + 1 FROM ` + tableName
|
||||
err = QueryRowSQL(sql, interfaceSlice(), interfaceSlice(&ID))
|
||||
return ID, err
|
||||
}
|
||||
|
||||
allBoardsArr, _ := GetAllBoards()
|
||||
AllBoards = append(AllBoards, allBoardsArr...)
|
||||
func doesTableExist(tableName string) (bool, error) {
|
||||
var existQuery string
|
||||
|
||||
allSectionsArr, _ := GetAllSections()
|
||||
AllSections = append(AllSections, allSectionsArr...)
|
||||
switch config.GetSystemCriticalConfig().DBtype {
|
||||
case "mysql":
|
||||
fallthrough
|
||||
case "postgresql":
|
||||
existQuery = `SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ?`
|
||||
case "sqlite3":
|
||||
existQuery = `SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?`
|
||||
default:
|
||||
return false, ErrUnsupportedDB
|
||||
}
|
||||
|
||||
var count int
|
||||
err := QueryRowSQL(existQuery, []interface{}{config.GetSystemCriticalConfig().DBprefix + tableName}, []interface{}{&count})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count == 1, nil
|
||||
}
|
||||
|
||||
// getDatabaseVersion gets the version of the database, or an error if none or multiple exist
|
||||
func getDatabaseVersion(componentKey string) (int, error) {
|
||||
const sql = `SELECT version FROM DBPREFIXdatabase_version WHERE component = ?`
|
||||
var version int
|
||||
err := QueryRowSQL(sql, []interface{}{componentKey}, []interface{}{&version})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return version, err
|
||||
}
|
||||
|
||||
// doesGochanPrefixTableExist returns true if any table with a gochan prefix was found.
|
||||
// Returns false if the prefix is an empty string
|
||||
func doesGochanPrefixTableExist() (bool, error) {
|
||||
systemCritical := config.GetSystemCriticalConfig()
|
||||
if systemCritical.DBprefix == "" {
|
||||
return false, nil
|
||||
}
|
||||
var prefixTableExist string
|
||||
switch systemCritical.DBtype {
|
||||
case "mysql":
|
||||
fallthrough
|
||||
case "postgresql":
|
||||
prefixTableExist = `SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'DBPREFIX%'`
|
||||
case "sqlite3":
|
||||
prefixTableExist = `SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name LIKE 'DBPREFIX%'`
|
||||
}
|
||||
|
||||
var count int
|
||||
err := QueryRowSQL(prefixTableExist, []interface{}{}, []interface{}{&count})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// interfaceSlice creates a new interface slice from an arbitrary collection of values
|
||||
|
|
|
@ -239,19 +239,19 @@ var funcMap = template.FuncMap{
|
|||
}
|
||||
return img[0:index] + thumbSuffix
|
||||
},
|
||||
"numReplies": func(boardid, threadid int) int {
|
||||
num, err := gcsql.GetReplyCount(threadid)
|
||||
"numReplies": func(boardid, opID int) int {
|
||||
num, err := gcsql.GetThreadReplyCountFromOP(opID)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return num
|
||||
},
|
||||
"getBoardDir": func(id int) string {
|
||||
var board gcsql.Board
|
||||
if err := board.PopulateData(id); err != nil {
|
||||
dir, err := gcsql.GetBoardDir(id)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return board.Dir
|
||||
return dir
|
||||
},
|
||||
"webPath": func(part ...string) string {
|
||||
return config.WebPath(part...)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue