1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-20 09:26:23 -07:00

generate pages for boards and threads

This commit is contained in:
Joshua Merrell 2015-12-24 23:26:13 -08:00
parent a8ab09a48f
commit 4a1d0c1efd
17 changed files with 1379 additions and 1447 deletions

View file

@ -8,6 +8,10 @@ fi
CGO_ENABLED=0
GOARCH=amd64
SUFFIX=""
if [[ $GOOS == "windows" ]]
then
SUFFIX=".exe"
fi
go build -v -ldflags "-w" -o gochan$SUFFIX ./src
# the -w ldflag omits debugging stuff

View file

@ -1,2 +0,0 @@
body{margin:0;padding:8px;margin-bottom:auto;}blockquote blockquote{margin-left:0em;}form{margin-bottom:0px;}form .trap{display:none;}.blotter{color:red;font-size:12pt;}.postarea{text-align:center;}.postarea table{margin:0px auto;text-align:left;}.thumb{border:none;float:left;margin:2px 20px;}.nothumb{float:left;background:inherit;border:2px dashed #aaa;text-align:center;margin:2px 20px;padding:1em 0.5em 1em 0.5em;font-family:none;}.reply blockquote,blockquote :last-child{margin-bottom:0em;}.reflink a{color:inherit;text-decoration:none;}.reflink a:hover{color:#800000;}.reply .filesize{
}.userdelete{float:right;text-align:center;white-space:nowrap;}.replypage .replylink{display:none;}.pagelist{max-width:600px;}.admin{color:#800080;font-weight:normal;}.mod{color:#FF0000;font-weight:normal;}.vip{color:#336600;font-weight:normal;}#watchedthreads{position:absolute;background-color:#F0E0D6;border:1px dotted #EEAA88;border-top:0px none;}#watchedthreadsdraghandle{text-align:center;font-family:Trebuchet MS;cursor:move;}#watchedthreadlist{padding:3px;font-size:0.8em;}#watchedthreadsbuttons{position:absolute;bottom:3px;left:3px;}.spoiler{color:black;background-color:black;}.extrabtns{vertical-align:middle;}.hidethread{background:transparent url('./icons/blue/icons.gif') -32px -16px no-repeat;}.unhidethread{background:transparent url('./icons/blue/icons.gif') -48px 0px no-repeat;}.watchthread{background:transparent url('./icons/blue/icons.gif') -32px 0px no-repeat;}.expandthread{background:transparent url('./icons/blue/icons.gif') 0px -16px no-repeat;}.quickreply{background:transparent url('./icons/blue/icons.gif') 0px 0px no-repeat;}.hidewatchedthreads{background:transparent url('./icons/blue/icons.gif') -48px -16px no-repeat;}.refreshwatchedthreads{background:transparent url('./icons/blue/icons.gif') -16px -16px no-repeat;}.restorewatchedthreads{background:transparent url('./icons/blue/icons.gif') -16px 0px no-repeat;}.reflinkpreview{position:absolute;padding:5px;background-color:#F0E0D6;border:1px dotted #000000;}

5
html/error/404.html Normal file → Executable file
View file

@ -5,9 +5,8 @@
</head>
<body>
<h1>404: File not found</h1>
<!--<img src="/error.png" border="0" alt=""> -->
<img src="/error/lol 404.gif" border="0" alt="">
<p>The requested file could not be found on this server. Are you just typing random stuff in the address bar? If you followed a link from this site here, then post <a href="/site">here</a></p>
<audio src="http://dl.dropbox.com/u/10266670/forever.wav" autoplay="autoplay"></audio>
<hr><address>http://lunachan.net powered by Gochan v0.8</address>
<hr><address>http://gochan.org powered by Gochan v0.9</address>
</body>
</html>

4
html/error/500.html Normal file → Executable file
View file

@ -6,7 +6,7 @@
<body>
<h1>500: Internal Server error</h1>
<marquee><img src="/error/derpy server.gif" border="0" alt=""></marquee>
<p>Someone let Derpy into the server room again...sorry about that. Please try reloading the page. If that doesn't fix the problem, you can email me <a href="mailto:admin@lunachan.net">here</a>, noting the time and the page you tried to access.
<hr><address>http://lunachan.net powered by Gochan v0.8</address>
<p>Someone dun goof'd...sorry about that. Please try reloading the page. If that doesn't fix the problem, you can email the admin at admin@email.here, noting the time and the page you tried to access.
<hr><address>http://gochan.org powered by Gochan v0.9</address>
</body>
</html>

2
html/error/500_example.html Normal file → Executable file
View file

@ -6,6 +6,6 @@
<body>
<h1>500: Internal Server error</h1>
<p>The server encountered an error while trying to serve the page. We apologize for the inconvenience.</p>
<hr><address>http://lunachan.net powered by Gochan v0.8</address>
<hr><address>http://gochan.org powered by Gochan v0.9</address>
</body>
</html>

BIN
html/error/lol 404.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

7
src/gochan.go Normal file → Executable file
View file

@ -4,8 +4,7 @@ import (
"fmt"
)
const version float32 = 0.8
const version float32 = 0.9
func main() {
defer db.Close()
@ -17,7 +16,7 @@ func main() {
initTemplates()
fmt.Println("Initializing server...")
if db != nil {
db.Exec("USE `"+config.DBname+"`;")
db.Exec("USE `" + config.DBname + "`;")
}
initServer()
}
}

File diff suppressed because it is too large Load diff

453
src/posting.go Normal file → Executable file
View file

@ -26,7 +26,9 @@ import (
const whitespace_match = "[\000-\040]"
var (
last_post PostTable
last_post PostTable
all_sections []interface{}
all_boards []interface{}
)
func generateTripCode(input string) string {
@ -38,143 +40,231 @@ func generateTripCode(input string) string {
return crypt.Crypt(input, salt)[3:]
}
func buildBoardPage(boardid int, boards []BoardsTable, sections []interface{}) (html string) {
start_time := benchmarkTimer("buildBoard"+string(boardid), time.Now(), true)
var board BoardsTable
for b, _ := range boards {
if boards[b].ID == boardid {
board = boards[b]
}
func buildAll() {
buildFrontPage()
buildBoards(true, 0)
}
func buildBoards(all bool, which int) (html string) {
// if all is set to true, ignore which, otherwise, which = build only specified boardid
if !all {
_board, _ := getBoardArr("`id` = " + strconv.Itoa(which))
board := _board[0]
html += buildBoardPages(&board) + "<br />\n"
html += buildThreads(true, board.ID, 0)
}
boards, _ := getBoardArr("")
if len(boards) == 0 {
return html + "No boards to build.<br />\n"
}
for _, board := range boards {
html += buildBoardPages(&board) + "<br />\n"
html += buildThreads(true, board.ID, 0)
}
return
}
func buildBoardPages(board *BoardsTable) (html string) {
// start_time := benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), time.Now(), true)
var boardinfo_i []interface{}
var current_page_file *os.File
var interfaces []interface{}
var threads []interface{}
var op_posts []interface{}
op_posts, err := getPostArr("SELECT * FROM `" + config.DBprefix + "posts` WHERE `boardid` = " + strconv.Itoa(board.ID) + " AND `parentid` = 0 AND `deleted_timestamp` = '" + nil_timestamp + "' ORDER BY `bumped` DESC LIMIT " + strconv.Itoa(config.ThreadsPerPage_img))
if err != nil {
html += err.Error() + "<br />"
op_posts = make([]interface{}, 0)
}
var thread_pages [][]interface{}
var stickied_threads []interface{}
var nonstickied_threads []interface{}
for _, op_post_i := range op_posts {
var thread Thread
var posts_in_thread []interface{}
op_post := op_post_i.(PostTable)
if op_post.Stickied {
thread.IName = "thread"
posts_in_thread, err = getPostArr("SELECT * FROM `" + config.DBprefix + "posts` WHERE `boardid` = " + strconv.Itoa(board.ID) + " AND `parentid` = " + strconv.Itoa(op_post.ID) + " AND `deleted_timestamp` = '" + nil_timestamp + "' ORDER BY `id` DESC LIMIT " + strconv.Itoa(config.StickyRepliesOnBoardPage))
if err != nil {
html += err.Error() + "<br />"
}
err = db.QueryRow("SELECT COUNT(*) FROM `" + config.DBprefix + "posts` WHERE `boardid` = " + strconv.Itoa(board.ID) + " AND `parentid` = " + strconv.Itoa(op_post.ID)).Scan(&thread.NumReplies)
if err != nil {
html += err.Error() + "<br />"
}
thread.OP = op_post_i
if len(posts_in_thread) > 0 {
thread.BoardReplies = posts_in_thread
}
threads = append(threads, thread)
defer func() {
if uhoh, ok := recover().(error); ok {
error_log.Print("buildBoardPages failed: " + uhoh.Error())
fmt.Println("buildBoardPages failed: " + uhoh.Error())
}
}
for _, op_post_i := range op_posts {
var thread Thread
var posts_in_thread []interface{}
op_post := op_post_i.(PostTable)
if !op_post.Stickied {
thread.IName = "thread"
posts_in_thread, err = getPostArr("SELECT * FROM (SELECT * FROM `" + config.DBprefix + "posts` WHERE `boardid` = " + strconv.Itoa(board.ID) + " AND `parentid` = " + strconv.Itoa(op_post.ID) + " AND `deleted_timestamp` = '" + nil_timestamp + "' ORDER BY `id` DESC LIMIT " + strconv.Itoa(config.RepliesOnBoardpage) + ") t ORDER BY `id` ASC")
if err != nil {
html += err.Error() + "<br />"
}
err = db.QueryRow("SELECT COUNT(*) FROM `" + config.DBprefix + "posts` WHERE `boardid` = " + strconv.Itoa(board.ID) + " AND `parentid` = " + strconv.Itoa(op_post.ID)).Scan(&thread.NumReplies)
if err != nil {
html += err.Error() + "<br />"
}
thread.OP = op_post_i
if len(posts_in_thread) > 0 {
thread.BoardReplies = posts_in_thread
}
threads = append(threads, thread)
}
}
interfaces = append(interfaces, config)
var boards_i []interface{}
for _, b := range boards {
boards_i = append(boards_i, b)
}
var boardinfo_i []interface{}
boardinfo_i = append(boardinfo_i, board)
interfaces = append(interfaces, &Wrapper{IName: "boards", Data: boards_i})
interfaces = append(interfaces, &Wrapper{IName: "sections", Data: sections})
interfaces = append(interfaces, &Wrapper{IName: "threads", Data: threads})
interfaces = append(interfaces, &Wrapper{IName: "boardinfo", Data: boardinfo_i})
wrapped := &Wrapper{IName: "boardpage", Data: interfaces}
os.Remove(path.Join(config.DocumentRoot, board.Dir, "board.html"))
current_page_file.Close()
}()
results, err := os.Stat(path.Join(config.DocumentRoot, board.Dir))
if err != nil {
err = os.Mkdir(path.Join(config.DocumentRoot, board.Dir), 0777)
if err != nil {
html += "Failed creating /" + board.Dir + "/: " + err.Error() + "<br />\n"
error_log.Println("Failed creating /" + board.Dir + "/: " + err.Error())
}
} else if !results.IsDir() {
html += "Error: /" + board.Dir + "/ exists, but is not a folder. <br />\n"
error_log.Println("Error: /" + board.Dir + "/ exists, but is not a folder.")
}
board_file, err := os.OpenFile(path.Join(config.DocumentRoot, board.Dir, "board.html"), os.O_CREATE|os.O_RDWR, 0777)
op_posts, err := getPostArr("`boardid` = " + strconv.Itoa(board.ID) + " AND `parentid` = 0 AND `deleted_timestamp` = '" + nil_timestamp + "' ORDER BY `bumped` DESC")
if err != nil {
html += err.Error() + "<br />\n"
html += err.Error() + "<br />"
op_posts = make([]interface{}, 0)
return
}
defer func() {
if uhoh, ok := recover().(error); ok {
error_log.Print("Failed executing template.")
fmt.Println(uhoh.Error())
for _, op_post_i := range op_posts {
var thread Thread
var posts_in_thread []interface{}
thread.IName = "thread"
op_post := op_post_i.(PostTable)
if op_post.Stickied {
posts_in_thread, err = getPostArr("`boardid` = " + strconv.Itoa(board.ID) + " AND `parentid` = " + strconv.Itoa(op_post.ID) + " AND `deleted_timestamp` = '" + nil_timestamp + "' ORDER BY `id` DESC LIMIT " + strconv.Itoa(config.StickyRepliesOnBoardPage))
if err != nil {
html += err.Error() + "<br />"
}
err = db.QueryRow("SELECT COUNT(*) FROM `" + config.DBprefix + "posts` WHERE `boardid` = " + strconv.Itoa(board.ID) + " AND `parentid` = " + strconv.Itoa(op_post.ID)).Scan(&thread.NumReplies)
if err != nil {
html += err.Error() + "<br />"
}
thread.OP = op_post_i
if len(posts_in_thread) > 0 {
thread.BoardReplies = posts_in_thread
}
stickied_threads = append(stickied_threads, thread)
} else {
posts_in_thread, err = getPostArr("`boardid` = " + strconv.Itoa(board.ID) + " AND `parentid` = " + strconv.Itoa(op_post.ID) + " AND `deleted_timestamp` = '" + nil_timestamp + "' ORDER BY `id` DESC LIMIT " + strconv.Itoa(config.RepliesOnBoardpage))
if err != nil {
html += err.Error() + "<br />"
}
err = db.QueryRow("SELECT COUNT(*) FROM `" + config.DBprefix + "posts` WHERE `boardid` = " + strconv.Itoa(board.ID) + " AND `parentid` = " + strconv.Itoa(op_post.ID)).Scan(&thread.NumReplies)
if err != nil {
html += err.Error() + "<br />"
}
thread.OP = op_post_i
if len(posts_in_thread) > 0 {
thread.BoardReplies = posts_in_thread
}
nonstickied_threads = append(nonstickied_threads, thread)
}
if board_file != nil {
board_file.Close()
}
}()
err = img_boardpage_tmpl.Execute(board_file, wrapped)
if err != nil {
html += "Failed building /" + board.Dir + "/: " + err.Error() + "<br />\n"
error_log.Print(err.Error())
} else {
html += "/" + board.Dir + "/ built successfully.\n"
}
benchmarkTimer("buildBoard"+string(boardid), start_time, false)
threads = append(stickied_threads, nonstickied_threads...)
thread_pages = paginate(config.ThreadsPerPage_img, threads)
deleteMatchingFiles(path.Join(config.DocumentRoot, board.Dir), "\\.html$")
board.NumPages = len(thread_pages) - 1
for page_num, page_threads := range thread_pages {
board.CurrentPage = page_num
boardinfo_i = nil
boardinfo_i = append(boardinfo_i, board)
interfaces = nil
interfaces = append(interfaces, config,
&Wrapper{IName: "boards", Data: all_boards},
&Wrapper{IName: "sections", Data: all_sections},
&Wrapper{IName: "threads", Data: page_threads},
&Wrapper{IName: "boardinfo", Data: boardinfo_i})
wrapped := &Wrapper{IName: "boardpage", Data: interfaces}
if board.CurrentPage == 0 {
current_page_file, err = os.OpenFile(path.Join(config.DocumentRoot, board.Dir, "board.html"), os.O_CREATE|os.O_RDWR, 0777)
} else {
current_page_file, err = os.OpenFile(path.Join(config.DocumentRoot, board.Dir, strconv.Itoa(page_num)+".html"), os.O_CREATE|os.O_RDWR, 0777)
}
if err != nil {
html += err.Error() + "<br />\n"
error_log.Println(err.Error())
}
err = img_boardpage_tmpl.Execute(current_page_file, wrapped)
if err != nil {
html += "Failed building /" + board.Dir + "/: " + err.Error() + "<br />\n"
error_log.Print(err.Error())
return
}
}
html += "/" + board.Dir + "/ built successfully.\n"
//benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), start_time, false)
return
}
func buildFrontPage(boards []BoardsTable, sections []interface{}) (html string) {
initTemplates()
func buildThreads(all bool, boardid, threadid int) (html string) {
// TODO: detect which page will be built and only build that one and the board page
// if all is set to true, ignore which, otherwise, which = build only specified boardid
if !all {
_thread, _ := getPostArr("`boardid` = " + strconv.Itoa(boardid) + " AND `id` = " + strconv.Itoa(threadid) + " AND `parentid` = 0 AND `deleted_timestamp` = '" + nil_timestamp + "'")
thread := _thread[0]
thread_struct := thread.(PostTable)
html += buildThreadPages(&thread_struct) + "<br />\n"
}
threads, _ := getPostArr("`boardid` = " + strconv.Itoa(boardid) + " AND `parentid` = 0 AND `deleted_timestamp` = '" + nil_timestamp + "'")
if len(threads) == 0 {
return html + "No threads to build.<br />\n"
}
for _, op := range threads {
op_struct := op.(PostTable)
html += buildThreadPages(&op_struct) + "<br />\n"
}
return
}
func buildThreadPages(op *PostTable) (html string) {
var board_dir string
var replies []interface{}
var current_page_file *os.File
row := db.QueryRow("SELECT `dir` FROM `" + config.DBprefix + "boards` WHERE `id` = '" + strconv.Itoa(op.BoardID) + "';")
err := row.Scan(&board_dir)
if err != nil {
error_log.Println(err.Error())
return err.Error()
}
replies, err = getPostArr("`boardid` = " + strconv.Itoa(op.BoardID) + " AND `parentid` = " + strconv.Itoa(op.ID) + " AND `deleted_timestamp` = '" + nil_timestamp + "'")
if err != nil {
return "Error building thread " + strconv.Itoa(op.ID) + ":" + err.Error()
}
thread_pages := paginate(config.PostsPerThreadpage, replies)
for i, _ := range thread_pages {
thread_pages[i] = append([]interface{}{op}, thread_pages[i]...)
}
deleteMatchingFiles(path.Join(config.DocumentRoot, board_dir, "res"), "\\.html$")
op.NumPages = len(thread_pages) - 1
for page_num, page_posts := range thread_pages {
fmt.Printf("len(page_posts): %d\n", len(page_posts))
op.CurrentPage = page_num
var interfaces []interface{}
interfaces = append(interfaces, config,
&Wrapper{IName: "boards_", Data: all_boards},
&Wrapper{IName: "sections_w", Data: all_sections},
&Wrapper{IName: "posts_w", Data: page_posts})
wrapped := &Wrapper{IName: "threadpage", Data: interfaces}
if op.CurrentPage == 0 {
current_page_file, err = os.OpenFile(path.Join(config.DocumentRoot, board_dir, "res", strconv.Itoa(op.ID)+".html"), os.O_CREATE|os.O_RDWR, 0777)
} else {
current_page_file, err = os.OpenFile(path.Join(config.DocumentRoot, board_dir, "res", strconv.Itoa(op.ID)+"p"+strconv.Itoa(op.CurrentPage)+".html"), os.O_CREATE|os.O_RDWR, 0777)
}
if err != nil {
html += err.Error() + "<br />\n"
error_log.Println(err.Error())
}
err = img_threadpage_tmpl.Execute(current_page_file, wrapped)
if err != nil {
html += "Failed building /" + board_dir + "/" + strconv.Itoa(op.ID) + ": " + err.Error() + "<br />\n"
error_log.Print(err.Error())
return
}
}
return
}
func buildFrontPage() (html string) {
initTemplates()
var front_arr []interface{}
var recent_posts_arr []interface{}
var boards_arr []interface{}
for _, board := range boards {
boards_arr = append(boards_arr, board)
}
os.Remove(path.Join(config.DocumentRoot, "index.html"))
front_file, err := os.OpenFile(path.Join(config.DocumentRoot, "index.html"), os.O_CREATE|os.O_RDWR, 0777)
/*defer func() {
defer func() {
if front_file != nil {
front_file.Close()
}
}()*/
}()
if err != nil {
return err.Error()
}
@ -227,17 +317,12 @@ func buildFrontPage(boards []BoardsTable, sections []interface{}) (html string)
recent_posts_arr = append(recent_posts_arr, recent_post)
}
page_data := &Wrapper{IName: "fronts", Data: front_arr}
board_data := &Wrapper{IName: "boards", Data: boards_arr}
section_data := &Wrapper{IName: "sections", Data: sections}
recent_posts_data := &Wrapper{IName: "recent posts", Data: recent_posts_arr}
var interfaces []interface{}
interfaces = append(interfaces, config)
interfaces = append(interfaces, page_data)
interfaces = append(interfaces, board_data)
interfaces = append(interfaces, section_data)
interfaces = append(interfaces, recent_posts_data)
interfaces = append(interfaces, config,
&Wrapper{IName: "fronts", Data: front_arr},
&Wrapper{IName: "boards", Data: all_boards},
&Wrapper{IName: "sections", Data: all_sections},
&Wrapper{IName: "recent posts", Data: recent_posts_arr})
wrapped := &Wrapper{IName: "frontpage", Data: interfaces}
err = front_page_tmpl.Execute(front_file, wrapped)
@ -251,104 +336,7 @@ func buildFrontPage(boards []BoardsTable, sections []interface{}) (html string)
return "Front page rebuilt successfully.<br />"
}
func buildThread(op_id int, board_id int) (err error) {
var posts []PostTable
var post_table_interface []interface{}
start_time := benchmarkTimer("buildThread"+string(op_id), time.Now(), true)
rows, err := db.Query("SELECT * FROM `" + config.DBprefix + "posts` WHERE `deleted_timestamp` = '" + nil_timestamp + "' AND (`parentid` = " + strconv.Itoa(op_id) + " OR `id` = " + strconv.Itoa(op_id) + ") AND `boardid` = " + strconv.Itoa(board_id))
if err != nil {
error_log.Print(err.Error())
return
}
for rows.Next() {
var post PostTable
err = rows.Scan(&post.ID, &post.BoardID, &post.ParentID, &post.Name, &post.Tripcode, &post.Email, &post.Subject, &post.Message, &post.Password, &post.Filename, &post.FilenameOriginal, &post.FileChecksum, &post.Filesize, &post.ImageW, &post.ImageH, &post.ThumbW, &post.ThumbH, &post.IP, &post.Tag, &post.Timestamp, &post.Autosage, &post.PosterAuthority, &post.DeletedTimestamp, &post.Bumped, &post.Stickied, &post.Locked, &post.Reviewed, &post.Sillytag)
if err != nil {
error_log.Print(err.Error())
return
}
posts = append(posts, post)
}
for _, post := range posts {
post_msg_before := post.Message
post.Message = parseBacklinks(post.Message, post.BoardID)
if post_msg_before != post.Message {
_, err := db.Exec("UPDATE `" + config.DBprefix + "posts` SET `message` = '" + post.Message + "' WHERE `id` = " + strconv.Itoa(post.ID))
if err != nil {
server.ServeErrorPage(writer, err.Error())
}
}
post_table_interface = append(post_table_interface, post)
}
board_arr := getBoardArr("")
sections_arr := getSectionArr("")
var board_dir string
for _, board_i := range board_arr {
board := board_i
if board.ID == board_id {
board_dir = board.Dir
break
}
}
// create a file object of the board's thread directory so we can search for and delete the given thread's sub pages to be rebuilt
threads_dir, err := os.Open(path.Join(config.DocumentRoot, board_dir, "res"))
if err != nil {
error_log.Print(err.Error())
return
}
thread_files, err := threads_dir.Readdir(-1)
if err != nil {
error_log.Print(err.Error())
return
}
for _, thread_file := range thread_files {
if strings.Index(thread_file.Name(), "p") > -1 {
os.Remove(path.Join(config.DocumentRoot, board_dir, "res", thread_file.Name()))
}
}
//var num_pages int
/*err = db.QueryRow("SELECT (SELECT COUNT(*) FROM `" + config.DBprefix + "posts` WHERE `boardid` = " + strconv.Itoa(board_id) + ") AS `count` WHERE `boardid` = " + strconv.Itoa(board_id) + " ORDER BY `" + config.DBprefix + "posts`.`id` DESC LIMIT 1").Scan(&num_posts)
if err != nil {
error_log.Print(err.Error())
return
}*/
var interfaces []interface{}
interfaces = append(interfaces, config)
interfaces = append(interfaces, post_table_interface)
var board_arr_i []interface{}
for _, b := range board_arr {
board_arr_i = append(board_arr_i, b)
}
interfaces = append(interfaces, &Wrapper{IName: "boards", Data: board_arr_i})
interfaces = append(interfaces, &Wrapper{IName: "sections", Data: sections_arr})
wrapped := &Wrapper{IName: "threadpage", Data: interfaces}
os.Remove(path.Join(config.DocumentRoot, board_dir+"/res/"+strconv.Itoa(op_id)+".html"))
thread_file, err := os.OpenFile(path.Join(config.DocumentRoot, board_dir+"/res/"+strconv.Itoa(op_id)+".html"), os.O_CREATE|os.O_RDWR, 0777)
if err != nil {
return err
}
defer func() {
if _, ok := recover().(error); ok {
error_log.Print("Failed executing template.")
}
if thread_file != nil {
thread_file.Close()
}
}()
err = img_thread_tmpl.Execute(thread_file, wrapped)
benchmarkTimer("buildThread"+string(op_id), start_time, false)
return err
}
// checks to see if the poster's tripcode/name is banned, if the IP is banned, or if the file checksum is banned
// Checks check poster's name/tripcode/file checksum (from PostTable post) for banned status
// returns true if the user is banned
func checkBannedStatus(post *PostTable, writer *http.ResponseWriter) ([]interface{}, error) {
var is_expired bool
@ -371,9 +359,7 @@ func checkBannedStatus(post *PostTable, writer *http.ResponseWriter) ([]interfac
return interfaces, err
}
} else {
is_expired = ban_entry.Expires.After(time.Now()) == false
if is_expired {
// if it is expired, send a message saying that it's expired, but still post
fmt.Println("expired")
@ -420,19 +406,6 @@ func createThumbnail(image_obj image.Image, size string) image.Image {
return image_obj
}
func getFiletype(name string) string {
filetype := strings.ToLower(name[len(name)-4:])
if filetype == ".gif" {
return "gif"
} else if filetype == ".jpg" || filetype == "jpeg" {
return "jpg"
} else if filetype == ".png" {
return "png"
} else {
return name[len(name)-3:]
}
}
func getNewFilename() string {
now := time.Now().Unix()
rand.Seed(now)
@ -525,7 +498,8 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
var email_command string
post_name := escapeString(request.FormValue("postname"))
post_name := html.EscapeString(escapeString(request.FormValue("postname")))
if strings.Index(post_name, "#") == -1 {
post.Name = post_name
} else if strings.Index(post_name, "#") == 0 {
@ -601,18 +575,19 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
server.ServeErrorPage(w, "Couldn't read file")
} else {
post.FilenameOriginal = handler.Filename
filetype := getFiletype(post.FilenameOriginal)
filetype := getFileExtension(post.FilenameOriginal)
thumb_filetype := filetype
if thumb_filetype == "gif" {
thumb_filetype = "jpg"
}
post.FilenameOriginal = escapeString(post.FilenameOriginal)
post.Filename = getNewFilename() + "." + getFiletype(post.FilenameOriginal)
board_arr := getBoardArr("`id` = " + request.FormValue("boardid"))
post.Filename = getNewFilename() + "." + getFileExtension(post.FilenameOriginal)
board_arr, _ := getBoardArr("`id` = " + request.FormValue("boardid"))
if len(board_arr) == 0 {
server.ServeErrorPage(w, "No boards have been created yet")
}
board_dir := getBoardArr("`id` = " + request.FormValue("boardid"))[0].Dir
_board_dir, _ := getBoardArr("`id` = " + request.FormValue("boardid"))
board_dir := _board_dir[0].Dir
file_path := path.Join(config.DocumentRoot, "/"+board_dir+"/src/", post.Filename)
thumb_path := path.Join(config.DocumentRoot, "/"+board_dir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "t."+thumb_filetype, -1))
catalog_thumb_path := path.Join(config.DocumentRoot, "/"+board_dir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "c."+thumb_filetype, -1))
@ -737,19 +712,11 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
}
}
// rebuild the thread page
if post.ParentID > 0 {
buildThread(post.ParentID, post.BoardID)
} else {
buildThread(post.ID, post.BoardID)
}
boards, _ := getBoardArr("")
// rebuild the board page
boards := getBoardArr("")
sections := getSectionArr("")
buildBoardPage(post.BoardID, boards, sections)
buildBoards(false, post.BoardID)
buildFrontPage(boards, sections)
buildFrontPage()
if email_command == "noko" {
if post.ParentID == 0 {

165
src/server.go Normal file → Executable file
View file

@ -1,4 +1,4 @@
package main
package main
import (
"database/sql"
@ -15,18 +15,18 @@ import (
)
var (
form url.Values
header http.Header
cookies []*http.Cookie
writer http.ResponseWriter
request http.Request
form url.Values
header http.Header
cookies []*http.Cookie
writer http.ResponseWriter
request http.Request
exit_error bool
server *GochanServer
server *GochanServer
)
type GochanServer struct{
writer http.ResponseWriter
request http.Request
type GochanServer struct {
writer http.ResponseWriter
request http.Request
namespaces map[string]func(http.ResponseWriter, *http.Request, interface{})
}
@ -37,9 +37,8 @@ func (s GochanServer) AddNamespace(base_path string, namespace_function func(htt
func (s GochanServer) getFileData(writer http.ResponseWriter, url string) ([]byte, bool) {
var file_bytes []byte
filepath := path.Join(config.DocumentRoot, url)
results,err := os.Stat(filepath)
results, err := os.Stat(filepath)
if err != nil {
fmt.Println("404 at ", filepath)
// the requested path isn't a file or directory, 404
return file_bytes, false
} else {
@ -50,12 +49,12 @@ func (s GochanServer) getFileData(writer http.ResponseWriter, url string) ([]byt
//check to see if one of the specified index pages exists
for i := 0; i < len(config.FirstPage); i++ {
newpath = path.Join(filepath,config.FirstPage[i])
_,err := os.Stat(newpath)
newpath = path.Join(filepath, config.FirstPage[i])
_, err := os.Stat(newpath)
if err == nil {
// serve the index page
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
file_bytes,err = ioutil.ReadFile(newpath)
file_bytes, err = ioutil.ReadFile(newpath)
return file_bytes, true
found_index = true
break
@ -71,27 +70,26 @@ func (s GochanServer) getFileData(writer http.ResponseWriter, url string) ([]byt
file_bytes, err = ioutil.ReadFile(filepath)
extension := getFileExtension(url)
switch {
case extension == "png":
writer.Header().Add("Content-Type", "image/png")
writer.Header().Add("Cache-Control", "max-age=86400")
case extension == "gif":
writer.Header().Add("Content-Type", "image/gif")
writer.Header().Add("Cache-Control", "max-age=86400")
case extension == "jpg":
writer.Header().Add("Content-Type", "image/jpeg")
writer.Header().Add("Cache-Control", "max-age=86400")
case extension == "css":
writer.Header().Add("Content-Type", "text/css")
writer.Header().Add("Cache-Control", "max-age=43200")
case extension == "js":
writer.Header().Add("Content-Type", "text/javascript")
writer.Header().Add("Cache-Control", "max-age=43200")
case extension == "png":
writer.Header().Add("Content-Type", "image/png")
writer.Header().Add("Cache-Control", "max-age=86400")
case extension == "gif":
writer.Header().Add("Content-Type", "image/gif")
writer.Header().Add("Cache-Control", "max-age=86400")
case extension == "jpg":
writer.Header().Add("Content-Type", "image/jpeg")
writer.Header().Add("Cache-Control", "max-age=86400")
case extension == "css":
writer.Header().Add("Content-Type", "text/css")
writer.Header().Add("Cache-Control", "max-age=43200")
case extension == "js":
writer.Header().Add("Content-Type", "text/javascript")
writer.Header().Add("Cache-Control", "max-age=43200")
}
if extension == "html" || extension == "htm" {
if extension == "html" || extension == "htm" {
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
}
//http.ServeFile(writer, request, filepath)
access_log.Print("Success: 200 from " + request.RemoteAddr + " @ " + request.RequestURI)
access_log.Print("Success: 200 from " + getRealIP(&request) + " @ " + request.RequestURI)
return file_bytes, true
}
}
@ -99,7 +97,7 @@ func (s GochanServer) getFileData(writer http.ResponseWriter, url string) ([]byt
}
func (s GochanServer) Redirect(location string) {
http.Redirect(writer,&request,location,http.StatusFound)
http.Redirect(writer, &request, location, http.StatusFound)
}
func (s GochanServer) serve404(writer http.ResponseWriter, request *http.Request) {
@ -109,13 +107,13 @@ func (s GochanServer) serve404(writer http.ResponseWriter, request *http.Request
} else {
writer.Write(error_page)
}
error_log.Print("Error: 404 Not Found from " + request.RemoteAddr + " @ " + request.RequestURI)
error_log.Print("Error: 404 Not Found from " + getRealIP(request) + " @ " + request.RequestURI)
}
func (s GochanServer) ServeErrorPage(writer http.ResponseWriter, err string) {
error_page_bytes,_ := ioutil.ReadFile("templates/error.html")
error_page_bytes, _ := ioutil.ReadFile("templates/error.html")
error_page := string(error_page_bytes)
error_page = strings.Replace(error_page,"{ERRORTEXT}", err,-1)
error_page = strings.Replace(error_page, "{ERRORTEXT}", err, -1)
writer.Write([]byte(error_page))
exit_error = true
}
@ -123,12 +121,12 @@ func (s GochanServer) ServeErrorPage(writer http.ResponseWriter, err string) {
func (s GochanServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
for name, namespace_function := range s.namespaces {
//if len(request.URL)
if request.URL.Path == "/" + name {
if request.URL.Path == "/"+name {
namespace_function(writer, request, nil)
return
}
}
fb,found := s.getFileData(writer, request.URL.Path)
fb, found := s.getFileData(writer, request.URL.Path)
writer.Header().Add("Cache-Control", "max-age=86400")
if !found {
s.serve404(writer, request)
@ -138,9 +136,9 @@ func (s GochanServer) ServeHTTP(writer http.ResponseWriter, request *http.Reques
}
func initServer() {
listener,err := net.Listen("tcp", config.Domain+":"+strconv.Itoa(config.Port))
if(err != nil) {
fmt.Printf("Failed listening on "+config.Domain+":%d, see log for details",config.Port)
listener, err := net.Listen("tcp", config.Domain+":"+strconv.Itoa(config.Port))
if err != nil {
fmt.Printf("Failed listening on "+config.Domain+":%d, see log for details", config.Port)
error_log.Fatal(err.Error())
}
server = new(GochanServer)
@ -155,22 +153,23 @@ func initServer() {
server.AddNamespace("manage", callManageFunction)
server.AddNamespace("post", makePost)
server.AddNamespace("util", utilHandler)
// eventually plugins will be able to register new namespaces. Or they will be restricted to something like /plugin
if config.UseFastCGI {
fcgi.Serve(listener,server)
fcgi.Serve(listener, server)
} else {
http.Serve(listener, server)
}
}
func getRealIP(request *http.Request) (ip string) {
func getRealIP(r *http.Request) (ip string) {
// HTTP_CF_CONNECTING_IP > X-Forwarded-For > RemoteAddr
if request.Header.Get("HTTP_CF_CONNECTING_IP") != "" {
ip = request.Header.Get("HTTP_CF_CONNECTING_IP")
if r.Header.Get("HTTP_CF_CONNECTING_IP") != "" {
ip = r.Header.Get("HTTP_CF_CONNECTING_IP")
} else {
if request.Header.Get("X-Forwarded-For") != "" {
ip = request.Header.Get("X-Forwarded-For")
if r.Header.Get("X-Forwarded-For") != "" {
ip = r.Header.Get("X-Forwarded-For")
} else {
ip = request.RemoteAddr
ip = r.RemoteAddr
}
}
return
@ -178,7 +177,7 @@ func getRealIP(request *http.Request) (ip string) {
func validReferrer(request http.Request) (valid bool) {
if request.Referer() == "" || request.Referer()[7:len(config.SiteDomain)+7] != config.SiteDomain {
// if request.Referer() == "" || request.Referer()[7:len(config.Domain)+7] != config.Domain {
// if request.Referer() == "" || request.Referer()[7:len(config.Domain)+7] != config.Domain {
valid = false
} else {
valid = true
@ -186,19 +185,20 @@ func validReferrer(request http.Request) (valid bool) {
return
}
// register /util handler
func utilHandler(writer http.ResponseWriter, request *http.Request, data interface{}) {
writer.Header().Add("Content-Type", "text/css")
action := request.FormValue("action")
board := request.FormValue("board")
var err error
if action == "" && request.PostFormValue("delete_btn") != "Delete" && request.PostFormValue("report_btn") != "Report" {
http.Redirect(writer,request,path.Join(config.SiteWebfolder,"/"),http.StatusFound)
http.Redirect(writer, request, path.Join(config.SiteWebfolder, "/"), http.StatusFound)
return
}
var posts_arr []string
for key,_ := range request.PostForm {
if strings.Index(key,"check") == 0 {
posts_arr = append(posts_arr,key[5:])
for key, _ := range request.PostForm {
if strings.Index(key, "check") == 0 {
posts_arr = append(posts_arr, key[5:])
}
}
if request.PostFormValue("delete_btn") == "Delete" {
@ -207,12 +207,12 @@ func utilHandler(writer http.ResponseWriter, request *http.Request, data interfa
password := md5_sum(request.FormValue("password"))
rank := getStaffRank()
if request.FormValue("password") == "" && rank == 0 {
if request.FormValue("password") == "" && rank == 0 {
server.ServeErrorPage(writer, "Password required for post deletion")
return
}
for _,post := range posts_arr {
for _, post := range posts_arr {
var parent_id int
var filename string
var filetype string
@ -220,21 +220,21 @@ func utilHandler(writer http.ResponseWriter, request *http.Request, data interfa
var board_id int
//post_int,err := strconv.Atoi(post)
err = db.QueryRow("SELECT `parentid`,`filename`,`password` FROM `" + config.DBprefix + "posts` WHERE `id` = " + post + " AND `deleted_timestamp` = '" + nil_timestamp + "'").Scan(&parent_id, &filename, &password_checksum)
err = db.QueryRow("SELECT `parentid`,`filename`,`password` FROM `"+config.DBprefix+"posts` WHERE `id` = "+post+" AND `deleted_timestamp` = '"+nil_timestamp+"'").Scan(&parent_id, &filename, &password_checksum)
if err == sql.ErrNoRows {
//the post has already been deleted
writer.Header().Add("refresh", "3;url=" + request.Referer())
fmt.Fprintf(writer, "%s has already been deleted or is a post in a deleted thread.\n<br />",post)
writer.Header().Add("refresh", "3;url="+request.Referer())
fmt.Fprintf(writer, "%s has already been deleted or is a post in a deleted thread.\n<br />", post)
continue
}
if err != nil {
server.ServeErrorPage(writer,err.Error())
server.ServeErrorPage(writer, err.Error())
return
}
err = db.QueryRow("SELECT `id` FROM `" + config.DBprefix + "boards` WHERE `dir` = '" + board + "'").Scan(&board_id)
if err != nil {
server.ServeErrorPage(writer,err.Error())
server.ServeErrorPage(writer, err.Error())
return
}
@ -245,62 +245,63 @@ func utilHandler(writer http.ResponseWriter, request *http.Request, data interfa
if file_only {
if filename != "" {
filetype = filename[strings.Index(filename,".")+1:]
filename = filename[:strings.Index(filename,".")]
err := os.Remove(path.Join(config.DocumentRoot,board,"/src/"+filename+"."+filetype))
filetype = filename[strings.Index(filename, ".")+1:]
filename = filename[:strings.Index(filename, ".")]
err := os.Remove(path.Join(config.DocumentRoot, board, "/src/"+filename+"."+filetype))
if err != nil {
server.ServeErrorPage(writer,err.Error())
server.ServeErrorPage(writer, err.Error())
return
}
err = os.Remove(path.Join(config.DocumentRoot,board,"/thumb/"+filename+"t."+filetype))
err = os.Remove(path.Join(config.DocumentRoot, board, "/thumb/"+filename+"t."+filetype))
if err != nil {
server.ServeErrorPage(writer,err.Error())
server.ServeErrorPage(writer, err.Error())
return
}
_,err = db.Exec("UPDATE `" + config.DBprefix + "posts` SET `filename` = 'deleted' WHERE `id` = " + post)
_, err = db.Exec("UPDATE `" + config.DBprefix + "posts` SET `filename` = 'deleted' WHERE `id` = " + post)
if err != nil {
server.ServeErrorPage(writer, err.Error())
return
}
}
writer.Header().Add("refresh", "3;url=" + request.Referer())
fmt.Fprintf(writer, "Attached image from %s deleted successfully<br />\n<meta http-equiv=\"refresh\" content=\"1;url=" + config.DocumentRoot + "/" + board + "/\">", post)
writer.Header().Add("refresh", "3;url="+request.Referer())
fmt.Fprintf(writer, "Attached image from %s deleted successfully<br />\n<meta http-equiv=\"refresh\" content=\"1;url="+config.DocumentRoot+"/"+board+"/\">", post)
} else {
// delete the post
_,err = db.Exec("UPDATE `" + config.DBprefix + "posts` SET `deleted_timestamp` = '" + getSQLDateTime() + "' WHERE `id` = " + post)
_, err = db.Exec("UPDATE `" + config.DBprefix + "posts` SET `deleted_timestamp` = '" + getSQLDateTime() + "' WHERE `id` = " + post)
if parent_id == 0 {
err = os.Remove(path.Join(config.DocumentRoot, board, "/res/" + post + ".html"))
err = os.Remove(path.Join(config.DocumentRoot, board, "/res/"+post+".html"))
} else {
err = buildThread(parent_id,board_id)
_board, _ := getBoardArr("`id` = " + strconv.Itoa(board_id))
buildBoardPages(&_board[0])
}
// if the deleted post is actually a thread, delete its posts
_,_ = db.Exec("UPDATE `" + config.DBprefix + "posts` SET `deleted_timestamp` = '" + getSQLDateTime() + "' WHERE `parentid` = " + post)
_, _ = db.Exec("UPDATE `" + config.DBprefix + "posts` SET `deleted_timestamp` = '" + getSQLDateTime() + "' WHERE `parentid` = " + post)
if err != nil {
server.ServeErrorPage(writer,err.Error())
server.ServeErrorPage(writer, err.Error())
return
}
// delete the
// delete the
var deleted_filename string
err = db.QueryRow("SELECT `filename` FROM `" + config.DBprefix + "posts` WHERE `id` = " + post + " AND `filename` != ''").Scan(&deleted_filename)
if err == nil {
os.Remove(path.Join(config.DocumentRoot, board, "/src/", deleted_filename))
os.Remove(path.Join(config.DocumentRoot, board, "/thumb/", strings.Replace(deleted_filename, ".", "t.",-1)))
os.Remove(path.Join(config.DocumentRoot, board, "/thumb/", strings.Replace(deleted_filename, ".", "c.",-1)))
os.Remove(path.Join(config.DocumentRoot, board, "/thumb/", strings.Replace(deleted_filename, ".", "t.", -1)))
os.Remove(path.Join(config.DocumentRoot, board, "/thumb/", strings.Replace(deleted_filename, ".", "c.", -1)))
}
err = db.QueryRow("SELECT `filename` FROM `" + config.DBprefix + "posts` WHERE `parentid` = " + post + " AND `filename` != ''").Scan(&deleted_filename)
if err == nil {
os.Remove(path.Join(config.DocumentRoot, board, "/src/", deleted_filename))
os.Remove(path.Join(config.DocumentRoot, board, "/thumb/", strings.Replace(deleted_filename, ".", "t.",-1)))
os.Remove(path.Join(config.DocumentRoot, board, "/thumb/", strings.Replace(deleted_filename, ".", "c.",-1)))
os.Remove(path.Join(config.DocumentRoot, board, "/thumb/", strings.Replace(deleted_filename, ".", "t.", -1)))
os.Remove(path.Join(config.DocumentRoot, board, "/thumb/", strings.Replace(deleted_filename, ".", "c.", -1)))
}
buildBoardPage(board_id, getBoardArr(""), getSectionArr(""))
buildBoards(false, board_id)
writer.Header().Add("refresh", "3;url=" + request.Referer())
writer.Header().Add("refresh", "3;url="+request.Referer())
fmt.Fprintf(writer, "%s deleted successfully\n<br />", post)
}
}

0
src/sql.go Normal file → Executable file
View file

182
src/template.go Normal file → Executable file
View file

@ -13,20 +13,16 @@ import (
"time"
)
type FooterData struct {
Version float32
Version float32
GeneratedTime float32
}
var funcMap = template.FuncMap{
"add": func(a,b int) int {
"add": func(a, b int) int {
return a + b
},
"subtract": func(a,b int) int {
"subtract": func(a, b int) int {
return a - b
},
"len": func(arr []interface{}) int {
@ -47,6 +43,9 @@ var funcMap = template.FuncMap{
"lt": func(a int, b int) bool {
return a < b
},
"makeLoop": func(n int) []struct{} {
return make([]struct{}, n)
},
"stringAppend": func(a, b string) string {
return a + b
},
@ -58,11 +57,11 @@ var funcMap = template.FuncMap{
},
"truncateMessage": func(msg string, limit int, max_lines int) string {
var truncated bool
split := strings.SplitN(msg,"<br />",-1)
split := strings.SplitN(msg, "<br />", -1)
if len(split) > max_lines {
split = split[:max_lines]
msg = strings.Join(split,"<br />")
msg = strings.Join(split, "<br />")
truncated = true
}
@ -93,11 +92,14 @@ var funcMap = template.FuncMap{
"isStyleNotDefault_img": func(style string) bool {
return style != config.DefaultStyle_img
},
"getInterface":func(in []interface{}, index int) interface{} {
"getElement": func(in []interface{}, element int) interface{} {
return in[element]
},
"getInterface": func(in []interface{}, index int) interface{} {
var nope interface{}
if len(in) == 0 {
return nope
} else if len(in) < index + 1 {
} else if len(in) < index+1 {
return nope
}
return in[index]
@ -118,22 +120,22 @@ var funcMap = template.FuncMap{
if name == "" {
return ""
}
if name[len(name) - 3:] == "gif" || name[len(name) - 3:] == "gif" {
name = name[:len(name) - 3] + "jpg"
if name[len(name)-3:] == "gif" || name[len(name)-3:] == "gif" {
name = name[:len(name)-3] + "jpg"
}
ext_begin := strings.LastIndex(name, ".")
new_name := name[:ext_begin] + "t." + getFiletype(name)
new_name := name[:ext_begin] + "t." + getFileExtension(name)
fmt.Println(new_name)
return new_name
},
"formatFilesize": func(size_int int) string {
size := float32(size_int)
if(size < 1000) {
if size < 1000 {
return fmt.Sprintf("%fB", size)
} else if(size <= 100000) {
} else if size <= 100000 {
//size = size * 0.2
return fmt.Sprintf("%0.1f KB", size/1024)
} else if(size <= 100000000) {
} else if size <= 100000000 {
//size = size * 0.2
return fmt.Sprintf("%0.2f MB", size/1024/1024)
}
@ -145,7 +147,7 @@ var funcMap = template.FuncMap{
filetype = "jpg"
}
index := strings.LastIndex(img, ".")
return img[0:index]+"t."+filetype
return img[0:index] + "t." + filetype
},
}
@ -153,142 +155,142 @@ var (
footer_data = FooterData{version, float32(0)}
banpage_tmpl_str string
banpage_tmpl *template.Template
banpage_tmpl *template.Template
global_footer_tmpl_str string
global_footer_tmpl *template.Template
global_footer_tmpl *template.Template
global_header_tmpl_str string
global_header_tmpl *template.Template
global_header_tmpl *template.Template
img_boardpage_tmpl_str string
img_boardpage_tmpl *template.Template
img_thread_tmpl_str string
img_thread_tmpl *template.Template
img_boardpage_tmpl *template.Template
img_threadpage_tmpl_str string
img_threadpage_tmpl *template.Template
manage_header_tmpl_str string
manage_header_tmpl *template.Template
manage_header_tmpl *template.Template
front_page_tmpl_str string
front_page_tmpl *template.Template
front_page_tmpl *template.Template
template_buffer bytes.Buffer
starting_time int
starting_time int
)
func initTemplates() {
banpage_tmpl_bytes,tmpl_err := ioutil.ReadFile(config.TemplateDir+"/banpage.html")
resetBoardSectionArrays()
banpage_tmpl_bytes, tmpl_err := ioutil.ReadFile(config.TemplateDir + "/banpage.html")
if tmpl_err != nil {
fmt.Println("Failed loading template \""+config.TemplateDir+"/banpage.html\": " + tmpl_err.Error())
fmt.Println("Failed loading template \"" + config.TemplateDir + "/banpage.html\": " + tmpl_err.Error())
os.Exit(2)
}
banpage_tmpl_str = "{{$config := getInterface .Data 0}}" +
"{{$ban := getInterface .Data 1}}" +
string(banpage_tmpl_bytes)
banpage_tmpl,tmpl_err = template.New("banpage_tmpl").Funcs(funcMap).Parse(string(banpage_tmpl_str))
"{{$ban := getInterface .Data 1}}" +
string(banpage_tmpl_bytes)
banpage_tmpl, tmpl_err = template.New("banpage_tmpl").Funcs(funcMap).Parse(string(banpage_tmpl_str))
if tmpl_err != nil {
fmt.Println("Failed loading template \""+config.TemplateDir+"/banpage.html\": " + tmpl_err.Error())
fmt.Println("Failed loading template \"" + config.TemplateDir + "/banpage.html\": " + tmpl_err.Error())
os.Exit(2)
}
global_footer_tmpl_bytes,tmpl_err := ioutil.ReadFile(config.TemplateDir+"/global_footer.html")
global_footer_tmpl_bytes, tmpl_err := ioutil.ReadFile(config.TemplateDir + "/global_footer.html")
if tmpl_err != nil {
fmt.Println("Failed loading template \""+config.TemplateDir+"/global_footer.html\": " + tmpl_err.Error())
fmt.Println("Failed loading template \"" + config.TemplateDir + "/global_footer.html\": " + tmpl_err.Error())
os.Exit(2)
}
global_footer_tmpl_str = string(global_footer_tmpl_bytes)
global_footer_tmpl,tmpl_err = template.New("global_footer_tmpl").Funcs(funcMap).Parse(string(global_footer_tmpl_str))
global_footer_tmpl, tmpl_err = template.New("global_footer_tmpl").Funcs(funcMap).Parse(string(global_footer_tmpl_str))
if tmpl_err != nil {
fmt.Println("Failed loading template \""+config.TemplateDir+"/global_footer.html\": " + tmpl_err.Error())
os.Exit(2)
}
global_header_tmpl_bytes,tmpl_err := ioutil.ReadFile(config.TemplateDir+"/global_header.html")
if tmpl_err != nil {
fmt.Println("Failed loading template \""+config.TemplateDir+"/global_header.html\": " + tmpl_err.Error())
os.Exit(2)
}
global_header_tmpl_str = string(global_header_tmpl_bytes)
global_header_tmpl,tmpl_err = template.New("global_header_tmpl").Funcs(funcMap).Parse(string(global_header_tmpl_str))
if tmpl_err != nil {
fmt.Println("Failed loading template \""+config.TemplateDir+"/global_header.html\": " + tmpl_err.Error())
fmt.Println("Failed loading template \"" + config.TemplateDir + "/global_footer.html\": " + tmpl_err.Error())
os.Exit(2)
}
img_boardpage_tmpl_bytes,_ := ioutil.ReadFile(path.Join(config.TemplateDir,"img_boardpage.html"))
global_header_tmpl_bytes, tmpl_err := ioutil.ReadFile(config.TemplateDir + "/global_header.html")
if tmpl_err != nil {
fmt.Println("Failed loading template \"" + config.TemplateDir + "/global_header.html\": " + tmpl_err.Error())
os.Exit(2)
}
global_header_tmpl_str = string(global_header_tmpl_bytes)
global_header_tmpl, tmpl_err = template.New("global_header_tmpl").Funcs(funcMap).Parse(string(global_header_tmpl_str))
if tmpl_err != nil {
fmt.Println("Failed loading template \"" + config.TemplateDir + "/global_header.html\": " + tmpl_err.Error())
os.Exit(2)
}
img_boardpage_tmpl_bytes, _ := ioutil.ReadFile(path.Join(config.TemplateDir, "img_boardpage.html"))
if tmpl_err != nil {
fmt.Println("Failed loading template \"" + config.TemplateDir + "/img_boardpage.html\": " + tmpl_err.Error())
os.Exit(2)
}
img_boardpage_tmpl_str = "{{$config := getInterface .Data 0}}" +
"{{$board_arr := getInterface .Data 1}}" +
"{{$section_arr := getInterface .Data 2}}" +
"{{$thread_arr := getInterface .Data 3}}" +
"{{$board_info := getInterface .Data 4}}" +
"{{$board := getInterface $board_info.Data 0}}" +
string(img_boardpage_tmpl_bytes)
img_boardpage_tmpl,tmpl_err = template.New("img_boardpage_tmpl").Funcs(funcMap).Parse(img_boardpage_tmpl_str)
"{{$board_arr := (getInterface .Data 1).Data}}" +
"{{$section_arr := (getInterface .Data 2).Data}}" +
"{{$thread_arr := (getInterface .Data 3).Data}}" +
"{{$board_info := (getInterface .Data 4).Data}}" +
"{{$board := getInterface $board_info 0}}" +
string(img_boardpage_tmpl_bytes)
img_boardpage_tmpl, tmpl_err = template.New("img_boardpage_tmpl").Funcs(funcMap).Parse(img_boardpage_tmpl_str)
if tmpl_err != nil {
fmt.Println("Failed loading template \""+config.TemplateDir+"/img_boardpage.html: \"" + tmpl_err.Error())
fmt.Println("Failed loading template \"" + config.TemplateDir + "/img_boardpage.html: \"" + tmpl_err.Error())
os.Exit(2)
}
img_thread_tmpl_bytes,_ := ioutil.ReadFile(path.Join(config.TemplateDir,"img_thread.html"))
img_threadpage_tmpl_bytes, _ := ioutil.ReadFile(path.Join(config.TemplateDir, "img_threadpage.html"))
if tmpl_err != nil {
fmt.Println("Failed loading template \""+config.TemplateDir+"/img_thread.html\": " + tmpl_err.Error())
fmt.Println("Failed loading template \"" + config.TemplateDir + "/img_threadpage.html\": " + tmpl_err.Error())
os.Exit(2)
}
img_thread_tmpl_str = "{{$config := getInterface .Data 0}}" +
"{{$post_arr := getInterface .Data 1}}" +
"{{$board_arr := getInterface .Data 2}}" +
"{{$section_arr := getInterface .Data 3}}" +
"{{$op := getInterface $post_arr 0}}" +
"{{$boardid := subtract $op.BoardID 1}}" +
"{{$board := getInterface $board_arr.Data $boardid}}" +
string(img_thread_tmpl_bytes)
img_thread_tmpl,tmpl_err = template.New("img_thread_tmpl").Funcs(funcMap).Parse(img_thread_tmpl_str)
img_threadpage_tmpl_str = "{{$config := getInterface .Data 0}}" +
"{{$board_arr := (getInterface .Data 1).Data}}" +
"{{$section_arr := (getInterface .Data 2).Data}}" +
"{{$post_arr := (getInterface .Data 3).Data}}" +
"{{$op := getElement $post_arr 0}}" +
"{{$board := getElement $board_arr (subtract $op.BoardID 1)}}" +
string(img_threadpage_tmpl_bytes)
img_threadpage_tmpl, tmpl_err = template.New("img_threadpage_tmpl").Funcs(funcMap).Parse(img_threadpage_tmpl_str)
if tmpl_err != nil {
fmt.Println("Failed loading template \""+config.TemplateDir+"/img_thread.html: \"" + tmpl_err.Error())
fmt.Println("Failed loading template \"" + config.TemplateDir + "/img_threadpage.html: \"" + tmpl_err.Error())
os.Exit(2)
}
manage_header_tmpl_bytes,err := ioutil.ReadFile(config.TemplateDir+"/manage_header.html")
manage_header_tmpl_bytes, err := ioutil.ReadFile(config.TemplateDir + "/manage_header.html")
if err != nil {
fmt.Println(err.Error())
}
manage_header_tmpl_str = string(manage_header_tmpl_bytes)
manage_header_tmpl,tmpl_err = template.New("manage_header_tmpl").Funcs(funcMap).Parse(manage_header_tmpl_str)
manage_header_tmpl, tmpl_err = template.New("manage_header_tmpl").Funcs(funcMap).Parse(manage_header_tmpl_str)
if tmpl_err != nil {
fmt.Println("Failed loading template \""+config.TemplateDir+"/manage_header.html\": "+tmpl_err.Error())
fmt.Println("Failed loading template \"" + config.TemplateDir + "/manage_header.html\": " + tmpl_err.Error())
os.Exit(2)
}
front_page_tmpl_bytes,err := ioutil.ReadFile(config.TemplateDir+"/front.html")
front_page_tmpl_bytes, err := ioutil.ReadFile(config.TemplateDir + "/front.html")
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
front_page_tmpl_str = "{{$config := getInterface .Data 0}}" +
"{{$page_arr := getInterface .Data 1}}" +
"{{$board_arr := getInterface .Data 2}}" +
"{{$section_arr := getInterface .Data 3}}" +
"{{$recent_posts_arr := getInterface .Data 4}}" +
string(front_page_tmpl_bytes)
front_page_tmpl,tmpl_err = template.New("front_page_tmpl").Funcs(funcMap).Parse(front_page_tmpl_str)
front_page_tmpl_str = "{{$config := getInterface .Data 0}}\n" +
"{{$page_arr := getInterface .Data 1}}\n" +
"{{$board_arr := getInterface .Data 2}}\n" +
"{{$section_arr := getInterface .Data 3}}\n" +
"{{$recent_posts_arr := getInterface .Data 4}}\n" +
string(front_page_tmpl_bytes)
front_page_tmpl, tmpl_err = template.New("front_page_tmpl").Funcs(funcMap).Parse(front_page_tmpl_str)
if tmpl_err != nil {
fmt.Println("Failed loading template \""+config.TemplateDir+"/front.html\": "+tmpl_err.Error())
fmt.Println("Failed loading template \"" + config.TemplateDir + "/front.html\": " + tmpl_err.Error())
os.Exit(2)
}
}
func getTemplateAsString(templ template.Template) (string,error) {
func getTemplateAsString(templ template.Template) (string, error) {
var buf bytes.Buffer
err := templ.Execute(&buf,config)
err := templ.Execute(&buf, config)
if err == nil {
return buf.String(),nil
return buf.String(), nil
}
return "",err
return "", err
}
func getStyleLinks(w http.ResponseWriter, stylesheet string) {
@ -297,7 +299,7 @@ func getStyleLinks(w http.ResponseWriter, stylesheet string) {
styles_map[i] = config.Styles_img[i]
}
err := manage_header_tmpl.Execute(w,config)
err := manage_header_tmpl.Execute(w, config)
if err != nil {
fmt.Println(err.Error())
os.Exit(2)

File diff suppressed because it is too large Load diff

View file

@ -5,11 +5,14 @@ import (
"crypto/md5"
"crypto/sha1"
"fmt"
//"golang.org/x/crypto/bcrypt"
"io"
"io/ioutil"
"math/rand"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
@ -69,23 +72,23 @@ func byteByByteReplace(input, from, to string) string {
return input
}
func walkfolder(path string, f os.FileInfo, err error) error {
func deleteMatchingFiles(root, match string) (int, error) {
files_deleted := 0
files, err := ioutil.ReadDir(root)
if err != nil {
return err
return 0, err
}
if !f.IsDir() {
return os.Remove(path)
} else {
return nil
for _, f := range files {
match, _ := regexp.MatchString(match, f.Name())
if match {
os.Remove(filepath.Join(root, f.Name()))
files_deleted++
}
}
return files_deleted, err
}
func deleteFolderContents(root string) error {
err := filepath.Walk(root, walkfolder)
return err
}
func getBoardArr(where string) (boards []BoardsTable) {
func getBoardArr(where string) (boards []BoardsTable, err error) {
if where == "" {
where = "1"
}
@ -136,8 +139,11 @@ func getBoardArr(where string) (boards []BoardsTable) {
return
}
func getPostArr(sql string) (posts []interface{}, err error) {
rows, err := db.Query(sql)
func getPostArr(where string) (posts []interface{}, err error) {
if where == "" {
where = "1"
}
rows, err := db.Query("SELECT * FROM `" + config.DBprefix + "posts` WHERE " + where)
if err != nil {
error_log.Print(err.Error())
return
@ -151,10 +157,10 @@ func getPostArr(sql string) (posts []interface{}, err error) {
}
posts = append(posts, post)
}
return posts, err
return
}
func getSectionArr(where string) (sections []interface{}) {
func getSectionArr(where string) (sections []interface{}, err error) {
if where == "" {
where = "1"
}
@ -231,6 +237,47 @@ func humanReadableTime(t time.Time) string {
return t.Format(config.DateTimeFormat)
}
func paginate(interface_length int, interf []interface{}) [][]interface{} {
// interface_length = the max number of interfaces per super-interface
// (for example, threads per page)
// interf = the raw interface to be split up
// paginated_interfaces = the finished interface array
// num_arrays = the current number of arrays (before remainder overflow)
// interfaces_remaining = if greater than 0, these are the remaining interfaces
// that will be added to the super-interface
var paginated_interfaces [][]interface{}
num_arrays := len(interf) / interface_length
interfaces_remaining := len(interf) % interface_length
paginated_interfaces = append(paginated_interfaces, interf)
current_interface := 0
for l := 0; l < num_arrays; l++ {
paginated_interfaces = append(paginated_interfaces,
interf[current_interface:current_interface+interface_length])
current_interface += interface_length
}
if interfaces_remaining > 0 {
paginated_interfaces = append(paginated_interfaces, interf[len(interf)-interfaces_remaining:])
}
fmt.Println(len(paginated_interfaces[0]))
return paginated_interfaces
}
func resetBoardSectionArrays() {
// run when the board list needs to be changed (board/section is added, deleted, etc)
all_boards = nil
all_sections = nil
all_boards_a, _ := getBoardArr("")
for _, b := range all_boards_a {
all_boards = append(all_boards, b)
}
all_sections_a, _ := getSectionArr("")
for _, b := range all_sections_a {
all_boards = append(all_sections, b)
}
}
func searchStrings(item string, arr []string, permissive bool) int {
var length = len(arr)
for i := 0; i < length; i++ {

4
templates/front.html Normal file → Executable file
View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>{{$config.SiteName}}</title>
<script type="text/javascript" src="/javascript/jquery/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="/javascript/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="/javascript/msgpack.js"></script>
<script type="text/javascript">
var styles = [{{range $ii, $style := $config.Styles_img}}{{if gt $ii 0}}, {{end}}"{{$style}}"{{end}}];
@ -124,7 +124,7 @@
</div>
<div id="footer">
<a href="{{$config.SiteWebfolder}}">Home</a> | <a href="{{$config.SiteWebfolder}}#boards">Boards</a> | <a href="{{$config.SiteWebfolder}}#rules">Rules</a> | <a href="{{$config.SiteWebfolder}}#faq">FAQ</a><br />
Powered by Gochan {{$config.Version}}<br />
Powered by <a href="http://github.com/eggbertx/gochan">Gochan {{$config.Version}}</a><br />
</div>
</body>
</html>

50
templates/img_boardpage.html Normal file → Executable file
View file

@ -3,9 +3,7 @@
<head>
<meta charset="UTF-8">
<title>{{$board.Title}}</title>
<script type="text/javascript" src="/javascript/jquery/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="/javascript/msgpack.js"></script>
<script type="text/javascript" src="/javascript/jquery-1.10.2.min.js"></script>
<script type="text/javascript">
var styles = [{{range $ii, $style := $config.Styles_img}}{{if gt $ii 0}}, {{end}}"{{$style}}"{{end}}];
var webroot = "{{$config.SiteWebfolder}}";
@ -19,8 +17,8 @@
</head>
<body>
<div id="topbar">
{{range $i, $board := $board_arr.Data}}
<a href="/{{$board.Dir}}/" class="topbar-item">/{{$board.Dir}}/</a>
{{range $i, $boardlink := $board_arr}}
<a href="/{{$boardlink.Dir}}/" class="topbar-item">/{{$boardlink.Dir}}/</a>
{{end}}
</div>
<div id="top-pane">
@ -37,12 +35,12 @@
<input type="hidden" name="threadid" value="0" />
<input type="hidden" name="boardid" value="{{$board.ID}}" />
<table id="postbox-static">
<tr><td class="postblock">Name</td><td><input type="text" id="postname" name="postname" maxlength="75" size="28" {{/* value="Name" onFocus="if(this.value=='Name') {this,value= ''}" onBlur="if(this.value == '') {this.value = 'Name'}"*/}}/></td></tr>
<tr><td class="postblock">Email</td><td><input type="text" id="postemail" name="postemail" maxlength="75" size="28" /></td></tr>
<tr><td class="postblock">Subject</td><td><input type="text" name="postsubject" maxlength="75" size="35" /><input type="submit" value="Post"/></td></tr>
<tr><td class="postblock">Message</td><td><textarea rows="4" cols="48" name="postmsg"></textarea></td></tr>
<tr><td class="postblock">File</td><td><input name="imagefile" type="file"><input type="checkbox" id="spoiler" name="spoiler"/><label for="spoiler">Spoiler</label></td></tr>
<tr><td class="postblock">Password</td><td><input type="password" id="postpassword" name="postpassword" size="14" /> (for post/file deletion)</td></tr>
<tr><th class="postblock">Name</th><td><input type="text" id="postname" name="postname" maxlength="75" size="28" {{/* value="Name" onFocus="if(this.value=='Name') {this,value= ''}" onBlur="if(this.value == '') {this.value = 'Name'}"*/}}/></td></tr>
<tr><th class="postblock">Email</th><td><input type="text" id="postemail" name="postemail" maxlength="75" size="28" /></td></tr>
<tr><th class="postblock">Subject</th><td><input type="text" name="postsubject" maxlength="75" size="35" /><input type="submit" value="Post"/></td></tr>
<tr><th class="postblock">Message</th><td><textarea rows="4" cols="48" name="postmsg"></textarea></td></tr>
<tr><th class="postblock">File</th><td><input name="imagefile" type="file"><input type="checkbox" id="spoiler" name="spoiler"/><label for="spoiler">Spoiler</label></td></tr>
<tr><th class="postblock">Password</th><td><input type="password" id="postpassword" name="postpassword" size="14" /> (for post/file deletion)</td></tr>
</table>
</form>
@ -50,7 +48,7 @@
<hr />
<div id="content">
<form action="/util" method="POST" id="main-form">
{{range $t, $thread := $thread_arr.Data}}
{{range $t, $thread := $thread_arr}}
{{$op := $thread.OP}}
<div class="thread">
<div class="op-post" id="op{{$op.ID}}">
@ -93,13 +91,6 @@
</div>
<hr />
{{end}}
<div id="left-bottom-content">
<span id="boardmenu-bottom">
[{{range $i, $board := $board_arr.Data}} {{if gt $i 0}}/{{end}} <a href="/{{$board.Dir}}/">{{$board.Dir}}</a> {{end}}]
</span>
</div>
<div id="right-bottom-content">
<div id="report-delbox">
<input type="hidden" name="board" value="{{$board.Dir}}" />
@ -108,10 +99,29 @@
</div>
</div>
</form>
<div id="left-bottom-content">
<table id="pages">
<tr><td>{{if gt $board.CurrentPage 1}}
<form method="GET" action="{{$config.SiteWebfolder}}{{$board.Dir}}/{{subtract $board.CurrentPage 1}}.html">
<input type="submit" value="Previous" />
</form>
{{else}}Previous{{end}}</td>
<td>[<a href="{{$config.SiteWebfolder}}{{$board.Dir}}/">Index</a>]{{range $i,$_ := makeLoop $board.NumPages}} [{{if eq $i $board.CurrentPage}}<b>{{end}}<a href="{{$config.SiteWebfolder}}{{$board.Dir}}/{{add $i 1}}.html">{{add $i 1}}</a>{{if eq $i $board.CurrentPage}}</b>{{end}}]{{end}}</td>
<td>{{if lt $board.CurrentPage $board.NumPages}}
<form method="GET" action="{{$config.SiteWebfolder}}{{$board.Dir}}/{{add $board.CurrentPage 1}}.html">
<input type="submit" value="Next" />
</form>
{{else}}Next{{end}}</td></tr>
</table>
<span id="boardmenu-bottom">
[{{range $i, $boardlink := $board_arr}} {{if gt $i 0}}/{{end}} <a href="/{{$boardlink.Dir}}/">{{$boardlink.Dir}}</a> {{end}}]
</span>
</div>
</div>
<div id="footer">
<a href="{{$config.SiteWebfolder}}">Home</a> | <a href="{{$config.SiteWebfolder}}#boards">Boards</a> | <a href="{{$config.SiteWebfolder}}#rules">Rules</a> | <a href="{{$config.SiteWebfolder}}#faq">FAQ</a><br />
Powered by Gochan {{$config.Version}}<br />
Powered by <a href="http://github.com/eggbertx/gochan/">Gochan {{$config.Version}}</a><br />
</div>
</body>
</html>

View file

@ -3,8 +3,7 @@
<head>
<meta charset="UTF-8">
<title>{{$board.Title}}</title>
<script type="text/javascript" src="/javascript/jquery/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="/javascript/msgpack.js"></script>
<script type="text/javascript" src="/javascript/jquery-1.10.2.min.js"></script>
<script type="text/javascript">
var styles = [{{range $ii, $style := $config.Styles_img}}{{if gt $ii 0}}, {{end}}"{{$style}}"{{end}}];
var webroot = "{{$config.SiteWebfolder}}";
@ -21,7 +20,7 @@
</head>
<body>
<div id="topbar">
{{range $i, $board := $board_arr.Data}}
{{range $i, $board := $board_arr}}
<a href="/{{$board.Dir}}/" class="topbar-item">/{{$board.Dir}}/</a>
{{end}}
</div>
@ -49,14 +48,14 @@
<div id="postbox-area">
<form name="postform" action="/post" method="POST" enctype="multipart/form-data">
<input type="hidden" name="threadid" value="{{$op.ID}}" />
<input type="hidden" name="boardid" value="{{$board.ID}}" />
<input type="hidden" name="boardid" value="{{$op.BoardID}}" />
<table id="postbox-static">
<tr><td class="postblock">Name</td><td><input type="text" id="postname" name="postname" maxlength="75" size="28" {{/* value="Name" onFocus="if(this.value=='Name') {this,value= ''}" onBlur="if(this.value == '') {this.value = 'Name'}"*/}}/></td></tr>
<tr><td class="postblock">Email</td><td><input type="text" id="postemail" name="postemail" maxlength="75" size="28" /></td></tr>
<tr><td class="postblock">Subject</td><td><input type="text" name="postsubject" maxlength="75" size="35" /><input type="submit" value="Post"/></td></tr>
<tr><td class="postblock">Message</td><td><textarea rows="4" cols="48" name="postmsg"></textarea></td></tr>
<tr><td class="postblock">File</td><td><input name="imagefile" type="file"><input type="checkbox" id="spoiler" name="spoiler"/><label for="spoiler">Spoiler</label></td></tr>
<tr><td class="postblock">Password</td><td><input type="password" id="postpassword" name="postpassword" size="14" /> (for post/file deletion)</td></tr>
<tr><th class="postblock">Name</th><td><input type="text" id="postname" name="postname" maxlength="75" size="28" {{/* value="Name" onFocus="if(this.value=='Name') {this,value= ''}" onBlur="if(this.value == '') {this.value = 'Name'}"*/}}/></td></tr>
<tr><th class="postblock">Email</th><td><input type="text" id="postemail" name="postemail" maxlength="75" size="28" /></td></tr>
<tr><th class="postblock">Subject</th><td><input type="text" name="postsubject" maxlength="75" size="35" /><input type="submit" value="Post"/></td></tr>
<tr><th class="postblock">Message</th><td><textarea rows="4" cols="48" name="postmsg"></textarea></td></tr>
<tr><th class="postblock">File</th><td><input name="imagefile" type="file"><input type="checkbox" id="spoiler" name="spoiler"/><label for="spoiler">Spoiler</label></td></tr>
<tr><th class="postblock">Password</th><td><input type="password" id="postpassword" name="postpassword" size="14" /> (for post/file deletion)</td></tr>
</table>
</form>
</div>
@ -99,32 +98,36 @@
{{end}}{{end}}
</div>
<hr />
<div id="left-bottom-content">
<table id="pages">
<tr><td>Previous</td><td>[0] [<a href="{{$op.ID}}p1.html">1</a>]</td><td><input type="button" value="Next" onclick="window.location.href = '{{$op.ID}}p1.html'" /></td></tr>
</table>
<span id="boardmenu-bottom">
[{{range $i, $board := $board_arr.Data}} {{if gt $i 0}}/{{end}} <a href="/{{$board.Dir}}/">{{$board.Dir}}</a> {{end}}]
</span>
</div>
<div id="right-bottom-content">
<span id="threadlinks-bottom">
[<a href="{{$config.SiteWebfolder}}{{$board.Dir}}/board.html">Return</a>] [<a href="{{$op.ID}}-100.html">First 100 posts</a>] [<a href="{{$op.ID}}+50.html">Last 50 posts</a>]
</span>
<div id="report-delbox">
<input type="hidden" name="board" value="{{$board.Dir}}" />
<label>[<input type="checkbox" name="fileonly"/>File only]</label> <input type="password" size="10" name="password" id="delete-password" /> <input type="submit" name="delete_btn" value="Delete" onclick="return confirm('Are you sure you want to delete these posts?')" /><br />
Reason: <input type="text" size="10" name="reason" id="reason" /> <input type="submit" value="Report" />
</div>
</div>
</form>
</form>
<div id="left-bottom-content">
<table id="pages">
<tr><td>{{if gt $op.CurrentPage 1}}
<form method="GET" action="{{$config.SiteWebfolder}}{{$board.Dir}}/res/{{$op.ID}}p{{subtract $op.CurrentPage 1}}.html">
<input type="submit" value="Previous" />
</form>
{{else}}Previous{{end}}</td>
<td>[<a href="{{$config.SiteWebfolder}}{{$board.Dir}}/res/{{$op.ID}}.html">Index</a>]{{range $i,$_ := makeLoop $op.NumPages}} [{{if eq $i (subtract $op.CurrentPage 1)}}<b>{{end}}<a href="{{$config.SiteWebfolder}}{{$board.Dir}}/res/{{$op.ID}}p{{add $i 1}}.html">{{add $i 1}}</a>{{if eq $i (subtract $op.CurrentPage 1)}}</b>{{end}}]{{end}}</td>
<td>{{if lt $op.CurrentPage $op.NumPages}}
<form method="GET" action="{{$config.SiteWebfolder}}{{$board.Dir}}/res/{{$op.ID}}p{{add $op.CurrentPage 1}}.html">
<input type="submit" value="Next" />
</form>
{{else}}Next{{end}}</td></tr>
</table>
<span id="boardmenu-bottom">
[{{range $i, $boardlink := $board_arr}} {{if gt $i 0}}/{{end}} <a href="/{{$boardlink.Dir}}/">{{$boardlink.Dir}}</a> {{end}}]
</span>
</div>
</div>
<div id="footer">
<a href="{{$config.SiteWebfolder}}">Home</a> | <a href="{{$config.SiteWebfolder}}#boards">Boards</a> | <a href="{{$config.SiteWebfolder}}#rules">Rules</a> | <a href="{{$config.SiteWebfolder}}#faq">FAQ</a><br />
Powered by Gochan {{$config.Version}}<br />
Powered by <a href="http://github.com/eggbertx/gochan/">Gochan {{$config.Version}}</a><br />
</div>
</body>
</html>