1
0
Fork 0
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:
Eggbertx 2022-10-11 14:26:31 -07:00
parent d23d8ccd8a
commit 6567da3300
29 changed files with 1094 additions and 2671 deletions

View file

@ -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.

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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
View 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

View file

@ -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 {

View file

@ -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")
}
}

View file

@ -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
}

View file

@ -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())
}
}

View file

@ -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)

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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())
}
}

View file

@ -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)
}

View file

@ -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

View file

@ -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
}

View file

@ -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(&section.ID, &section.Name, &section.Abbreviation, &section.ListOrder, &section.Hidden)
var section Section
err = rows.Scan(&section.ID, &section.Name, &section.Abbreviation, &section.Position, &section.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(&section)
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(&section.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{}{&section.Name, &section.Abbreviation, &section.ListOrder, &section.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(&section.ID))
// }

View file

@ -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())
}
}

View file

@ -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
}

View file

@ -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
View 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
View 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
}

View file

@ -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

View file

@ -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...)