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

Add post editing, fix log not showing IP/path

This commit is contained in:
Joshua Merrell 2018-06-09 23:40:20 -07:00
parent 53553b8ac4
commit 7e1110214d
13 changed files with 315 additions and 230 deletions

View file

@ -1,2 +0,0 @@
Joshua Merrell <joshuamerrell@gmail.com>
Darren VanBuren <onekopaka@theoks.net>

View file

@ -1,4 +1,4 @@
Copyright (c) 2013-2018, Joshua Merrell
Copyright (c) 2013-2018, Eggbertx
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -3,7 +3,7 @@ GOCHAN_DEBUG=1
GOCHAN_VERBOSE=2
GOCHAN_VERBOSITY=0 # This is set by "make release/debug/verbose"
GOCHAN_VERSION=1.9.3
GOCHAN_VERSION=1.10.0
GOCHAN_BUILDTIME=$(shell date +%y%m%d.%H%M)
ifeq ($(GOOS), windows)
GOCHAN_BIN=gochan.exe

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
VERSION=1.9.3
VERSION=1.10.0
GOOS_ORIG=$GOOS
function copyStuff {

View file

@ -7,6 +7,6 @@
<h1>404: File not found</h1>
<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>
<hr><address>http://gochan.org powered by Gochan v1.9.3</address>
<hr><address>http://gochan.org powered by Gochan v1.10.0</address>
</body>
</html>

View file

@ -7,6 +7,6 @@
<h1>500: Internal Server error</h1>
<img src="/error/derpy server.gif" border="0" alt="">
<p>The server encountered an error while trying to serve the page, and we apologize for the inconvenience. The <a href="https://en.wikipedia.org/wiki/Idiot">system administrator</a> will try to fix things as soon has he/she/it can.</p>
<hr><address>http://gochan.org powered by Gochan v1.9.3</address>
<hr><address>http://gochan.org powered by Gochan v1.10.0</address>
</body>
</html>

View file

@ -17,23 +17,19 @@ import (
// ManageFunction represents the functions accessed by staff members at /manage?action=<functionname>.
// Eventually a plugin system might allow you to add more
type ManageFunction struct {
Permissions int // 0 -> non-staff, 1 => janitor, 2 => moderator, 3 => administrator
Callback func() string //return string of html output
Permissions int // 0 -> non-staff, 1 => janitor, 2 => moderator, 3 => administrator
Callback func(writer http.ResponseWriter, request *http.Request) string //return string of html output
}
func callManageFunction(w http.ResponseWriter, r *http.Request, data interface{}) {
request = *r
writer = w
cookies = r.Cookies()
err := request.ParseForm()
if err != nil {
func callManageFunction(writer http.ResponseWriter, request *http.Request) {
var err error
if err = request.ParseForm(); err != nil {
serveErrorPage(writer, err.Error())
errorLog.Println(customError(err))
}
action := request.FormValue("action")
staffRank := getStaffRank()
staffRank := getStaffRank(request)
var managePageBuffer bytes.Buffer
mangePageHTML := ""
@ -57,11 +53,11 @@ func callManageFunction(w http.ResponseWriter, r *http.Request, data interface{}
if _, ok := manage_functions[action]; ok {
if staffRank >= manage_functions[action].Permissions {
managePageBuffer.Write([]byte(manage_functions[action].Callback()))
managePageBuffer.Write([]byte(manage_functions[action].Callback(writer, request)))
} else if staffRank == 0 && manage_functions[action].Permissions == 0 {
managePageBuffer.Write([]byte(manage_functions[action].Callback()))
managePageBuffer.Write([]byte(manage_functions[action].Callback(writer, request)))
} else if staffRank == 0 {
managePageBuffer.Write([]byte(manage_functions["login"].Callback()))
managePageBuffer.Write([]byte(manage_functions["login"].Callback(writer, request)))
} else {
managePageBuffer.Write([]byte(action + " is undefined."))
}
@ -79,14 +75,12 @@ func callManageFunction(w http.ResponseWriter, r *http.Request, data interface{}
fmt.Fprintf(writer, managePageBuffer.String())
}
func getCurrentStaff() (string, error) {
sessionCookie := getCookie("sessiondata")
var key string
if sessionCookie == nil {
func getCurrentStaff(request *http.Request) (string, error) {
sessionCookie, err := request.Cookie("sessiondata")
if err != nil {
return "", nil
}
key = sessionCookie.Value
key := sessionCookie.Value
current_session := new(SessionsTable)
if err := queryRowSQL(
"SELECT `data` FROM `"+config.DBprefix+"sessions` WHERE `key` = ?",
@ -108,13 +102,13 @@ func getStaff(name string) (*StaffTable, error) {
return staff_obj, err
}
func getStaffRank() int {
staffname, err := getCurrentStaff()
println(1, customError(err))
func getStaffRank(request *http.Request) int {
staffname, err := getCurrentStaff(request)
if staffname == "" {
return 0
}
if err != nil {
handleError(1, customError(err))
return 0
}
@ -126,14 +120,14 @@ func getStaffRank() int {
return staff.Rank
}
func createSession(key string, username string, password string, request *http.Request, writer *http.ResponseWriter) int {
func createSession(key string, username string, password string, request *http.Request, writer http.ResponseWriter) int {
//returns 0 for successful, 1 for password mismatch, and 2 for other
domain := request.Host
var err error
chopPortNumRegex := regexp.MustCompile("(.+|\\w+):(\\d+)$")
domain = chopPortNumRegex.Split(domain, -1)[0]
if !validReferrer(*request) {
if !validReferrer(request) {
modLog.Print("Rejected login from possible spambot @ : " + request.RemoteAddr)
return 2
}
@ -150,7 +144,7 @@ func createSession(key string, username string, password string, request *http.R
} else {
// successful login, add cookie that expires in one month
cookie := &http.Cookie{Name: "sessiondata", Value: key, Path: "/", Domain: domain, Expires: time.Now().Add(time.Duration(time.Hour * 730))}
http.SetCookie(*writer, cookie)
http.SetCookie(writer, cookie)
if _, err = execSQL(
"INSERT INTO `"+config.DBprefix+"sessions` (`key`, `data`, `expires`) VALUES(?,?,?)",
key, username, getSpecificSQLDateTime(time.Now().Add(time.Duration(time.Hour*730))),
@ -172,7 +166,7 @@ func createSession(key string, username string, password string, request *http.R
var manage_functions = map[string]ManageFunction{
"cleanup": {
Permissions: 3,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
html = "<h2>Cleanup</h2><br />"
var err error
if request.FormValue("run") == "Run Cleanup" {
@ -213,7 +207,7 @@ var manage_functions = map[string]ManageFunction{
}},
"config": {
Permissions: 3,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
do := request.FormValue("do")
if do == "save" {
// configJSON, err := json.Marshal(config)
@ -239,7 +233,7 @@ var manage_functions = map[string]ManageFunction{
}},
"purgeeverything": {
Permissions: 3,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
html = "<img src=\"/css/purge.jpg\" />"
rows, err := querySQL("SELECT `dir` FROM `" + config.DBprefix + "boards`")
defer closeRows(rows)
@ -294,7 +288,7 @@ var manage_functions = map[string]ManageFunction{
}},
"executesql": {
Permissions: 3,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
statement := request.FormValue("sql")
html = "<h1>Execute SQL statement(s)</h1><form method = \"POST\" action=\"/manage?action=executesql\">\n<textarea name=\"sql\" id=\"sql-statement\">" + statement + "</textarea>\n<input type=\"submit\" />\n</form>"
if statement != "" {
@ -309,9 +303,9 @@ var manage_functions = map[string]ManageFunction{
}},
"login": {
Permissions: 0,
Callback: func() (html string) {
if getStaffRank() > 0 {
http.Redirect(writer, &request, path.Join(config.SiteWebfolder, "manage"), http.StatusFound)
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
if getStaffRank(request) > 0 {
http.Redirect(writer, request, path.Join(config.SiteWebfolder, "manage"), http.StatusFound)
}
username := request.FormValue("username")
password := request.FormValue("password")
@ -329,15 +323,18 @@ var manage_functions = map[string]ManageFunction{
"\t</form>"
} else {
key := md5Sum(request.RemoteAddr + username + password + config.RandomSeed + generateSalt())[0:10]
createSession(key, username, password, &request, &writer)
http.Redirect(writer, &request, path.Join(config.SiteWebfolder, "/manage?action="+request.FormValue("redirect")), http.StatusFound)
createSession(key, username, password, request, writer)
http.Redirect(writer, request, path.Join(config.SiteWebfolder, "/manage?action="+request.FormValue("redirect")), http.StatusFound)
}
return
}},
"logout": {
Permissions: 1,
Callback: func() (html string) {
cookie := getCookie("sessiondata")
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
cookie, err := request.Cookie("sessiondata")
if err != nil {
serveErrorPage(writer, err.Error())
}
var key string
if cookie != nil {
key = cookie.Value
@ -361,7 +358,7 @@ var manage_functions = map[string]ManageFunction{
}},
"announcements": {
Permissions: 1,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
html = "<h1>Announcements</h1><br />"
rows, err := querySQL("SELECT `subject`,`message`,`poster`,`timestamp` FROM `" + config.DBprefix + "announcements` ORDER BY `id` DESC")
@ -391,7 +388,7 @@ var manage_functions = map[string]ManageFunction{
}},
"bans": {
Permissions: 1,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
var ban_which string // user, image, or both
if request.PostFormValue("ban-user-button") == "Ban user" {
@ -508,13 +505,13 @@ var manage_functions = map[string]ManageFunction{
}},
"getstaffjquery": {
Permissions: 0,
Callback: func() (html string) {
current_staff, err := getCurrentStaff()
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
current_staff, err := getCurrentStaff(request)
if err != nil {
html = "nobody;0;"
return
}
staff_rank := getStaffRank()
staff_rank := getStaffRank(request)
if staff_rank == 0 {
html = "nobody;0;"
return
@ -532,7 +529,7 @@ var manage_functions = map[string]ManageFunction{
}},
"boards": {
Permissions: 3,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
do := request.FormValue("do")
var done bool
board := new(BoardsTable)
@ -774,8 +771,8 @@ var manage_functions = map[string]ManageFunction{
}},
"staffmenu": {
Permissions: 1,
Callback: func() (html string) {
rank := getStaffRank()
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
rank := getStaffRank(request)
html = "<a href=\"javascript:void(0)\" id=\"logout\" class=\"staffmenu-item\">Log out</a><br />\n" +
"<a href=\"javascript:void(0)\" id=\"announcements\" class=\"staffmenu-item\">Announcements</a><br />\n"
@ -804,13 +801,13 @@ var manage_functions = map[string]ManageFunction{
}},
"rebuildfront": {
Permissions: 3,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
initTemplates()
return buildFrontPage()
}},
"rebuildall": {
Permissions: 3,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
initTemplates()
return buildFrontPage() + "<hr />\n" +
buildBoardListJSON() + "<hr />\n" +
@ -818,13 +815,13 @@ var manage_functions = map[string]ManageFunction{
}},
"rebuildboards": {
Permissions: 3,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
initTemplates()
return buildBoards(true, 0)
}},
"reparsehtml": {
Permissions: 3,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
posts, err := getPostArr(map[string]interface{}{
"deleted_timestamp": nilTimestamp,
}, "")
@ -850,7 +847,7 @@ var manage_functions = map[string]ManageFunction{
}},
"recentposts": {
Permissions: 1,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
limit := request.FormValue("limit")
if limit == "" {
limit = "50"
@ -896,13 +893,13 @@ var manage_functions = map[string]ManageFunction{
}},
"killserver": {
Permissions: 3,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
os.Exit(0)
return
}},
"staff": {
Permissions: 3,
Callback: func() (html string) {
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
do := request.FormValue("do")
html = "<h1>Staff</h1><br />\n" +
"<table id=\"stafftable\" border=\"1\">\n" +

View file

@ -67,7 +67,7 @@ func buildBoards(all bool, which int) (html string) {
// buildBoardPages builds the pages for the board archive. board is a BoardsTable object representing the board to
// build archive pages for. The return value is a string of HTML with debug information from the build process.
func buildBoardPages(board *BoardsTable) (html string) {
// start_time := benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), time.Now(), true)
start_time := benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), time.Now(), true)
var current_page_file *os.File
var threads []interface{}
var thread_pages [][]interface{}
@ -210,7 +210,7 @@ func buildBoardPages(board *BoardsTable) (html string) {
}
html += "/" + board.Dir + "/ built successfully, no threads to build.\n"
//benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), start_time, false)
benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), start_time, false)
return
} else {
// Create the archive pages.
@ -306,7 +306,7 @@ func buildBoardPages(board *BoardsTable) (html string) {
}
html += "/" + board.Dir + "/ built successfully.\n"
}
//benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), start_time, false)
benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), start_time, false)
return
}
@ -567,7 +567,7 @@ func bumpThread(postID, boardID int) error {
// 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) {
func checkBannedStatus(post *PostTable, writer http.ResponseWriter) ([]interface{}, error) {
var isExpired bool
var ban_entry BanlistTable
var interfaces []interface{}
@ -711,6 +711,21 @@ func getThumbnailSize(w int, h int, size string) (new_w int, new_h int) {
return
}
func parseName(name string) map[string]string {
parsed := make(map[string]string)
if !strings.Contains(name, "#") {
parsed["name"] = name
parsed["tripcode"] = ""
} else if strings.Index(name, "#") == 0 {
parsed["tripcode"] = tripcode.Tripcode(name[1:])
} else if strings.Index(name, "#") > 0 {
postNameArr := strings.SplitN(name, "#", 2)
parsed["name"] = postNameArr[0]
parsed["tripcode"] = tripcode.Tripcode(postNameArr[1])
}
return parsed
}
// inserts prepared post object into the SQL table so that it can be rendered
func insertPost(post PostTable, bump bool) (sql.Result, error) {
var result sql.Result
@ -748,13 +763,11 @@ func insertPost(post PostTable, bump bool) (sql.Result, error) {
}
// called when a user accesses /post. Parse form data, then insert and build
func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
func makePost(writer http.ResponseWriter, request *http.Request) {
startTime := benchmarkTimer("makePost", time.Now(), true)
request = *r
writer = w
var maxMessageLength int
var post PostTable
domain := r.Host
domain := request.Host
var formName string
var nameCookie string
var formEmail string
@ -769,25 +782,15 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
var emailCommand string
formName = request.FormValue("postname")
if strings.Index(formName, "#") == -1 {
post.Name = formName
} else if strings.Index(formName, "#") == 0 {
post.Tripcode = tripcode.Tripcode(formName[1:])
} else if strings.Index(formName, "#") > 0 {
postNameArr := strings.SplitN(formName, "#", 2)
post.Name = postNameArr[0]
post.Tripcode = tripcode.Tripcode(postNameArr[1])
}
if strings.Index(post.Tripcode, "PipesTtB.A") > -1 {
http.Redirect(writer, r, "https://i.imgur.com/caMm6N8.jpg", 302)
}
parsedName := parseName(formName)
post.Name = parsedName["name"]
post.Tripcode = parsedName["tripcode"]
nameCookie = post.Name + post.Tripcode
formEmail = request.FormValue("postemail")
http.SetCookie(writer, &http.Cookie{Name: "email", Value: formEmail, Path: "/", Domain: domain, RawExpires: getSpecificSQLDateTime(time.Now().Add(time.Duration(yearInSeconds))), MaxAge: yearInSeconds})
if strings.Index(formEmail, "noko") == -1 && strings.Index(formEmail, "sage") == -1 {
if !strings.Contains(formEmail, "noko") && !strings.Contains(formEmail, "sage") {
post.Email = formEmail
} else if strings.Index(formEmail, "#") > 1 {
formEmailArr := strings.SplitN(formEmail, "#", 2)
@ -805,16 +808,15 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
[]interface{}{post.BoardID},
[]interface{}{&maxMessageLength},
); err != nil {
serveErrorPage(w, handleError(0, "Error getting board info: "+err.Error()))
serveErrorPage(writer, handleError(0, "Error getting board info: "+err.Error()))
return
}
if len(post.MessageText) > maxMessageLength {
serveErrorPage(w, "Post body is too long")
serveErrorPage(writer, "Post body is too long")
return
}
post.MessageHTML = formatMessage(post.MessageText)
post.Password = md5Sum(request.FormValue("postpassword"))
// Reverse escapes
@ -826,9 +828,9 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
http.SetCookie(writer, &http.Cookie{Name: "name", Value: nameCookie, Path: "/", Domain: domain, RawExpires: getSpecificSQLDateTime(time.Now().Add(time.Duration(yearInSeconds))), MaxAge: yearInSeconds})
http.SetCookie(writer, &http.Cookie{Name: "password", Value: request.FormValue("postpassword"), Path: "/", Domain: domain, RawExpires: getSpecificSQLDateTime(time.Now().Add(time.Duration(yearInSeconds))), MaxAge: yearInSeconds})
post.IP = getRealIP(&request)
post.IP = getRealIP(request)
post.Timestamp = time.Now()
post.PosterAuthority = getStaffRank()
post.PosterAuthority = getStaffRank(request)
post.Bumped = time.Now()
post.Stickied = request.FormValue("modstickied") == "on"
post.Locked = request.FormValue("modlocked") == "on"
@ -844,11 +846,11 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
switch checkPostForSpam(post.IP, request.Header["User-Agent"][0], request.Referer(),
post.Name, post.Email, post.MessageText) {
case "discard":
serveErrorPage(w, "Your post looks like spam.")
serveErrorPage(writer, "Your post looks like spam.")
accessLog.Print("Akismet recommended discarding post from: " + post.IP)
return
case "spam":
serveErrorPage(w, "Your post looks like spam.")
serveErrorPage(writer, "Your post looks like spam.")
accessLog.Print("Akismet suggested post is spam from " + post.IP)
return
default:
@ -867,7 +869,7 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
} else {
data, err := ioutil.ReadAll(file)
if err != nil {
serveErrorPage(w, handleError(1, "Couldn't read file: "+err.Error()))
serveErrorPage(writer, handleError(1, "Couldn't read file: "+err.Error()))
} else {
post.FilenameOriginal = html.EscapeString(handler.Filename)
filetype := getFileExtension(post.FilenameOriginal)
@ -879,7 +881,7 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
post.Filename = getNewFilename() + "." + getFileExtension(post.FilenameOriginal)
boardArr, _ := getBoardArr(map[string]interface{}{"id": request.FormValue("boardid")}, "")
if len(boardArr) == 0 {
serveErrorPage(w, "No boards have been created yet")
serveErrorPage(writer, "No boards have been created yet")
return
}
_boardDir, _ := getBoardArr(map[string]interface{}{"id": request.FormValue("boardid")}, "")
@ -890,7 +892,7 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
if err := ioutil.WriteFile(filePath, data, 0777); err != nil {
handleError(0, "Couldn't write file \""+post.Filename+"\""+err.Error())
serveErrorPage(w, "Couldn't write file \""+post.FilenameOriginal+"\"")
serveErrorPage(writer, "Couldn't write file \""+post.FilenameOriginal+"\"")
return
}
@ -902,13 +904,13 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
[]interface{}{post.BoardID},
[]interface{}{&allowsVids},
); err != nil {
serveErrorPage(w, handleError(1, "Couldn't get board info: "+err.Error()))
serveErrorPage(writer, handleError(1, "Couldn't get board info: "+err.Error()))
return
}
if filetype == "webm" {
if !allowsVids || !config.AllowVideoUploads {
serveErrorPage(w, "Video uploading is not currently enabled for this board.")
serveErrorPage(writer, "Video uploading is not currently enabled for this board.")
os.Remove(filePath)
return
}
@ -917,25 +919,25 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
if post.ParentID == 0 {
err := createVideoThumbnail(filePath, thumbPath, config.ThumbWidth)
if err != nil {
serveErrorPage(w, handleError(1, err.Error()))
serveErrorPage(writer, handleError(1, err.Error()))
return
}
} else {
err := createVideoThumbnail(filePath, thumbPath, config.ThumbWidth_reply)
if err != nil {
serveErrorPage(w, handleError(1, err.Error()))
serveErrorPage(writer, handleError(1, err.Error()))
return
}
}
if err := createVideoThumbnail(filePath, catalogThumbPath, config.ThumbWidth_catalog); err != nil {
serveErrorPage(w, handleError(1, err.Error()))
serveErrorPage(writer, handleError(1, err.Error()))
return
}
outputBytes, err := exec.Command("ffprobe", "-v", "quiet", "-show_format", "-show_streams", filePath).CombinedOutput()
if err != nil {
serveErrorPage(w, handleError(1, "Error getting video info: "+err.Error()))
serveErrorPage(writer, handleError(1, "Error getting video info: "+err.Error()))
return
}
if err == nil && outputBytes != nil {
@ -968,13 +970,13 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
if err != nil {
os.Remove(filePath)
handleError(1, "Couldn't open uploaded file \""+post.Filename+"\""+err.Error())
serveErrorPage(w, "Upload filetype not supported")
serveErrorPage(writer, "Upload filetype not supported")
return
} else {
// Get image filesize
stat, err := os.Stat(filePath)
if err != nil {
serveErrorPage(w, handleError(1, "Couldn't get image filesize: "+err.Error()))
serveErrorPage(writer, handleError(1, "Couldn't get image filesize: "+err.Error()))
return
} else {
post.Filesize = int(stat.Size())
@ -994,12 +996,12 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
if request.FormValue("spoiler") == "on" {
// If spoiler is enabled, symlink thumbnail to spoiler image
if _, err := os.Stat(path.Join(config.DocumentRoot, "spoiler.png")); err != nil {
serveErrorPage(w, "missing /spoiler.png")
serveErrorPage(writer, "missing /spoiler.png")
return
} else {
err = syscall.Symlink(path.Join(config.DocumentRoot, "spoiler.png"), thumbPath)
if err != nil {
serveErrorPage(w, err.Error())
serveErrorPage(writer, err.Error())
return
}
}
@ -1008,7 +1010,7 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
post.ThumbW = img.Bounds().Max.X
post.ThumbH = img.Bounds().Max.Y
if err := syscall.Symlink(filePath, thumbPath); err != nil {
serveErrorPage(w, err.Error())
serveErrorPage(writer, err.Error())
return
}
} else {
@ -1019,14 +1021,14 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
thumbnail = createImageThumbnail(img, "op")
catalogThumbnail = createImageThumbnail(img, "catalog")
if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil {
serveErrorPage(w, handleError(1, "Couldn't generate catalog thumbnail: "+err.Error()))
serveErrorPage(writer, handleError(1, "Couldn't generate catalog thumbnail: "+err.Error()))
return
}
} else {
thumbnail = createImageThumbnail(img, "reply")
}
if err = imaging.Save(thumbnail, thumbPath); err != nil {
serveErrorPage(w, handleError(1, "Couldn't save thumbnail: "+err.Error()))
serveErrorPage(writer, handleError(1, "Couldn't save thumbnail: "+err.Error()))
return
}
}
@ -1036,25 +1038,25 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
}
if strings.TrimSpace(post.MessageText) == "" && post.Filename == "" {
serveErrorPage(w, "Post must contain a message if no image is uploaded.")
serveErrorPage(writer, "Post must contain a message if no image is uploaded.")
return
}
postDelay := sinceLastPost(&post)
if postDelay > -1 {
if post.ParentID == 0 && postDelay < config.NewThreadDelay {
serveErrorPage(w, "Please wait before making a new thread.")
serveErrorPage(writer, "Please wait before making a new thread.")
return
} else if post.ParentID > 0 && postDelay < config.ReplyDelay {
serveErrorPage(w, "Please wait before making a reply.")
serveErrorPage(writer, "Please wait before making a reply.")
return
}
}
isBanned, err := checkBannedStatus(&post, &w)
isBanned, err := checkBannedStatus(&post, writer)
if err != nil {
handleError(1, "Error in checkBannedStatus: "+err.Error())
serveErrorPage(w, err.Error())
serveErrorPage(writer, err.Error())
return
}
@ -1068,14 +1070,14 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
fmt.Fprintf(writer, banpage_html+handleError(1, err.Error())+"\n</body>\n</html>")
return
}
fmt.Fprintf(w, banpage_buffer.String())
fmt.Fprintf(writer, banpage_buffer.String())
return
}
sanitizePost(&post)
result, err := insertPost(post, emailCommand != "sage")
if err != nil {
serveErrorPage(w, handleError(1, err.Error()))
serveErrorPage(writer, handleError(1, err.Error()))
return
}
postid, _ := result.LastInsertId()
@ -1084,17 +1086,16 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
boards, _ := getBoardArr(nil, "")
// rebuild the board page
buildBoards(false, post.BoardID)
buildFrontPage()
if emailCommand == "noko" {
if post.ParentID == 0 {
http.Redirect(writer, &request, "/"+boards[post.BoardID-1].Dir+"/res/"+strconv.Itoa(post.ID)+".html", http.StatusFound)
http.Redirect(writer, request, "/"+boards[post.BoardID-1].Dir+"/res/"+strconv.Itoa(post.ID)+".html", http.StatusFound)
} else {
http.Redirect(writer, &request, "/"+boards[post.BoardID-1].Dir+"/res/"+strconv.Itoa(post.ParentID)+".html#"+strconv.Itoa(post.ID), http.StatusFound)
http.Redirect(writer, request, "/"+boards[post.BoardID-1].Dir+"/res/"+strconv.Itoa(post.ParentID)+".html#"+strconv.Itoa(post.ID), http.StatusFound)
}
} else {
http.Redirect(writer, &request, "/"+boards[post.BoardID-1].Dir+"/", http.StatusFound)
http.Redirect(writer, request, "/"+boards[post.BoardID-1].Dir+"/", http.StatusFound)
}
benchmarkTimer("makePost", startTime, false)
}

View file

@ -15,29 +15,27 @@ import (
)
var (
cookies []*http.Cookie
writer http.ResponseWriter
request http.Request
server *GochanServer
referrerRegex *regexp.Regexp
)
type GochanServer struct {
/* writer http.ResponseWriter
request http.Request */
namespaces map[string]func(http.ResponseWriter, *http.Request, interface{})
namespaces map[string]func(http.ResponseWriter, *http.Request)
}
func (s GochanServer) AddNamespace(basePath string, namespaceFunction func(http.ResponseWriter, *http.Request, interface{})) {
func (s GochanServer) AddNamespace(basePath string, namespaceFunction func(http.ResponseWriter, *http.Request)) {
s.namespaces[basePath] = namespaceFunction
}
func (s GochanServer) getFileData(writer http.ResponseWriter, url string) (fileBytes []byte) {
filePath := path.Join(config.DocumentRoot, url)
func (s GochanServer) serveFile(writer http.ResponseWriter, request *http.Request) {
filePath := path.Join(config.DocumentRoot, request.URL.Path)
var fileBytes []byte
results, err := os.Stat(filePath)
if err != nil {
// the requested path isn't a file or directory, 404
fileBytes = nil
writer.WriteHeader(404)
serveNotFound(writer, request)
return
} else {
//the file exists, or there is a folder here
if results.IsDir() {
@ -46,16 +44,13 @@ func (s GochanServer) getFileData(writer http.ResponseWriter, url string) (fileB
newPath := path.Join(filePath, value)
_, err := os.Stat(newPath)
if err == nil {
// serve the index page
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
fileBytes, _ := ioutil.ReadFile(newPath)
return fileBytes
filePath = newPath
break
}
}
} else {
//the file exists, and is not a folder
fileBytes, _ = ioutil.ReadFile(filePath)
extension := getFileExtension(url)
extension := getFileExtension(request.URL.Path)
switch extension {
case "png":
writer.Header().Add("Content-Type", "image/png")
@ -83,10 +78,14 @@ func (s GochanServer) getFileData(writer http.ResponseWriter, url string) (fileB
writer.Header().Add("Content-Type", "text/html")
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
}
accessLog.Print("Success: 200 from " + getRealIP(&request) + " @ " + request.RequestURI)
accessLog.Print("Success: 200 from " + getRealIP(request) + " @ " + request.URL.Path)
}
}
return
// serve the index page
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
fileBytes, _ = ioutil.ReadFile(filePath)
writer.Header().Add("Cache-Control", "max-age=86400")
_, _ = writer.Write(fileBytes)
}
func serveNotFound(writer http.ResponseWriter, request *http.Request) {
@ -98,29 +97,28 @@ func serveNotFound(writer http.ResponseWriter, request *http.Request) {
} else {
_, _ = writer.Write(errorPage)
}
errorLog.Print("Error: 404 Not Found from " + getRealIP(request) + " @ " + request.RequestURI)
errorLog.Print("Error: 404 Not Found from " + getRealIP(request) + " @ " + request.URL.Path)
}
func serveErrorPage(writer http.ResponseWriter, err string) {
errorPageBytes, _ := ioutil.ReadFile("templates/error.html")
errorPage := strings.Replace(string(errorPageBytes), "{ERRORTEXT}", err, -1)
_, _ = writer.Write([]byte(errorPage))
errorpage_tmpl.Execute(writer, map[string]interface{}{
"config": config,
"ErrorTitle": "Error :c",
"ErrorImage": "/error/lol 404.gif",
"ErrorHeader": "Error",
"ErrorText": err,
})
}
func (s GochanServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
for name, namespaceFunction := range s.namespaces {
if request.URL.Path == "/"+name {
namespaceFunction(writer, request, nil)
// writer.WriteHeader(200)
namespaceFunction(writer, request)
return
}
}
fb := s.getFileData(writer, request.URL.Path)
writer.Header().Add("Cache-Control", "max-age=86400")
if fb == nil {
serveNotFound(writer, request)
return
}
_, _ = writer.Write(fb)
s.serveFile(writer, request)
}
func initServer() {
@ -130,7 +128,7 @@ func initServer() {
os.Exit(2)
}
server = new(GochanServer)
server.namespaces = make(map[string]func(http.ResponseWriter, *http.Request, interface{}))
server.namespaces = make(map[string]func(http.ResponseWriter, *http.Request))
// Check if Akismet API key is usable at startup.
if config.AkismetAPIKey != "" {
@ -140,11 +138,12 @@ func initServer() {
// Compile regex for checking referrers.
referrerRegex = regexp.MustCompile(config.DomainRegex)
testfunc := func(writer http.ResponseWriter, response *http.Request, data interface{}) {
testfunc := func(writer http.ResponseWriter, request *http.Request) {
if writer != nil {
_, _ = writer.Write([]byte("hahahaha"))
}
}
server.AddNamespace("example", testfunc)
server.AddNamespace("manage", callManageFunction)
server.AddNamespace("post", makePost)
@ -163,23 +162,23 @@ func initServer() {
}
}
func getRealIP(r *http.Request) string {
func getRealIP(request *http.Request) string {
// HTTP_CF_CONNECTING_IP > X-Forwarded-For > RemoteAddr
if r.Header.Get("HTTP_CF_CONNECTING_IP") != "" {
return r.Header.Get("HTTP_CF_CONNECTING_IP")
if request.Header.Get("HTTP_CF_CONNECTING_IP") != "" {
return request.Header.Get("HTTP_CF_CONNECTING_IP")
}
if r.Header.Get("X-Forwarded-For") != "" {
return r.Header.Get("X-Forwarded-For")
if request.Header.Get("X-Forwarded-For") != "" {
return request.Header.Get("X-Forwarded-For")
}
return r.RemoteAddr
return request.RemoteAddr
}
func validReferrer(request http.Request) bool {
func validReferrer(request *http.Request) bool {
return referrerRegex.MatchString(request.Referer())
}
// register /util handler
func utilHandler(writer http.ResponseWriter, request *http.Request, data interface{}) {
func utilHandler(writer http.ResponseWriter, request *http.Request) {
//writer.Header().Add("Content-Type", "text/css")
action := request.FormValue("action")
password := request.FormValue("password")
@ -189,8 +188,9 @@ func utilHandler(writer http.ResponseWriter, request *http.Request, data interfa
deleteBtn := request.PostFormValue("delete_btn")
reportBtn := request.PostFormValue("report_btn")
editBtn := request.PostFormValue("edit_btn")
doEdit := request.PostFormValue("doedit")
if action == "" && deleteBtn != "Delete" && reportBtn != "Report" && editBtn != "Edit" {
if action == "" && deleteBtn != "Delete" && reportBtn != "Report" && editBtn != "Edit" && doEdit != "1" {
http.Redirect(writer, request, path.Join(config.SiteWebfolder, "/"), http.StatusFound)
return
}
@ -202,6 +202,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request, data interfa
}
if editBtn == "Edit" {
var err error
if len(postsArr) == 0 {
serveErrorPage(writer, "You need to select one post to edit.")
return
@ -209,40 +210,99 @@ func utilHandler(writer http.ResponseWriter, request *http.Request, data interfa
serveErrorPage(writer, "You can only edit one post at a time.")
return
} else {
passwordMD5 := md5Sum(password)
rank := getStaffRank()
if passwordMD5 == "" && rank == 0 {
rank := getStaffRank(request)
if password == "" && rank == 0 {
serveErrorPage(writer, "Password required for post editing")
return
}
passwordMD5 := md5Sum(password)
var post PostTable
post.ID, _ = strconv.Atoi(postsArr[0])
post.BoardID, _ = strconv.Atoi(boardid)
stmt, err := db.Prepare("SELECT `parentid`,` password`,`message_raw` FROM `" + config.DBprefix + "posts` WHERE `id` = ? AND `deleted_timestamp` = ?")
if err != nil {
serveErrorPage(writer, handleError(1, err.Error()+"\n"))
}
defer closeStatement(stmt)
/* var post_edit_buffer bytes.Buffer
if err = renderTemplate(post_edit_tmpl, "post_edit", post_edit_buffer,
&Wrapper{IName: "boards_", Data: all_boards},
&Wrapper{IName: "sections_w", Data: all_sections},
&Wrapper{IName: "posts_w", Data: []interface{}{
PostTable{BoardID: board.ID},
}},
&Wrapper{IName: "op", Data: []interface{}{PostTable{}}},
&Wrapper{IName: "board", Data: []interface{}{board}},
if err = queryRowSQL("SELECT `parentid`,`name`,`tripcode`,`email`,`subject`,`password`,`message_raw` FROM `"+config.DBprefix+"posts` WHERE `id` = ? AND `boardid` = ? AND `deleted_timestamp` = ?",
[]interface{}{post.ID, post.BoardID, nilTimestamp},
[]interface{}{
&post.ParentID, &post.Name, &post.Tripcode, &post.Email, &post.Subject,
&post.Password, &post.MessageText},
); err != nil {
html += handleError(1, fmt.Sprintf("Failed building /%s/res/%d.html: %s", board.Dir, 0, err.Error())) + "<br />"
serveErrorPage(writer, handleError(0, err.Error()))
return
} */
}
if post.Password != passwordMD5 && rank == 0 {
serveErrorPage(writer, "Wrong password")
return
}
if err = post_edit_tmpl.Execute(writer, map[string]interface{}{
"config": config,
"post": post,
"referrer": request.Referer(),
}); err != nil {
serveErrorPage(writer, handleError(0, err.Error()))
return
}
}
}
if doEdit == "1" {
var postPassword string
postid, err := strconv.Atoi(request.FormValue("postid"))
if err != nil {
serveErrorPage(writer, handleError(0, "Invalid form data: %s", err.Error()))
return
}
boardid, err := strconv.Atoi(request.FormValue("boardid"))
if err != nil {
serveErrorPage(writer, handleError(0, "Invalid form data: %s", err.Error()))
return
}
if err = queryRowSQL("SELECT `password` FROM `"+config.DBprefix+"posts` WHERE `id` = ? AND `boardid` = ?",
[]interface{}{postid, boardid},
[]interface{}{&postPassword},
); err != nil {
serveErrorPage(writer, handleError(0, "Invalid form data: %s", err.Error()))
}
rank := getStaffRank(request)
if request.FormValue("password") != password && rank == 0 {
serveErrorPage(writer, "Wrong password")
return
}
board, err := getBoardFromID(boardid)
if err != nil {
serveErrorPage(writer, handleError(0, "Invalid form data: %s", err.Error()))
return
}
if _, err = execSQL("UPDATE `"+config.DBprefix+"posts` SET "+
"`email` = ?, `subject` = ?, `message` = ?, `message_raw` = ? WHERE `id` = ? AND `boardid` = ?",
request.FormValue("editemail"), request.FormValue("editsubject"), formatMessage(request.FormValue("editmsg")), request.FormValue("editmsg"),
postid, boardid,
); err != nil {
serveErrorPage(writer, handleError(0, "editing post: %s", err.Error()))
return
}
buildBoards(false, boardid)
if request.FormValue("parentid") == "0" {
http.Redirect(writer, request, "/"+board.Dir+"/res/"+strconv.Itoa(postid)+".html", http.StatusFound)
} else {
http.Redirect(writer, request, "/"+board.Dir+"/res/"+request.FormValue("parentid")+".html#"+strconv.Itoa(postid), http.StatusFound)
}
return
}
if deleteBtn == "Delete" {
// Delete a post or thread
writer.Header().Add("Content-Type", "text/plain")
passwordMD5 := md5Sum(password)
rank := getStaffRank()
rank := getStaffRank(request)
if passwordMD5 == "" && rank == 0 {
serveErrorPage(writer, "Password required for post deletion")
@ -264,7 +324,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request, data interfa
); err == sql.ErrNoRows {
//the post has already been deleted
writer.Header().Add("refresh", "4;url="+request.Referer())
fmt.Fprintf(writer, "%d has already been deleted or is a post in a deleted thread.\n<br />", post.ID)
fmt.Fprintf(writer, "%d has already been deleted or is a post in a deleted thread.\n", post.ID)
continue
} else if err != nil {
serveErrorPage(writer, handleError(1, err.Error()+"\n"))
@ -313,7 +373,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request, data interfa
buildThreadPages(&postBoard)
writer.Header().Add("refresh", "4;url="+request.Referer())
fmt.Fprintf(writer, "Attached image from %d deleted successfully<br />\n<meta http-equiv=\"refresh\" content=\"1;url=/"+board+"/\">", post.ID)
fmt.Fprintf(writer, "Attached image from %d deleted successfully\n", post.ID) //<br />\n<meta http-equiv=\"refresh\" content=\"1;url=/"+board+"/\">", post.ID)
} else {
// delete the post
if _, err = execSQL(
@ -362,7 +422,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request, data interfa
buildBoards(false, post.BoardID)
writer.Header().Add("refresh", "4;url="+request.Referer())
fmt.Fprintf(writer, "%d deleted successfully\n<br />", post.ID)
fmt.Fprintf(writer, "%d deleted successfully\n", post.ID)
}
}
}

View file

@ -4,8 +4,6 @@ import (
"errors"
"fmt"
"html"
"net/http"
"os"
"strconv"
"strings"
"text/template"
@ -57,8 +55,12 @@ var funcMap = template.FuncMap{
println(v, i...)
return ""
},
"stringAppend": func(a, b string) string {
return a + b
"stringAppend": func(strings ...string) string {
var appended string
for _, str := range strings {
appended += str
}
return appended
},
"truncateMessage": func(msg string, limit int, max_lines int) string {
var truncated bool
@ -89,12 +91,10 @@ var funcMap = template.FuncMap{
if len(msg) > limit {
if ellipsis {
return msg[:limit] + "..."
} else {
return msg[:limit]
}
} else {
return msg
return msg[:limit]
}
return msg
},
"escapeString": func(a string) string {
return html.EscapeString(a)
@ -129,9 +129,9 @@ var funcMap = template.FuncMap{
} else if name[len(name)-4:] == "webm" {
name = name[:len(name)-4] + "jpg"
}
ext_begin := strings.LastIndex(name, ".")
new_name := name[:ext_begin] + "t." + getFileExtension(name)
return new_name
extBegin := strings.LastIndex(name, ".")
newName := name[:extBegin] + "t." + getFileExtension(name)
return newName
},
"getUploadType": func(name string) string {
extension := getFileExtension(name)
@ -177,26 +177,29 @@ var funcMap = template.FuncMap{
var (
banpage_tmpl *template.Template
errorpage_tmpl *template.Template
front_page_tmpl *template.Template
global_header_tmpl *template.Template
img_boardpage_tmpl *template.Template
img_threadpage_tmpl *template.Template
img_post_form_tmpl *template.Template
post_edit_tmpl *template.Template
manage_header_tmpl *template.Template
manage_boards_tmpl *template.Template
manage_config_tmpl *template.Template
front_page_tmpl *template.Template
// manage_bans_tmpl *template.Template
manage_boards_tmpl *template.Template
manage_config_tmpl *template.Template
manage_header_tmpl *template.Template
post_edit_tmpl *template.Template
)
func loadTemplate(files ...string) (*template.Template, error) {
if len(files) == 0 {
return nil, errors.New("ERROR: no files named in call to loadTemplate")
return nil, errors.New("No files named in call to loadTemplate")
}
var templates []string
for i, file := range files {
templates = append(templates, file)
files[i] = config.TemplateDir + "/" + files[i]
}
return template.New(templates[0]).Funcs(funcMap).ParseFiles(files...)
}
@ -212,6 +215,11 @@ func initTemplates() error {
return templateError("banpage.html", err)
}
errorpage_tmpl, err = loadTemplate("error.html")
if err != nil {
return templateError("error.html", err)
}
global_header_tmpl, err = loadTemplate("global_header.html")
if err != nil {
return templateError("global_header.html", err)
@ -227,16 +235,16 @@ func initTemplates() error {
return templateError("img_threadpage.html", err)
}
/* post_edit_tmpl, err = loadTemplate("post_edit_tmpl", "post_edit.html")
post_edit_tmpl, err = loadTemplate("post_edit.html", "img_header.html", "global_footer.html")
if err != nil {
return templateError("img_threadpage.html", err)
} */
manage_header_tmpl, err = loadTemplate("manage_header.html")
if err != nil {
return templateError("manage_header.html", err)
}
/* manage_bans_tmpl, err = loadTemplate("manage_bans.html")
if err != nil {
return templateError("manage_bans.html", err)
} */
manage_boards_tmpl, err = loadTemplate("manage_boards.html")
if err != nil {
return templateError("manage_boards.html", err)
@ -247,21 +255,14 @@ func initTemplates() error {
return templateError("manage_config.html", err)
}
manage_header_tmpl, err = loadTemplate("manage_header.html")
if err != nil {
return templateError("manage_header.html", err)
}
front_page_tmpl, err = loadTemplate("front.html", "global_footer.html")
if err != nil {
return templateError("front.html", err)
}
return nil
}
func getStyleLinks(w http.ResponseWriter, stylesheet string) {
styles_map := make(map[int]string)
for i := 0; i < len(config.Styles_img); i++ {
styles_map[i] = config.Styles_img[i]
}
if err := manage_header_tmpl.Execute(w, config); err != nil {
handleError(0, customError(err))
os.Exit(2)
}
}

View file

@ -328,16 +328,6 @@ func getSectionArr(where string) (sections []interface{}, err error) {
return
}
func getCookie(name string) *http.Cookie {
numCookies := len(cookies)
for c := 0; c < numCookies; c++ {
if cookies[c].Name == name {
return cookies[c]
}
}
return nil
}
func getCountryCode(ip string) (string, error) {
if config.EnableGeoIP && config.GeoIPDBlocation != "" {
gi, err := libgeo.Load(config.GeoIPDBlocation)

View file

@ -1,13 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Error :c</title>
<meta charset="UTF-8">
<title>{{.ErrorTitle}}</title>
</head>
<body>
<center>
<h1>Error!</h1>
<h2>{ERRORTEXT}</h2>
</center>
<h1>{{.ErrorHeader}}</h1>
<img src="{{.ErrorImage}}" border="0" alt="">
<p>{{.ErrorText}}</p>
<hr><address>http://gochan.org powered by Gochan {{.config.Version}}</address>
</body>
</html>
</html>

38
templates/post_edit.html Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Post</title>
<script type="text/javascript" src="/javascript/jquery-3.3.1.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}}";
</script>
<script type="text/javascript" src="/javascript/gochan.js"></script>
<script type="text/javascript" src="/javascript/manage.js"></script>
<link rel="stylesheet" href="/css/global/img.css" />
{{range $_, $style := .config.Styles_img}}
<link rel="{{if not (isStyleDefault_img $style)}}alternate {{end}}stylesheet" href="/css/{{$style}}/img.css" />{{end}}
<link rel="shortcut icon" href="/favicon.png" />
</head>
<body>
<header>
<h1>Edit post</h1><br />
<div class="subtitle"><a href="{{.referrer}}">Return</a></div>
</header>
<form action="/util" method="POST" id="edit-form">
<input name="postid" type="hidden" value="{{.post.ID}}" />
<input name="boardid" type="hidden" value="{{.post.BoardID}}" />
<input name="parentid" type="hidden" value="{{.post.ParentID}}" />
<input name="password" type="hidden" value="{{.post.Password}}" />
<input name="doedit" type="hidden" value="1" />
<table id="postbox-static" align="center">
<tr><th class="postblock">Name</th><td>{{stringAppend .post.Name "#" .post.Tripcode}}</td></tr>
<tr><th class="postblock">Email</th><td>{{.post.Email}}</td></tr>
<tr><th class="postblock">Subject</th><td><input type="text" name="editsubject" maxlength="100" size="30" autocomplete="off" value="{{.post.Subject}}"/><input type="submit" value="{{with .op}}Reply{{else}}Post{{end}}"/></td></tr>
<tr><th class="postblock">Message</th><td><textarea rows="4" cols="48" name="editmsg" id="editmsg">{{.post.MessageText}}</textarea></td></tr>
</table>
</form><br />
{{template "global_footer.html" .}}