mirror of
https://github.com/Eggbertx/gochan.git
synced 2025-08-17 10:56:24 -07:00
refactor/reorganize gochan's source code into subpackages
Also use Go version 1.11 in vagrant for module support
This commit is contained in:
parent
76f2934f14
commit
d1292bd9fe
51 changed files with 4412 additions and 4085 deletions
22
.gitignore
vendored
22
.gitignore
vendored
|
@ -1,14 +1,14 @@
|
|||
gochan*
|
||||
lib/
|
||||
log/
|
||||
releases/
|
||||
vagrant/.vagrant/
|
||||
vagrant/*.log
|
||||
html/boards.json
|
||||
html/index.html
|
||||
html/test*
|
||||
html/javascript/consts.js
|
||||
templates/override
|
||||
/gochan*
|
||||
/lib/
|
||||
/log/
|
||||
/releases/
|
||||
/vagrant/.vagrant/
|
||||
/vagrant/*.log
|
||||
/html/boards.json
|
||||
/html/index.html
|
||||
/html/test*
|
||||
/html/javascript/consts.js
|
||||
/templates/override
|
||||
**/*.bak
|
||||
*.log
|
||||
__debug_bin
|
13
Makefile
13
Makefile
|
@ -6,6 +6,7 @@ ifeq (${GCOS_NAME},darwin)
|
|||
GCOS_NAME=macos
|
||||
endif
|
||||
|
||||
GOCHAN_PKG=github.com/gochan-org/gochan
|
||||
DOCUMENT_ROOT=/srv/gochan
|
||||
RELEASE_NAME=${BIN}-v${VERSION}_${GCOS_NAME}
|
||||
RELEASE_DIR=releases/${RELEASE_NAME}
|
||||
|
@ -30,15 +31,16 @@ DOCUMENT_ROOT_FILES= \
|
|||
hittheroad*
|
||||
|
||||
build:
|
||||
GOOS=${GCOS} ${GO_CMD} -gcflags=${GCFLAGS} -asmflags=${ASMFLAGS} -ldflags="${LDFLAGS} -w -s" ./src
|
||||
GOOS=${GCOS} ${GO_CMD} -gcflags=${GCFLAGS} -asmflags=${ASMFLAGS} -ldflags="${LDFLAGS} -w -s" ./cmd/gochan
|
||||
|
||||
build-debug:
|
||||
GOOS=${GCOS} ${GO_CMD} -gcflags="${GCFLAGS} -l -N" -asmflags=${ASMFLAGS} -ldflags="${LDFLAGS}" ./src
|
||||
GOOS=${GCOS} ${GO_CMD} -gcflags="${GCFLAGS} -l -N" -asmflags=${ASMFLAGS} -ldflags="${LDFLAGS}" ./cmd/gochan
|
||||
|
||||
clean:
|
||||
rm -f ${BIN}
|
||||
rm -f ${BIN}.exe
|
||||
rm -rf releases/
|
||||
rm -rf ~/go/src/${GOCHAN_PKG}
|
||||
|
||||
dependencies:
|
||||
go get -v \
|
||||
|
@ -52,7 +54,7 @@ dependencies:
|
|||
github.com/frustra/bbcode \
|
||||
github.com/mattn/go-sqlite3 \
|
||||
github.com/tdewolff/minify \
|
||||
gopkg.in/mojocn/base64Captcha.v1
|
||||
github.com/mojocn/base64Captcha
|
||||
|
||||
install:
|
||||
mkdir -p \
|
||||
|
@ -124,7 +126,6 @@ else
|
|||
tar -C releases -zcvf ${RELEASE_DIR}.tar.gz ${RELEASE_NAME}
|
||||
endif
|
||||
|
||||
.PHONY: sass
|
||||
sass:
|
||||
sass --no-source-map sass:html/css
|
||||
|
||||
|
@ -132,4 +133,6 @@ sass-minified:
|
|||
sass --style compressed --no-source-map sass:html/css
|
||||
|
||||
test:
|
||||
go test -v ./src
|
||||
go test -v ./src
|
||||
|
||||
.PHONY: subpackages ${INTERNALS} sass
|
|
@ -46,7 +46,7 @@ html/javascript
|
|||
"@
|
||||
|
||||
function build {
|
||||
$cmd = "& go build -v -gcflags=-trimpath=$PWD -asmflags=-trimpath=$PWD -ldflags=`"$LDFLAGS`" -o $BINEXE ./src "
|
||||
$cmd = "& go build -v -gcflags=-trimpath=$PWD -asmflags=-trimpath=$PWD -ldflags=`"$LDFLAGS`" -o $BINEXE ./cmd/gochan "
|
||||
$env:GOOS=$platform; Invoke-Expression $cmd
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ function dependencies {
|
|||
github.com/frustra/bbcode `
|
||||
github.com/mattn/go-sqlite3 `
|
||||
github.com/tdewolff/minify `
|
||||
gopkg.in/mojocn/base64Captcha.v1
|
||||
github.com/mojocn/base64Captcha
|
||||
}
|
||||
|
||||
function dockerImage {
|
||||
|
|
91
cmd/gochan/main.go
Normal file
91
cmd/gochan/main.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/gochan-org/gochan/pkg/posting"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var (
|
||||
versionStr string
|
||||
stdFatal = gclog.LStdLog | gclog.LFatal
|
||||
)
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
gclog.Print(gclog.LStdLog, "Cleaning up")
|
||||
gcsql.ExecSQL("DROP TABLE DBPREFIXsessions")
|
||||
gcsql.Close()
|
||||
}()
|
||||
|
||||
gclog.Printf(gclog.LStdLog, "Starting gochan v%s", versionStr)
|
||||
config.InitConfig(versionStr)
|
||||
|
||||
gcsql.ConnectToDB(
|
||||
config.Config.DBhost, config.Config.DBtype, config.Config.DBname,
|
||||
config.Config.DBusername, config.Config.DBpassword, config.Config.DBprefix)
|
||||
parseCommandLine()
|
||||
gcutil.InitMinifier()
|
||||
|
||||
posting.InitCaptcha()
|
||||
|
||||
sc := make(chan os.Signal, 1)
|
||||
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||
posting.InitPosting()
|
||||
go initServer()
|
||||
<-sc
|
||||
}
|
||||
|
||||
func parseCommandLine() {
|
||||
var newstaff string
|
||||
var delstaff string
|
||||
var rank int
|
||||
var err error
|
||||
flag.StringVar(&newstaff, "newstaff", "", "<newusername>:<newpassword>")
|
||||
flag.StringVar(&delstaff, "delstaff", "", "<username>")
|
||||
flag.IntVar(&rank, "rank", 0, "New staff member rank, to be used with -newstaff or -delstaff")
|
||||
flag.Parse()
|
||||
|
||||
if newstaff != "" {
|
||||
arr := strings.Split(newstaff, ":")
|
||||
if len(arr) < 2 || delstaff != "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
gclog.Printf(gclog.LStdLog|gclog.LStaffLog, "Creating new staff: %q, with password: %q and rank: %d from command line", arr[0], arr[1], rank)
|
||||
if err = gcsql.NewStaff(arr[0], arr[1], rank); err != nil {
|
||||
gclog.Print(stdFatal, err.Error())
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
if delstaff != "" {
|
||||
if newstaff != "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
gclog.Printf(gclog.LStdLog, "Are you sure you want to delete the staff account %q? [y/N]: ", delstaff)
|
||||
var answer string
|
||||
fmt.Scanln(&answer)
|
||||
answer = strings.ToLower(answer)
|
||||
if answer == "y" || answer == "yes" {
|
||||
if err = gcsql.DeleteStaff(delstaff); err != nil {
|
||||
gclog.Printf(stdFatal, "Error deleting %q: %s", delstaff, err.Error())
|
||||
}
|
||||
} else {
|
||||
gclog.Print(stdFatal, "Not deleting.")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,31 +9,35 @@ import (
|
|||
"net/http/fcgi"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/building"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gctemplates"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/gochan-org/gochan/pkg/manage"
|
||||
"github.com/gochan-org/gochan/pkg/posting"
|
||||
"github.com/gochan-org/gochan/pkg/serverutil"
|
||||
)
|
||||
|
||||
var (
|
||||
server *GochanServer
|
||||
referrerRegex *regexp.Regexp
|
||||
server *gochanServer
|
||||
)
|
||||
|
||||
type GochanServer struct {
|
||||
type gochanServer struct {
|
||||
namespaces map[string]func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func (s GochanServer) AddNamespace(basePath string, namespaceFunction func(http.ResponseWriter, *http.Request)) {
|
||||
s.namespaces[basePath] = namespaceFunction
|
||||
}
|
||||
|
||||
func (s GochanServer) serveFile(writer http.ResponseWriter, request *http.Request) {
|
||||
filePath := path.Join(config.DocumentRoot, request.URL.Path)
|
||||
func (s gochanServer) serveFile(writer http.ResponseWriter, request *http.Request) {
|
||||
filePath := path.Join(config.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
|
||||
serveNotFound(writer, request)
|
||||
serverutil.ServeNotFound(writer, request)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -42,7 +46,7 @@ func (s GochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
|
|||
if results.IsDir() {
|
||||
//check to see if one of the specified index pages exists
|
||||
var found bool
|
||||
for _, value := range config.FirstPage {
|
||||
for _, value := range config.Config.FirstPage {
|
||||
newPath := path.Join(filePath, value)
|
||||
_, err := os.Stat(newPath)
|
||||
if err == nil {
|
||||
|
@ -52,12 +56,12 @@ func (s GochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
|
|||
}
|
||||
}
|
||||
if !found {
|
||||
serveNotFound(writer, request)
|
||||
serverutil.ServeNotFound(writer, request)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
//the file exists, and is not a folder
|
||||
extension = strings.ToLower(getFileExtension(request.URL.Path))
|
||||
extension = strings.ToLower(gcutil.GetFileExtension(request.URL.Path))
|
||||
switch extension {
|
||||
case "png":
|
||||
writer.Header().Add("Content-Type", "image/png")
|
||||
|
@ -88,7 +92,7 @@ func (s GochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
|
|||
writer.Header().Add("Content-Type", "text/html")
|
||||
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
|
||||
}
|
||||
gclog.Printf(lAccessLog, "Success: 200 from %s @ %s", getRealIP(request), request.URL.Path)
|
||||
gclog.Printf(gclog.LAccessLog, "Success: 200 from %s @ %s", gcutil.GetRealIP(request), request.URL.Path)
|
||||
}
|
||||
|
||||
// serve the index page
|
||||
|
@ -98,31 +102,9 @@ func (s GochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
|
|||
writer.Write(fileBytes)
|
||||
}
|
||||
|
||||
func serveNotFound(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||
writer.WriteHeader(404)
|
||||
errorPage, err := ioutil.ReadFile(config.DocumentRoot + "/error/404.html")
|
||||
if err != nil {
|
||||
writer.Write([]byte("Requested page not found, and /error/404.html not found"))
|
||||
} else {
|
||||
minifyWriter(writer, errorPage, "text/html")
|
||||
}
|
||||
gclog.Printf(lAccessLog, "Error: 404 Not Found from %s @ %s", getRealIP(request), request.URL.Path)
|
||||
}
|
||||
|
||||
func serveErrorPage(writer http.ResponseWriter, err string) {
|
||||
minifyTemplate(errorpageTmpl, map[string]interface{}{
|
||||
"config": config,
|
||||
"ErrorTitle": "Error :c",
|
||||
// "ErrorImage": "/error/lol 404.gif",
|
||||
"ErrorHeader": "Error",
|
||||
"ErrorText": err,
|
||||
}, writer, "text/html")
|
||||
}
|
||||
|
||||
func (s GochanServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
func (s gochanServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
for name, namespaceFunction := range s.namespaces {
|
||||
if request.URL.Path == config.SiteWebfolder+name {
|
||||
if request.URL.Path == config.Config.SiteWebfolder+name {
|
||||
// writer.WriteHeader(200)
|
||||
namespaceFunction(writer, request)
|
||||
return
|
||||
|
@ -132,69 +114,45 @@ func (s GochanServer) ServeHTTP(writer http.ResponseWriter, request *http.Reques
|
|||
}
|
||||
|
||||
func initServer() {
|
||||
listener, err := net.Listen("tcp", config.ListenIP+":"+strconv.Itoa(config.Port))
|
||||
listener, err := net.Listen("tcp", config.Config.ListenIP+":"+strconv.Itoa(config.Config.Port))
|
||||
if err != nil {
|
||||
gclog.Printf(lErrorLog|lStdLog|lFatal,
|
||||
"Failed listening on %s:%d: %s", config.ListenIP, config.Port, err.Error())
|
||||
gclog.Printf(gclog.LErrorLog|gclog.LStdLog|gclog.LFatal,
|
||||
"Failed listening on %s:%d: %s", config.Config.ListenIP, config.Config.Port, err.Error())
|
||||
}
|
||||
server = new(GochanServer)
|
||||
server = new(gochanServer)
|
||||
server.namespaces = make(map[string]func(http.ResponseWriter, *http.Request))
|
||||
|
||||
// Check if Akismet API key is usable at startup.
|
||||
if err = checkAkismetAPIKey(config.AkismetAPIKey); err != nil {
|
||||
config.AkismetAPIKey = ""
|
||||
if err = serverutil.CheckAkismetAPIKey(config.Config.AkismetAPIKey); err != nil {
|
||||
config.Config.AkismetAPIKey = ""
|
||||
}
|
||||
|
||||
// Compile regex for checking referrers.
|
||||
referrerRegex = regexp.MustCompile(config.DomainRegex)
|
||||
|
||||
server.AddNamespace("banned", banHandler)
|
||||
server.AddNamespace("captcha", serveCaptcha)
|
||||
server.AddNamespace("manage", callManageFunction)
|
||||
server.AddNamespace("post", makePost)
|
||||
server.AddNamespace("util", utilHandler)
|
||||
server.AddNamespace("example", func(writer http.ResponseWriter, request *http.Request) {
|
||||
server.namespaces["banned"] = posting.BanHandler
|
||||
server.namespaces["captcha"] = posting.ServeCaptcha
|
||||
server.namespaces["manage"] = manage.CallManageFunction
|
||||
server.namespaces["post"] = posting.MakePost
|
||||
server.namespaces["util"] = utilHandler
|
||||
server.namespaces["example"] = func(writer http.ResponseWriter, request *http.Request) {
|
||||
if writer != nil {
|
||||
http.Redirect(writer, request, "https://www.youtube.com/watch?v=dQw4w9WgXcQ", http.StatusFound)
|
||||
}
|
||||
})
|
||||
// eventually plugins will be able to register new namespaces. Or they will be restricted to something like /plugin
|
||||
}
|
||||
// Eventually plugins will be able to register new namespaces (assuming they ever get it working on Windows or macOS)
|
||||
// or they will be restricted to something like /plugin
|
||||
|
||||
if config.UseFastCGI {
|
||||
if config.Config.UseFastCGI {
|
||||
err = fcgi.Serve(listener, server)
|
||||
} else {
|
||||
err = http.Serve(listener, server)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
gclog.Print(lErrorLog|lStdLog|lFatal,
|
||||
gclog.Print(gclog.LErrorLog|gclog.LStdLog|gclog.LFatal,
|
||||
"Error initializing server: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func getRealIP(request *http.Request) string {
|
||||
// HTTP_CF_CONNECTING_IP > X-Forwarded-For > RemoteAddr
|
||||
if request.Header.Get("HTTP_CF_CONNECTING_IP") != "" {
|
||||
return request.Header.Get("HTTP_CF_CONNECTING_IP")
|
||||
}
|
||||
if request.Header.Get("X-Forwarded-For") != "" {
|
||||
return request.Header.Get("X-Forwarded-For")
|
||||
}
|
||||
remoteHost, _, err := net.SplitHostPort(request.RemoteAddr)
|
||||
if err != nil {
|
||||
return request.RemoteAddr
|
||||
}
|
||||
return remoteHost
|
||||
}
|
||||
|
||||
func validReferrer(request *http.Request) bool {
|
||||
if config.DebugMode {
|
||||
return true
|
||||
}
|
||||
return referrerRegex.MatchString(request.Referer())
|
||||
}
|
||||
|
||||
// register /util handler
|
||||
// handles requests to /util
|
||||
func utilHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
action := request.FormValue("action")
|
||||
password := request.FormValue("password")
|
||||
|
@ -207,7 +165,8 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
doEdit := request.PostFormValue("doedit")
|
||||
|
||||
if action == "" && deleteBtn != "Delete" && reportBtn != "Report" && editBtn != "Edit" && doEdit != "1" {
|
||||
http.Redirect(writer, request, path.Join(config.SiteWebfolder, "/"), http.StatusFound)
|
||||
gclog.Printf(gclog.LAccessLog, "Received invalid /util request from %q", request.Host)
|
||||
http.Redirect(writer, request, path.Join(config.Config.SiteWebfolder, "/"), http.StatusFound)
|
||||
return
|
||||
}
|
||||
var postsArr []string
|
||||
|
@ -220,39 +179,39 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
if editBtn == "Edit" {
|
||||
var err error
|
||||
if len(postsArr) == 0 {
|
||||
serveErrorPage(writer, "You need to select one post to edit.")
|
||||
serverutil.ServeErrorPage(writer, "You need to select one post to edit.")
|
||||
return
|
||||
} else if len(postsArr) > 1 {
|
||||
serveErrorPage(writer, "You can only edit one post at a time.")
|
||||
serverutil.ServeErrorPage(writer, "You can only edit one post at a time.")
|
||||
return
|
||||
} else {
|
||||
rank := getStaffRank(request)
|
||||
rank := manage.GetStaffRank(request)
|
||||
if password == "" && rank == 0 {
|
||||
serveErrorPage(writer, "Password required for post editing")
|
||||
serverutil.ServeErrorPage(writer, "Password required for post editing")
|
||||
return
|
||||
}
|
||||
passwordMD5 := md5Sum(password)
|
||||
passwordMD5 := gcutil.Md5Sum(password)
|
||||
|
||||
var post Post
|
||||
var post gcsql.Post
|
||||
postid, _ := strconv.Atoi(postsArr[0])
|
||||
post, err = GetSpecificPost(postid, true)
|
||||
post, err = gcsql.GetSpecificPost(postid, true)
|
||||
if err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error getting post information: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if post.Password != passwordMD5 && rank == 0 {
|
||||
serveErrorPage(writer, "Wrong password")
|
||||
serverutil.ServeErrorPage(writer, "Wrong password")
|
||||
return
|
||||
}
|
||||
|
||||
if err = postEditTmpl.Execute(writer, map[string]interface{}{
|
||||
"config": config,
|
||||
if err = gctemplates.PostEdit.Execute(writer, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"post": post,
|
||||
"referrer": request.Referer(),
|
||||
}); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error executing edit post template: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
@ -262,43 +221,44 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
var password string
|
||||
postid, err := strconv.Atoi(request.FormValue("postid"))
|
||||
if err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Invalid form data: ", err.Error()))
|
||||
return
|
||||
}
|
||||
boardid, err := strconv.Atoi(request.FormValue("boardid"))
|
||||
if err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Invalid form data: ", err.Error()))
|
||||
return
|
||||
}
|
||||
password, err = GetPostPassword(postid)
|
||||
password, err = gcsql.GetPostPassword(postid)
|
||||
if err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Invalid form data: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
rank := getStaffRank(request)
|
||||
rank := manage.GetStaffRank(request)
|
||||
if request.FormValue("password") != password && rank == 0 {
|
||||
serveErrorPage(writer, "Wrong password")
|
||||
serverutil.ServeErrorPage(writer, "Wrong password")
|
||||
return
|
||||
}
|
||||
|
||||
var board Board
|
||||
var board gcsql.Board
|
||||
if err = board.PopulateData(boardid); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Invalid form data: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err = UpdatePost(postid, request.FormValue("editemail"), request.FormValue("editsubject"),
|
||||
formatMessage(request.FormValue("editmsg")), request.FormValue("editmsg")); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog, "Unable to edit post: ", err.Error()))
|
||||
if err = gcsql.UpdatePost(postid, request.FormValue("editemail"), request.FormValue("editsubject"),
|
||||
posting.FormatMessage(request.FormValue("editmsg")), request.FormValue("editmsg")); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Unable to edit post: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
buildBoards(boardid)
|
||||
building.BuildBoards(boardid)
|
||||
if request.FormValue("parentid") == "0" {
|
||||
http.Redirect(writer, request, "/"+board.Dir+"/res/"+strconv.Itoa(postid)+".html", http.StatusFound)
|
||||
} else {
|
||||
|
@ -311,27 +271,28 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
if deleteBtn == "Delete" {
|
||||
// Delete a post or thread
|
||||
writer.Header().Add("Content-Type", "text/plain")
|
||||
passwordMD5 := md5Sum(password)
|
||||
rank := getStaffRank(request)
|
||||
passwordMD5 := gcutil.Md5Sum(password)
|
||||
rank := manage.GetStaffRank(request)
|
||||
|
||||
if passwordMD5 == "" && rank == 0 {
|
||||
serveErrorPage(writer, "Password required for post deletion")
|
||||
serverutil.ServeErrorPage(writer, "Password required for post deletion")
|
||||
return
|
||||
}
|
||||
|
||||
for _, checkedPostID := range postsArr {
|
||||
var post Post
|
||||
var post gcsql.Post
|
||||
var err error
|
||||
post.ID, _ = strconv.Atoi(checkedPostID)
|
||||
post.BoardID, _ = strconv.Atoi(boardid)
|
||||
|
||||
if post, err = GetSpecificPost(post.ID, true); err == sql.ErrNoRows {
|
||||
if post, err = gcsql.GetSpecificPost(post.ID, true); 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", post.ID)
|
||||
continue
|
||||
} else if err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog, "Error deleting post: ", err.Error()))
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error deleting post: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -343,30 +304,33 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
if fileOnly {
|
||||
fileName := post.Filename
|
||||
if fileName != "" && fileName != "deleted" {
|
||||
if err = DeleteFilesFromPost(post.ID); err != nil {
|
||||
serveErrorPage(writer, err.Error())
|
||||
if err = gcsql.DeleteFilesFromPost(post.ID); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error deleting files from post: ", err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
_board, _ := GetBoardFromID(post.BoardID)
|
||||
buildBoardPages(&_board)
|
||||
postBoard, _ := GetSpecificPost(post.ID, true)
|
||||
buildThreadPages(&postBoard)
|
||||
_board, _ := gcsql.GetBoardFromID(post.BoardID)
|
||||
building.BuildBoardPages(&_board)
|
||||
postBoard, _ := gcsql.GetSpecificPost(post.ID, true)
|
||||
building.BuildThreadPages(&postBoard)
|
||||
|
||||
writer.Header().Add("refresh", "4;url="+request.Referer())
|
||||
fmt.Fprintf(writer, "Attached image from %d deleted successfully\n", post.ID)
|
||||
} else {
|
||||
// delete the post
|
||||
if err = DeletePost(post.ID); err != nil {
|
||||
serveErrorPage(writer, err.Error())
|
||||
if err = gcsql.DeletePost(post.ID); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error deleting post: ", err.Error()))
|
||||
}
|
||||
if post.ParentID == 0 {
|
||||
os.Remove(path.Join(config.DocumentRoot, board, "/res/"+strconv.Itoa(post.ID)+".html"))
|
||||
os.Remove(path.Join(
|
||||
config.Config.DocumentRoot, board, "/res/"+strconv.Itoa(post.ID)+".html"))
|
||||
} else {
|
||||
_board, _ := GetBoardFromID(post.BoardID)
|
||||
buildBoardPages(&_board)
|
||||
_board, _ := gcsql.GetBoardFromID(post.BoardID)
|
||||
building.BuildBoardPages(&_board)
|
||||
}
|
||||
buildBoards(post.BoardID)
|
||||
building.BuildBoards(post.BoardID)
|
||||
|
||||
writer.Header().Add("refresh", "4;url="+request.Referer())
|
||||
fmt.Fprintf(writer, "%d deleted successfully\n", post.ID)
|
18
go.mod
Normal file
18
go.mod
Normal file
|
@ -0,0 +1,18 @@
|
|||
module github.com/gochan-org/gochan
|
||||
|
||||
go 1.11
|
||||
|
||||
require (
|
||||
github.com/aquilax/tripcode v1.0.0
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/frustra/bbcode v0.0.0-20180807171629-48be21ce690c
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/lib/pq v1.4.0
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/mojocn/base64Captcha v1.3.1
|
||||
github.com/nranchev/go-libGeoIP v0.0.0-20170629073846-d6d4a9a4c7e8 // indirect
|
||||
github.com/tdewolff/minify v2.3.6+incompatible
|
||||
github.com/tdewolff/parse v2.3.4+incompatible // indirect
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0
|
||||
)
|
42
go.sum
Normal file
42
go.sum
Normal file
|
@ -0,0 +1,42 @@
|
|||
github.com/aquilax/tripcode v1.0.0 h1:uPW1T2brVth0t6YiDPlouncHXFGneflsAvkh4zEBN58=
|
||||
github.com/aquilax/tripcode v1.0.0/go.mod h1:Tucn/H6BM/DEmxzj/tnmR7Vs/NV/bgCKo8Wi0yXrtzQ=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/frustra/bbcode v0.0.0-20180807171629-48be21ce690c h1:qo8GNweP9HFxZJ5S7GSsiqut9k4H0MM29Ne/Mnp2BHc=
|
||||
github.com/frustra/bbcode v0.0.0-20180807171629-48be21ce690c/go.mod h1:0QBxkXxN+o4FyZgLI9FHY/oUizheze3+bNY/kgCKL+4=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/lib/pq v1.4.0 h1:TmtCFbH+Aw0AixwyttznSMQDgbR5Yed/Gg6S8Funrhc=
|
||||
github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mojocn/base64Captcha v1.2.2 h1:NTFnThPVrb3tR66JO/N8/ZHsyFrNc7ho+xRpxBUEIlo=
|
||||
github.com/mojocn/base64Captcha v1.2.2/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ8bRfPWUuz/iY=
|
||||
github.com/mojocn/base64Captcha v1.3.1 h1:2Wbkt8Oc8qjmNJ5GyOfSo4tgVQPsbKMftqASnq8GlT0=
|
||||
github.com/mojocn/base64Captcha v1.3.1/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ8bRfPWUuz/iY=
|
||||
github.com/nranchev/go-libGeoIP v0.0.0-20170629073846-d6d4a9a4c7e8 h1:IeI4GVfCGrGx4tZROZ/ju+nO9rKpgKJ7o4XmQgAM/2g=
|
||||
github.com/nranchev/go-libGeoIP v0.0.0-20170629073846-d6d4a9a4c7e8/go.mod h1:CSS25pAr1pT+qxFdpFZIJFHraF4zZfZYeFirlVvLXb4=
|
||||
github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo=
|
||||
github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs=
|
||||
github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxpP46ghf0Qzh38=
|
||||
github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ=
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs=
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc h1:ZGI/fILM2+ueot/UixBSoj9188jCAxVHEZEGhqq67I4=
|
||||
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
239
pkg/building/boards.go
Normal file
239
pkg/building/boards.go
Normal file
|
@ -0,0 +1,239 @@
|
|||
package building
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gctemplates"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
// BuildBoardPages builds the pages for the board archive.
|
||||
// `board` is a Board 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 *gcsql.Board) (html string) {
|
||||
err := gctemplates.InitTemplates("boardpage")
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
var currentPageFile *os.File
|
||||
var threads []interface{}
|
||||
var threadPages [][]interface{}
|
||||
var stickiedThreads []interface{}
|
||||
var nonStickiedThreads []interface{}
|
||||
var opPosts []gcsql.Post
|
||||
|
||||
// Get all top level posts for the board.
|
||||
if opPosts, err = gcsql.GetTopPosts(board.ID, true); err != nil {
|
||||
return html + gclog.Printf(gclog.LErrorLog,
|
||||
"Error getting OP posts for /%s/: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
// For each top level post, start building a Thread struct
|
||||
for _, op := range opPosts {
|
||||
var thread gcsql.Thread
|
||||
var postsInThread []gcsql.Post
|
||||
|
||||
var replyCount, err = gcsql.GetReplyCount(op.ID)
|
||||
if err == nil {
|
||||
return html + gclog.Printf(gclog.LErrorLog,
|
||||
"Error getting replies to /%s/%d: %s",
|
||||
board.Dir, op.ID, err.Error()) + "<br />"
|
||||
}
|
||||
thread.NumReplies = replyCount
|
||||
|
||||
fileCount, err := gcsql.GetReplyFileCount(op.ID)
|
||||
if err == nil {
|
||||
return html + gclog.Printf(gclog.LErrorLog,
|
||||
"Error getting file count to /%s/%d: %s",
|
||||
board.Dir, op.ID, err.Error()) + "<br />"
|
||||
}
|
||||
thread.NumImages = fileCount
|
||||
|
||||
thread.OP = op
|
||||
|
||||
var numRepliesOnBoardPage int
|
||||
|
||||
if op.Stickied {
|
||||
// If the thread is stickied, limit replies on the archive page to the
|
||||
// configured value for stickied threads.
|
||||
numRepliesOnBoardPage = config.Config.StickyRepliesOnBoardPage
|
||||
} else {
|
||||
// Otherwise, limit the replies to the configured value for normal threads.
|
||||
numRepliesOnBoardPage = config.Config.RepliesOnBoardPage
|
||||
}
|
||||
|
||||
postsInThread, err = gcsql.GetExistingRepliesLimitedRev(op.ID, numRepliesOnBoardPage)
|
||||
if err != nil {
|
||||
return html + gclog.Printf(gclog.LErrorLog,
|
||||
"Error getting posts in /%s/%d: %s",
|
||||
board.Dir, op.ID, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
var reversedPosts []gcsql.Post
|
||||
for i := len(postsInThread); i > 0; i-- {
|
||||
reversedPosts = append(reversedPosts, postsInThread[i-1])
|
||||
}
|
||||
|
||||
if len(postsInThread) > 0 {
|
||||
// Store the posts to show on board page
|
||||
//thread.BoardReplies = postsInThread
|
||||
thread.BoardReplies = reversedPosts
|
||||
|
||||
// Count number of images on board page
|
||||
imageCount := 0
|
||||
for _, reply := range postsInThread {
|
||||
if reply.Filesize != 0 {
|
||||
imageCount++
|
||||
}
|
||||
}
|
||||
// Then calculate number of omitted images.
|
||||
thread.OmittedImages = thread.NumImages - imageCount
|
||||
}
|
||||
|
||||
// Add thread struct to appropriate list
|
||||
if op.Stickied {
|
||||
stickiedThreads = append(stickiedThreads, thread)
|
||||
} else {
|
||||
nonStickiedThreads = append(nonStickiedThreads, thread)
|
||||
}
|
||||
}
|
||||
|
||||
gcutil.DeleteMatchingFiles(path.Join(config.Config.DocumentRoot, board.Dir), "\\d.html$")
|
||||
// Order the threads, stickied threads first, then nonstickied threads.
|
||||
threads = append(stickiedThreads, nonStickiedThreads...)
|
||||
|
||||
// If there are no posts on the board
|
||||
if len(threads) == 0 {
|
||||
board.CurrentPage = 1
|
||||
// Open board.html for writing to the first page.
|
||||
boardPageFile, err := os.OpenFile(path.Join(config.Config.DocumentRoot, board.Dir, "board.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
return html + gclog.Printf(gclog.LErrorLog,
|
||||
"Failed opening /%s/board.html: %s",
|
||||
board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
// Render board page template to the file,
|
||||
// packaging the board/section list, threads, and board info
|
||||
if err = gcutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"boards": gcsql.AllBoards,
|
||||
"sections": gcsql.AllSections,
|
||||
"threads": threads,
|
||||
"board": board,
|
||||
}, boardPageFile, "text/html"); err != nil {
|
||||
return html + gclog.Printf(gclog.LErrorLog,
|
||||
"Failed building /%s/: %s",
|
||||
board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
html += "/" + board.Dir + "/ built successfully.\n"
|
||||
return
|
||||
}
|
||||
|
||||
// Create the archive pages.
|
||||
threadPages = paginate(config.Config.ThreadsPerPage, threads)
|
||||
board.NumPages = len(threadPages)
|
||||
|
||||
// Create array of page wrapper objects, and open the file.
|
||||
pagesArr := make([]map[string]interface{}, board.NumPages)
|
||||
|
||||
catalogJSONFile, err := os.OpenFile(path.Join(config.Config.DocumentRoot, board.Dir, "catalog.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
return gclog.Printf(gclog.LErrorLog,
|
||||
"Failed opening /%s/catalog.json: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
defer catalogJSONFile.Close()
|
||||
|
||||
currentBoardPage := board.CurrentPage
|
||||
for _, pageThreads := range threadPages {
|
||||
board.CurrentPage++
|
||||
var currentPageFilepath string
|
||||
pageFilename := strconv.Itoa(board.CurrentPage) + ".html"
|
||||
currentPageFilepath = path.Join(config.Config.DocumentRoot, board.Dir, pageFilename)
|
||||
currentPageFile, err = os.OpenFile(currentPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
html += gclog.Printf(gclog.LErrorLog, "Failed opening /%s/%s: %s",
|
||||
board.Dir, pageFilename, err.Error()) + "<br />"
|
||||
continue
|
||||
}
|
||||
defer currentPageFile.Close()
|
||||
|
||||
// Render the boardpage template
|
||||
if err = gcutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"boards": gcsql.AllBoards,
|
||||
"sections": gcsql.AllSections,
|
||||
"threads": pageThreads,
|
||||
"board": board,
|
||||
"posts": []interface{}{
|
||||
gcsql.Post{BoardID: board.ID},
|
||||
},
|
||||
}, currentPageFile, "text/html"); err != nil {
|
||||
return html + gclog.Printf(gclog.LErrorLog,
|
||||
"Failed building /%s/ boardpage: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
if board.CurrentPage == 1 {
|
||||
boardPage := path.Join(config.Config.DocumentRoot, board.Dir, "board.html")
|
||||
os.Remove(boardPage)
|
||||
if err = syscall.Symlink(currentPageFilepath, boardPage); !os.IsExist(err) && err != nil {
|
||||
html += gclog.Printf(gclog.LErrorLog, "Failed building /%s/: %s",
|
||||
board.Dir, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Collect up threads for this page.
|
||||
pageMap := make(map[string]interface{})
|
||||
pageMap["page"] = board.CurrentPage
|
||||
pageMap["threads"] = pageThreads
|
||||
pagesArr = append(pagesArr, pageMap)
|
||||
}
|
||||
board.CurrentPage = currentBoardPage
|
||||
|
||||
catalogJSON, err := json.Marshal(pagesArr)
|
||||
if err != nil {
|
||||
return html + gclog.Print(gclog.LErrorLog, "Failed to marshal to JSON: ", err.Error()) + "<br />"
|
||||
}
|
||||
if _, err = catalogJSONFile.Write(catalogJSON); err != nil {
|
||||
return html + gclog.Printf(gclog.LErrorLog,
|
||||
"Failed writing /%s/catalog.json: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
html += "/" + board.Dir + "/ built successfully."
|
||||
return
|
||||
}
|
||||
|
||||
// BuildBoards builds the specified board IDs, or all boards if no arguments are passed
|
||||
// The return value is a string of HTML with debug information produced by the build process.
|
||||
func BuildBoards(which ...int) (html string) {
|
||||
var boards []gcsql.Board
|
||||
var err error
|
||||
if which == nil {
|
||||
boards = gcsql.AllBoards
|
||||
} else {
|
||||
for b, id := range which {
|
||||
boards = append(boards, gcsql.Board{})
|
||||
if err = boards[b].PopulateData(id); err != nil {
|
||||
return gclog.Printf(gclog.LErrorLog, "Error getting board information (ID: %d)", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(boards) == 0 {
|
||||
return "No boards to build."
|
||||
}
|
||||
|
||||
for _, board := range boards {
|
||||
if err = board.Build(false, true); err != nil {
|
||||
return gclog.Printf(gclog.LErrorLog,
|
||||
"Error building /%s/: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
html += "Built /" + board.Dir + "/ successfully."
|
||||
}
|
||||
return
|
||||
}
|
152
pkg/building/building.go
Normal file
152
pkg/building/building.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
package building
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gctemplates"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
// BuildFrontPage builds the front page using templates/front.html
|
||||
func BuildFrontPage() string {
|
||||
err := gctemplates.InitTemplates("front")
|
||||
if err != nil {
|
||||
return gclog.Print(gclog.LErrorLog, "Error loading front page template: ", err.Error())
|
||||
}
|
||||
os.Remove(path.Join(config.Config.DocumentRoot, "index.html"))
|
||||
frontFile, err := os.OpenFile(path.Join(config.Config.DocumentRoot, "index.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
return gclog.Print(gclog.LErrorLog, "Failed opening front page for writing: ", err.Error()) + "<br />"
|
||||
}
|
||||
defer frontFile.Close()
|
||||
|
||||
var recentPostsArr []gcsql.RecentPost
|
||||
recentPostsArr, err = gcsql.GetRecentPostsGlobal(config.Config.MaxRecentPosts, !config.Config.RecentPostsWithNoFile)
|
||||
if err == nil {
|
||||
return gclog.Print(gclog.LErrorLog, "Failed loading recent posts: "+err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
for b := range gcsql.AllBoards {
|
||||
if gcsql.AllBoards[b].Section == 0 {
|
||||
gcsql.AllBoards[b].Section = 1
|
||||
}
|
||||
}
|
||||
|
||||
if err = gcutil.MinifyTemplate(gctemplates.FrontPage, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"sections": gcsql.AllSections,
|
||||
"boards": gcsql.AllBoards,
|
||||
"recent_posts": recentPostsArr,
|
||||
}, frontFile, "text/html"); err != nil {
|
||||
return gclog.Print(gclog.LErrorLog, "Failed executing front page template: "+err.Error()) + "<br />"
|
||||
}
|
||||
return "Front page rebuilt successfully."
|
||||
}
|
||||
|
||||
// BuildBoardListJSON generates a JSON file with info about the boards
|
||||
func BuildBoardListJSON() (html string) {
|
||||
boardListFile, err := os.OpenFile(path.Join(config.Config.DocumentRoot, "boards.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
return gclog.Print(gclog.LErrorLog, "Failed opening boards.json for writing: ", err.Error()) + "<br />"
|
||||
}
|
||||
defer boardListFile.Close()
|
||||
|
||||
boardsMap := map[string][]gcsql.Board{
|
||||
"boards": []gcsql.Board{},
|
||||
}
|
||||
|
||||
// Our cooldowns are site-wide currently.
|
||||
cooldowns := gcsql.BoardCooldowns{
|
||||
NewThread: config.Config.NewThreadDelay,
|
||||
Reply: config.Config.ReplyDelay,
|
||||
ImageReply: config.Config.ReplyDelay}
|
||||
|
||||
for b := range gcsql.AllBoards {
|
||||
gcsql.AllBoards[b].Cooldowns = cooldowns
|
||||
boardsMap["boards"] = append(boardsMap["boards"], gcsql.AllBoards[b])
|
||||
}
|
||||
|
||||
boardJSON, err := json.Marshal(boardsMap)
|
||||
if err != nil {
|
||||
return gclog.Print(gclog.LErrorLog, "Failed to create boards.json: ", err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
if _, err = gcutil.MinifyWriter(boardListFile, boardJSON, "application/json"); err != nil {
|
||||
return gclog.Print(gclog.LErrorLog, "Failed writing boards.json file: ", err.Error()) + "<br />"
|
||||
}
|
||||
return "Board list JSON rebuilt successfully.<br />"
|
||||
}
|
||||
|
||||
// BuildJS minifies (if enabled) gochan.js and consts.js (the latter is built from a template)
|
||||
func BuildJS() string {
|
||||
// minify gochan.js (if enabled)
|
||||
gochanMinJSPath := path.Join(config.Config.DocumentRoot, "javascript", "gochan.min.js")
|
||||
gochanMinJSFile, err := os.OpenFile(gochanMinJSPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return gclog.Printf(gclog.LErrorLog, "Error opening %q for writing: %s",
|
||||
gochanMinJSPath, err.Error()) + "<br />"
|
||||
}
|
||||
defer gochanMinJSFile.Close()
|
||||
|
||||
gochanJSPath := path.Join(config.Config.DocumentRoot, "javascript", "gochan.js")
|
||||
gochanJSBytes, err := ioutil.ReadFile(gochanJSPath)
|
||||
if err != nil {
|
||||
return gclog.Printf(gclog.LErrorLog, "Error opening %q for writing: %s",
|
||||
gochanJSPath, err.Error()) + "<br />"
|
||||
}
|
||||
if _, err := gcutil.MinifyWriter(gochanMinJSFile, gochanJSBytes, "text/javascript"); err != nil {
|
||||
config.Config.UseMinifiedGochanJS = false
|
||||
return gclog.Printf(gclog.LErrorLog, "Error minifying %q: %s:",
|
||||
gochanMinJSPath, err.Error()) + "<br />"
|
||||
}
|
||||
config.Config.UseMinifiedGochanJS = true
|
||||
|
||||
// build consts.js from template
|
||||
if err = gctemplates.InitTemplates("js"); err != nil {
|
||||
return gclog.Print(gclog.LErrorLog, "Error loading consts.js template: ", err.Error())
|
||||
}
|
||||
constsJSPath := path.Join(config.Config.DocumentRoot, "javascript", "consts.js")
|
||||
constsJSFile, err := os.OpenFile(constsJSPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return gclog.Printf(gclog.LErrorLog, "Error opening %q for writing: %s",
|
||||
constsJSPath, err.Error()) + "<br />"
|
||||
}
|
||||
defer constsJSFile.Close()
|
||||
|
||||
if err = gcutil.MinifyTemplate(gctemplates.JsConsts, config.Config, constsJSFile, "text/javascript"); err != nil {
|
||||
return gclog.Printf(gclog.LErrorLog, "Error building %q: %s",
|
||||
constsJSPath, err.Error()) + "<br />"
|
||||
}
|
||||
return "Built gochan.min.js and consts.js successfully.<br />"
|
||||
}
|
||||
|
||||
// paginate returns a 2d array of a specified interface from a 1d array passed in,
|
||||
// with a specified number of values per array in the 2d array.
|
||||
// interfaceLength is the number of interfaces per array in the 2d array (e.g, threads per page)
|
||||
// interf is the array of interfaces to be split up.
|
||||
func paginate(interfaceLength int, interf []interface{}) [][]interface{} {
|
||||
// paginatedInterfaces = the finished interface array
|
||||
// numArrays = the current number of arrays (before remainder overflow)
|
||||
// interfacesRemaining = if greater than 0, these are the remaining interfaces
|
||||
// that will be added to the super-interface
|
||||
|
||||
var paginatedInterfaces [][]interface{}
|
||||
numArrays := len(interf) / interfaceLength
|
||||
interfacesRemaining := len(interf) % interfaceLength
|
||||
currentInterface := 0
|
||||
for l := 0; l < numArrays; l++ {
|
||||
paginatedInterfaces = append(paginatedInterfaces,
|
||||
interf[currentInterface:currentInterface+interfaceLength])
|
||||
currentInterface += interfaceLength
|
||||
}
|
||||
if interfacesRemaining > 0 {
|
||||
paginatedInterfaces = append(paginatedInterfaces, interf[len(interf)-interfacesRemaining:])
|
||||
}
|
||||
return paginatedInterfaces
|
||||
}
|
108
pkg/building/threads.go
Normal file
108
pkg/building/threads.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package building
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gctemplates"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
// BuildThreads builds thread(s) given a boardid, or if all = false, also given a threadid.
|
||||
// if all is set to true, ignore which, otherwise, which = build only specified boardid
|
||||
// TODO: make it variadic
|
||||
func BuildThreads(all bool, boardid, threadid int) error {
|
||||
var threads []gcsql.Post
|
||||
var err error
|
||||
if all {
|
||||
threads, err = gcsql.GetTopPostsNoSort(boardid)
|
||||
} else {
|
||||
var post gcsql.Post
|
||||
post, err = gcsql.GetSpecificTopPost(threadid)
|
||||
threads = []gcsql.Post{post}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, op := range threads {
|
||||
if err = BuildThreadPages(&op); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildThreadPages builds the pages for a thread given by a Post object.
|
||||
func BuildThreadPages(op *gcsql.Post) error {
|
||||
err := gctemplates.InitTemplates("threadpage")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var replies []gcsql.Post
|
||||
var threadPageFile *os.File
|
||||
var board gcsql.Board
|
||||
if err = board.PopulateData(op.BoardID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
replies, err = gcsql.GetExistingReplies(op.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error building thread %d: %s", op.ID, err.Error())
|
||||
}
|
||||
os.Remove(path.Join(config.Config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".html"))
|
||||
|
||||
var repliesInterface []interface{}
|
||||
for _, reply := range replies {
|
||||
repliesInterface = append(repliesInterface, reply)
|
||||
}
|
||||
|
||||
threadPageFilepath := path.Join(config.Config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".html")
|
||||
threadPageFile, err = os.OpenFile(threadPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed opening /%s/res/%d.html: %s", board.Dir, op.ID, err.Error())
|
||||
}
|
||||
|
||||
// render thread page
|
||||
if err = gcutil.MinifyTemplate(gctemplates.ThreadPage, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"boards": gcsql.AllBoards,
|
||||
"board": board,
|
||||
"sections": gcsql.AllSections,
|
||||
"posts": replies,
|
||||
"op": op,
|
||||
}, threadPageFile, "text/html"); err != nil {
|
||||
return fmt.Errorf("Failed building /%s/res/%d threadpage: %s", board.Dir, op.ID, err.Error())
|
||||
}
|
||||
|
||||
// Put together the thread JSON
|
||||
threadJSONFile, err := os.OpenFile(path.Join(config.Config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed opening /%s/res/%d.json: %s", board.Dir, op.ID, err.Error())
|
||||
}
|
||||
defer threadJSONFile.Close()
|
||||
|
||||
threadMap := make(map[string][]gcsql.Post)
|
||||
|
||||
// Handle the OP, of type *Post
|
||||
threadMap["posts"] = []gcsql.Post{*op}
|
||||
|
||||
// Iterate through each reply, which are of type Post
|
||||
threadMap["posts"] = append(threadMap["posts"], replies...)
|
||||
threadJSON, err := json.Marshal(threadMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to marshal to JSON: %s", err.Error())
|
||||
}
|
||||
|
||||
if _, err = threadJSONFile.Write(threadJSON); err != nil {
|
||||
return fmt.Errorf("Failed writing /%s/res/%d.json: %s", board.Dir, op.ID, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
267
pkg/config/config.go
Normal file
267
pkg/config/config.go
Normal file
|
@ -0,0 +1,267 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
)
|
||||
|
||||
var Config GochanConfig
|
||||
|
||||
// Style represents a theme (Pipes, Dark, etc)
|
||||
type Style struct {
|
||||
Name string
|
||||
Filename string
|
||||
}
|
||||
|
||||
// GochanConfig stores crucial info and is read from/written to gochan.json
|
||||
type GochanConfig struct {
|
||||
ListenIP string
|
||||
Port int
|
||||
FirstPage []string
|
||||
Username string
|
||||
UseFastCGI bool
|
||||
DebugMode bool `description:"Disables several spam/browser checks that can cause problems when hosting an instance locally."`
|
||||
|
||||
DocumentRoot string
|
||||
TemplateDir string
|
||||
LogDir string
|
||||
|
||||
DBtype string
|
||||
DBhost string
|
||||
DBname string
|
||||
DBusername string
|
||||
DBpassword string
|
||||
DBprefix string
|
||||
|
||||
Lockdown bool `description:"Disables posting." default:"unchecked"`
|
||||
LockdownMessage string `description:"Message displayed when someone tries to post while the site is on lockdown."`
|
||||
Sillytags []string `description:"List of randomly selected staff tags separated by line, e.g. <span style=\"color: red;\">## Mod</span>, to be randomly assigned to posts if UseSillytags is checked. Don't include the \"## \""`
|
||||
UseSillytags bool `description:"Use Sillytags" default:"unchecked"`
|
||||
Modboard string `description:"A super secret clubhouse board that only staff can view/post to." default:"staff"`
|
||||
|
||||
SiteName string `description:"The name of the site that appears in the header of the front page." default:"Gochan"`
|
||||
SiteSlogan string `description:"The text that appears below SiteName on the home page"`
|
||||
SiteHeaderURL string `description:"To be honest, I'm not even sure what this does. It'll probably be removed later."`
|
||||
SiteWebfolder string `description:"The HTTP root appearing in the browser (e.g. https://gochan.org/<SiteWebFolder>" default:"/"`
|
||||
SiteDomain string `description:"The server's domain (duh). Do not edit this unless you know what you are doing or BAD THINGS WILL HAPPEN!" default:"127.0.0.1" critical:"true"`
|
||||
|
||||
Styles []Style `description:"List of styles (one per line) that should be accessed online at <SiteWebFolder>/css/<Style>/"`
|
||||
DefaultStyle string `description:"Filename of the default Style. This should appear in the list above or bad things might happen."`
|
||||
|
||||
AllowDuplicateImages bool `description:"Disabling this will cause gochan to reject a post if the image has already been uploaded for another post.<br />This may end up being removed or being made board-specific in the future." default:"checked"`
|
||||
AllowVideoUploads bool `description:"Allows users to upload .webm videos. <br />This may end up being removed or being made board-specific in the future."`
|
||||
NewThreadDelay int `description:"The amount of time in seconds that is required before an IP can make a new thread.<br />This may end up being removed or being made board-specific in the future." default:"30"`
|
||||
ReplyDelay int `description:"Same as the above, but for replies." default:"7"`
|
||||
MaxLineLength int `description:"Any line in a post that exceeds this will be split into two (or more) lines.<br />I'm not really sure why this is here, so it may end up being removed." default:"150"`
|
||||
ReservedTrips []string `description:"Secure tripcodes (!!Something) can be reserved here.<br />Each reservation should go on its own line and should look like this:<br />TripPassword1##Tripcode1<br />TripPassword2##Tripcode2"`
|
||||
|
||||
ThumbWidth int `description:"OP thumbnails use this as their max width.<br />To keep the aspect ratio, the image will be scaled down to the ThumbWidth or ThumbHeight, whichever is larger." default:"200"`
|
||||
ThumbHeight int `description:"OP thumbnails use this as their max height.<br />To keep the aspect ratio, the image will be scaled down to the ThumbWidth or ThumbHeight, whichever is larger." default:"200"`
|
||||
ThumbWidthReply int `description:"Same as ThumbWidth and ThumbHeight but for reply images." default:"125"`
|
||||
ThumbHeightReply int `description:"Same as ThumbWidth and ThumbHeight but for reply images." default:"125"`
|
||||
ThumbWidthCatalog int `description:"Same as ThumbWidth and ThumbHeight but for catalog images." default:"50"`
|
||||
ThumbHeightCatalog int `description:"Same as ThumbWidth and ThumbHeight but for catalog images." default:"50"`
|
||||
|
||||
ThreadsPerPage int `default:"15"`
|
||||
RepliesOnBoardPage int `description:"Number of replies to a thread to show on the board page." default:"3"`
|
||||
StickyRepliesOnBoardPage int `description:"Same as above for stickied threads." default:"1"`
|
||||
BanColors []string `description:"Colors to be used for public ban messages (e.g. USER WAS BANNED FOR THIS POST).<br />Each entry should be on its own line, and should look something like this:<br />username1:#FF0000<br />username2:#FAF00F<br />username3:blue<br />Invalid entries/nonexistent usernames will show a warning and use the default red."`
|
||||
BanMsg string `description:"The default public ban message." default:"USER WAS BANNED FOR THIS POST"`
|
||||
EmbedWidth int `description:"The width for inline/expanded webm videos." default:"200"`
|
||||
EmbedHeight int `description:"The height for inline/expanded webm videos." default:"164"`
|
||||
ExpandButton bool `description:"If checked, adds [Embed] after a Youtube, Vimeo, etc link to toggle an inline video frame." default:"checked"`
|
||||
ImagesOpenNewTab bool `description:"If checked, thumbnails will open the respective image/video in a new tab instead of expanding them." default:"unchecked"`
|
||||
MakeURLsHyperlinked bool `description:"If checked, URLs in posts will be turned into a hyperlink. If unchecked, ExpandButton and NewTabOnOutlinks are ignored." default:"checked"`
|
||||
NewTabOnOutlinks bool `description:"If checked, links to external sites will open in a new tab." default:"checked"`
|
||||
DisableBBcode bool `description:"If checked, gochan will not compile bbcode into HTML" default:"unchecked"`
|
||||
|
||||
MinifyHTML bool `description:"If checked, gochan will minify html files when building" default:"checked"`
|
||||
MinifyJS bool `description:"If checked, gochan will minify js and json files when building" default:"checked"`
|
||||
UseMinifiedGochanJS bool `json:"-"`
|
||||
|
||||
DateTimeFormat string `description:"The format used for dates. See <a href=\"https://golang.org/pkg/time/#Time.Format\">here</a> for more info." default:"Mon, January 02, 2006 15:04 PM"`
|
||||
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
|
||||
UseCaptcha bool `description:"If checked, a captcha will be generated"`
|
||||
CaptchaWidth int `description:"Width of the generated captcha image" default:"240"`
|
||||
CaptchaHeight int `description:"Height of the generated captcha image" default:"80"`
|
||||
CaptchaMinutesExpire int `description:"Number of minutes before a user has to enter a new CAPTCHA before posting. If <1 they have to submit one for every post." default:"15"`
|
||||
EnableGeoIP bool `description:"If checked, this enables the usage of GeoIP for posts." default:"checked"`
|
||||
GeoIPDBlocation string `description:"Specifies the location of the GeoIP database file. If you're using CloudFlare, you can set it to cf to rely on CloudFlare for GeoIP information." default:"/usr/share/GeoIP/GeoIP.dat"`
|
||||
MaxRecentPosts int `description:"The maximum number of posts to show on the Recent Posts list on the front page." default:"3"`
|
||||
RecentPostsWithNoFile bool `description:"If checked, recent posts with no image/upload are shown on the front page (as well as those with images" default:"unchecked"`
|
||||
EnableAppeals bool `description:"If checked, allow banned users to appeal their bans.<br />This will likely be removed (permanently allowing appeals) or made board-specific in the future." default:"checked"`
|
||||
MaxLogDays int `description:"The maximum number of days to keep messages in the moderation/staff log file."`
|
||||
RandomSeed string `critical:"true"`
|
||||
|
||||
TimeZone int `json:"-"`
|
||||
Version *GochanVersion `json:"-"`
|
||||
}
|
||||
|
||||
func (cfg *GochanConfig) checkString(val, defaultVal string, critical bool, msg string) string {
|
||||
if val == "" {
|
||||
val = defaultVal
|
||||
flags := gclog.LStdLog | gclog.LErrorLog
|
||||
if critical {
|
||||
flags |= gclog.LFatal
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (cfg *GochanConfig) checkInt(val, defaultVal int, critical bool, msg string) int {
|
||||
if val == 0 {
|
||||
val = defaultVal
|
||||
flags := gclog.LStdLog | gclog.LErrorLog
|
||||
if critical {
|
||||
flags |= gclog.LFatal
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func InitConfig(versionStr string) {
|
||||
cfgPath := findResource("gochan.json", "/etc/gochan/gochan.json")
|
||||
if cfgPath == "" {
|
||||
fmt.Println("gochan.json not found")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
jfile, err := ioutil.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading %s: %s\n", cfgPath, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(jfile, &Config); err != nil {
|
||||
fmt.Printf("Error parsing %s: %s\n", cfgPath, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
Config.LogDir = findResource(Config.LogDir, "log", "/var/log/gochan/")
|
||||
if err = gclog.InitLogs(
|
||||
path.Join(Config.LogDir, "access.log"),
|
||||
path.Join(Config.LogDir, "error.log"),
|
||||
path.Join(Config.LogDir, "staff.log"),
|
||||
Config.DebugMode); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
Config.checkString(Config.ListenIP, "", true, "ListenIP not set in gochan.json, halting.")
|
||||
|
||||
if Config.Port == 0 {
|
||||
Config.Port = 80
|
||||
}
|
||||
|
||||
if len(Config.FirstPage) == 0 {
|
||||
Config.FirstPage = []string{"index.html", "board.html", "firstrun.html"}
|
||||
}
|
||||
|
||||
Config.Username = Config.checkString(Config.Username, "gochan", false, "Username not set in gochan.json, using 'gochan' as default")
|
||||
Config.DocumentRoot = Config.checkString(Config.DocumentRoot, "gochan", true, "DocumentRoot not set in gochan.json, halting.")
|
||||
|
||||
wd, wderr := os.Getwd()
|
||||
if wderr == nil {
|
||||
_, staterr := os.Stat(path.Join(wd, Config.DocumentRoot, "css"))
|
||||
if staterr == nil {
|
||||
Config.DocumentRoot = path.Join(wd, Config.DocumentRoot)
|
||||
}
|
||||
}
|
||||
|
||||
Config.TemplateDir = Config.checkString(
|
||||
findResource(Config.TemplateDir, "templates", "/usr/local/share/gochan/templates/", "/usr/share/gochan/templates/"),
|
||||
"", true, "Unable to locate template directory, halting.")
|
||||
|
||||
Config.checkString(Config.DBtype, "", true, "DBtype not set in gochan.json, halting (currently supported values: mysql,postgresql,sqlite3)")
|
||||
Config.checkString(Config.DBhost, "", true, "DBhost not set in gochan.json, halting.")
|
||||
Config.DBname = Config.checkString(Config.DBname, "gochan", false,
|
||||
"DBname not set in gochan.json, setting to 'gochan'")
|
||||
|
||||
Config.checkString(Config.DBusername, "", true, "DBusername not set in gochan, halting.")
|
||||
Config.checkString(Config.DBpassword, "", true, "DBpassword not set in gochan, halting.")
|
||||
Config.LockdownMessage = Config.checkString(Config.LockdownMessage,
|
||||
"The administrator has temporarily disabled posting. We apologize for the inconvenience", false, "")
|
||||
|
||||
Config.checkString(Config.SiteName, "", true, "SiteName not set in gochan.json, halting.")
|
||||
Config.checkString(Config.SiteDomain, "", true, "SiteName not set in gochan.json, halting.")
|
||||
|
||||
if Config.SiteWebfolder == "" {
|
||||
gclog.Print(gclog.LErrorLog|gclog.LStdLog, "SiteWebFolder not set in gochan.json, using / as default.")
|
||||
} else if string(Config.SiteWebfolder[0]) != "/" {
|
||||
Config.SiteWebfolder = "/" + Config.SiteWebfolder
|
||||
}
|
||||
if Config.SiteWebfolder[len(Config.SiteWebfolder)-1:] != "/" {
|
||||
Config.SiteWebfolder += "/"
|
||||
}
|
||||
|
||||
if Config.Styles == nil {
|
||||
gclog.Print(gclog.LErrorLog|gclog.LStdLog|gclog.LFatal, "Styles not set in gochan.json, halting.")
|
||||
}
|
||||
|
||||
Config.DefaultStyle = Config.checkString(Config.DefaultStyle, Config.Styles[0].Filename, false, "")
|
||||
|
||||
Config.NewThreadDelay = Config.checkInt(Config.NewThreadDelay, 30, false, "")
|
||||
Config.ReplyDelay = Config.checkInt(Config.ReplyDelay, 7, false, "")
|
||||
Config.MaxLineLength = Config.checkInt(Config.MaxLineLength, 150, false, "")
|
||||
//ReservedTrips string //eventually this will be map[string]string
|
||||
|
||||
Config.ThumbWidth = Config.checkInt(Config.ThumbWidth, 200, false, "")
|
||||
Config.ThumbHeight = Config.checkInt(Config.ThumbHeight, 200, false, "")
|
||||
Config.ThumbWidthReply = Config.checkInt(Config.ThumbWidthReply, 125, false, "")
|
||||
Config.ThumbHeightReply = Config.checkInt(Config.ThumbHeightReply, 125, false, "")
|
||||
Config.ThumbWidthCatalog = Config.checkInt(Config.ThumbWidthCatalog, 50, false, "")
|
||||
Config.ThumbHeightCatalog = Config.checkInt(Config.ThumbHeightCatalog, 50, false, "")
|
||||
|
||||
Config.ThreadsPerPage = Config.checkInt(Config.ThreadsPerPage, 10, false, "")
|
||||
Config.RepliesOnBoardPage = Config.checkInt(Config.RepliesOnBoardPage, 3, false, "")
|
||||
Config.StickyRepliesOnBoardPage = Config.checkInt(Config.StickyRepliesOnBoardPage, 1, false, "")
|
||||
|
||||
/*config.BanColors, err = c.GetString("threads", "ban_colors") //eventually this will be map[string] string
|
||||
if err != nil {
|
||||
config.BanColors = "admin:#CC0000"
|
||||
}*/
|
||||
|
||||
Config.BanMsg = Config.checkString(Config.BanMsg, "(USER WAS BANNED FOR THIS POST)", false, "")
|
||||
Config.DateTimeFormat = Config.checkString(Config.DateTimeFormat, "Mon, January 02, 2006 15:04 PM", false, "")
|
||||
|
||||
Config.CaptchaWidth = Config.checkInt(Config.CaptchaWidth, 240, false, "")
|
||||
Config.CaptchaHeight = Config.checkInt(Config.CaptchaHeight, 80, false, "")
|
||||
|
||||
if Config.EnableGeoIP {
|
||||
if Config.GeoIPDBlocation == "" {
|
||||
gclog.Print(gclog.LErrorLog|gclog.LStdLog, "GeoIPDBlocation not set in gochan.json, disabling EnableGeoIP")
|
||||
Config.EnableGeoIP = false
|
||||
}
|
||||
}
|
||||
|
||||
if Config.MaxLogDays == 0 {
|
||||
Config.MaxLogDays = 15
|
||||
}
|
||||
|
||||
if Config.RandomSeed == "" {
|
||||
gclog.Print(gclog.LErrorLog|gclog.LStdLog, "RandomSeed not set in gochan.json, Generating a random one.")
|
||||
for i := 0; i < 8; i++ {
|
||||
num := rand.Intn(127-32) + 32
|
||||
Config.RandomSeed += fmt.Sprintf("%c", num)
|
||||
}
|
||||
configJSON, _ := json.MarshalIndent(Config, "", "\t")
|
||||
if err = ioutil.WriteFile(cfgPath, configJSON, 0777); err != nil {
|
||||
gclog.Printf(gclog.LErrorLog|gclog.LStdLog|gclog.LFatal, "Unable to write %s with randomly generated seed: %s", cfgPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
_, zoneOffset := time.Now().Zone()
|
||||
Config.TimeZone = zoneOffset / 60 / 60
|
||||
|
||||
// msgfmtr.InitBBcode()
|
||||
|
||||
Config.Version = ParseVersion(versionStr)
|
||||
Config.Version.Normalize()
|
||||
}
|
14
pkg/config/util.go
Normal file
14
pkg/config/util.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
// copied from gcutil to avoid import loop
|
||||
func findResource(paths ...string) string {
|
||||
var err error
|
||||
for _, filepath := range paths {
|
||||
if _, err = os.Stat(filepath); err == nil {
|
||||
return filepath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
// used for version parsing, printing, and comparison
|
||||
|
||||
package main
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
159
pkg/gclog/logging.go
Normal file
159
pkg/gclog/logging.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
package gclog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
var gclog *GcLogger
|
||||
|
||||
const (
|
||||
logTimeFmt = "2006/01/02 15:04:05 "
|
||||
logFileFlags = os.O_APPEND | os.O_CREATE | os.O_RDWR
|
||||
// LAccessLog should be used for incoming requests
|
||||
LAccessLog = 1 << iota
|
||||
// LErrorLog should be used for internal errors, not HTTP errors like 4xx
|
||||
LErrorLog
|
||||
// LStaffLog is comparable to LAccessLog for staff actions
|
||||
LStaffLog
|
||||
// LStdLog prints directly to standard output
|
||||
LStdLog
|
||||
// LFatal exits gochan after printing (used for fatal initialization errors)
|
||||
LFatal
|
||||
)
|
||||
|
||||
// GcLogger is used for printing to access, error, and staff logs
|
||||
type GcLogger struct {
|
||||
accessFile *os.File
|
||||
errorFile *os.File
|
||||
staffFile *os.File
|
||||
debug bool
|
||||
}
|
||||
|
||||
func (gcl *GcLogger) selectLogs(flags int) []*os.File {
|
||||
var logs []*os.File
|
||||
if flags&LAccessLog > 0 {
|
||||
logs = append(logs, gcl.accessFile)
|
||||
}
|
||||
if flags&LErrorLog > 0 {
|
||||
logs = append(logs, gcl.errorFile)
|
||||
}
|
||||
if flags&LStaffLog > 0 {
|
||||
logs = append(logs, gcl.staffFile)
|
||||
}
|
||||
if (flags&LStdLog > 0) || gcl.debug {
|
||||
logs = append(logs, os.Stdout)
|
||||
}
|
||||
return logs
|
||||
}
|
||||
|
||||
func (gcl *GcLogger) getPrefix() string {
|
||||
prefix := time.Now().Format(logTimeFmt)
|
||||
_, file, line, _ := runtime.Caller(2)
|
||||
prefix += fmt.Sprint(file, ":", line, ": ")
|
||||
|
||||
return prefix
|
||||
}
|
||||
|
||||
// Print is comparable to log.Print but takes binary flags
|
||||
func (gcl *GcLogger) Print(flags int, v ...interface{}) string {
|
||||
str := fmt.Sprint(v...)
|
||||
logs := gcl.selectLogs(flags)
|
||||
for _, l := range logs {
|
||||
if l == os.Stdout {
|
||||
io.WriteString(l, str+"\n")
|
||||
} else {
|
||||
io.WriteString(l, gcl.getPrefix()+str+"\n")
|
||||
}
|
||||
}
|
||||
if flags&LFatal > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// Printf is comparable to log.Logger.Printf but takes binary OR'd flags
|
||||
func (gcl *GcLogger) Printf(flags int, format string, v ...interface{}) string {
|
||||
str := fmt.Sprintf(format, v...)
|
||||
logs := gcl.selectLogs(flags)
|
||||
for _, l := range logs {
|
||||
if l == os.Stdout {
|
||||
io.WriteString(l, str+"\n")
|
||||
} else {
|
||||
io.WriteString(l, gcl.getPrefix()+str+"\n")
|
||||
}
|
||||
}
|
||||
if flags&LFatal > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// Println is comparable to log.Logger.Println but takes binary OR'd flags
|
||||
func (gcl *GcLogger) Println(flags int, v ...interface{}) string {
|
||||
str := fmt.Sprintln(v...)
|
||||
logs := gcl.selectLogs(flags)
|
||||
for _, l := range logs {
|
||||
if l == os.Stdout {
|
||||
io.WriteString(l, str+"\n")
|
||||
} else {
|
||||
io.WriteString(l, gcl.getPrefix()+str+"\n")
|
||||
}
|
||||
}
|
||||
if flags&LFatal > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// Close closes the log file handles
|
||||
func (gcl *GcLogger) Close() {
|
||||
gcl.accessFile.Close()
|
||||
gcl.errorFile.Close()
|
||||
gcl.staffFile.Close()
|
||||
}
|
||||
|
||||
// Print is comparable to log.Print but takes binary OR'd flags
|
||||
func Print(flags int, v ...interface{}) string {
|
||||
if gclog == nil {
|
||||
return ""
|
||||
}
|
||||
return gclog.Print(flags, v...)
|
||||
}
|
||||
|
||||
// Printf is comparable to log.Printf but takes binary OR'd flags
|
||||
func Printf(flags int, format string, v ...interface{}) string {
|
||||
if gclog == nil {
|
||||
return ""
|
||||
}
|
||||
return gclog.Printf(flags, format, v...)
|
||||
}
|
||||
|
||||
// Println is comparable to log.Println but takes binary OR'd flags
|
||||
func Println(flags int, v ...interface{}) string {
|
||||
if gclog == nil {
|
||||
return ""
|
||||
}
|
||||
return gclog.Println(flags, v...)
|
||||
}
|
||||
|
||||
// InitLogs initializes the log files to be used by gochan
|
||||
func InitLogs(accessLogPath, errorLogPath, staffLogPath string, debugMode bool) error {
|
||||
gclog = &GcLogger{debug: debugMode}
|
||||
var err error
|
||||
if gclog.accessFile, err = os.OpenFile(accessLogPath, logFileFlags, 0777); err != nil {
|
||||
return errors.New("Error loading " + accessLogPath + ": " + err.Error())
|
||||
}
|
||||
if gclog.errorFile, err = os.OpenFile(errorLogPath, logFileFlags, 0777); err != nil {
|
||||
return errors.New("Error loading " + errorLogPath + ": " + err.Error())
|
||||
}
|
||||
if gclog.staffFile, err = os.OpenFile(staffLogPath, logFileFlags, 0777); err != nil {
|
||||
return errors.New("Error loading " + staffLogPath + ": " + err.Error())
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
18
pkg/gclog/logging_test.go
Normal file
18
pkg/gclog/logging_test.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package gclog
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGochanLog(t *testing.T) {
|
||||
err := InitLogs("../access.log", "../error.log", "../staff.log", true)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
gclog.Print(LStdLog, "os.Stdout log")
|
||||
gclog.Print(LStdLog|LAccessLog|LErrorLog|LStaffLog, "all logs")
|
||||
gclog.Print(LAccessLog, "Access log")
|
||||
gclog.Print(LErrorLog, "Error log")
|
||||
gclog.Print(LStaffLog, "Staff log")
|
||||
gclog.Print(LAccessLog|LErrorLog, "Access and error log")
|
||||
gclog.Print(LAccessLog|LStaffLog|LFatal, "Fatal access and staff log")
|
||||
gclog.Print(LAccessLog, "This shouldn't be here")
|
||||
}
|
110
pkg/gcsql/connect.go
Normal file
110
pkg/gcsql/connect.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
var (
|
||||
db *sql.DB
|
||||
dbDriver string
|
||||
fatalSQLFlags = gclog.LErrorLog | gclog.LStdLog | gclog.LFatal
|
||||
nilTimestamp string
|
||||
sqlReplacer *strings.Replacer // used during SQL string preparation
|
||||
)
|
||||
|
||||
// ConnectToDB initializes the database connection and exits if there are any errors
|
||||
func ConnectToDB(host string, dbType string, dbName string, username string, password string, prefix string) {
|
||||
var err error
|
||||
var connStr string
|
||||
sqlReplacer = strings.NewReplacer(
|
||||
"DBNAME", dbName,
|
||||
"DBPREFIX", prefix,
|
||||
"\n", " ")
|
||||
gclog.Print(gclog.LStdLog|gclog.LErrorLog, "Initializing server...")
|
||||
|
||||
switch dbType {
|
||||
case "mysql":
|
||||
connStr = fmt.Sprintf("%s:%s@%s/%s?parseTime=true&collation=utf8mb4_unicode_ci",
|
||||
username, password, host, dbName)
|
||||
nilTimestamp = "0000-00-00 00:00:00"
|
||||
case "postgres":
|
||||
connStr = fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable",
|
||||
username, password, host, dbName)
|
||||
nilTimestamp = "0001-01-01 00:00:00"
|
||||
case "sqlite3":
|
||||
gclog.Print(gclog.LStdLog, "sqlite3 support is still flaky, consider using mysql or postgres")
|
||||
connStr = fmt.Sprintf("file:%s?mode=rwc&_auth&_auth_user=%s&_auth_pass=%s&cache=shared",
|
||||
host, username, password)
|
||||
nilTimestamp = "0001-01-01 00:00:00+00:00"
|
||||
default:
|
||||
gclog.Printf(fatalSQLFlags,
|
||||
`Invalid DBtype %q in gochan.json, valid values are "mysql", "postgres", and "sqlite3"`, dbType)
|
||||
}
|
||||
dbDriver = dbType
|
||||
if db, err = sql.Open(dbType, connStr); err != nil {
|
||||
gclog.Print(fatalSQLFlags, "Failed to connect to the database: ", err.Error())
|
||||
}
|
||||
|
||||
if err = initDB("initdb_" + dbType + ".sql"); err != nil {
|
||||
gclog.Print(fatalSQLFlags, "Failed initializing DB: ", err.Error())
|
||||
}
|
||||
|
||||
// Create generic "Main" section if one doesn't already exist
|
||||
if err = CreateDefaultSectionIfNotExist(); err != nil {
|
||||
gclog.Print(fatalSQLFlags, "Failed initializing DB: ", err.Error())
|
||||
}
|
||||
//TODO fix new install thing once it works with existing database
|
||||
// var sqlVersionStr string
|
||||
// isNewInstall := false
|
||||
// if err = queryRowSQL("SELECT value FROM DBPREFIXinfo WHERE name = 'version'",
|
||||
// []interface{}{}, []interface{}{&sqlVersionStr},
|
||||
// ); err == sql.ErrNoRows {
|
||||
// isNewInstall = true
|
||||
// } else if err != nil {
|
||||
// gclog.Print(lErrorLog|lStdLog|lFatal, "Failed initializing DB: ", err.Error())
|
||||
// }
|
||||
|
||||
err = CreateDefaultBoardIfNoneExist()
|
||||
if err != nil {
|
||||
gclog.Print(fatalSQLFlags, "Failed creating default board: ", err.Error())
|
||||
}
|
||||
err = CreateDefaultAdminIfNoStaff()
|
||||
if err != nil {
|
||||
gclog.Print(fatalSQLFlags, "Failed creating default admin account: ", err.Error())
|
||||
}
|
||||
//fix versioning thing
|
||||
}
|
||||
|
||||
func initDB(initFile string) error {
|
||||
var err error
|
||||
filePath := gcutil.FindResource(initFile,
|
||||
"/usr/local/share/gochan/"+initFile,
|
||||
"/usr/share/gochan/"+initFile)
|
||||
if filePath == "" {
|
||||
return fmt.Errorf("SQL database initialization file (%s) missing. Please reinstall gochan", initFile)
|
||||
}
|
||||
|
||||
sqlBytes, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sqlStr := regexp.MustCompile("--.*\n?").ReplaceAllString(string(sqlBytes), " ")
|
||||
sqlArr := strings.Split(sqlReplacer.Replace(sqlStr), ";")
|
||||
|
||||
for _, statement := range sqlArr {
|
||||
if statement != "" && statement != " " {
|
||||
if _, err = db.Exec(statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,11 +1,17 @@
|
|||
package main
|
||||
package gcsql
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strconv"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
//ErrNotImplemented is a not implemented exception
|
||||
|
@ -139,28 +145,27 @@ func getRecentPostsInternal(amount int, onlyWithFile bool, boardID int, onSpecif
|
|||
if onlyWithFile && onSpecificBoard {
|
||||
recentQueryStr += `\nWHERE singlefiles.filename IS NOT NULL AND recentposts.boardid = ?
|
||||
ORDER BY recentposts.created_on DESC LIMIT ?`
|
||||
rows, err = querySQL(recentQueryStr, boardID, amount)
|
||||
rows, err = QuerySQL(recentQueryStr, boardID, amount)
|
||||
}
|
||||
if onlyWithFile && !onSpecificBoard {
|
||||
recentQueryStr += `\nWHERE singlefiles.filename IS NOT NULL
|
||||
ORDER BY recentposts.created_on DESC LIMIT ?`
|
||||
rows, err = querySQL(recentQueryStr, amount)
|
||||
rows, err = QuerySQL(recentQueryStr, amount)
|
||||
}
|
||||
if !onlyWithFile && onSpecificBoard {
|
||||
recentQueryStr += `\nWHERE recentposts.boardid = ?
|
||||
ORDER BY recentposts.created_on DESC LIMIT ?`
|
||||
rows, err = querySQL(recentQueryStr, boardID, amount)
|
||||
rows, err = QuerySQL(recentQueryStr, boardID, amount)
|
||||
}
|
||||
if !onlyWithFile && !onSpecificBoard {
|
||||
recentQueryStr += `\nORDER BY recentposts.created_on DESC LIMIT ?`
|
||||
rows, err = querySQL(recentQueryStr, amount)
|
||||
rows, err = QuerySQL(recentQueryStr, amount)
|
||||
}
|
||||
|
||||
defer closeHandle(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var recentPostsArr []RecentPost
|
||||
|
||||
for rows.Next() {
|
||||
|
@ -226,14 +231,14 @@ func GetStaffByName(name string) (*Staff, error) { //TODO not upt to date with o
|
|||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
func newStaff(username string, password string, rank int) error { //TODO not up to date with old db yet
|
||||
func NewStaff(username string, password string, rank int) error { //TODO not up to date with old db yet
|
||||
// _, err := execSQL("INSERT INTO DBPREFIXstaff (username, password_checksum, rank) VALUES(?,?,?)",
|
||||
// &username, bcryptSum(password), &rank)
|
||||
// return err
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func deleteStaff(username string) error { //TODO not up to date with old db yet
|
||||
func DeleteStaff(username string) error { //TODO not up to date with old db yet
|
||||
// _, err := execSQL("DELETE FROM DBPREFIXstaff WHERE username = ?", username)
|
||||
// return err
|
||||
return ErrNotImplemented
|
||||
|
@ -299,6 +304,33 @@ func UserBan(IP net.IP, threadBan bool, staffName string, boardURI string, expir
|
|||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetBannedStatus checks check poster's name/tripcode/file checksum (from Post post) for banned status
|
||||
// returns ban table if the user is banned or sql.ErrNoRows if they aren't
|
||||
func GetBannedStatus(request *http.Request) (*BanInfo, error) {
|
||||
formName := request.FormValue("postname")
|
||||
var tripcode string
|
||||
if formName != "" {
|
||||
parsedName := gcutil.ParseName(formName)
|
||||
tripcode += parsedName["name"]
|
||||
if tc, ok := parsedName["tripcode"]; ok {
|
||||
tripcode += "!" + tc
|
||||
}
|
||||
}
|
||||
ip := gcutil.GetRealIP(request)
|
||||
|
||||
var filename string
|
||||
var checksum string
|
||||
file, fileHandler, err := request.FormFile("imagefile")
|
||||
if err == nil {
|
||||
html.EscapeString(fileHandler.Filename)
|
||||
if data, err2 := ioutil.ReadAll(file); err2 == nil {
|
||||
checksum = fmt.Sprintf("%x", md5.Sum(data))
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
return CheckBan(ip, tripcode, filename, checksum)
|
||||
}
|
||||
|
||||
func GetStaffRankAndBoards(username string) (rank int, boardUris []string, err error) {
|
||||
|
||||
return 420, nil, ErrNotImplemented
|
||||
|
@ -393,12 +425,6 @@ func GetAllBans() ([]BanInfo, error) {
|
|||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
//HackyStringToInt parses a string to an int, or 0 if error
|
||||
func HackyStringToInt(text string) int {
|
||||
value, _ := strconv.Atoi(text)
|
||||
return value
|
||||
}
|
||||
|
||||
// BumpThread the given thread on the given board and returns true if there were no errors
|
||||
func BumpThread(postID, boardID int) error { //NOT UP TO DATE
|
||||
// _, err := execSQL("UPDATE DBPREFIXposts SET bumped = ? WHERE id = ? AND boardid = ?",
|
471
pkg/gcsql/tables.go
Normal file
471
pkg/gcsql/tables.go
Normal file
|
@ -0,0 +1,471 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
dirIsAFileStr = `unable to create "%s", path exists and is a file`
|
||||
genericErrStr = `unable to create "%s": %s`
|
||||
pathExistsStr = `unable to create "%s", path already exists`
|
||||
|
||||
_ = iota
|
||||
threadBan
|
||||
imageBan
|
||||
fullBan
|
||||
)
|
||||
|
||||
var (
|
||||
// AllSections is a cached list of all of the board sections
|
||||
AllSections []BoardSection
|
||||
// AllBoards is a cached list of all of the boards
|
||||
AllBoards []Board
|
||||
// TempPosts is a cached list of all of the posts in the temporary posts table
|
||||
TempPosts []Post
|
||||
)
|
||||
|
||||
type Announcement struct {
|
||||
ID uint `json:"no"`
|
||||
Subject string `json:"sub"`
|
||||
Message string `json:"com"`
|
||||
Poster string `json:"name"`
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type BanAppeal struct {
|
||||
ID int
|
||||
Ban int
|
||||
Message string
|
||||
Denied bool
|
||||
StaffResponse string
|
||||
}
|
||||
|
||||
type BanInfo struct {
|
||||
ID uint
|
||||
AllowRead bool
|
||||
IP string
|
||||
Name string
|
||||
NameIsRegex bool
|
||||
SilentBan uint8
|
||||
Boards string
|
||||
Staff string
|
||||
Timestamp time.Time
|
||||
Expires time.Time
|
||||
Permaban bool
|
||||
Reason string
|
||||
Type int
|
||||
StaffNote string
|
||||
AppealAt time.Time
|
||||
CanAppeal bool
|
||||
}
|
||||
|
||||
// BannedForever returns true if the ban is an unappealable permaban
|
||||
func (ban *BanInfo) BannedForever() bool {
|
||||
return ban.Permaban && !ban.CanAppeal && ban.Type == fullBan && ban.Boards == ""
|
||||
}
|
||||
|
||||
// IsActive returns true if the ban is still active (unexpired or a permaban)
|
||||
func (ban *BanInfo) IsActive(board string) bool {
|
||||
if ban.Boards == "" && (ban.Expires.After(time.Now()) || ban.Permaban) {
|
||||
return true
|
||||
}
|
||||
boardsArr := strings.Split(ban.Boards, ",")
|
||||
for _, b := range boardsArr {
|
||||
if b == board && (ban.Expires.After(time.Now()) || ban.Permaban) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsBanned checks to see if the ban applies to the given board
|
||||
func (ban *BanInfo) IsBanned(board string) bool {
|
||||
if ban.Boards == "" && (ban.Expires.After(time.Now()) || ban.Permaban) {
|
||||
return true
|
||||
}
|
||||
boardsArr := strings.Split(ban.Boards, ",")
|
||||
for _, b := range boardsArr {
|
||||
if b == board && (ban.Expires.After(time.Now()) || ban.Permaban) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type BannedHash struct {
|
||||
ID uint
|
||||
Checksum string
|
||||
Description string
|
||||
}
|
||||
|
||||
type Board struct {
|
||||
ID int `json:"-"`
|
||||
CurrentPage int `json:"-"`
|
||||
NumPages int `json:"pages"`
|
||||
ListOrder int `json:"-"`
|
||||
Dir string `json:"board"`
|
||||
Type int `json:"-"`
|
||||
UploadType int `json:"-"`
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"meta_description"`
|
||||
Description string `json:"-"`
|
||||
Section int `json:"-"`
|
||||
MaxFilesize int `json:"max_filesize"`
|
||||
MaxPages int `json:"max_pages"`
|
||||
DefaultStyle string `json:"-"`
|
||||
Locked bool `json:"is_archived"`
|
||||
CreatedOn time.Time `json:"-"`
|
||||
Anonymous string `json:"-"`
|
||||
ForcedAnon bool `json:"-"`
|
||||
MaxAge int `json:"-"`
|
||||
AutosageAfter int `json:"bump_limit"`
|
||||
NoImagesAfter int `json:"image_limit"`
|
||||
MaxMessageLength int `json:"max_comment_chars"`
|
||||
EmbedsAllowed bool `json:"-"`
|
||||
RedirectToThread bool `json:"-"`
|
||||
ShowID bool `json:"-"`
|
||||
RequireFile bool `json:"-"`
|
||||
EnableCatalog bool `json:"-"`
|
||||
EnableSpoileredImages bool `json:"-"`
|
||||
EnableSpoileredThreads bool `json:"-"`
|
||||
Worksafe bool `json:"ws_board"`
|
||||
ThreadPage int `json:"-"`
|
||||
Cooldowns BoardCooldowns `json:"cooldowns"`
|
||||
ThreadsPerPage int `json:"per_page"`
|
||||
}
|
||||
|
||||
// AbsolutePath returns the full filepath of the board directory
|
||||
func (board *Board) AbsolutePath(subpath ...string) string {
|
||||
return path.Join(config.Config.DocumentRoot, board.Dir, path.Join(subpath...))
|
||||
}
|
||||
|
||||
// WebPath returns a string that represents the file's path as accessible by a browser
|
||||
// fileType should be "boardPage", "threadPage", "upload", or "thumb"
|
||||
func (board *Board) WebPath(fileName string, fileType string) string {
|
||||
var filePath string
|
||||
switch fileType {
|
||||
case "":
|
||||
fallthrough
|
||||
case "boardPage":
|
||||
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, fileName)
|
||||
case "threadPage":
|
||||
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, "res", fileName)
|
||||
case "upload":
|
||||
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, "src", fileName)
|
||||
case "thumb":
|
||||
filePath = path.Join(config.Config.SiteWebfolder, board.Dir, "thumb", fileName)
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
||||
func (board *Board) PagePath(pageNum interface{}) string {
|
||||
var page string
|
||||
pageNumStr := fmt.Sprintf("%v", pageNum)
|
||||
if pageNumStr == "prev" {
|
||||
if board.CurrentPage < 2 {
|
||||
page = "1"
|
||||
} else {
|
||||
page = strconv.Itoa(board.CurrentPage - 1)
|
||||
}
|
||||
} else if pageNumStr == "next" {
|
||||
if board.CurrentPage >= board.NumPages {
|
||||
page = strconv.Itoa(board.NumPages)
|
||||
} else {
|
||||
page = strconv.Itoa(board.CurrentPage + 1)
|
||||
}
|
||||
} else {
|
||||
page = pageNumStr
|
||||
}
|
||||
return board.WebPath(page+".html", "boardPage")
|
||||
}
|
||||
|
||||
// Build builds the board and its thread files
|
||||
// if newBoard is true, it adds a row to DBPREFIXboards and fails if it exists
|
||||
// if force is true, it doesn't fail if the directories exist but does fail if it is a file
|
||||
func (board *Board) Build(newBoard bool, force bool) error {
|
||||
var err error
|
||||
if board.Dir == "" {
|
||||
return errors.New("board must have a directory before it is built")
|
||||
}
|
||||
if board.Title == "" {
|
||||
return errors.New("board must have a title before it is built")
|
||||
}
|
||||
|
||||
dirPath := board.AbsolutePath()
|
||||
resPath := board.AbsolutePath("res")
|
||||
srcPath := board.AbsolutePath("src")
|
||||
thumbPath := board.AbsolutePath("thumb")
|
||||
dirInfo, _ := os.Stat(dirPath)
|
||||
resInfo, _ := os.Stat(resPath)
|
||||
srcInfo, _ := os.Stat(srcPath)
|
||||
thumbInfo, _ := os.Stat(thumbPath)
|
||||
if dirInfo != nil {
|
||||
if !force {
|
||||
return fmt.Errorf(pathExistsStr, dirPath)
|
||||
}
|
||||
if !dirInfo.IsDir() {
|
||||
return fmt.Errorf(dirIsAFileStr, dirPath)
|
||||
}
|
||||
} else {
|
||||
if err = os.Mkdir(dirPath, 0666); err != nil {
|
||||
return fmt.Errorf(genericErrStr, dirPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if resInfo != nil {
|
||||
if !force {
|
||||
return fmt.Errorf(pathExistsStr, resPath)
|
||||
}
|
||||
if !resInfo.IsDir() {
|
||||
return fmt.Errorf(dirIsAFileStr, resPath)
|
||||
}
|
||||
} else {
|
||||
if err = os.Mkdir(resPath, 0666); err != nil {
|
||||
return fmt.Errorf(genericErrStr, resPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if srcInfo != nil {
|
||||
if !force {
|
||||
return fmt.Errorf(pathExistsStr, srcPath)
|
||||
}
|
||||
if !srcInfo.IsDir() {
|
||||
return fmt.Errorf(dirIsAFileStr, srcPath)
|
||||
}
|
||||
} else {
|
||||
if err = os.Mkdir(srcPath, 0666); err != nil {
|
||||
return fmt.Errorf(genericErrStr, srcPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if thumbInfo != nil {
|
||||
if !force {
|
||||
return fmt.Errorf(pathExistsStr, thumbPath)
|
||||
}
|
||||
if !thumbInfo.IsDir() {
|
||||
return fmt.Errorf(dirIsAFileStr, thumbPath)
|
||||
}
|
||||
} else {
|
||||
if err = os.Mkdir(thumbPath, 0666); err != nil {
|
||||
return fmt.Errorf(genericErrStr, thumbPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if newBoard {
|
||||
board.CreatedOn = time.Now()
|
||||
err := CreateBoard(board)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = board.UpdateID(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
/* buildBoardPages(board)
|
||||
buildThreads(true, board.ID, 0)
|
||||
resetBoardSectionArrays()
|
||||
buildFrontPage()
|
||||
if board.EnableCatalog {
|
||||
buildCatalog(board.ID)
|
||||
}
|
||||
buildBoardListJSON() */
|
||||
return nil
|
||||
}
|
||||
|
||||
func (board *Board) SetDefaults() {
|
||||
board.ListOrder = 0
|
||||
board.Section = 1
|
||||
board.MaxFilesize = 4096
|
||||
board.MaxPages = 11
|
||||
board.DefaultStyle = config.Config.DefaultStyle
|
||||
board.Locked = false
|
||||
board.Anonymous = "Anonymous"
|
||||
board.ForcedAnon = false
|
||||
board.MaxAge = 0
|
||||
board.AutosageAfter = 200
|
||||
board.NoImagesAfter = 0
|
||||
board.MaxMessageLength = 8192
|
||||
board.EmbedsAllowed = true
|
||||
board.RedirectToThread = false
|
||||
board.ShowID = false
|
||||
board.RequireFile = false
|
||||
board.EnableCatalog = true
|
||||
board.EnableSpoileredImages = true
|
||||
board.EnableSpoileredThreads = true
|
||||
board.Worksafe = true
|
||||
board.ThreadsPerPage = 10
|
||||
}
|
||||
|
||||
type BoardSection struct {
|
||||
ID int
|
||||
ListOrder int
|
||||
Hidden bool
|
||||
Name string
|
||||
Abbreviation string
|
||||
}
|
||||
|
||||
// Post represents each post in the database
|
||||
type Post struct {
|
||||
ID int `json:"no"`
|
||||
ParentID int `json:"resto"`
|
||||
CurrentPage int `json:"-"`
|
||||
BoardID int `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Tripcode string `json:"trip"`
|
||||
Email string `json:"email"`
|
||||
Subject string `json:"sub"`
|
||||
MessageHTML string `json:"com"`
|
||||
MessageText string `json:"-"`
|
||||
Password string `json:"-"`
|
||||
Filename string `json:"tim"`
|
||||
FilenameOriginal string `json:"filename"`
|
||||
FileChecksum string `json:"md5"`
|
||||
FileExt string `json:"extension"`
|
||||
Filesize int `json:"fsize"`
|
||||
ImageW int `json:"w"`
|
||||
ImageH int `json:"h"`
|
||||
ThumbW int `json:"tn_w"`
|
||||
ThumbH int `json:"tn_h"`
|
||||
IP string `json:"-"`
|
||||
Capcode string `json:"capcode"`
|
||||
Timestamp time.Time `json:"time"`
|
||||
Autosage bool `json:"-"`
|
||||
DeletedTimestamp time.Time `json:"-"`
|
||||
Bumped time.Time `json:"last_modified"`
|
||||
Stickied bool `json:"-"`
|
||||
Locked bool `json:"-"`
|
||||
Reviewed bool `json:"-"`
|
||||
}
|
||||
|
||||
func (p *Post) GetURL(includeDomain bool) string {
|
||||
postURL := ""
|
||||
if includeDomain {
|
||||
postURL += config.Config.SiteDomain
|
||||
}
|
||||
var board Board
|
||||
if err := board.PopulateData(p.BoardID); err != nil {
|
||||
return postURL
|
||||
}
|
||||
|
||||
idStr := strconv.Itoa(p.ID)
|
||||
postURL += config.Config.SiteWebfolder + board.Dir + "/res/"
|
||||
if p.ParentID == 0 {
|
||||
postURL += idStr + ".html#" + idStr
|
||||
} else {
|
||||
postURL += strconv.Itoa(p.ParentID) + ".html#" + idStr
|
||||
}
|
||||
return postURL
|
||||
}
|
||||
|
||||
// Sanitize escapes HTML strings in a post. This should be run immediately before
|
||||
// the post is inserted into the database
|
||||
func (p *Post) Sanitize() {
|
||||
p.Name = html.EscapeString(p.Name)
|
||||
p.Email = html.EscapeString(p.Email)
|
||||
p.Subject = html.EscapeString(p.Subject)
|
||||
p.Password = html.EscapeString(p.Password)
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
ID uint
|
||||
Board string
|
||||
PostID uint
|
||||
Timestamp time.Time
|
||||
IP string
|
||||
Reason string
|
||||
Cleared bool
|
||||
IsTemp bool
|
||||
}
|
||||
|
||||
type LoginSession struct {
|
||||
ID uint
|
||||
Name string
|
||||
Data string
|
||||
Expires string
|
||||
}
|
||||
|
||||
// Staff represents a single staff member's info stored in the database
|
||||
type Staff struct {
|
||||
ID int
|
||||
Username string
|
||||
PasswordChecksum string
|
||||
Rank int
|
||||
Boards string
|
||||
AddedOn time.Time
|
||||
LastActive time.Time
|
||||
}
|
||||
|
||||
type WordFilter struct {
|
||||
ID int
|
||||
From string
|
||||
To string
|
||||
Boards string
|
||||
RegEx bool
|
||||
}
|
||||
|
||||
type BoardCooldowns struct {
|
||||
NewThread int `json:"threads"`
|
||||
Reply int `json:"replies"`
|
||||
ImageReply int `json:"images"`
|
||||
}
|
||||
|
||||
type MessagePostContainer struct {
|
||||
ID int
|
||||
MessageRaw string
|
||||
Message string
|
||||
}
|
||||
|
||||
type RecentPost struct {
|
||||
BoardName string
|
||||
BoardID int
|
||||
PostID int
|
||||
ParentID int
|
||||
Name string
|
||||
Tripcode string
|
||||
Message string
|
||||
Filename string
|
||||
ThumbW int
|
||||
ThumbH int
|
||||
IP string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// GetURL returns the full URL of the recent post, or the full path if includeDomain is false
|
||||
func (p *RecentPost) GetURL(includeDomain bool) string {
|
||||
postURL := ""
|
||||
if includeDomain {
|
||||
postURL += config.Config.SiteDomain
|
||||
}
|
||||
idStr := strconv.Itoa(p.PostID)
|
||||
postURL += config.Config.SiteWebfolder + p.BoardName + "/res/"
|
||||
if p.ParentID == 0 {
|
||||
postURL += idStr + ".html#" + idStr
|
||||
} else {
|
||||
postURL += strconv.Itoa(p.ParentID) + ".html#" + idStr
|
||||
}
|
||||
return postURL
|
||||
}
|
||||
|
||||
type Thread struct {
|
||||
OP Post `json:"-"`
|
||||
NumReplies int `json:"replies"`
|
||||
NumImages int `json:"images"`
|
||||
OmittedPosts int `json:"omitted_posts"`
|
||||
OmittedImages int `json:"omitted_images"`
|
||||
BoardReplies []Post `json:"-"`
|
||||
Sticky int `json:"sticky"`
|
||||
Locked int `json:"locked"`
|
||||
ThreadPage int `json:"-"`
|
||||
}
|
139
pkg/gcsql/util.go
Normal file
139
pkg/gcsql/util.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
package gcsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
MySQLDatetimeFormat = "2006-01-02 15:04:05"
|
||||
unsupportedSQLVersionMsg = `Received syntax error while preparing a SQL string.
|
||||
This means that either there is a bug in gochan's code (hopefully not) or that you are using an unsupported My/Postgre/SQLite version.
|
||||
Before reporting an error, make sure that you are using the up to date version of your selected SQL server.
|
||||
Error text: %s`
|
||||
)
|
||||
|
||||
func sqlVersionErr(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errText := err.Error()
|
||||
switch dbDriver {
|
||||
case "mysql":
|
||||
if !strings.Contains(errText, "You have an error in your SQL syntax") {
|
||||
return err
|
||||
}
|
||||
case "postgres":
|
||||
if !strings.Contains(errText, "syntax error at or near") {
|
||||
return err
|
||||
}
|
||||
case "sqlite3":
|
||||
if !strings.Contains(errText, "Error: near ") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf(unsupportedSQLVersionMsg, errText)
|
||||
}
|
||||
|
||||
// used for generating a prepared SQL statement formatted according to config.DBtype
|
||||
func prepareSQL(query string) (*sql.Stmt, error) {
|
||||
var preparedStr string
|
||||
switch dbDriver {
|
||||
case "mysql":
|
||||
fallthrough
|
||||
case "sqlite3":
|
||||
preparedStr = query
|
||||
case "postgres":
|
||||
arr := strings.Split(query, "?")
|
||||
for i := range arr {
|
||||
if i == len(arr)-1 {
|
||||
break
|
||||
}
|
||||
arr[i] += fmt.Sprintf("$%d", i+1)
|
||||
}
|
||||
preparedStr = strings.Join(arr, "")
|
||||
}
|
||||
stmt, err := db.Prepare(sqlReplacer.Replace(preparedStr))
|
||||
return stmt, sqlVersionErr(err)
|
||||
}
|
||||
|
||||
// Close closes the connection to the SQL database
|
||||
func Close() {
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ExecSQL automatically escapes the given values and caches the statement
|
||||
Example:
|
||||
var intVal int
|
||||
var stringVal string
|
||||
result, err := gcsql.ExecSQL(
|
||||
"INSERT INTO tablename (intval,stringval) VALUES(?,?)", intVal, stringVal)
|
||||
*/
|
||||
func ExecSQL(query string, values ...interface{}) (sql.Result, error) {
|
||||
stmt, err := prepareSQL(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
return stmt.Exec(values...)
|
||||
}
|
||||
|
||||
/*
|
||||
QueryRowSQL gets a row from the db with the values in values[] and fills the respective pointers in out[]
|
||||
Automatically escapes the given values and caches the query
|
||||
Example:
|
||||
id := 32
|
||||
var intVal int
|
||||
var stringVal string
|
||||
err := queryRowSQL("SELECT intval,stringval FROM table WHERE id = ?",
|
||||
[]interface{}{&id},
|
||||
[]interface{}{&intVal, &stringVal})
|
||||
*/
|
||||
func QueryRowSQL(query string, values []interface{}, out []interface{}) error {
|
||||
stmt, err := prepareSQL(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
return stmt.QueryRow(values...).Scan(out...)
|
||||
}
|
||||
|
||||
/*
|
||||
QuerySQL gets all rows from the db with the values in values[] and fills the respective pointers in out[]
|
||||
Automatically escapes the given values and caches the query
|
||||
Example:
|
||||
rows, err := gcsql.QuerySQL("SELECT * FROM table")
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var intVal int
|
||||
var stringVal string
|
||||
rows.Scan(&intVal, &stringVal)
|
||||
// do something with intVal and stringVal
|
||||
}
|
||||
}
|
||||
*/
|
||||
func QuerySQL(query string, a ...interface{}) (*sql.Rows, error) {
|
||||
stmt, err := prepareSQL(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
return stmt.Query(a...)
|
||||
}
|
||||
|
||||
// ResetBoardSectionArrays is run when the board list needs to be changed
|
||||
// (board/section is added, deleted, etc)
|
||||
func ResetBoardSectionArrays() {
|
||||
AllBoards = nil
|
||||
AllSections = nil
|
||||
|
||||
allBoardsArr, _ := GetAllBoards()
|
||||
AllBoards = append(AllBoards, allBoardsArr...)
|
||||
|
||||
allSectionsArr, _ := GetAllSections()
|
||||
AllSections = append(AllSections, allSectionsArr...)
|
||||
}
|
199
src/template.go → pkg/gctemplates/funcs.go
Executable file → Normal file
199
src/template.go → pkg/gctemplates/funcs.go
Executable file → Normal file
|
@ -1,15 +1,17 @@
|
|||
package main
|
||||
package gctemplates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"os"
|
||||
"path"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
x_html "golang.org/x/net/html"
|
||||
)
|
||||
|
||||
|
@ -57,7 +59,7 @@ var funcMap = template.FuncMap{
|
|||
},
|
||||
|
||||
// String functions
|
||||
"arrToString": arrToString,
|
||||
// "arrToString": arrToString,
|
||||
"intToString": strconv.Itoa,
|
||||
"escapeString": func(a string) string {
|
||||
return html.EscapeString(a)
|
||||
|
@ -73,7 +75,9 @@ var funcMap = template.FuncMap{
|
|||
}
|
||||
return fmt.Sprintf("%0.2f GB", size/1024/1024/1024)
|
||||
},
|
||||
"formatTimestamp": humanReadableTime,
|
||||
"formatTimestamp": func(t time.Time) string {
|
||||
return t.Format(config.Config.DateTimeFormat)
|
||||
},
|
||||
"stringAppend": func(strings ...string) string {
|
||||
var appended string
|
||||
for _, str := range strings {
|
||||
|
@ -96,10 +100,9 @@ var funcMap = template.FuncMap{
|
|||
msg = msg + "..."
|
||||
}
|
||||
return msg
|
||||
} else {
|
||||
msg = msg[:limit]
|
||||
truncated = true
|
||||
}
|
||||
msg = msg[:limit]
|
||||
truncated = true
|
||||
|
||||
if truncated {
|
||||
msg = msg + "..."
|
||||
|
@ -132,12 +135,11 @@ var funcMap = template.FuncMap{
|
|||
},
|
||||
|
||||
// Imageboard functions
|
||||
"bannedForever": bannedForever,
|
||||
"getCatalogThumbnail": func(img string) string {
|
||||
return getThumbnailPath("catalog", img)
|
||||
return gcutil.GetThumbnailPath("catalog", img)
|
||||
},
|
||||
"getThreadID": func(postInterface interface{}) (thread int) {
|
||||
post, ok := postInterface.(Post)
|
||||
post, ok := postInterface.(gcsql.Post)
|
||||
if !ok {
|
||||
thread = 0
|
||||
} else if post.ParentID == 0 {
|
||||
|
@ -149,18 +151,18 @@ var funcMap = template.FuncMap{
|
|||
},
|
||||
"getPostURL": func(postInterface interface{}, typeOf string, withDomain bool) (postURL string) {
|
||||
if withDomain {
|
||||
postURL = config.SiteDomain
|
||||
postURL = config.Config.SiteDomain
|
||||
}
|
||||
postURL += config.SiteWebfolder
|
||||
postURL += config.Config.SiteWebfolder
|
||||
|
||||
if typeOf == "recent" {
|
||||
post, ok := postInterface.(*RecentPost)
|
||||
post, ok := postInterface.(*gcsql.RecentPost)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
postURL = post.GetURL(withDomain)
|
||||
} else {
|
||||
post, ok := postInterface.(*Post)
|
||||
post, ok := postInterface.(*gcsql.Post)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
@ -169,10 +171,10 @@ var funcMap = template.FuncMap{
|
|||
return
|
||||
},
|
||||
"getThreadThumbnail": func(img string) string {
|
||||
return getThumbnailPath("thread", img)
|
||||
return gcutil.GetThumbnailPath("thread", img)
|
||||
},
|
||||
"getUploadType": func(name string) string {
|
||||
extension := getFileExtension(name)
|
||||
extension := gcutil.GetFileExtension(name)
|
||||
var uploadType string
|
||||
switch extension {
|
||||
case "":
|
||||
|
@ -207,10 +209,15 @@ var funcMap = template.FuncMap{
|
|||
}
|
||||
return img[0:index] + thumbSuffix
|
||||
},
|
||||
"isBanned": isBanned,
|
||||
"numReplies": numReplies,
|
||||
"numReplies": func(boardid, threadid int) int {
|
||||
num, err := gcsql.GetReplyCount(threadid)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return num
|
||||
},
|
||||
"getBoardDir": func(id int) string {
|
||||
var board Board
|
||||
var board gcsql.Board
|
||||
if err := board.PopulateData(id); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
@ -226,8 +233,8 @@ var funcMap = template.FuncMap{
|
|||
return loopArr
|
||||
},
|
||||
"generateConfigTable": func() string {
|
||||
configType := reflect.TypeOf(config)
|
||||
tableOut := "<table style=\"border-collapse: collapse;\" id=\"config\"><tr><th>Field name</th><th>Value</th><th>Type</th><th>Description</th></tr>\n"
|
||||
configType := reflect.TypeOf(config.Config)
|
||||
tableOut := `<table style="border-collapse: collapse;" id="config"><tr><th>Field name</th><th>Value</th><th>Type</th><th>Description</th></tr>`
|
||||
numFields := configType.NumField()
|
||||
for f := 17; f < numFields-2; f++ {
|
||||
// starting at Lockdown because the earlier fields can't be safely edited from a web interface
|
||||
|
@ -237,22 +244,22 @@ var funcMap = template.FuncMap{
|
|||
}
|
||||
name := field.Name
|
||||
tableOut += "<tr><th>" + name + "</th><td>"
|
||||
f := reflect.Indirect(reflect.ValueOf(config)).FieldByName(name)
|
||||
f := reflect.Indirect(reflect.ValueOf(config.Config)).FieldByName(name)
|
||||
|
||||
kind := f.Kind()
|
||||
switch kind {
|
||||
case reflect.Int:
|
||||
tableOut += "<input name=\"" + name + "\" type=\"number\" value=\"" + html.EscapeString(fmt.Sprintf("%v", f)) + "\" class=\"config-text\"/>"
|
||||
tableOut += `<input name="` + name + `" type="number" value="` + html.EscapeString(fmt.Sprintf("%v", f)) + `" class="config-text"/>`
|
||||
case reflect.String:
|
||||
tableOut += "<input name=\"" + name + "\" type=\"text\" value=\"" + html.EscapeString(fmt.Sprintf("%v", f)) + "\" class=\"config-text\"/>"
|
||||
tableOut += `<input name="` + name + `" type="text" value="` + html.EscapeString(fmt.Sprintf("%v", f)) + `" class="config-text"/>`
|
||||
case reflect.Bool:
|
||||
checked := ""
|
||||
if f.Bool() {
|
||||
checked = "checked"
|
||||
}
|
||||
tableOut += "<input name=\"" + name + "\" type=\"checkbox\" " + checked + " />"
|
||||
tableOut += `<input name="` + name + `" type="checkbox" ` + checked + " />"
|
||||
case reflect.Slice:
|
||||
tableOut += "<textarea name=\"" + name + "\" rows=\"4\" cols=\"28\">"
|
||||
tableOut += `<textarea name="` + name + `" rows="4" cols="28">`
|
||||
arrLength := f.Len()
|
||||
for s := 0; s < arrLength; s++ {
|
||||
newLine := "\n"
|
||||
|
@ -272,143 +279,15 @@ var funcMap = template.FuncMap{
|
|||
defaultTagHTML = " <b>Default: " + defaultTag + "</b>"
|
||||
}
|
||||
tableOut += field.Tag.Get("description") + defaultTagHTML + "</td>"
|
||||
tableOut += "</tr>\n"
|
||||
tableOut += "</tr>"
|
||||
}
|
||||
tableOut += "</table>\n"
|
||||
tableOut += "</table>"
|
||||
return tableOut
|
||||
},
|
||||
"isStyleDefault": func(style string) bool {
|
||||
return style == config.DefaultStyle
|
||||
return style == config.Config.DefaultStyle
|
||||
},
|
||||
|
||||
// Version functions
|
||||
"version": func() string {
|
||||
return version.String()
|
||||
return config.Config.Version.String()
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
banpageTmpl *template.Template
|
||||
captchaTmpl *template.Template
|
||||
catalogTmpl *template.Template
|
||||
errorpageTmpl *template.Template
|
||||
frontPageTmpl *template.Template
|
||||
boardpageTmpl *template.Template
|
||||
threadpageTmpl *template.Template
|
||||
postEditTmpl *template.Template
|
||||
manageBansTmpl *template.Template
|
||||
manageBoardsTmpl *template.Template
|
||||
manageConfigTmpl *template.Template
|
||||
manageHeaderTmpl *template.Template
|
||||
jsTmpl *template.Template
|
||||
)
|
||||
|
||||
func loadTemplate(files ...string) (*template.Template, error) {
|
||||
var templates []string
|
||||
for i, file := range files {
|
||||
templates = append(templates, file)
|
||||
tmplPath := path.Join(config.TemplateDir, "override", file)
|
||||
|
||||
if _, err := os.Stat(tmplPath); !os.IsNotExist(err) {
|
||||
files[i] = tmplPath
|
||||
} else {
|
||||
files[i] = path.Join(config.TemplateDir, file)
|
||||
}
|
||||
}
|
||||
|
||||
return template.New(templates[0]).Funcs(funcMap).ParseFiles(files...)
|
||||
}
|
||||
|
||||
func templateError(name string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Failed loading template '%s/%s': %s", config.TemplateDir, name, err.Error())
|
||||
}
|
||||
|
||||
func initTemplates(which ...string) error {
|
||||
var err error
|
||||
buildAll := len(which) == 0 || which[0] == "all"
|
||||
resetBoardSectionArrays()
|
||||
for _, t := range which {
|
||||
if buildAll || t == "banpage" {
|
||||
banpageTmpl, err = loadTemplate("banpage.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("banpage.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "captcha" {
|
||||
captchaTmpl, err = loadTemplate("captcha.html")
|
||||
if err != nil {
|
||||
return templateError("captcha.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "catalog" {
|
||||
catalogTmpl, err = loadTemplate("catalog.html", "page_header.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("catalog.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "error" {
|
||||
errorpageTmpl, err = loadTemplate("error.html")
|
||||
if err != nil {
|
||||
return templateError("error.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "front" {
|
||||
frontPageTmpl, err = loadTemplate("front.html", "front_intro.html", "page_header.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("front.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "boardpage" {
|
||||
boardpageTmpl, err = loadTemplate("boardpage.html", "page_header.html", "postbox.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("boardpage.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "threadpage" {
|
||||
threadpageTmpl, err = loadTemplate("threadpage.html", "page_header.html", "postbox.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("threadpage.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "postedit" {
|
||||
postEditTmpl, err = loadTemplate("post_edit.html", "page_header.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("threadpage.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "managebans" {
|
||||
manageBansTmpl, err = loadTemplate("manage_bans.html")
|
||||
if err != nil {
|
||||
return templateError("manage_bans.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "manageboards" {
|
||||
manageBoardsTmpl, err = loadTemplate("manage_boards.html")
|
||||
if err != nil {
|
||||
return templateError("manage_boards.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "manageconfig" {
|
||||
manageConfigTmpl, err = loadTemplate("manage_config.html")
|
||||
if err != nil {
|
||||
return templateError("manage_config.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "manageheader" {
|
||||
manageHeaderTmpl, err = loadTemplate("manage_header.html")
|
||||
if err != nil {
|
||||
return templateError("manage_header.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "js" {
|
||||
jsTmpl, err = loadTemplate("consts.js")
|
||||
if err != nil {
|
||||
return templateError("consts.js", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
140
pkg/gctemplates/templates.go
Normal file
140
pkg/gctemplates/templates.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package gctemplates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
)
|
||||
|
||||
var (
|
||||
Banpage *template.Template
|
||||
Captcha *template.Template
|
||||
Catalog *template.Template
|
||||
ErrorPage *template.Template
|
||||
FrontPage *template.Template
|
||||
BoardPage *template.Template
|
||||
JsConsts *template.Template
|
||||
ManageBans *template.Template
|
||||
ManageBoards *template.Template
|
||||
ManageConfig *template.Template
|
||||
ManageHeader *template.Template
|
||||
PostEdit *template.Template
|
||||
ThreadPage *template.Template
|
||||
)
|
||||
|
||||
func loadTemplate(files ...string) (*template.Template, error) {
|
||||
var templates []string
|
||||
for i, file := range files {
|
||||
templates = append(templates, file)
|
||||
tmplPath := path.Join(config.Config.TemplateDir, "override", file)
|
||||
|
||||
if _, err := os.Stat(tmplPath); !os.IsNotExist(err) {
|
||||
files[i] = tmplPath
|
||||
} else {
|
||||
files[i] = path.Join(config.Config.TemplateDir, file)
|
||||
}
|
||||
}
|
||||
|
||||
return template.New(templates[0]).Funcs(funcMap).ParseFiles(files...)
|
||||
}
|
||||
|
||||
func templateError(name string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Failed loading template '%s/%s': %s",
|
||||
config.Config.TemplateDir, name, err.Error())
|
||||
}
|
||||
|
||||
// InitTemplates loads the given templates by name. If no parameters are given,
|
||||
// or the first one is "all", all templates are (re)loaded
|
||||
func InitTemplates(which ...string) error {
|
||||
var err error
|
||||
buildAll := len(which) == 0 || which[0] == "all"
|
||||
gcsql.ResetBoardSectionArrays()
|
||||
for _, t := range which {
|
||||
if buildAll || t == "banpage" {
|
||||
Banpage, err = loadTemplate("banpage.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("banpage.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "captcha" {
|
||||
Captcha, err = loadTemplate("captcha.html")
|
||||
if err != nil {
|
||||
return templateError("captcha.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "catalog" {
|
||||
Catalog, err = loadTemplate("catalog.html", "page_header.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("catalog.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "error" {
|
||||
ErrorPage, err = loadTemplate("error.html")
|
||||
if err != nil {
|
||||
return templateError("error.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "front" {
|
||||
FrontPage, err = loadTemplate("front.html", "front_intro.html", "page_header.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("front.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "boardpage" {
|
||||
BoardPage, err = loadTemplate("boardpage.html", "page_header.html", "postbox.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("boardpage.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "threadpage" {
|
||||
ThreadPage, err = loadTemplate("threadpage.html", "page_header.html", "postbox.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("threadpage.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "postedit" {
|
||||
PostEdit, err = loadTemplate("post_edit.html", "page_header.html", "page_footer.html")
|
||||
if err != nil {
|
||||
return templateError("threadpage.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "managebans" {
|
||||
ManageBans, err = loadTemplate("manage_bans.html")
|
||||
if err != nil {
|
||||
return templateError("manage_bans.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "manageboards" {
|
||||
ManageBoards, err = loadTemplate("manage_boards.html")
|
||||
if err != nil {
|
||||
return templateError("manage_boards.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "manageconfig" {
|
||||
ManageConfig, err = loadTemplate("manage_config.html")
|
||||
if err != nil {
|
||||
return templateError("manage_config.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "manageheader" {
|
||||
ManageHeader, err = loadTemplate("manage_header.html")
|
||||
if err != nil {
|
||||
return templateError("manage_header.html", err)
|
||||
}
|
||||
}
|
||||
if buildAll || t == "js" {
|
||||
JsConsts, err = loadTemplate("consts.js")
|
||||
if err != nil {
|
||||
return templateError("consts.js", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package gcutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
@ -6,13 +6,13 @@ import (
|
|||
)
|
||||
|
||||
func TestAPI(t *testing.T) {
|
||||
failedPost, _ := marshalJSON(map[string]interface{}{
|
||||
failedPost, _ := MarshalJSON(map[string]interface{}{
|
||||
"action": "post",
|
||||
"success": false,
|
||||
"message": errors.New("Post failed").Error(),
|
||||
}, true)
|
||||
|
||||
madePost, _ := marshalJSON(map[string]interface{}{
|
||||
madePost, _ := MarshalJSON(map[string]interface{}{
|
||||
"action": "post",
|
||||
"success": true,
|
||||
"board": "test",
|
||||
|
@ -21,6 +21,5 @@ func TestAPI(t *testing.T) {
|
|||
|
||||
t.Log(
|
||||
"failedPost:", failedPost,
|
||||
"\nmadePost:", madePost,
|
||||
)
|
||||
"\nmadePost:", madePost)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package gcutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -6,19 +6,19 @@ import (
|
|||
)
|
||||
|
||||
func TestDurationParse(t *testing.T) {
|
||||
duration, err := parseDurationString("7y6mo5w4d3h2m1s")
|
||||
duration, err := ParseDurationString("7y6mo5w4d3h2m1s")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
fmt.Println(duration)
|
||||
|
||||
duration, err = parseDurationString("7year6month5weeks4days3hours2minutes1second")
|
||||
duration, err = ParseDurationString("7year6month5weeks4days3hours2minutes1second")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
fmt.Println(duration)
|
||||
|
||||
duration, err = parseDurationString("7 years 6 months 5 weeks 4 days 3 hours 2 minutes 1 seconds")
|
||||
duration, err = ParseDurationString("7 years 6 months 5 weeks 4 days 3 hours 2 minutes 1 seconds")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
61
pkg/gcutil/minifier.go
Normal file
61
pkg/gcutil/minifier.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package gcutil
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/tdewolff/minify"
|
||||
minifyHTML "github.com/tdewolff/minify/html"
|
||||
minifyJS "github.com/tdewolff/minify/js"
|
||||
minifyJSON "github.com/tdewolff/minify/json"
|
||||
)
|
||||
|
||||
var minifier *minify.M
|
||||
|
||||
// InitMinifier sets up the HTML/JS/JSON minifier if enabled in gochan.json
|
||||
func InitMinifier() {
|
||||
if !config.Config.MinifyHTML && !config.Config.MinifyJS {
|
||||
return
|
||||
}
|
||||
minifier = minify.New()
|
||||
if config.Config.MinifyHTML {
|
||||
minifier.AddFunc("text/html", minifyHTML.Minify)
|
||||
}
|
||||
if config.Config.MinifyJS {
|
||||
minifier.AddFunc("text/javascript", minifyJS.Minify)
|
||||
minifier.AddFunc("application/json", minifyJSON.Minify)
|
||||
}
|
||||
}
|
||||
|
||||
func canMinify(mediaType string) bool {
|
||||
if mediaType == "text/html" && config.Config.MinifyHTML {
|
||||
return true
|
||||
}
|
||||
if (mediaType == "application/json" || mediaType == "text/javascript") && config.Config.MinifyJS {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MinifyTemplate minifies the given template/data (if enabled) and returns any errors
|
||||
func MinifyTemplate(tmpl *template.Template, data interface{}, writer io.Writer, mediaType string) error {
|
||||
if !canMinify(mediaType) {
|
||||
return tmpl.Execute(writer, data)
|
||||
}
|
||||
|
||||
minWriter := minifier.Writer(mediaType, writer)
|
||||
defer minWriter.Close()
|
||||
return tmpl.Execute(minWriter, data)
|
||||
}
|
||||
|
||||
// MinifyWriter minifies the given writer/data (if enabled) and returns the number of bytes written and any errors
|
||||
func MinifyWriter(writer io.Writer, data []byte, mediaType string) (int, error) {
|
||||
if !canMinify(mediaType) {
|
||||
return writer.Write(data)
|
||||
}
|
||||
|
||||
minWriter := minifier.Writer(mediaType, writer)
|
||||
defer minWriter.Close()
|
||||
return minWriter.Write(data)
|
||||
}
|
249
pkg/gcutil/util.go
Normal file
249
pkg/gcutil/util.go
Normal file
|
@ -0,0 +1,249 @@
|
|||
package gcutil
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aquilax/tripcode"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyDurationString = errors.New("Empty Duration string")
|
||||
ErrInvalidDurationString = errors.New("Invalid Duration string")
|
||||
durationRegexp = regexp.MustCompile(`^((\d+)\s?ye?a?r?s?)?\s?((\d+)\s?mon?t?h?s?)?\s?((\d+)\s?we?e?k?s?)?\s?((\d+)\s?da?y?s?)?\s?((\d+)\s?ho?u?r?s?)?\s?((\d+)\s?mi?n?u?t?e?s?)?\s?((\d+)\s?s?e?c?o?n?d?s?)?$`)
|
||||
)
|
||||
|
||||
// BcryptSum generates and returns a checksum using the bcrypt hashing function
|
||||
func BcryptSum(str string) string {
|
||||
digest, err := bcrypt.GenerateFromPassword([]byte(str), 4)
|
||||
if err == nil {
|
||||
return string(digest)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Md5Sum generates and returns a checksum using the MD5 hashing function
|
||||
func Md5Sum(str string) string {
|
||||
hash := md5.New()
|
||||
io.WriteString(hash, str)
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
|
||||
// Sha1Sum generates and returns a checksum using the SHA-1 hashing function
|
||||
func Sha1Sum(str string) string {
|
||||
hash := sha1.New()
|
||||
io.WriteString(hash, str)
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
|
||||
/* func byteByByteReplace(input, from, to string) string {
|
||||
if len(from) != len(to) {
|
||||
return ""
|
||||
}
|
||||
for i := 0; i < len(from); i++ {
|
||||
input = strings.Replace(input, from[i:i+1], to[i:i+1], -1)
|
||||
}
|
||||
return input
|
||||
} */
|
||||
|
||||
// CloseHandle closes the given closer object only if it is non-nil
|
||||
func CloseHandle(handle io.Closer) {
|
||||
if handle != nil {
|
||||
handle.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteMatchingFiles deletes files in a folder (root) that match a given regular expression.
|
||||
// Returns the number of files that were deleted, and any error encountered.
|
||||
func DeleteMatchingFiles(root, match string) (filesDeleted int, err error) {
|
||||
files, err := ioutil.ReadDir(root)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, f := range files {
|
||||
match, _ := regexp.MatchString(match, f.Name())
|
||||
if match {
|
||||
os.Remove(filepath.Join(root, f.Name()))
|
||||
filesDeleted++
|
||||
}
|
||||
}
|
||||
return filesDeleted, err
|
||||
}
|
||||
|
||||
// FindResource searches for a file in the given paths and returns the first one it finds
|
||||
// or a blank string if none of the paths exist
|
||||
func FindResource(paths ...string) string {
|
||||
var err error
|
||||
for _, filepath := range paths {
|
||||
if _, err = os.Stat(filepath); err == nil {
|
||||
return filepath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetFileExtension returns the given file's extension, or a blank string if it has none
|
||||
func GetFileExtension(filename string) string {
|
||||
if !strings.Contains(filename, ".") {
|
||||
return ""
|
||||
}
|
||||
|
||||
return filename[strings.LastIndex(filename, ".")+1:]
|
||||
}
|
||||
|
||||
// GetFormattedFilesize returns a human readable filesize
|
||||
func GetFormattedFilesize(size float64) string {
|
||||
if size < 1000 {
|
||||
return fmt.Sprintf("%dB", int(size))
|
||||
} else if size <= 100000 {
|
||||
return fmt.Sprintf("%fKB", size/1024)
|
||||
} else if size <= 100000000 {
|
||||
return fmt.Sprintf("%fMB", size/1024.0/1024.0)
|
||||
}
|
||||
return fmt.Sprintf("%0.2fGB", size/1024.0/1024.0/1024.0)
|
||||
}
|
||||
|
||||
// GetRealIP checks the HTTP_CF_CONNCTING_IP and X-Forwarded-For HTTP headers to get a
|
||||
// potentially obfuscated IP address, before getting the requests reported remote address
|
||||
// if neither header is set
|
||||
func GetRealIP(request *http.Request) string {
|
||||
if request.Header.Get("HTTP_CF_CONNECTING_IP") != "" {
|
||||
return request.Header.Get("HTTP_CF_CONNECTING_IP")
|
||||
}
|
||||
if request.Header.Get("X-Forwarded-For") != "" {
|
||||
return request.Header.Get("X-Forwarded-For")
|
||||
}
|
||||
remoteHost, _, err := net.SplitHostPort(request.RemoteAddr)
|
||||
if err != nil {
|
||||
return request.RemoteAddr
|
||||
}
|
||||
return remoteHost
|
||||
}
|
||||
|
||||
// GetThumbnailPath returns the thumbnail path of the given filename
|
||||
func GetThumbnailPath(thumbType string, img string) string {
|
||||
filetype := strings.ToLower(img[strings.LastIndex(img, ".")+1:])
|
||||
if filetype == "gif" || filetype == "webm" {
|
||||
filetype = "jpg"
|
||||
}
|
||||
index := strings.LastIndex(img, ".")
|
||||
if index < 0 || index > len(img) {
|
||||
return ""
|
||||
}
|
||||
thumbSuffix := "t." + filetype
|
||||
if thumbType == "catalog" {
|
||||
thumbSuffix = "c." + filetype
|
||||
}
|
||||
return img[0:index] + thumbSuffix
|
||||
}
|
||||
|
||||
// HackyStringToInt parses a string to an int, or 0 if error
|
||||
func HackyStringToInt(text string) int {
|
||||
value, _ := strconv.Atoi(text)
|
||||
return value
|
||||
}
|
||||
|
||||
// MarshalJSON creates a JSON string with the given data and returns the string and any errors
|
||||
func MarshalJSON(data interface{}, indent bool) (string, error) {
|
||||
var jsonBytes []byte
|
||||
var err error
|
||||
|
||||
if indent {
|
||||
jsonBytes, err = json.MarshalIndent(data, "", " ")
|
||||
} else {
|
||||
jsonBytes, err = json.Marshal(data)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
jsonBytes, _ = json.Marshal(map[string]string{"error": err.Error()})
|
||||
}
|
||||
return string(jsonBytes), err
|
||||
}
|
||||
|
||||
// ParseDurationString parses the given string into a duration and returns any errors
|
||||
// based on TinyBoard's parse_time function
|
||||
func ParseDurationString(str string) (time.Duration, error) {
|
||||
if str == "" {
|
||||
return 0, ErrEmptyDurationString
|
||||
}
|
||||
|
||||
matches := durationRegexp.FindAllStringSubmatch(str, -1)
|
||||
if len(matches) == 0 {
|
||||
return 0, ErrInvalidDurationString
|
||||
}
|
||||
|
||||
var expire int
|
||||
if matches[0][2] != "" {
|
||||
years, _ := strconv.Atoi(matches[0][2])
|
||||
expire += years * 60 * 60 * 24 * 365
|
||||
}
|
||||
if matches[0][4] != "" {
|
||||
months, _ := strconv.Atoi(matches[0][4])
|
||||
expire += months * 60 * 60 * 24 * 30
|
||||
}
|
||||
if matches[0][6] != "" {
|
||||
weeks, _ := strconv.Atoi(matches[0][6])
|
||||
expire += weeks * 60 * 60 * 24 * 7
|
||||
}
|
||||
if matches[0][8] != "" {
|
||||
days, _ := strconv.Atoi(matches[0][8])
|
||||
expire += days * 60 * 60 * 24
|
||||
}
|
||||
if matches[0][10] != "" {
|
||||
hours, _ := strconv.Atoi(matches[0][10])
|
||||
expire += hours * 60 * 60
|
||||
}
|
||||
if matches[0][12] != "" {
|
||||
minutes, _ := strconv.Atoi(matches[0][12])
|
||||
expire += minutes * 60
|
||||
}
|
||||
if matches[0][14] != "" {
|
||||
seconds, _ := strconv.Atoi(matches[0][14])
|
||||
expire += seconds
|
||||
}
|
||||
return time.ParseDuration(strconv.Itoa(expire) + "s")
|
||||
}
|
||||
|
||||
// ParseName takes a name string from a request object and returns the name and tripcode parts
|
||||
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
|
||||
}
|
||||
|
||||
// RandomString returns a randomly generated string of the given length
|
||||
func RandomString(length int) string {
|
||||
var str string
|
||||
for i := 0; i < length; i++ {
|
||||
num := rand.Intn(127)
|
||||
if num < 32 {
|
||||
num += 32
|
||||
}
|
||||
str += string(num)
|
||||
}
|
||||
return str
|
||||
}
|
818
pkg/manage/funcs.go
Normal file
818
pkg/manage/funcs.go
Normal file
|
@ -0,0 +1,818 @@
|
|||
package manage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/building"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gctemplates"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/gochan-org/gochan/pkg/posting"
|
||||
"github.com/gochan-org/gochan/pkg/serverutil"
|
||||
)
|
||||
|
||||
var (
|
||||
chopPortNumRegex = regexp.MustCompile(`(.+|\w+):(\d+)$`)
|
||||
)
|
||||
|
||||
// ManageFunction represents the functions accessed by staff members at /manage?action=<functionname>.
|
||||
type ManageFunction struct {
|
||||
Title string
|
||||
Permissions int // 0 -> non-staff, 1 => janitor, 2 => moderator, 3 => administrator
|
||||
Callback func(writer http.ResponseWriter, request *http.Request) string `json:"-"` //return string of html output
|
||||
}
|
||||
|
||||
var manageFunctions = map[string]ManageFunction{
|
||||
"cleanup": {
|
||||
Title: "Cleanup",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
html = `<h2 class="manage-header">Cleanup</h2><br />`
|
||||
var err error
|
||||
if request.FormValue("run") == "Run Cleanup" {
|
||||
html += "Removing deleted posts from the database.<hr />"
|
||||
if err = gcsql.PermanentlyRemoveDeletedPosts(); err != nil {
|
||||
return html + "<tr><td>" +
|
||||
gclog.Print(gclog.LErrorLog, "Error removing deleted posts from database: ", err.Error()) +
|
||||
"</td></tr></table>"
|
||||
}
|
||||
// TODO: remove orphaned replies and uploads
|
||||
|
||||
html += "Optimizing all tables in database.<hr />"
|
||||
err = gcsql.OptimizeDatabase()
|
||||
if err != nil {
|
||||
return html + "<tr><td>" +
|
||||
gclog.Print(gclog.LErrorLog, "Error optimizing SQL tables: ", err.Error()) +
|
||||
"</td></tr></table>"
|
||||
}
|
||||
|
||||
html += "Cleanup finished"
|
||||
} else {
|
||||
html += `<form action="/manage?action=cleanup" method="post">` +
|
||||
`<input name="run" id="run" type="submit" value="Run Cleanup" />` +
|
||||
`</form>`
|
||||
}
|
||||
return
|
||||
}},
|
||||
"config": {
|
||||
Title: "Configuration",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
do := request.FormValue("do")
|
||||
var status string
|
||||
if do == "save" {
|
||||
configJSON, err := json.MarshalIndent(config.Config, "", "\t")
|
||||
if err != nil {
|
||||
status += gclog.Println(gclog.LErrorLog, err.Error()) + "<br />"
|
||||
} else if err = ioutil.WriteFile("gochan.json", configJSON, 0777); err != nil {
|
||||
status += gclog.Println(gclog.LErrorLog,
|
||||
"Error backing up old gochan.json, cancelling save:", err.Error())
|
||||
} else {
|
||||
config.Config.Lockdown = (request.PostFormValue("Lockdown") == "on")
|
||||
config.Config.LockdownMessage = request.PostFormValue("LockdownMessage")
|
||||
SillytagsArr := strings.Split(request.PostFormValue("Sillytags"), "\n")
|
||||
var Sillytags []string
|
||||
for _, tag := range SillytagsArr {
|
||||
Sillytags = append(Sillytags, strings.Trim(tag, " \n\r"))
|
||||
}
|
||||
|
||||
config.Config.Sillytags = Sillytags
|
||||
config.Config.UseSillytags = (request.PostFormValue("UseSillytags") == "on")
|
||||
config.Config.Modboard = request.PostFormValue("Modboard")
|
||||
config.Config.SiteName = request.PostFormValue("SiteName")
|
||||
config.Config.SiteSlogan = request.PostFormValue("SiteSlogan")
|
||||
config.Config.SiteHeaderURL = request.PostFormValue("SiteHeaderURL")
|
||||
config.Config.SiteWebfolder = request.PostFormValue("SiteWebfolder")
|
||||
// TODO: Change this to match the new Style type in gochan.json
|
||||
/* Styles_arr := strings.Split(request.PostFormValue("Styles"), "\n")
|
||||
var Styles []string
|
||||
for _, style := range Styles_arr {
|
||||
Styles = append(Styles, strings.Trim(style, " \n\r"))
|
||||
}
|
||||
config.Styles = Styles */
|
||||
config.Config.DefaultStyle = request.PostFormValue("DefaultStyle")
|
||||
config.Config.AllowDuplicateImages = (request.PostFormValue("AllowDuplicateImages") == "on")
|
||||
config.Config.AllowVideoUploads = (request.PostFormValue("AllowVideoUploads") == "on")
|
||||
NewThreadDelay, err := strconv.Atoi(request.PostFormValue("NewThreadDelay"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.NewThreadDelay = NewThreadDelay
|
||||
}
|
||||
|
||||
ReplyDelay, err := strconv.Atoi(request.PostFormValue("ReplyDelay"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ReplyDelay = ReplyDelay
|
||||
}
|
||||
|
||||
MaxLineLength, err := strconv.Atoi(request.PostFormValue("MaxLineLength"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.MaxLineLength = MaxLineLength
|
||||
}
|
||||
|
||||
ReservedTripsArr := strings.Split(request.PostFormValue("ReservedTrips"), "\n")
|
||||
var ReservedTrips []string
|
||||
for _, trip := range ReservedTripsArr {
|
||||
ReservedTrips = append(ReservedTrips, strings.Trim(trip, " \n\r"))
|
||||
|
||||
}
|
||||
config.Config.ReservedTrips = ReservedTrips
|
||||
|
||||
ThumbWidth, err := strconv.Atoi(request.PostFormValue("ThumbWidth"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbWidth = ThumbWidth
|
||||
}
|
||||
|
||||
ThumbHeight, err := strconv.Atoi(request.PostFormValue("ThumbHeight"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbHeight = ThumbHeight
|
||||
}
|
||||
|
||||
ThumbWidthReply, err := strconv.Atoi(request.PostFormValue("ThumbWidthReply"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbWidthReply = ThumbWidthReply
|
||||
}
|
||||
|
||||
ThumbHeightReply, err := strconv.Atoi(request.PostFormValue("ThumbHeightReply"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbHeightReply = ThumbHeightReply
|
||||
}
|
||||
|
||||
ThumbWidthCatalog, err := strconv.Atoi(request.PostFormValue("ThumbWidthCatalog"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbWidthCatalog = ThumbWidthCatalog
|
||||
}
|
||||
|
||||
ThumbHeightCatalog, err := strconv.Atoi(request.PostFormValue("ThumbHeightCatalog"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.ThumbHeightCatalog = ThumbHeightCatalog
|
||||
}
|
||||
|
||||
RepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("RepliesOnBoardPage"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.RepliesOnBoardPage = RepliesOnBoardPage
|
||||
}
|
||||
|
||||
StickyRepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("StickyRepliesOnBoardPage"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.StickyRepliesOnBoardPage = StickyRepliesOnBoardPage
|
||||
}
|
||||
|
||||
BanColorsArr := strings.Split(request.PostFormValue("BanColors"), "\n")
|
||||
var BanColors []string
|
||||
for _, color := range BanColorsArr {
|
||||
BanColors = append(BanColors, strings.Trim(color, " \n\r"))
|
||||
|
||||
}
|
||||
config.Config.BanColors = BanColors
|
||||
|
||||
config.Config.BanMsg = request.PostFormValue("BanMsg")
|
||||
EmbedWidth, err := strconv.Atoi(request.PostFormValue("EmbedWidth"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.EmbedWidth = EmbedWidth
|
||||
}
|
||||
|
||||
EmbedHeight, err := strconv.Atoi(request.PostFormValue("EmbedHeight"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.EmbedHeight = EmbedHeight
|
||||
}
|
||||
|
||||
config.Config.ExpandButton = (request.PostFormValue("ExpandButton") == "on")
|
||||
config.Config.ImagesOpenNewTab = (request.PostFormValue("ImagesOpenNewTab") == "on")
|
||||
config.Config.MakeURLsHyperlinked = (request.PostFormValue("MakeURLsHyperlinked") == "on")
|
||||
config.Config.NewTabOnOutlinks = (request.PostFormValue("NewTabOnOutlinks") == "on")
|
||||
config.Config.MinifyHTML = (request.PostFormValue("MinifyHTML") == "on")
|
||||
config.Config.MinifyJS = (request.PostFormValue("MinifyJS") == "on")
|
||||
config.Config.DateTimeFormat = request.PostFormValue("DateTimeFormat")
|
||||
AkismetAPIKey := request.PostFormValue("AkismetAPIKey")
|
||||
|
||||
if err = serverutil.CheckAkismetAPIKey(AkismetAPIKey); err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.AkismetAPIKey = AkismetAPIKey
|
||||
}
|
||||
|
||||
config.Config.UseCaptcha = (request.PostFormValue("UseCaptcha") == "on")
|
||||
CaptchaWidth, err := strconv.Atoi(request.PostFormValue("CaptchaWidth"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.CaptchaWidth = CaptchaWidth
|
||||
}
|
||||
CaptchaHeight, err := strconv.Atoi(request.PostFormValue("CaptchaHeight"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.CaptchaHeight = CaptchaHeight
|
||||
}
|
||||
|
||||
config.Config.EnableGeoIP = (request.PostFormValue("EnableGeoIP") == "on")
|
||||
config.Config.GeoIPDBlocation = request.PostFormValue("GeoIPDBlocation")
|
||||
|
||||
MaxRecentPosts, err := strconv.Atoi(request.PostFormValue("MaxRecentPosts"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.MaxRecentPosts = MaxRecentPosts
|
||||
}
|
||||
|
||||
config.Config.EnableAppeals = (request.PostFormValue("EnableAppeals") == "on")
|
||||
MaxLogDays, err := strconv.Atoi(request.PostFormValue("MaxLogDays"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.Config.MaxLogDays = MaxLogDays
|
||||
}
|
||||
|
||||
configJSON, err = json.MarshalIndent(config.Config, "", "\t")
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else if err = ioutil.WriteFile("gochan.json", configJSON, 0777); err != nil {
|
||||
status = gclog.Print(gclog.LErrorLog, "Error writing gochan.json: ", err.Error())
|
||||
} else {
|
||||
status = "Wrote gochan.json successfully<br />"
|
||||
building.BuildJS()
|
||||
}
|
||||
}
|
||||
}
|
||||
manageConfigBuffer := bytes.NewBufferString("")
|
||||
if err := gctemplates.ManageConfig.Execute(manageConfigBuffer,
|
||||
map[string]interface{}{"config": config.Config, "status": status},
|
||||
); err != nil {
|
||||
return html + gclog.Print(gclog.LErrorLog, "Error executing config management page: ", err.Error())
|
||||
}
|
||||
html += manageConfigBuffer.String()
|
||||
return
|
||||
}},
|
||||
"login": {
|
||||
Title: "Login",
|
||||
Permissions: 0,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
if GetStaffRank(request) > 0 {
|
||||
http.Redirect(writer, request, path.Join(config.Config.SiteWebfolder, "manage"), http.StatusFound)
|
||||
}
|
||||
username := request.FormValue("username")
|
||||
password := request.FormValue("password")
|
||||
redirectAction := request.FormValue("action")
|
||||
if redirectAction == "" {
|
||||
redirectAction = "announcements"
|
||||
}
|
||||
if username == "" || password == "" {
|
||||
//assume that they haven't logged in
|
||||
html = `<form method="POST" action="` + config.Config.SiteWebfolder + `manage?action=login" id="login-box" class="staff-form">` +
|
||||
`<input type="hidden" name="redirect" value="` + redirectAction + `" />` +
|
||||
`<input type="text" name="username" class="logindata" /><br />` +
|
||||
`<input type="password" name="password" class="logindata" /><br />` +
|
||||
`<input type="submit" value="Login" />` +
|
||||
`</form>`
|
||||
} else {
|
||||
key := gcutil.Md5Sum(request.RemoteAddr + username + password + config.Config.RandomSeed + gcutil.RandomString(3))[0:10]
|
||||
createSession(key, username, password, request, writer)
|
||||
http.Redirect(writer, request, path.Join(config.Config.SiteWebfolder, "manage?action="+request.FormValue("redirect")), http.StatusFound)
|
||||
}
|
||||
return
|
||||
}},
|
||||
"logout": {
|
||||
Title: "Logout",
|
||||
Permissions: 1,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
cookie, _ := request.Cookie("sessiondata")
|
||||
cookie.MaxAge = 0
|
||||
cookie.Expires = time.Now().Add(-7 * 24 * time.Hour)
|
||||
http.SetCookie(writer, cookie)
|
||||
return "Logged out successfully"
|
||||
}},
|
||||
"announcements": {
|
||||
Title: "Announcements",
|
||||
Permissions: 1,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
html = `<h1 class="manage-header">Announcements</h1><br />`
|
||||
|
||||
//get all announcements to announcement list
|
||||
//loop to html if exist, no announcement if empty
|
||||
announcements, err := gcsql.GetAllAccouncements()
|
||||
if err != nil {
|
||||
return html + gclog.Print(gclog.LErrorLog, "Error getting announcements: ", err.Error())
|
||||
}
|
||||
if len(announcements) == 0 {
|
||||
html += "No announcements"
|
||||
} else {
|
||||
for _, announcement := range announcements {
|
||||
html += `<div class="section-block">` +
|
||||
`<div class="section-title-block"><b>` + announcement.Subject + `</b> by ` + announcement.Poster + ` at ` + announcement.Timestamp.Format(config.Config.DateTimeFormat) + `</div>` +
|
||||
`<div class="section-body">` + announcement.Message + `</div></div>`
|
||||
}
|
||||
}
|
||||
return html
|
||||
}},
|
||||
"bans": {
|
||||
Title: "Bans",
|
||||
Permissions: 1,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (pageHTML string) { //TODO whatever this does idk man
|
||||
var post gcsql.Post
|
||||
if request.FormValue("do") == "add" {
|
||||
ip := net.ParseIP(request.FormValue("ip"))
|
||||
name := request.FormValue("name")
|
||||
nameIsRegex := (request.FormValue("nameregex") == "on")
|
||||
checksum := request.FormValue("checksum")
|
||||
filename := request.FormValue("filename")
|
||||
durationForm := request.FormValue("duration")
|
||||
permaban := (durationForm == "" || durationForm == "0" || durationForm == "forever")
|
||||
duration, err := gcutil.ParseDurationString(durationForm)
|
||||
if err != nil {
|
||||
serverutil.ServeErrorPage(writer, err.Error())
|
||||
}
|
||||
expires := time.Now().Add(duration)
|
||||
|
||||
boards := request.FormValue("boards")
|
||||
reason := html.EscapeString(request.FormValue("reason"))
|
||||
staffNote := html.EscapeString(request.FormValue("staffnote"))
|
||||
currentStaff, _ := getCurrentStaff(request)
|
||||
|
||||
err = nil
|
||||
if filename != "" {
|
||||
err = gcsql.FileNameBan(filename, nameIsRegex, currentStaff, expires, permaban, staffNote, boards)
|
||||
}
|
||||
if err != nil {
|
||||
pageHTML += err.Error()
|
||||
err = nil
|
||||
}
|
||||
if name != "" {
|
||||
err = gcsql.UserNameBan(name, nameIsRegex, currentStaff, expires, permaban, staffNote, boards)
|
||||
}
|
||||
if err != nil {
|
||||
pageHTML += err.Error()
|
||||
err = nil
|
||||
}
|
||||
|
||||
if request.FormValue("fullban") == "on" {
|
||||
err = gcsql.UserBan(ip, false, currentStaff, boards, expires, permaban, staffNote, reason, true, time.Now())
|
||||
if err != nil {
|
||||
pageHTML += err.Error()
|
||||
err = nil
|
||||
}
|
||||
} else {
|
||||
if request.FormValue("threadban") == "on" {
|
||||
err = gcsql.UserBan(ip, true, currentStaff, boards, expires, permaban, staffNote, reason, true, time.Now())
|
||||
if err != nil {
|
||||
pageHTML += err.Error()
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if request.FormValue("imageban") == "on" {
|
||||
err = gcsql.FileBan(checksum, currentStaff, expires, permaban, staffNote, boards)
|
||||
if err != nil {
|
||||
pageHTML += err.Error()
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if request.FormValue("postid") != "" {
|
||||
var err error
|
||||
post, err = gcsql.GetSpecificPostByString(request.FormValue("postid"))
|
||||
if err != nil {
|
||||
return pageHTML + gclog.Print(gclog.LErrorLog, "Error getting post: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
banlist, err := gcsql.GetAllBans()
|
||||
if err != nil {
|
||||
return pageHTML + gclog.Print(gclog.LErrorLog, "Error getting ban list: ", err.Error())
|
||||
}
|
||||
manageBansBuffer := bytes.NewBufferString("")
|
||||
|
||||
if err := gctemplates.ManageBans.Execute(manageBansBuffer,
|
||||
map[string]interface{}{"config": config.Config, "banlist": banlist, "post": post},
|
||||
); err != nil {
|
||||
return pageHTML + gclog.Print(gclog.LErrorLog, "Error executing ban management page template: ", err.Error())
|
||||
}
|
||||
pageHTML += manageBansBuffer.String()
|
||||
return
|
||||
}},
|
||||
"getstaffjquery": {
|
||||
Permissions: 0,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
staff, err := getCurrentFullStaff(request)
|
||||
if err != nil {
|
||||
html = "nobody;0;"
|
||||
return
|
||||
}
|
||||
html = staff.Username + ";" + strconv.Itoa(staff.Rank) + ";" + staff.Boards
|
||||
return
|
||||
}},
|
||||
"boards": {
|
||||
Title: "Boards",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
do := request.FormValue("do")
|
||||
var done bool
|
||||
board := new(gcsql.Board)
|
||||
var boardCreationStatus string
|
||||
var err error
|
||||
for !done {
|
||||
switch {
|
||||
case do == "add":
|
||||
board.Dir = request.FormValue("dir")
|
||||
if board.Dir == "" {
|
||||
boardCreationStatus = `Error: "Directory" cannot be blank`
|
||||
do = ""
|
||||
continue
|
||||
}
|
||||
orderStr := request.FormValue("order")
|
||||
board.ListOrder, err = strconv.Atoi(orderStr)
|
||||
if err != nil {
|
||||
board.ListOrder = 0
|
||||
}
|
||||
board.Title = request.FormValue("title")
|
||||
if board.Title == "" {
|
||||
boardCreationStatus = `Error: "Title" cannot be blank`
|
||||
do = ""
|
||||
continue
|
||||
}
|
||||
board.Subtitle = request.FormValue("subtitle")
|
||||
board.Description = request.FormValue("description")
|
||||
sectionStr := request.FormValue("section")
|
||||
if sectionStr == "none" {
|
||||
sectionStr = "0"
|
||||
}
|
||||
|
||||
board.CreatedOn = time.Now()
|
||||
board.Section, err = strconv.Atoi(sectionStr)
|
||||
if err != nil {
|
||||
board.Section = 0
|
||||
}
|
||||
board.MaxFilesize, err = strconv.Atoi(request.FormValue("maximagesize"))
|
||||
if err != nil {
|
||||
board.MaxFilesize = 1024 * 4
|
||||
}
|
||||
|
||||
board.MaxPages, err = strconv.Atoi(request.FormValue("maxpages"))
|
||||
if err != nil {
|
||||
board.MaxPages = 11
|
||||
}
|
||||
|
||||
board.DefaultStyle = strings.Trim(request.FormValue("defaultstyle"), "\n")
|
||||
board.Locked = (request.FormValue("locked") == "on")
|
||||
board.ForcedAnon = (request.FormValue("forcedanon") == "on")
|
||||
|
||||
board.Anonymous = request.FormValue("anonymous")
|
||||
if board.Anonymous == "" {
|
||||
board.Anonymous = "Anonymous"
|
||||
}
|
||||
|
||||
board.MaxAge, err = strconv.Atoi(request.FormValue("maxage"))
|
||||
if err != nil {
|
||||
board.MaxAge = 0
|
||||
}
|
||||
|
||||
board.AutosageAfter, err = strconv.Atoi(request.FormValue("autosageafter"))
|
||||
if err != nil {
|
||||
board.AutosageAfter = 200
|
||||
}
|
||||
|
||||
board.NoImagesAfter, err = strconv.Atoi(request.FormValue("noimagesafter"))
|
||||
if err != nil {
|
||||
board.NoImagesAfter = 0
|
||||
}
|
||||
|
||||
board.MaxMessageLength, err = strconv.Atoi(request.FormValue("maxmessagelength"))
|
||||
if err != nil {
|
||||
board.MaxMessageLength = 1024 * 8
|
||||
}
|
||||
|
||||
board.EmbedsAllowed = (request.FormValue("embedsallowed") == "on")
|
||||
board.RedirectToThread = (request.FormValue("redirecttothread") == "on")
|
||||
board.RequireFile = (request.FormValue("require_file") == "on")
|
||||
board.EnableCatalog = (request.FormValue("enablecatalog") == "on")
|
||||
|
||||
//actually start generating stuff
|
||||
if err = os.Mkdir(path.Join(config.Config.DocumentRoot, board.Dir), 0666); err != nil {
|
||||
do = ""
|
||||
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/ already exists.",
|
||||
config.Config.DocumentRoot, board.Dir)
|
||||
break
|
||||
}
|
||||
|
||||
if err = os.Mkdir(path.Join(config.Config.DocumentRoot, board.Dir, "res"), 0666); err != nil {
|
||||
do = ""
|
||||
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/res/ already exists.",
|
||||
config.Config.DocumentRoot, board.Dir)
|
||||
break
|
||||
}
|
||||
|
||||
if err = os.Mkdir(path.Join(config.Config.DocumentRoot, board.Dir, "src"), 0666); err != nil {
|
||||
do = ""
|
||||
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/src/ already exists.",
|
||||
config.Config.DocumentRoot, board.Dir)
|
||||
break
|
||||
}
|
||||
|
||||
if err = os.Mkdir(path.Join(config.Config.DocumentRoot, board.Dir, "thumb"), 0666); err != nil {
|
||||
do = ""
|
||||
boardCreationStatus = gclog.Printf(gclog.LStaffLog|gclog.LErrorLog, "Directory %s/%s/thumb/ already exists.",
|
||||
config.Config.DocumentRoot, board.Dir)
|
||||
break
|
||||
}
|
||||
|
||||
if err := gcsql.CreateBoard(board); err != nil {
|
||||
do = ""
|
||||
boardCreationStatus = gclog.Print(gclog.LErrorLog, "Error creating board: ", err.Error())
|
||||
break
|
||||
} else {
|
||||
boardCreationStatus = "Board created successfully"
|
||||
building.BuildBoards()
|
||||
gcsql.ResetBoardSectionArrays()
|
||||
gclog.Print(gclog.LStaffLog, "Boards rebuilt successfully")
|
||||
done = true
|
||||
}
|
||||
case do == "del":
|
||||
// resetBoardSectionArrays()
|
||||
case do == "edit":
|
||||
// resetBoardSectionArrays()
|
||||
default:
|
||||
// put the default column values in the text boxes
|
||||
board.Section = 1
|
||||
board.MaxFilesize = 4718592
|
||||
board.MaxPages = 11
|
||||
board.DefaultStyle = "pipes.css"
|
||||
board.Anonymous = "Anonymous"
|
||||
board.AutosageAfter = 200
|
||||
board.MaxMessageLength = 8192
|
||||
board.EmbedsAllowed = true
|
||||
board.EnableCatalog = true
|
||||
board.Worksafe = true
|
||||
board.ThreadsPerPage = config.Config.ThreadsPerPage
|
||||
}
|
||||
|
||||
html = `<h1 class="manage-header">Manage boards</h1><form action="/manage?action=boards" method="POST"><input type="hidden" name="do" value="existing" /><select name="boardselect"><option>Select board...</option>`
|
||||
boards, err := gcsql.GetBoardUris()
|
||||
if err != nil {
|
||||
return html + gclog.Print(gclog.LErrorLog, "Error getting board list: ", err.Error())
|
||||
}
|
||||
for _, boardDir := range boards {
|
||||
html += "<option>" + boardDir + "</option>"
|
||||
}
|
||||
|
||||
html += `</select><input type="submit" value="Edit" /><input type="submit" value="Delete" /></form><hr />` +
|
||||
`<h2 class="manage-header">Create new board</h2><span id="board-creation-message">` + boardCreationStatus + `</span><br />`
|
||||
|
||||
manageBoardsBuffer := bytes.NewBufferString("")
|
||||
gcsql.AllSections, _ = gcsql.GetAllSectionsOrCreateDefault()
|
||||
|
||||
if err := gctemplates.ManageBoards.Execute(manageBoardsBuffer, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"board": board,
|
||||
"section_arr": gcsql.AllSections,
|
||||
}); err != nil {
|
||||
return html + gclog.Print(gclog.LErrorLog,
|
||||
"Error executing board management page template: ", err.Error())
|
||||
}
|
||||
html += manageBoardsBuffer.String()
|
||||
return
|
||||
}
|
||||
gcsql.ResetBoardSectionArrays()
|
||||
return
|
||||
}},
|
||||
"staffmenu": {
|
||||
Title: "Staff menu",
|
||||
Permissions: 1,
|
||||
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 />` +
|
||||
`<a href="javascript:void(0)" id="announcements" class="staffmenu-item">Announcements</a><br />`
|
||||
if rank == 3 {
|
||||
html += `<b>Admin stuff</b><br /><a href="javascript:void(0)" id="staff" class="staffmenu-item">Manage staff</a><br />` +
|
||||
//`<a href="javascript:void(0)" id="purgeeverything" class="staffmenu-item">Purge everything!</a><br />` +
|
||||
`<a href="javascript:void(0)" id="executesql" class="staffmenu-item">Execute SQL statement(s)</a><br />` +
|
||||
`<a href="javascript:void(0)" id="cleanup" class="staffmenu-item">Run cleanup</a><br />` +
|
||||
`<a href="javascript:void(0)" id="rebuildall" class="staffmenu-item">Rebuild all</a><br />` +
|
||||
`<a href="javascript:void(0)" id="rebuildfront" class="staffmenu-item">Rebuild front page</a><br />` +
|
||||
`<a href="javascript:void(0)" id="rebuildboards" class="staffmenu-item">Rebuild board pages</a><br />` +
|
||||
`<a href="javascript:void(0)" id="reparsehtml" class="staffmenu-item">Reparse all posts</a><br />` +
|
||||
`<a href="javascript:void(0)" id="boards" class="staffmenu-item">Add/edit/delete boards</a><br />`
|
||||
}
|
||||
if rank >= 2 {
|
||||
html += `<b>Mod stuff</b><br />` +
|
||||
`<a href="javascript:void(0)" id="bans" class="staffmenu-item">Ban User(s)</a><br />`
|
||||
}
|
||||
|
||||
if rank >= 1 {
|
||||
html += `<a href="javascript:void(0)" id="recentimages" class="staffmenu-item">Recently uploaded images</a><br />` +
|
||||
`<a href="javascript:void(0)" id="recentposts" class="staffmenu-item">Recent posts</a><br />` +
|
||||
`<a href="javascript:void(0)" id="searchip" class="staffmenu-item">Search posts by IP</a><br />`
|
||||
}
|
||||
return
|
||||
}},
|
||||
"rebuildfront": {
|
||||
Title: "Rebuild front page",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
gctemplates.InitTemplates()
|
||||
return building.BuildFrontPage()
|
||||
}},
|
||||
"rebuildall": {
|
||||
Title: "Rebuild everything",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
gctemplates.InitTemplates()
|
||||
gcsql.ResetBoardSectionArrays()
|
||||
return building.BuildFrontPage() + "<hr />" +
|
||||
building.BuildBoardListJSON() + "<hr />" +
|
||||
building.BuildBoards() + "<hr />" +
|
||||
building.BuildJS() + "<hr />"
|
||||
}},
|
||||
"rebuildboards": {
|
||||
Title: "Rebuild boards",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
gctemplates.InitTemplates()
|
||||
return building.BuildBoards()
|
||||
}},
|
||||
"reparsehtml": {
|
||||
Title: "Reparse HTML",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
messages, err := gcsql.GetAllNondeletedMessageRaw()
|
||||
if err != nil {
|
||||
html += err.Error() + "<br />"
|
||||
return
|
||||
}
|
||||
|
||||
for _, message := range messages {
|
||||
message.Message = posting.FormatMessage(message.MessageRaw)
|
||||
}
|
||||
err = gcsql.SetMessages(messages)
|
||||
|
||||
if err != nil {
|
||||
return html + gclog.Printf(gclog.LErrorLog, err.Error())
|
||||
}
|
||||
html += "Done reparsing HTML<hr />" +
|
||||
building.BuildFrontPage() + "<hr />" +
|
||||
building.BuildBoardListJSON() + "<hr />" +
|
||||
building.BuildBoards() + "<hr />"
|
||||
return
|
||||
}},
|
||||
"recentposts": {
|
||||
Title: "Recent posts",
|
||||
Permissions: 1,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
limit := request.FormValue("limit")
|
||||
if limit == "" {
|
||||
limit = "50"
|
||||
}
|
||||
html = `<h1 class="manage-header">Recent posts</h1>` +
|
||||
`Limit by: <select id="limit">` +
|
||||
`<option>25</option><option>50</option><option>100</option><option>200</option>` +
|
||||
`</select><br /><table width="100%%d" border="1">` +
|
||||
`<colgroup><col width="25%%" /><col width="50%%" /><col width="17%%" /></colgroup>` +
|
||||
`<tr><th></th><th>Message</th><th>Time</th></tr>`
|
||||
recentposts, err := gcsql.GetRecentPostsGlobal(gcutil.HackyStringToInt(limit), false) //only uses boardname, boardid, postid, parentid, message, ip and timestamp
|
||||
|
||||
if err != nil {
|
||||
return html + "<tr><td>" + gclog.Print(gclog.LErrorLog, "Error getting recent posts: ",
|
||||
err.Error()) + "</td></tr></table>"
|
||||
}
|
||||
|
||||
for _, recentpost := range recentposts {
|
||||
html += fmt.Sprintf(
|
||||
`<tr><td><b>Post:</b> <a href="%s">%s/%d</a><br /><b>IP:</b> %s</td><td>%s</td><td>%s</td></tr>`,
|
||||
path.Join(config.Config.SiteWebfolder, recentpost.BoardName, "/res/", strconv.Itoa(recentpost.ParentID)+".html#"+strconv.Itoa(recentpost.PostID)),
|
||||
recentpost.BoardName, recentpost.PostID, recentpost.IP, recentpost.Message,
|
||||
recentpost.Timestamp.Format("01/02/06, 15:04"),
|
||||
)
|
||||
}
|
||||
html += "</table>"
|
||||
return
|
||||
}},
|
||||
"postinfo": {
|
||||
Title: "Post info",
|
||||
Permissions: 2,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
errMap := map[string]interface{}{
|
||||
"action": "postInfo",
|
||||
"success": false,
|
||||
}
|
||||
post, err := gcsql.GetSpecificPost(gcutil.HackyStringToInt(request.FormValue("postid")), false)
|
||||
if err != nil {
|
||||
errMap["message"] = err.Error()
|
||||
jsonErr, _ := gcutil.MarshalJSON(errMap, false)
|
||||
return jsonErr
|
||||
}
|
||||
jsonStr, _ := gcutil.MarshalJSON(post, false)
|
||||
return jsonStr
|
||||
}},
|
||||
"staff": {
|
||||
Title: "Staff",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
do := request.FormValue("do")
|
||||
html = `<h1 class="manage-header">Staff</h1><br />` +
|
||||
`<table id="stafftable" border="1">` +
|
||||
"<tr><td><b>Username</b></td><td><b>Rank</b></td><td><b>Boards</b></td><td><b>Added on</b></td><td><b>Action</b></td></tr>"
|
||||
allStaff, err := gcsql.GetAllStaffNopass()
|
||||
if err != nil {
|
||||
return html + gclog.Print(gclog.LErrorLog, "Error getting staff list: ", err.Error())
|
||||
}
|
||||
|
||||
for _, staff := range allStaff {
|
||||
username := request.FormValue("username")
|
||||
password := request.FormValue("password")
|
||||
rank := request.FormValue("rank")
|
||||
rankI, _ := strconv.Atoi(rank)
|
||||
if do == "add" {
|
||||
if err := gcsql.NewStaff(username, password, rankI); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Printf(gclog.LErrorLog,
|
||||
"Error creating new staff account %q: %s", username, err.Error()))
|
||||
return
|
||||
}
|
||||
} else if do == "del" && username != "" {
|
||||
if err = gcsql.DeleteStaff(request.FormValue("username")); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Printf(gclog.LErrorLog,
|
||||
"Error deleting staff account %q : %s", username, err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case staff.Rank == 3:
|
||||
rank = "admin"
|
||||
case staff.Rank == 2:
|
||||
rank = "mod"
|
||||
case staff.Rank == 1:
|
||||
rank = "janitor"
|
||||
}
|
||||
html += fmt.Sprintf(
|
||||
`<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td><a href="/manage?action=staff&do=del&username=%s" style="float:right;color:red;">X</a></td></tr>`,
|
||||
staff.Username, rank, staff.Boards, staff.AddedOn.Format(config.Config.DateTimeFormat), staff.Username)
|
||||
|
||||
}
|
||||
html += `</table><hr /><h2 class="manage-header">Add new staff</h2>` +
|
||||
`<form action="/manage?action=staff" onsubmit="return makeNewStaff();" method="POST">` +
|
||||
`<input type="hidden" name="do" value="add" />` +
|
||||
`Username: <input id="username" name="username" type="text" /><br />` +
|
||||
`Password: <input id="password" name="password" type="password" /><br />` +
|
||||
`Rank: <select id="rank" name="rank">` +
|
||||
`<option value="3">Admin</option>` +
|
||||
`<option value="2">Moderator</option>` +
|
||||
`<option value="1">Janitor</option>` +
|
||||
`</select><br />` +
|
||||
`<input id="submitnewstaff" type="submit" value="Add" />` +
|
||||
`</form>`
|
||||
return
|
||||
}},
|
||||
"tempposts": {
|
||||
Title: "Temporary posts lists",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
html += `<h1 class="manage-header">Temporary posts</h1>`
|
||||
if len(gcsql.TempPosts) == 0 {
|
||||
html += "No temporary posts<br />"
|
||||
return
|
||||
}
|
||||
for p, post := range gcsql.TempPosts {
|
||||
html += fmt.Sprintf("Post[%d]: %#v<br />", p, post)
|
||||
}
|
||||
return
|
||||
}},
|
||||
}
|
59
pkg/manage/handler.go
Normal file
59
pkg/manage/handler.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package manage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gctemplates"
|
||||
"github.com/gochan-org/gochan/pkg/serverutil"
|
||||
)
|
||||
|
||||
// CallManageFunction is called when a user accesses /manage to use manage tools
|
||||
// or log in to a staff account
|
||||
func CallManageFunction(writer http.ResponseWriter, request *http.Request) {
|
||||
var err error
|
||||
if err = request.ParseForm(); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error parsing form data: ", err.Error()))
|
||||
}
|
||||
|
||||
action := request.FormValue("action")
|
||||
staffRank := GetStaffRank(request)
|
||||
var managePageBuffer bytes.Buffer
|
||||
if action == "" {
|
||||
action = "announcements"
|
||||
} else if action == "postinfo" {
|
||||
writer.Header().Add("Content-Type", "application/json")
|
||||
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
|
||||
}
|
||||
|
||||
if action != "getstaffjquery" && action != "postinfo" {
|
||||
managePageBuffer.WriteString("<!DOCTYPE html><html><head>")
|
||||
if err = gctemplates.ManageHeader.Execute(&managePageBuffer, config.Config); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog|gclog.LStaffLog,
|
||||
"Error executing manage page header template: ", err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := manageFunctions[action]; ok {
|
||||
if staffRank >= manageFunctions[action].Permissions {
|
||||
managePageBuffer.Write([]byte(manageFunctions[action].Callback(writer, request)))
|
||||
} else if staffRank == 0 && manageFunctions[action].Permissions == 0 {
|
||||
managePageBuffer.Write([]byte(manageFunctions[action].Callback(writer, request)))
|
||||
} else if staffRank == 0 {
|
||||
managePageBuffer.Write([]byte(manageFunctions["login"].Callback(writer, request)))
|
||||
} else {
|
||||
managePageBuffer.Write([]byte(action + " is undefined."))
|
||||
}
|
||||
} else {
|
||||
managePageBuffer.Write([]byte(action + " is undefined."))
|
||||
}
|
||||
if action != "getstaffjquery" && action != "postinfo" {
|
||||
managePageBuffer.Write([]byte("</body></html>"))
|
||||
}
|
||||
|
||||
writer.Write(managePageBuffer.Bytes())
|
||||
}
|
88
pkg/manage/util.go
Normal file
88
pkg/manage/util.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package manage
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/serverutil"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
sSuccess
|
||||
sInvalidPassword
|
||||
sOtherError
|
||||
)
|
||||
|
||||
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
|
||||
domain = chopPortNumRegex.Split(domain, -1)[0]
|
||||
|
||||
if !serverutil.ValidReferer(request) {
|
||||
gclog.Print(gclog.LStaffLog, "Rejected login from possible spambot @ "+request.RemoteAddr)
|
||||
return 2
|
||||
}
|
||||
staff, err := gcsql.GetStaffByName(username)
|
||||
if err != nil {
|
||||
gclog.Print(gclog.LErrorLog, err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
success := bcrypt.CompareHashAndPassword([]byte(staff.PasswordChecksum), []byte(password))
|
||||
if success == bcrypt.ErrMismatchedHashAndPassword {
|
||||
// password mismatch
|
||||
gclog.Print(gclog.LStaffLog, "Failed login (password mismatch) from "+request.RemoteAddr+" at "+time.Now().Format(gcsql.MySQLDatetimeFormat))
|
||||
return 1
|
||||
}
|
||||
|
||||
// successful login, add cookie that expires in one month
|
||||
http.SetCookie(writer, &http.Cookie{
|
||||
Name: "sessiondata",
|
||||
Value: key,
|
||||
Path: "/",
|
||||
Domain: domain,
|
||||
MaxAge: 60 * 60 * 24 * 7,
|
||||
})
|
||||
|
||||
if err = gcsql.CreateSession(key, username); err != nil {
|
||||
gclog.Print(gclog.LErrorLog, "Error creating new staff session: ", err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func getCurrentStaff(request *http.Request) (string, error) { //TODO after refactor, check if still used
|
||||
sessionCookie, err := request.Cookie("sessiondata")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name, err := gcsql.GetStaffName(sessionCookie.Value)
|
||||
if err == nil {
|
||||
return "", err
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func getCurrentFullStaff(request *http.Request) (*gcsql.Staff, error) {
|
||||
sessionCookie, err := request.Cookie("sessiondata")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gcsql.GetStaffBySession(sessionCookie.Value)
|
||||
}
|
||||
|
||||
// GetStaffRank returns the rank number of the staff referenced in the request
|
||||
func GetStaffRank(request *http.Request) int {
|
||||
staff, err := getCurrentFullStaff(request)
|
||||
if err != nil {
|
||||
gclog.Print(gclog.LErrorLog, "Error getting current staff: ", err.Error())
|
||||
return 0
|
||||
}
|
||||
return staff.Rank
|
||||
}
|
88
pkg/posting/bans.go
Normal file
88
pkg/posting/bans.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package posting
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gctemplates"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/gochan-org/gochan/pkg/serverutil"
|
||||
)
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
ThreadBan
|
||||
ImageBan
|
||||
FullBan
|
||||
)
|
||||
|
||||
// BanHandler is used for serving ban pages
|
||||
func BanHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
appealMsg := request.FormValue("appealmsg")
|
||||
// banStatus, err := getBannedStatus(request)
|
||||
var banStatus gcsql.BanInfo
|
||||
var err error
|
||||
|
||||
if appealMsg != "" {
|
||||
if banStatus.BannedForever() {
|
||||
fmt.Fprint(writer, "No.")
|
||||
return
|
||||
}
|
||||
escapedMsg := html.EscapeString(appealMsg)
|
||||
if err = gcsql.AddBanAppeal(banStatus.ID, escapedMsg); err != nil {
|
||||
serverutil.ServeErrorPage(writer, err.Error())
|
||||
}
|
||||
fmt.Fprint(writer,
|
||||
"Appeal sent. It will (hopefully) be read by a staff member. check "+config.Config.SiteWebfolder+"banned occasionally for a response",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error getting banned status:", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err = gcutil.MinifyTemplate(gctemplates.Banpage, map[string]interface{}{
|
||||
"config": config.Config, "ban": banStatus, "banBoards": banStatus.Boards, "post": gcsql.Post{},
|
||||
}, writer, "text/html"); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error minifying page template: ", err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Checks check poster's name/tripcode/file checksum (from Post post) for banned status
|
||||
// returns ban table if the user is banned or sql.ErrNoRows if they aren't
|
||||
func getBannedStatus(request *http.Request) (*gcsql.BanInfo, error) {
|
||||
formName := request.FormValue("postname")
|
||||
var tripcode string
|
||||
if formName != "" {
|
||||
parsedName := gcutil.ParseName(formName)
|
||||
tripcode += parsedName["name"]
|
||||
if tc, ok := parsedName["tripcode"]; ok {
|
||||
tripcode += "!" + tc
|
||||
}
|
||||
}
|
||||
ip := gcutil.GetRealIP(request)
|
||||
|
||||
var filename string
|
||||
var checksum string
|
||||
file, fileHandler, err := request.FormFile("imagefile")
|
||||
if err == nil {
|
||||
html.EscapeString(fileHandler.Filename)
|
||||
if data, err2 := ioutil.ReadAll(file); err2 == nil {
|
||||
checksum = fmt.Sprintf("%x", md5.Sum(data))
|
||||
}
|
||||
}
|
||||
file.Close()
|
||||
return gcsql.CheckBan(ip, tripcode, filename, checksum)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package posting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -6,8 +6,14 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
//"github.com/mojocn/base64Captcha"
|
||||
"gopkg.in/mojocn/base64Captcha.v1"
|
||||
"github.com/gochan-org/gochan/pkg/building"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gctemplates"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/gochan-org/gochan/pkg/serverutil"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -23,23 +29,25 @@ type captchaJSON struct {
|
|||
EmailCmd string `json:"-"`
|
||||
}
|
||||
|
||||
func initCaptcha() {
|
||||
if !config.UseCaptcha {
|
||||
// InitCaptcha prepares the captcha driver for use
|
||||
func InitCaptcha() {
|
||||
if !config.Config.UseCaptcha {
|
||||
return
|
||||
}
|
||||
driver = base64Captcha.NewDriverString(
|
||||
config.CaptchaHeight, config.CaptchaWidth, 0, 0, 6,
|
||||
config.Config.CaptchaHeight, config.Config.CaptchaWidth, 0, 0, 6,
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
&color.RGBA{0, 0, 0, 0}, nil).ConvertFonts()
|
||||
}
|
||||
|
||||
func serveCaptcha(writer http.ResponseWriter, request *http.Request) {
|
||||
if !config.UseCaptcha {
|
||||
// ServeCaptcha handles requests to /captcha if UseCaptcha is enabled in gochan.json
|
||||
func ServeCaptcha(writer http.ResponseWriter, request *http.Request) {
|
||||
if !config.Config.UseCaptcha {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
if err = request.ParseForm(); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog, "Error parsing request form: ", err.Error()))
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog, "Error parsing request form: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -56,14 +64,15 @@ func serveCaptcha(writer http.ResponseWriter, request *http.Request) {
|
|||
useJSON := request.FormValue("json") == "1"
|
||||
if useJSON {
|
||||
writer.Header().Add("Content-Type", "application/json")
|
||||
str, _ := marshalJSON(captchaStruct, false)
|
||||
minifyWriter(writer, []byte(str), "application/json")
|
||||
|
||||
str, _ := gcutil.MarshalJSON(captchaStruct, false)
|
||||
gcutil.MinifyWriter(writer, []byte(str), "application/json")
|
||||
return
|
||||
}
|
||||
if request.FormValue("reload") == "Reload" {
|
||||
request.Form.Del("reload")
|
||||
request.Form.Add("didreload", "1")
|
||||
serveCaptcha(writer, request)
|
||||
ServeCaptcha(writer, request)
|
||||
return
|
||||
}
|
||||
writer.Header().Add("Content-Type", "text/html")
|
||||
|
@ -72,18 +81,20 @@ func serveCaptcha(writer http.ResponseWriter, request *http.Request) {
|
|||
if captchaID != "" && request.FormValue("didreload") != "1" {
|
||||
goodAnswer := base64Captcha.DefaultMemStore.Verify(captchaID, captchaAnswer, true)
|
||||
if goodAnswer {
|
||||
if tempPostIndex > -1 && tempPostIndex < len(tempPosts) {
|
||||
if tempPostIndex > -1 && tempPostIndex < len(gcsql.TempPosts) {
|
||||
// came from a /post redirect, insert the specified temporary post
|
||||
// and redirect to the thread
|
||||
InsertPost(&tempPosts[tempPostIndex], emailCommand == "noko")
|
||||
buildBoards(tempPosts[tempPostIndex].BoardID)
|
||||
buildFrontPage()
|
||||
url := tempPosts[tempPostIndex].GetURL(false)
|
||||
|
||||
gcsql.InsertPost(&gcsql.TempPosts[tempPostIndex], emailCommand == "noko")
|
||||
building.BuildBoards(gcsql.TempPosts[tempPostIndex].BoardID)
|
||||
building.BuildFrontPage()
|
||||
|
||||
url := gcsql.TempPosts[tempPostIndex].GetURL(false)
|
||||
|
||||
// move the end Post to the current index and remove the old end Post. We don't
|
||||
// really care about order as long as tempPost validation doesn't get jumbled up
|
||||
tempPosts[tempPostIndex] = tempPosts[len(tempPosts)-1]
|
||||
tempPosts = tempPosts[:len(tempPosts)-1]
|
||||
gcsql.TempPosts[tempPostIndex] = gcsql.TempPosts[len(gcsql.TempPosts)-1]
|
||||
gcsql.TempPosts = gcsql.TempPosts[:len(gcsql.TempPosts)-1]
|
||||
http.Redirect(writer, request, url, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
@ -91,15 +102,14 @@ func serveCaptcha(writer http.ResponseWriter, request *http.Request) {
|
|||
captchaStruct.Result = "Incorrect CAPTCHA"
|
||||
}
|
||||
}
|
||||
if err = minifyTemplate(captchaTmpl, captchaStruct, writer, "text/html"); err != nil {
|
||||
if err = gcutil.MinifyTemplate(gctemplates.Captcha, captchaStruct, writer, "text/html"); err != nil {
|
||||
fmt.Fprintf(writer,
|
||||
gclog.Print(lErrorLog, "Error executing captcha template: ", err.Error()),
|
||||
)
|
||||
gclog.Print(gclog.LErrorLog, "Error executing captcha template: ", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func getCaptchaImage() (captchaID string, chaptchaB64 string) {
|
||||
if !config.UseCaptcha {
|
||||
if !config.Config.UseCaptcha {
|
||||
return
|
||||
}
|
||||
captcha := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore)
|
98
pkg/posting/formatting.go
Normal file
98
pkg/posting/formatting.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package posting
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/frustra/bbcode"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
)
|
||||
|
||||
var (
|
||||
msgfmtr *MessageFormatter
|
||||
)
|
||||
|
||||
// InitPosting prepares the formatter and the temp post pruner
|
||||
func InitPosting() {
|
||||
msgfmtr = new(MessageFormatter)
|
||||
msgfmtr.InitBBcode()
|
||||
tempCleanerTicker = time.NewTicker(time.Minute * 5)
|
||||
go tempCleaner()
|
||||
}
|
||||
|
||||
type MessageFormatter struct {
|
||||
// Go's garbage collection does weird things with bbcode's internal tag map.
|
||||
// Moving the bbcode compiler isntance (and eventually a Markdown compiler) to a struct
|
||||
// appears to fix this
|
||||
bbCompiler bbcode.Compiler
|
||||
}
|
||||
|
||||
func (mf *MessageFormatter) InitBBcode() {
|
||||
if config.Config.DisableBBcode {
|
||||
return
|
||||
}
|
||||
mf.bbCompiler = bbcode.NewCompiler(true, true)
|
||||
mf.bbCompiler.SetTag("center", nil)
|
||||
mf.bbCompiler.SetTag("code", nil)
|
||||
mf.bbCompiler.SetTag("color", nil)
|
||||
mf.bbCompiler.SetTag("img", nil)
|
||||
mf.bbCompiler.SetTag("quote", nil)
|
||||
mf.bbCompiler.SetTag("size", nil)
|
||||
}
|
||||
|
||||
func (mf *MessageFormatter) Compile(msg string) string {
|
||||
if config.Config.DisableBBcode {
|
||||
return msg
|
||||
}
|
||||
return mf.bbCompiler.Compile(msg)
|
||||
}
|
||||
|
||||
func FormatMessage(message string) string {
|
||||
message = msgfmtr.Compile(message)
|
||||
// prepare each line to be formatted
|
||||
postLines := strings.Split(message, "<br>")
|
||||
for i, line := range postLines {
|
||||
trimmedLine := strings.TrimSpace(line)
|
||||
lineWords := strings.Split(trimmedLine, " ")
|
||||
isGreentext := false // if true, append </span> to end of line
|
||||
for w, word := range lineWords {
|
||||
if strings.LastIndex(word, ">>") == 0 {
|
||||
//word is a backlink
|
||||
if postID, err := strconv.Atoi(word[8:]); err == nil {
|
||||
// the link is in fact, a valid int
|
||||
var boardDir string
|
||||
var linkParent int
|
||||
|
||||
if boardDir, err = gcsql.GetBoardFromPostID(postID); err != nil {
|
||||
gclog.Print(gclog.LErrorLog, "Error getting board dir for backlink: ", err.Error())
|
||||
}
|
||||
if linkParent, err = gcsql.GetThreadIDZeroIfTopPost(postID); err != nil {
|
||||
gclog.Print(gclog.LErrorLog, "Error getting post parent for backlink: ", err.Error())
|
||||
}
|
||||
|
||||
// get post board dir
|
||||
if boardDir == "" {
|
||||
lineWords[w] = `<a href="javascript:;"><strike>` + word + `</strike></a>`
|
||||
} else if linkParent == 0 {
|
||||
lineWords[w] = `<a href="` + config.Config.SiteWebfolder + boardDir + `/res/` + word[8:] + `.html" class="postref">` + word + `</a>`
|
||||
} else {
|
||||
lineWords[w] = `<a href="` + config.Config.SiteWebfolder + boardDir + `/res/` + strconv.Itoa(linkParent) + `.html#` + word[8:] + `" class="postref">` + word + `</a>`
|
||||
}
|
||||
}
|
||||
} else if strings.Index(word, ">") == 0 && w == 0 {
|
||||
// word is at the beginning of a line, and is greentext
|
||||
isGreentext = true
|
||||
lineWords[w] = "<span class=\"greentext\">" + word
|
||||
}
|
||||
}
|
||||
line = strings.Join(lineWords, " ")
|
||||
if isGreentext {
|
||||
line += "</span>"
|
||||
}
|
||||
postLines[i] = line
|
||||
}
|
||||
return strings.Join(postLines, "<br />")
|
||||
}
|
396
pkg/posting/post.go
Normal file
396
pkg/posting/post.go
Normal file
|
@ -0,0 +1,396 @@
|
|||
package posting
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"html"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gochan-org/gochan/pkg/building"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gctemplates"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/gochan-org/gochan/pkg/serverutil"
|
||||
)
|
||||
|
||||
const (
|
||||
yearInSeconds = 31536000
|
||||
errStdLogs = gclog.LErrorLog | gclog.LStdLog
|
||||
)
|
||||
|
||||
// MakePost is called when a user accesses /post. Parse form data, then insert and build
|
||||
func MakePost(writer http.ResponseWriter, request *http.Request) {
|
||||
var maxMessageLength int
|
||||
var post gcsql.Post
|
||||
// domain := request.Host
|
||||
var formName string
|
||||
var nameCookie string
|
||||
var formEmail string
|
||||
|
||||
if request.Method == "GET" {
|
||||
http.Redirect(writer, request, config.Config.SiteWebfolder, http.StatusFound)
|
||||
return
|
||||
}
|
||||
// fix new cookie domain for when you use a port number
|
||||
// domain = chopPortNumRegex.Split(domain, -1)[0]
|
||||
|
||||
post.ParentID, _ = strconv.Atoi(request.FormValue("threadid"))
|
||||
post.BoardID, _ = strconv.Atoi(request.FormValue("boardid"))
|
||||
|
||||
var emailCommand string
|
||||
formName = request.FormValue("postname")
|
||||
parsedName := gcutil.ParseName(formName)
|
||||
post.Name = parsedName["name"]
|
||||
post.Tripcode = parsedName["tripcode"]
|
||||
|
||||
formEmail = request.FormValue("postemail")
|
||||
|
||||
http.SetCookie(writer, &http.Cookie{Name: "email", Value: formEmail, MaxAge: yearInSeconds})
|
||||
|
||||
if !strings.Contains(formEmail, "noko") && !strings.Contains(formEmail, "sage") {
|
||||
post.Email = formEmail
|
||||
} else if strings.Index(formEmail, "#") > 1 {
|
||||
formEmailArr := strings.SplitN(formEmail, "#", 2)
|
||||
post.Email = formEmailArr[0]
|
||||
emailCommand = formEmailArr[1]
|
||||
} else if formEmail == "noko" || formEmail == "sage" {
|
||||
emailCommand = formEmail
|
||||
post.Email = ""
|
||||
}
|
||||
|
||||
post.Subject = request.FormValue("postsubject")
|
||||
post.MessageText = strings.Trim(request.FormValue("postmsg"), "\r\n")
|
||||
var err error
|
||||
if maxMessageLength, err = gcsql.GetMaxMessageLength(post.BoardID); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error getting board info: ", err.Error()))
|
||||
}
|
||||
|
||||
if len(post.MessageText) > maxMessageLength {
|
||||
serverutil.ServeErrorPage(writer, "Post body is too long")
|
||||
return
|
||||
}
|
||||
post.MessageHTML = FormatMessage(post.MessageText)
|
||||
password := request.FormValue("postpassword")
|
||||
if password == "" {
|
||||
password = gcutil.RandomString(8)
|
||||
}
|
||||
post.Password = gcutil.Md5Sum(password)
|
||||
|
||||
// Reverse escapes
|
||||
nameCookie = strings.Replace(formName, "&", "&", -1)
|
||||
nameCookie = strings.Replace(nameCookie, "\\'", "'", -1)
|
||||
nameCookie = strings.Replace(url.QueryEscape(nameCookie), "+", "%20", -1)
|
||||
|
||||
// add name and email cookies that will expire in a year (31536000 seconds)
|
||||
http.SetCookie(writer, &http.Cookie{Name: "name", Value: nameCookie, MaxAge: yearInSeconds})
|
||||
http.SetCookie(writer, &http.Cookie{Name: "password", Value: password, MaxAge: yearInSeconds})
|
||||
|
||||
post.IP = gcutil.GetRealIP(request)
|
||||
post.Timestamp = time.Now()
|
||||
// post.PosterAuthority = getStaffRank(request)
|
||||
post.Bumped = time.Now()
|
||||
post.Stickied = request.FormValue("modstickied") == "on"
|
||||
post.Locked = request.FormValue("modlocked") == "on"
|
||||
|
||||
//post has no referrer, or has a referrer from a different domain, probably a spambot
|
||||
if !serverutil.ValidReferer(request) {
|
||||
gclog.Print(gclog.LAccessLog, "Rejected post from possible spambot @ "+post.IP)
|
||||
return
|
||||
}
|
||||
|
||||
switch serverutil.CheckPostForSpam(post.IP, request.Header["User-Agent"][0], request.Referer(),
|
||||
post.Name, post.Email, post.MessageText) {
|
||||
case "discard":
|
||||
serverutil.ServeErrorPage(writer, "Your post looks like spam.")
|
||||
gclog.Print(gclog.LAccessLog, "Akismet recommended discarding post from: "+post.IP)
|
||||
return
|
||||
case "spam":
|
||||
serverutil.ServeErrorPage(writer, "Your post looks like spam.")
|
||||
gclog.Print(gclog.LAccessLog, "Akismet suggested post is spam from "+post.IP)
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
file, handler, err := request.FormFile("imagefile")
|
||||
|
||||
if err != nil || handler.Size == 0 {
|
||||
// no file was uploaded
|
||||
post.Filename = ""
|
||||
gclog.Printf(gclog.LAccessLog,
|
||||
"Receiving post from %s, referred from: %s", post.IP, request.Referer())
|
||||
} else {
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
serverutil.ServeErrorPage(writer,
|
||||
gclog.Print(gclog.LErrorLog, "Error while trying to read file: ", err.Error()))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
post.FilenameOriginal = html.EscapeString(handler.Filename)
|
||||
filetype := gcutil.GetFileExtension(post.FilenameOriginal)
|
||||
thumbFiletype := strings.ToLower(filetype)
|
||||
if thumbFiletype == "gif" || thumbFiletype == "webm" {
|
||||
thumbFiletype = "jpg"
|
||||
}
|
||||
|
||||
post.Filename = getNewFilename() + "." + gcutil.GetFileExtension(post.FilenameOriginal)
|
||||
boardExists, err := gcsql.DoesBoardExistByID(
|
||||
gcutil.HackyStringToInt(request.FormValue("boardid")))
|
||||
if err != nil {
|
||||
serverutil.ServeErrorPage(writer, "Server error: "+err.Error())
|
||||
return
|
||||
}
|
||||
if !boardExists {
|
||||
serverutil.ServeErrorPage(writer, "No boards have been created yet")
|
||||
return
|
||||
}
|
||||
var _board = gcsql.Board{}
|
||||
err = _board.PopulateData(gcutil.HackyStringToInt(request.FormValue("boardid")))
|
||||
if err != nil {
|
||||
serverutil.ServeErrorPage(writer, "Server error: "+err.Error())
|
||||
return
|
||||
}
|
||||
boardDir := _board.Dir
|
||||
filePath := path.Join(config.Config.DocumentRoot, "/"+boardDir+"/src/", post.Filename)
|
||||
thumbPath := path.Join(config.Config.DocumentRoot, "/"+boardDir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "t."+thumbFiletype, -1))
|
||||
catalogThumbPath := path.Join(config.Config.DocumentRoot, "/"+boardDir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "c."+thumbFiletype, -1))
|
||||
|
||||
if err = ioutil.WriteFile(filePath, data, 0777); err != nil {
|
||||
gclog.Printf(gclog.LErrorLog, "Couldn't write file %q: %s", post.Filename, err.Error())
|
||||
serverutil.ServeErrorPage(writer, `Couldn't write file "`+post.FilenameOriginal+`"`)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate image checksum
|
||||
post.FileChecksum = fmt.Sprintf("%x", md5.Sum(data))
|
||||
|
||||
var allowsVids bool
|
||||
if allowsVids, err = gcsql.GetEmbedsAllowed(post.BoardID); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Couldn't get board info: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if filetype == "webm" {
|
||||
if !allowsVids || !config.Config.AllowVideoUploads {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LAccessLog,
|
||||
"Video uploading is not currently enabled for this board."))
|
||||
os.Remove(filePath)
|
||||
return
|
||||
}
|
||||
|
||||
gclog.Printf(gclog.LAccessLog, "Receiving post with video: %s from %s, referrer: %s",
|
||||
handler.Filename, post.IP, request.Referer())
|
||||
if post.ParentID == 0 {
|
||||
if err := createVideoThumbnail(filePath, thumbPath, config.Config.ThumbWidth); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error creating video thumbnail: ", err.Error()))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := createVideoThumbnail(filePath, thumbPath, config.Config.ThumbWidthReply); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error creating video thumbnail: ", err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := createVideoThumbnail(filePath, catalogThumbPath, config.Config.ThumbWidthCatalog); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error creating video thumbnail: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
outputBytes, err := exec.Command("ffprobe", "-v", "quiet", "-show_format", "-show_streams", filePath).CombinedOutput()
|
||||
if err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error getting video info: ", err.Error()))
|
||||
return
|
||||
}
|
||||
if outputBytes != nil {
|
||||
outputStringArr := strings.Split(string(outputBytes), "\n")
|
||||
for _, line := range outputStringArr {
|
||||
lineArr := strings.Split(line, "=")
|
||||
if len(lineArr) < 2 {
|
||||
continue
|
||||
}
|
||||
value, _ := strconv.Atoi(lineArr[1])
|
||||
switch lineArr[0] {
|
||||
case "width":
|
||||
post.ImageW = value
|
||||
case "height":
|
||||
post.ImageH = value
|
||||
case "size":
|
||||
post.Filesize = value
|
||||
}
|
||||
}
|
||||
if post.ParentID == 0 {
|
||||
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "op")
|
||||
} else {
|
||||
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "reply")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Attempt to load uploaded file with imaging library
|
||||
img, err := imaging.Open(filePath)
|
||||
if err != nil {
|
||||
os.Remove(filePath)
|
||||
gclog.Printf(gclog.LErrorLog, "Couldn't open uploaded file %q: %s", post.Filename, err.Error())
|
||||
serverutil.ServeErrorPage(writer, "Upload filetype not supported")
|
||||
return
|
||||
}
|
||||
// Get image filesize
|
||||
stat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Couldn't get image filesize: "+err.Error()))
|
||||
return
|
||||
}
|
||||
post.Filesize = int(stat.Size())
|
||||
|
||||
// Get image width and height, as well as thumbnail width and height
|
||||
post.ImageW = img.Bounds().Max.X
|
||||
post.ImageH = img.Bounds().Max.Y
|
||||
if post.ParentID == 0 {
|
||||
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "op")
|
||||
} else {
|
||||
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "reply")
|
||||
}
|
||||
|
||||
gclog.Printf(gclog.LAccessLog, "Receiving post with image: %q from %s, referrer: %s",
|
||||
handler.Filename, post.IP, request.Referer())
|
||||
|
||||
if request.FormValue("spoiler") == "on" {
|
||||
// If spoiler is enabled, symlink thumbnail to spoiler image
|
||||
if _, err := os.Stat(path.Join(config.Config.DocumentRoot, "spoiler.png")); err != nil {
|
||||
serverutil.ServeErrorPage(writer, "missing /spoiler.png")
|
||||
return
|
||||
}
|
||||
if err = syscall.Symlink(path.Join(config.Config.DocumentRoot, "spoiler.png"), thumbPath); err != nil {
|
||||
serverutil.ServeErrorPage(writer, err.Error())
|
||||
return
|
||||
}
|
||||
} else if config.Config.ThumbWidth >= post.ImageW && config.Config.ThumbHeight >= post.ImageH {
|
||||
// If image fits in thumbnail size, symlink thumbnail to original
|
||||
post.ThumbW = img.Bounds().Max.X
|
||||
post.ThumbH = img.Bounds().Max.Y
|
||||
if err := syscall.Symlink(filePath, thumbPath); err != nil {
|
||||
serverutil.ServeErrorPage(writer, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var thumbnail image.Image
|
||||
var catalogThumbnail image.Image
|
||||
if post.ParentID == 0 {
|
||||
// If this is a new thread, generate thumbnail and catalog thumbnail
|
||||
thumbnail = createImageThumbnail(img, "op")
|
||||
catalogThumbnail = createImageThumbnail(img, "catalog")
|
||||
if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Couldn't generate catalog thumbnail: ", err.Error()))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
thumbnail = createImageThumbnail(img, "reply")
|
||||
}
|
||||
if err = imaging.Save(thumbnail, thumbPath); err != nil {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Couldn't save thumbnail: ", err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(post.MessageText) == "" && post.Filename == "" {
|
||||
serverutil.ServeErrorPage(writer, "Post must contain a message if no image is uploaded.")
|
||||
return
|
||||
}
|
||||
|
||||
postDelay := gcsql.SinceLastPost(&post)
|
||||
if postDelay > -1 {
|
||||
if post.ParentID == 0 && postDelay < config.Config.NewThreadDelay {
|
||||
serverutil.ServeErrorPage(writer, "Please wait before making a new thread.")
|
||||
return
|
||||
} else if post.ParentID > 0 && postDelay < config.Config.ReplyDelay {
|
||||
serverutil.ServeErrorPage(writer, "Please wait before making a reply.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
banStatus, err := getBannedStatus(request)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog,
|
||||
"Error getting banned status: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
boards, _ := gcsql.GetAllBoards()
|
||||
|
||||
postBoard, _ := gcsql.GetBoardFromID(post.BoardID)
|
||||
if banStatus.IsBanned(postBoard.Dir) {
|
||||
var banpageBuffer bytes.Buffer
|
||||
|
||||
if err = gcutil.MinifyTemplate(gctemplates.Banpage, map[string]interface{}{
|
||||
"config": config.Config, "ban": banStatus, "banBoards": boards[post.BoardID-1].Dir,
|
||||
}, writer, "text/html"); err != nil {
|
||||
serverutil.ServeErrorPage(writer,
|
||||
gclog.Print(gclog.LErrorLog, "Error minifying page: ", err.Error()))
|
||||
return
|
||||
}
|
||||
writer.Write(banpageBuffer.Bytes())
|
||||
return
|
||||
}
|
||||
|
||||
post.Sanitize()
|
||||
|
||||
if config.Config.UseCaptcha {
|
||||
captchaID := request.FormValue("captchaid")
|
||||
captchaAnswer := request.FormValue("captchaanswer")
|
||||
if captchaID == "" && captchaAnswer == "" {
|
||||
// browser isn't using JS, save post data to tempPosts and show captcha
|
||||
request.Form.Add("temppostindex", strconv.Itoa(len(gcsql.TempPosts)))
|
||||
request.Form.Add("emailcmd", emailCommand)
|
||||
gcsql.TempPosts = append(gcsql.TempPosts, post)
|
||||
|
||||
ServeCaptcha(writer, request)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = gcsql.InsertPost(&post, emailCommand != "sage"); err != nil {
|
||||
serverutil.ServeErrorPage(writer,
|
||||
gclog.Print(gclog.LErrorLog, "Error inserting post: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// rebuild the board page
|
||||
building.BuildBoards(post.BoardID)
|
||||
building.BuildFrontPage()
|
||||
|
||||
if emailCommand == "noko" {
|
||||
if post.ParentID < 1 {
|
||||
http.Redirect(writer, request, config.Config.SiteWebfolder+postBoard.Dir+"/res/"+strconv.Itoa(post.ID)+".html", http.StatusFound)
|
||||
} else {
|
||||
http.Redirect(writer, request, config.Config.SiteWebfolder+postBoard.Dir+"/res/"+strconv.Itoa(post.ParentID)+".html#"+strconv.Itoa(post.ID), http.StatusFound)
|
||||
}
|
||||
} else {
|
||||
http.Redirect(writer, request, config.Config.SiteWebfolder+postBoard.Dir+"/", http.StatusFound)
|
||||
}
|
||||
}
|
58
pkg/posting/tmpcleaner.go
Normal file
58
pkg/posting/tmpcleaner.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package posting
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
var tempCleanerTicker *time.Ticker
|
||||
|
||||
func tempCleaner() {
|
||||
for {
|
||||
select {
|
||||
case <-tempCleanerTicker.C:
|
||||
for p, post := range gcsql.TempPosts {
|
||||
if !time.Now().After(post.Timestamp.Add(time.Minute * 5)) {
|
||||
continue
|
||||
}
|
||||
// temporary post is >= 5 minutes, time to prune it
|
||||
gcsql.TempPosts[p] = gcsql.TempPosts[len(gcsql.TempPosts)-1]
|
||||
gcsql.TempPosts = gcsql.TempPosts[:len(gcsql.TempPosts)-1]
|
||||
if post.FilenameOriginal == "" {
|
||||
continue
|
||||
}
|
||||
var board gcsql.Board
|
||||
err := board.PopulateData(post.BoardID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fileSrc := path.Join(config.Config.DocumentRoot, board.Dir, "src", post.FilenameOriginal)
|
||||
if err = os.Remove(fileSrc); err != nil {
|
||||
gclog.Printf(errStdLogs,
|
||||
"Error pruning temporary upload for %q: %s", fileSrc, err.Error())
|
||||
}
|
||||
|
||||
thumbSrc := gcutil.GetThumbnailPath("thread", fileSrc)
|
||||
if err = os.Remove(thumbSrc); err != nil {
|
||||
gclog.Printf(errStdLogs,
|
||||
"Error pruning temporary upload for %q: %s", thumbSrc, err.Error())
|
||||
}
|
||||
|
||||
if post.ParentID == 0 {
|
||||
catalogSrc := gcutil.GetThumbnailPath("catalog", fileSrc)
|
||||
if err = os.Remove(catalogSrc); err != nil {
|
||||
gclog.Printf(errStdLogs,
|
||||
"Error pruning temporary upload for %s: %s", catalogSrc, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
pkg/posting/uploads.go
Normal file
111
pkg/posting/uploads.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package posting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"math/rand"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
)
|
||||
|
||||
func createImageThumbnail(imageObj image.Image, size string) image.Image {
|
||||
var thumbWidth int
|
||||
var thumbHeight int
|
||||
|
||||
switch size {
|
||||
case "op":
|
||||
thumbWidth = config.Config.ThumbWidth
|
||||
thumbHeight = config.Config.ThumbHeight
|
||||
case "reply":
|
||||
thumbWidth = config.Config.ThumbWidthReply
|
||||
thumbHeight = config.Config.ThumbHeightReply
|
||||
case "catalog":
|
||||
thumbWidth = config.Config.ThumbWidthCatalog
|
||||
thumbHeight = config.Config.ThumbHeightCatalog
|
||||
}
|
||||
oldRect := imageObj.Bounds()
|
||||
if thumbWidth >= oldRect.Max.X && thumbHeight >= oldRect.Max.Y {
|
||||
return imageObj
|
||||
}
|
||||
|
||||
thumbW, thumbH := getThumbnailSize(oldRect.Max.X, oldRect.Max.Y, size)
|
||||
imageObj = imaging.Resize(imageObj, thumbW, thumbH, imaging.CatmullRom) // resize to 600x400 px using CatmullRom cubic filter
|
||||
return imageObj
|
||||
}
|
||||
|
||||
func createVideoThumbnail(video, thumb string, size int) error {
|
||||
sizeStr := strconv.Itoa(size)
|
||||
outputBytes, err := exec.Command("ffmpeg", "-y", "-itsoffset", "-1", "-i", video, "-vframes", "1", "-filter:v", "scale='min("+sizeStr+"\\, "+sizeStr+"):-1'", thumb).CombinedOutput()
|
||||
if err != nil {
|
||||
outputStringArr := strings.Split(string(outputBytes), "\n")
|
||||
if len(outputStringArr) > 1 {
|
||||
outputString := outputStringArr[len(outputStringArr)-2]
|
||||
err = errors.New(outputString)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func getVideoInfo(path string) (map[string]int, error) {
|
||||
vidInfo := make(map[string]int)
|
||||
|
||||
outputBytes, err := exec.Command("ffprobe", "-v quiet", "-show_format", "-show_streams", path).CombinedOutput()
|
||||
if err == nil && outputBytes != nil {
|
||||
outputStringArr := strings.Split(string(outputBytes), "\n")
|
||||
for _, line := range outputStringArr {
|
||||
lineArr := strings.Split(line, "=")
|
||||
if len(lineArr) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if lineArr[0] == "width" || lineArr[0] == "height" || lineArr[0] == "size" {
|
||||
value, _ := strconv.Atoi(lineArr[1])
|
||||
vidInfo[lineArr[0]] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return vidInfo, err
|
||||
}
|
||||
|
||||
func getNewFilename() string {
|
||||
now := time.Now().Unix()
|
||||
rand.Seed(now)
|
||||
return strconv.Itoa(int(now)) + strconv.Itoa(rand.Intn(98)+1)
|
||||
}
|
||||
|
||||
// find out what out thumbnail's width and height should be, partially ripped from Kusaba X
|
||||
func getThumbnailSize(w int, h int, size string) (newWidth int, newHeight int) {
|
||||
var thumbWidth int
|
||||
var thumbHeight int
|
||||
|
||||
switch {
|
||||
case size == "op":
|
||||
thumbWidth = config.Config.ThumbWidth
|
||||
thumbHeight = config.Config.ThumbHeight
|
||||
case size == "reply":
|
||||
thumbWidth = config.Config.ThumbWidthReply
|
||||
thumbHeight = config.Config.ThumbHeightReply
|
||||
case size == "catalog":
|
||||
thumbWidth = config.Config.ThumbWidthCatalog
|
||||
thumbHeight = config.Config.ThumbHeightCatalog
|
||||
}
|
||||
if w == h {
|
||||
newWidth = thumbWidth
|
||||
newHeight = thumbHeight
|
||||
} else {
|
||||
var percent float32
|
||||
if w > h {
|
||||
percent = float32(thumbWidth) / float32(w)
|
||||
} else {
|
||||
percent = float32(thumbHeight) / float32(h)
|
||||
}
|
||||
newWidth = int(float32(w) * percent)
|
||||
newHeight = int(float32(h) * percent)
|
||||
}
|
||||
return
|
||||
}
|
98
pkg/serverutil/antispam.go
Normal file
98
pkg/serverutil/antispam.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package serverutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
)
|
||||
|
||||
// CheckAkismetAPIKey checks the validity of the Akismet API key given in the config file.
|
||||
func CheckAkismetAPIKey(key string) error {
|
||||
if key == "" {
|
||||
return errors.New("blank key given, Akismet spam checking won't be used")
|
||||
}
|
||||
resp, err := http.PostForm("https://rest.akismet.com/1.1/verify-key", url.Values{"key": {key}, "blog": {"http://" + config.Config.SiteDomain}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(body) == "invalid" {
|
||||
// This should disable the Akismet checks if the API key is not valid.
|
||||
errmsg := "Akismet API key is invalid, Akismet spam protection will be disabled."
|
||||
gclog.Print(gclog.LErrorLog, errmsg)
|
||||
return errors.New(errmsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPostForSpam checks a given post for spam with Akismet. Only checks if Akismet API key is set.
|
||||
func CheckPostForSpam(userIP string, userAgent string, referrer string,
|
||||
author string, email string, postContent string) string {
|
||||
if config.Config.AkismetAPIKey != "" {
|
||||
client := &http.Client{}
|
||||
data := url.Values{"blog": {"http://" + config.Config.SiteDomain}, "user_ip": {userIP}, "user_agent": {userAgent}, "referrer": {referrer},
|
||||
"comment_type": {"forum-post"}, "comment_author": {author}, "comment_author_email": {email},
|
||||
"comment_content": {postContent}}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://"+config.Config.AkismetAPIKey+".rest.akismet.com/1.1/comment-check",
|
||||
strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
gclog.Print(gclog.LErrorLog, err.Error())
|
||||
return "other_failure"
|
||||
}
|
||||
req.Header.Set("User-Agent", "gochan/1.0 | Akismet/0.1")
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
gclog.Print(gclog.LErrorLog, err.Error())
|
||||
return "other_failure"
|
||||
}
|
||||
if resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
gclog.Print(gclog.LErrorLog, err.Error())
|
||||
return "other_failure"
|
||||
}
|
||||
gclog.Print(gclog.LErrorLog, "Response from Akismet: ", string(body))
|
||||
|
||||
if string(body) == "true" {
|
||||
if proTip, ok := resp.Header["X-akismet-pro-tip"]; ok && proTip[0] == "discard" {
|
||||
return "discard"
|
||||
}
|
||||
return "spam"
|
||||
} else if string(body) == "invalid" {
|
||||
return "invalid"
|
||||
} else if string(body) == "false" {
|
||||
return "ham"
|
||||
}
|
||||
}
|
||||
return "other_failure"
|
||||
}
|
||||
|
||||
// ValidReferer checks to make sure that the incoming request is from the same domain (or if debug mode is enabled)
|
||||
func ValidReferer(request *http.Request) bool {
|
||||
if config.Config.DebugMode {
|
||||
return true
|
||||
}
|
||||
rURL, err := url.ParseRequestURI(request.Referer())
|
||||
if err != nil {
|
||||
gclog.Println(gclog.LAccessLog|gclog.LErrorLog, "Error parsing referer URL:", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return rURL.Host == config.Config.SiteDomain && strings.Index(rURL.Path, config.Config.SiteWebfolder) == 0
|
||||
}
|
35
pkg/serverutil/util.go
Normal file
35
pkg/serverutil/util.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package serverutil
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gclog"
|
||||
"github.com/gochan-org/gochan/pkg/gctemplates"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
)
|
||||
|
||||
// ServeErrorPage shows a general error page if something goes wrong
|
||||
func ServeErrorPage(writer http.ResponseWriter, err string) {
|
||||
gcutil.MinifyTemplate(gctemplates.ErrorPage, map[string]interface{}{
|
||||
"config": config.Config,
|
||||
"ErrorTitle": "Error :c",
|
||||
// "ErrorImage": "/error/lol 404.gif",
|
||||
"ErrorHeader": "Error",
|
||||
"ErrorText": err,
|
||||
}, writer, "text/html")
|
||||
}
|
||||
|
||||
// ServeNotFound shows an error page if a requested file is not found
|
||||
func ServeNotFound(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||
writer.WriteHeader(404)
|
||||
errorPage, err := ioutil.ReadFile(config.Config.DocumentRoot + "/error/404.html")
|
||||
if err != nil {
|
||||
writer.Write([]byte("Requested page not found, and /error/404.html not found"))
|
||||
} else {
|
||||
gcutil.MinifyWriter(writer, errorPage, "text/html")
|
||||
}
|
||||
gclog.Printf(gclog.LAccessLog, "Error: 404 Not Found from %s @ %s", gcutil.GetRealIP(request), request.URL.Path)
|
||||
}
|
40
refactorgcfs.sh
Executable file
40
refactorgcfs.sh
Executable file
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
rootPkg="github.com/gochan-org/gochan"
|
||||
pkgBase="pkg"
|
||||
gopath=`go env GOPATH`
|
||||
pkgDir=$gopath/pkg/linux_amd64/$rootPkg
|
||||
|
||||
if [ -z "$1" ]||[ "$1" == "--help" ]; then
|
||||
cat - <<EOF
|
||||
Usage:
|
||||
$0 --all
|
||||
Build/install all subpackages
|
||||
$0 --clean
|
||||
Removes the source and compiled package from the GOPATH
|
||||
$0 [--help]
|
||||
Show this message
|
||||
$0 $pkgBase/path/to/subpkg
|
||||
Compiles and installes the package to the GOPATH
|
||||
EOF
|
||||
elif [ "$1" == "--all" ]; then
|
||||
for f in pkg/*; do
|
||||
$0 $f
|
||||
done
|
||||
echo
|
||||
find $pkgDir
|
||||
elif [ "$1" == "--clean" ]; then
|
||||
rm -rf ${pkgDir%/gochan}
|
||||
else
|
||||
target=${1#*$pkgBase/}
|
||||
target=${target%/}
|
||||
|
||||
echo "Building/installing $target in $rootPkg/$pkgBase/$target"
|
||||
go install $rootPkg/$pkgBase/$target
|
||||
|
||||
if [ "$2" == "-v" ]; then
|
||||
find $pkgDir
|
||||
fi
|
||||
fi
|
|
@ -30,7 +30,6 @@
|
|||
"SiteDomain": "127.0.0.1",
|
||||
"SiteHeaderURL": "",
|
||||
"SiteWebfolder": "/",
|
||||
"DomainRegex": "(https|http):\\/\\/(gochan\\.org)\\/(.*)",
|
||||
|
||||
"Styles": [
|
||||
{ "Name": "Pipes", "Filename": "pipes.css" },
|
||||
|
|
527
src/building.go
527
src/building.go
|
@ -1,527 +0,0 @@
|
|||
// functions for post, thread, board, and page building
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"text/template"
|
||||
|
||||
"github.com/tdewolff/minify"
|
||||
minifyHTML "github.com/tdewolff/minify/html"
|
||||
minifyJS "github.com/tdewolff/minify/js"
|
||||
minifyJSON "github.com/tdewolff/minify/json"
|
||||
)
|
||||
|
||||
var minifier *minify.M
|
||||
|
||||
func initMinifier() {
|
||||
if !config.MinifyHTML && !config.MinifyJS {
|
||||
return
|
||||
}
|
||||
minifier = minify.New()
|
||||
if config.MinifyHTML {
|
||||
minifier.AddFunc("text/html", minifyHTML.Minify)
|
||||
}
|
||||
if config.MinifyJS {
|
||||
minifier.AddFunc("text/javascript", minifyJS.Minify)
|
||||
minifier.AddFunc("application/json", minifyJSON.Minify)
|
||||
}
|
||||
}
|
||||
|
||||
func canMinify(mediaType string) bool {
|
||||
return (mediaType == "text/html" && config.MinifyHTML) || ((mediaType == "application/json" || mediaType == "text/javascript") && config.MinifyJS)
|
||||
}
|
||||
|
||||
func minifyTemplate(tmpl *template.Template, data interface{}, writer io.Writer, mediaType string) error {
|
||||
if !canMinify(mediaType) {
|
||||
return tmpl.Execute(writer, data)
|
||||
}
|
||||
|
||||
minWriter := minifier.Writer(mediaType, writer)
|
||||
defer closeHandle(minWriter)
|
||||
return tmpl.Execute(minWriter, data)
|
||||
}
|
||||
|
||||
func minifyWriter(writer io.Writer, data []byte, mediaType string) (int, error) {
|
||||
if !canMinify(mediaType) {
|
||||
return writer.Write(data)
|
||||
}
|
||||
|
||||
minWriter := minifier.Writer(mediaType, writer)
|
||||
defer closeHandle(minWriter)
|
||||
return minWriter.Write(data)
|
||||
}
|
||||
|
||||
// build front page using templates/front.html
|
||||
func buildFrontPage() string {
|
||||
err := initTemplates("front")
|
||||
if err != nil {
|
||||
return gclog.Print(lErrorLog, "Error loading front page template: ", err.Error())
|
||||
}
|
||||
os.Remove(path.Join(config.DocumentRoot, "index.html"))
|
||||
frontFile, err := os.OpenFile(path.Join(config.DocumentRoot, "index.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
defer closeHandle(frontFile)
|
||||
if err != nil {
|
||||
return gclog.Print(lErrorLog, "Failed opening front page for writing: ", err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
var recentPostsArr []RecentPost
|
||||
recentPostsArr, err = GetRecentPostsGlobal(config.MaxRecentPosts, !config.RecentPostsWithNoFile)
|
||||
if err == nil {
|
||||
return gclog.Print(lErrorLog, "Failed loading recent posts: "+err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
for _, board := range allBoards {
|
||||
if board.Section == 0 {
|
||||
board.Section = 1
|
||||
}
|
||||
}
|
||||
|
||||
if err = minifyTemplate(frontPageTmpl, map[string]interface{}{
|
||||
"config": config,
|
||||
"sections": allSections,
|
||||
"boards": allBoards,
|
||||
"recent_posts": recentPostsArr,
|
||||
}, frontFile, "text/html"); err != nil {
|
||||
return gclog.Print(lErrorLog, "Failed executing front page template: "+err.Error()) + "<br />"
|
||||
}
|
||||
return "Front page rebuilt successfully."
|
||||
}
|
||||
|
||||
func buildBoardListJSON() (html string) {
|
||||
boardListFile, err := os.OpenFile(path.Join(config.DocumentRoot, "boards.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
defer closeHandle(boardListFile)
|
||||
if err != nil {
|
||||
return gclog.Print(lErrorLog, "Failed opening boards.json for writing: ", err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
boardsMap := map[string][]Board{
|
||||
"boards": []Board{},
|
||||
}
|
||||
|
||||
// Our cooldowns are site-wide currently.
|
||||
cooldowns := BoardCooldowns{NewThread: config.NewThreadDelay, Reply: config.ReplyDelay, ImageReply: config.ReplyDelay}
|
||||
|
||||
for _, board := range allBoards {
|
||||
board.Cooldowns = cooldowns
|
||||
boardsMap["boards"] = append(boardsMap["boards"], board)
|
||||
}
|
||||
|
||||
boardJSON, err := json.Marshal(boardsMap)
|
||||
if err != nil {
|
||||
return gclog.Print(lErrorLog, "Failed to create boards.json: ", err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
if _, err = minifyWriter(boardListFile, boardJSON, "application/json"); err != nil {
|
||||
return gclog.Print(lErrorLog, "Failed writing boards.json file: ", err.Error()) + "<br />"
|
||||
}
|
||||
return "Board list JSON rebuilt successfully.<br />"
|
||||
}
|
||||
|
||||
// buildBoardPages builds the pages for the board archive.
|
||||
// `board` is a Board 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 *Board) (html string) {
|
||||
err := initTemplates("boardpage")
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
var currentPageFile *os.File
|
||||
var threads []interface{}
|
||||
var threadPages [][]interface{}
|
||||
var stickiedThreads []interface{}
|
||||
var nonStickiedThreads []interface{}
|
||||
var opPosts []Post
|
||||
|
||||
// Get all top level posts for the board.
|
||||
if opPosts, err = GetTopPosts(board.ID, true); err != nil {
|
||||
return html + gclog.Printf(lErrorLog, "Error getting OP posts for /%s/: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
// For each top level post, start building a Thread struct
|
||||
for _, op := range opPosts {
|
||||
var thread Thread
|
||||
var postsInThread []Post
|
||||
|
||||
var replyCount, err = GetReplyCount(op.ID)
|
||||
if err == nil {
|
||||
return html + gclog.Printf(lErrorLog,
|
||||
"Error getting replies to /%s/%d: %s",
|
||||
board.Dir, op.ID, err.Error()) + "<br />"
|
||||
}
|
||||
thread.NumReplies = replyCount
|
||||
|
||||
fileCount, err := GetReplyFileCount(op.ID)
|
||||
if err == nil {
|
||||
return html + gclog.Printf(lErrorLog,
|
||||
"Error getting file count to /%s/%d: %s",
|
||||
board.Dir, op.ID, err.Error()) + "<br />"
|
||||
}
|
||||
thread.NumImages = fileCount
|
||||
|
||||
thread.OP = op
|
||||
|
||||
var numRepliesOnBoardPage int
|
||||
|
||||
if op.Stickied {
|
||||
// If the thread is stickied, limit replies on the archive page to the
|
||||
// configured value for stickied threads.
|
||||
numRepliesOnBoardPage = config.StickyRepliesOnBoardPage
|
||||
} else {
|
||||
// Otherwise, limit the replies to the configured value for normal threads.
|
||||
numRepliesOnBoardPage = config.RepliesOnBoardPage
|
||||
}
|
||||
|
||||
postsInThread, err = GetExistingRepliesLimitedRev(op.ID, numRepliesOnBoardPage)
|
||||
if err != nil {
|
||||
return html + gclog.Printf(lErrorLog,
|
||||
"Error getting posts in /%s/%d: %s",
|
||||
board.Dir, op.ID, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
var reversedPosts []Post
|
||||
for i := len(postsInThread); i > 0; i-- {
|
||||
reversedPosts = append(reversedPosts, postsInThread[i-1])
|
||||
}
|
||||
|
||||
if len(postsInThread) > 0 {
|
||||
// Store the posts to show on board page
|
||||
//thread.BoardReplies = postsInThread
|
||||
thread.BoardReplies = reversedPosts
|
||||
|
||||
// Count number of images on board page
|
||||
imageCount := 0
|
||||
for _, reply := range postsInThread {
|
||||
if reply.Filesize != 0 {
|
||||
imageCount++
|
||||
}
|
||||
}
|
||||
// Then calculate number of omitted images.
|
||||
thread.OmittedImages = thread.NumImages - imageCount
|
||||
}
|
||||
|
||||
// Add thread struct to appropriate list
|
||||
if op.Stickied {
|
||||
stickiedThreads = append(stickiedThreads, thread)
|
||||
} else {
|
||||
nonStickiedThreads = append(nonStickiedThreads, thread)
|
||||
}
|
||||
}
|
||||
|
||||
deleteMatchingFiles(path.Join(config.DocumentRoot, board.Dir), "\\d.html$")
|
||||
// Order the threads, stickied threads first, then nonstickied threads.
|
||||
threads = append(stickiedThreads, nonStickiedThreads...)
|
||||
|
||||
// If there are no posts on the board
|
||||
if len(threads) == 0 {
|
||||
board.CurrentPage = 1
|
||||
// Open board.html for writing to the first page.
|
||||
boardPageFile, err := os.OpenFile(path.Join(config.DocumentRoot, board.Dir, "board.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
return html + gclog.Printf(lErrorLog,
|
||||
"Failed opening /%s/board.html: %s",
|
||||
board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
// Render board page template to the file,
|
||||
// packaging the board/section list, threads, and board info
|
||||
if err = minifyTemplate(boardpageTmpl, map[string]interface{}{
|
||||
"config": config,
|
||||
"boards": allBoards,
|
||||
"sections": allSections,
|
||||
"threads": threads,
|
||||
"board": board,
|
||||
}, boardPageFile, "text/html"); err != nil {
|
||||
return html + gclog.Printf(lErrorLog,
|
||||
"Failed building /%s/: %s",
|
||||
board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
html += "/" + board.Dir + "/ built successfully.\n"
|
||||
return
|
||||
}
|
||||
|
||||
// Create the archive pages.
|
||||
threadPages = paginate(config.ThreadsPerPage, threads)
|
||||
board.NumPages = len(threadPages)
|
||||
|
||||
// Create array of page wrapper objects, and open the file.
|
||||
pagesArr := make([]map[string]interface{}, board.NumPages)
|
||||
|
||||
catalogJSONFile, err := os.OpenFile(path.Join(config.DocumentRoot, board.Dir, "catalog.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
defer closeHandle(catalogJSONFile)
|
||||
if err != nil {
|
||||
return gclog.Printf(lErrorLog,
|
||||
"Failed opening /%s/catalog.json: %s",
|
||||
board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
currentBoardPage := board.CurrentPage
|
||||
for _, pageThreads := range threadPages {
|
||||
board.CurrentPage++
|
||||
var currentPageFilepath string
|
||||
pageFilename := strconv.Itoa(board.CurrentPage) + ".html"
|
||||
currentPageFilepath = path.Join(config.DocumentRoot, board.Dir, pageFilename)
|
||||
currentPageFile, err = os.OpenFile(currentPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
defer closeHandle(currentPageFile)
|
||||
if err != nil {
|
||||
html += gclog.Printf(lErrorLog,
|
||||
"Failed opening /%s/%s: %s",
|
||||
board.Dir, pageFilename, err.Error()) + "<br />"
|
||||
continue
|
||||
}
|
||||
|
||||
// Render the boardpage template
|
||||
if err = minifyTemplate(boardpageTmpl, map[string]interface{}{
|
||||
"config": config,
|
||||
"boards": allBoards,
|
||||
"sections": allSections,
|
||||
"threads": pageThreads,
|
||||
"board": board,
|
||||
"posts": []interface{}{
|
||||
Post{BoardID: board.ID},
|
||||
},
|
||||
}, currentPageFile, "text/html"); err != nil {
|
||||
return html + gclog.Printf(lErrorLog,
|
||||
"Failed building /%s/ boardpage: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
if board.CurrentPage == 1 {
|
||||
boardPage := path.Join(config.DocumentRoot, board.Dir, "board.html")
|
||||
os.Remove(boardPage)
|
||||
if err = syscall.Symlink(currentPageFilepath, boardPage); !os.IsExist(err) && err != nil {
|
||||
html += gclog.Printf(lErrorLog, "Failed building /%s/: %s",
|
||||
board.Dir, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Collect up threads for this page.
|
||||
pageMap := make(map[string]interface{})
|
||||
pageMap["page"] = board.CurrentPage
|
||||
pageMap["threads"] = pageThreads
|
||||
pagesArr = append(pagesArr, pageMap)
|
||||
}
|
||||
board.CurrentPage = currentBoardPage
|
||||
|
||||
catalogJSON, err := json.Marshal(pagesArr)
|
||||
if err != nil {
|
||||
return html + gclog.Print(lErrorLog, "Failed to marshal to JSON: ", err.Error()) + "<br />"
|
||||
}
|
||||
if _, err = catalogJSONFile.Write(catalogJSON); err != nil {
|
||||
return html + gclog.Printf(lErrorLog,
|
||||
"Failed writing /%s/catalog.json: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
html += "/" + board.Dir + "/ built successfully."
|
||||
return
|
||||
}
|
||||
|
||||
// buildBoards builds the specified board IDs, or all boards if no arguments are passed
|
||||
// The return value is a string of HTML with debug information produced by the build process.
|
||||
func buildBoards(which ...int) (html string) {
|
||||
var boards []Board
|
||||
var err error
|
||||
if which == nil {
|
||||
boards = allBoards
|
||||
} else {
|
||||
for b, id := range which {
|
||||
boards = append(boards, Board{})
|
||||
if err = boards[b].PopulateData(id); err != nil {
|
||||
return gclog.Printf(lErrorLog, "Error getting board information (ID: %d)", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(boards) == 0 {
|
||||
return "No boards to build."
|
||||
}
|
||||
|
||||
for _, board := range boards {
|
||||
if err = board.Build(false, true); err != nil {
|
||||
return gclog.Printf(lErrorLog,
|
||||
"Error building /%s/: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
html += "Built /" + board.Dir + "/ successfully."
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func buildJS() string {
|
||||
// minify gochan.js (if enabled)
|
||||
gochanMinJSPath := path.Join(config.DocumentRoot, "javascript", "gochan.min.js")
|
||||
gochanMinJSFile, err := os.OpenFile(gochanMinJSPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
defer closeHandle(gochanMinJSFile)
|
||||
if err != nil {
|
||||
return gclog.Printf(lErrorLog, "Error opening %q for writing: %s",
|
||||
gochanMinJSPath, err.Error()) + "<br />"
|
||||
}
|
||||
gochanJSPath := path.Join(config.DocumentRoot, "javascript", "gochan.js")
|
||||
gochanJSBytes, err := ioutil.ReadFile(gochanJSPath)
|
||||
if err != nil {
|
||||
return gclog.Printf(lErrorLog, "Error opening %q for writing: %s",
|
||||
gochanJSPath, err.Error()) + "<br />"
|
||||
}
|
||||
if _, err := minifyWriter(gochanMinJSFile, gochanJSBytes, "text/javascript"); err != nil {
|
||||
config.UseMinifiedGochanJS = false
|
||||
return gclog.Printf(lErrorLog, "Error minifying %q: %s:",
|
||||
gochanMinJSPath, err.Error()) + "<br />"
|
||||
}
|
||||
config.UseMinifiedGochanJS = true
|
||||
|
||||
// build consts.js from template
|
||||
if err = initTemplates("js"); err != nil {
|
||||
return gclog.Print(lErrorLog, "Error loading consts.js template: ", err.Error())
|
||||
}
|
||||
constsJSPath := path.Join(config.DocumentRoot, "javascript", "consts.js")
|
||||
constsJSFile, err := os.OpenFile(constsJSPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
defer closeHandle(constsJSFile)
|
||||
if err != nil {
|
||||
return gclog.Printf(lErrorLog, "Error opening %q for writing: %s",
|
||||
constsJSPath, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
if err = minifyTemplate(jsTmpl, config, constsJSFile, "text/javascript"); err != nil {
|
||||
return gclog.Printf(lErrorLog, "Error building %q: %s",
|
||||
constsJSPath, err.Error()) + "<br />"
|
||||
}
|
||||
return "Built gochan.min.js and consts.js successfully.<br />"
|
||||
}
|
||||
|
||||
func buildCatalog(which int) string {
|
||||
err := initTemplates("catalog")
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
var board Board
|
||||
if err = board.PopulateData(which); err != nil {
|
||||
return gclog.Printf(lErrorLog, "Error getting board information (ID: %d)", which)
|
||||
}
|
||||
|
||||
catalogPath := path.Join(config.DocumentRoot, board.Dir, "catalog.html")
|
||||
catalogFile, err := os.OpenFile(catalogPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
return gclog.Printf(lErrorLog,
|
||||
"Failed opening /%s/catalog.html: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
threadOPs, err := GetTopPosts(which, false)
|
||||
if err != nil {
|
||||
return gclog.Printf(lErrorLog,
|
||||
"Error building catalog for /%s/: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
|
||||
var threadInterfaces []interface{}
|
||||
for _, thread := range threadOPs {
|
||||
threadInterfaces = append(threadInterfaces, thread)
|
||||
}
|
||||
|
||||
if err = minifyTemplate(catalogTmpl, map[string]interface{}{
|
||||
"boards": allBoards,
|
||||
"config": config,
|
||||
"board": board,
|
||||
"sections": allSections,
|
||||
}, catalogFile, "text/html"); err != nil {
|
||||
return gclog.Printf(lErrorLog,
|
||||
"Error building catalog for /%s/: %s", board.Dir, err.Error()) + "<br />"
|
||||
}
|
||||
return fmt.Sprintf("Built catalog for /%s/ successfully", board.Dir)
|
||||
}
|
||||
|
||||
// buildThreadPages builds the pages for a thread given by a Post object.
|
||||
func buildThreadPages(op *Post) error {
|
||||
err := initTemplates("threadpage")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var replies []Post
|
||||
var threadPageFile *os.File
|
||||
var board Board
|
||||
if err = board.PopulateData(op.BoardID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
replies, err = GetExistingReplies(op.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error building thread %d: %s", op.ID, err.Error())
|
||||
}
|
||||
os.Remove(path.Join(config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".html"))
|
||||
|
||||
var repliesInterface []interface{}
|
||||
for _, reply := range replies {
|
||||
repliesInterface = append(repliesInterface, reply)
|
||||
}
|
||||
|
||||
threadPageFilepath := path.Join(config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".html")
|
||||
threadPageFile, err = os.OpenFile(threadPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed opening /%s/res/%d.html: %s", board.Dir, op.ID, err.Error())
|
||||
}
|
||||
|
||||
// render thread page
|
||||
if err = minifyTemplate(threadpageTmpl, map[string]interface{}{
|
||||
"config": config,
|
||||
"boards": allBoards,
|
||||
"board": board,
|
||||
"sections": allSections,
|
||||
"posts": replies,
|
||||
"op": op,
|
||||
}, threadPageFile, "text/html"); err != nil {
|
||||
return fmt.Errorf("Failed building /%s/res/%d threadpage: %s", board.Dir, op.ID, err.Error())
|
||||
}
|
||||
|
||||
// Put together the thread JSON
|
||||
threadJSONFile, err := os.OpenFile(path.Join(config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
|
||||
defer closeHandle(threadJSONFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed opening /%s/res/%d.json: %s", board.Dir, op.ID, err.Error())
|
||||
}
|
||||
|
||||
threadMap := make(map[string][]Post)
|
||||
|
||||
// Handle the OP, of type *Post
|
||||
threadMap["posts"] = []Post{*op}
|
||||
|
||||
// Iterate through each reply, which are of type Post
|
||||
threadMap["posts"] = append(threadMap["posts"], replies...)
|
||||
threadJSON, err := json.Marshal(threadMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to marshal to JSON: %s", err.Error())
|
||||
}
|
||||
|
||||
if _, err = threadJSONFile.Write(threadJSON); err != nil {
|
||||
return fmt.Errorf("Failed writing /%s/res/%d.json: %s", board.Dir, op.ID, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildThreads builds thread(s) given a boardid, or if all = false, also given a threadid.
|
||||
// if all is set to true, ignore which, otherwise, which = build only specified boardid
|
||||
// TODO: make it variadic
|
||||
func buildThreads(all bool, boardid, threadid int) error {
|
||||
var threads []Post
|
||||
var err error
|
||||
if all {
|
||||
threads, err = GetTopPostsNoSort(boardid)
|
||||
} else {
|
||||
var post Post
|
||||
post, err = GetSpecificTopPost(threadid)
|
||||
threads = []Post{post}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, op := range threads {
|
||||
if err = buildThreadPages(&op); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetCaptchaImage(t *testing.T) {
|
||||
config.UseCaptcha = true
|
||||
config.CaptchaWidth = 240
|
||||
config.CaptchaHeight = 80
|
||||
initCaptcha()
|
||||
captchaID, captchaB64 := getCaptchaImage()
|
||||
fmt.Printf("captchaID: %s\ncaptchaB64: %s\n", captchaID, captchaB64)
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var versionStr string
|
||||
var buildtimeString string // set with build command, format: YRMMDD.HHMM
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
gclog.Print(lErrorLog|lStdLog, "Cleaning up")
|
||||
execSQL("DROP TABLE DBPREFIXsessions")
|
||||
closeHandle(db)
|
||||
}()
|
||||
initConfig()
|
||||
initMinifier()
|
||||
|
||||
gclog.Printf(lErrorLog|lStdLog, "Starting gochan v%s", versionStr)
|
||||
connectToSQLServer()
|
||||
parseCommandLine()
|
||||
|
||||
gclog.Print(lErrorLog|lStdLog, "Loading and parsing templates")
|
||||
if err := initTemplates("all"); err != nil {
|
||||
gclog.Printf(lErrorLog|lStdLog|lFatal, err.Error())
|
||||
}
|
||||
buildJS()
|
||||
initCaptcha()
|
||||
tempCleanerTicker = time.NewTicker(time.Minute * 5)
|
||||
go tempCleaner()
|
||||
|
||||
sc := make(chan os.Signal, 1)
|
||||
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||
go func() {
|
||||
initServer()
|
||||
}()
|
||||
<-sc
|
||||
}
|
||||
|
||||
func parseCommandLine() {
|
||||
var newstaff string
|
||||
var delstaff string
|
||||
var rank int
|
||||
var err error
|
||||
flag.StringVar(&newstaff, "newstaff", "", "<newusername>:<newpassword>")
|
||||
flag.StringVar(&delstaff, "delstaff", "", "<username>")
|
||||
flag.IntVar(&rank, "rank", 0, "New staff member rank, to be used with -newstaff or -delstaff")
|
||||
flag.Parse()
|
||||
|
||||
if newstaff != "" {
|
||||
arr := strings.Split(newstaff, ":")
|
||||
if len(arr) < 2 || delstaff != "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
gclog.Printf(lStdLog|lErrorLog, "Creating new staff: %q, with password: %q and rank: %d", arr[0], arr[1], rank)
|
||||
if err = newStaff(arr[0], arr[1], rank); err != nil {
|
||||
gclog.Print(lStdLog|lFatal, err.Error())
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
if delstaff != "" {
|
||||
if newstaff != "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
gclog.Printf(lStdLog, "Are you sure you want to delete the staff account %q? [y/N]: ", delstaff)
|
||||
var answer string
|
||||
fmt.Scanln(&answer)
|
||||
answer = strings.ToLower(answer)
|
||||
if answer == "y" || answer == "yes" {
|
||||
if err = deleteStaff(delstaff); err != nil {
|
||||
gclog.Printf(lStdLog|lFatal, "Error deleting %q: %s", delstaff, err.Error())
|
||||
}
|
||||
} else {
|
||||
gclog.Print(lStdLog|lFatal, "Not deleting.")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
tName = "UrA"
|
||||
tTripcode = "#EAA30Kmm"
|
||||
tEmail = "something@example.com"
|
||||
tSubject = "Subject line"
|
||||
tMessage = "Message text"
|
||||
)
|
||||
|
||||
func TestGochan(t *testing.T) {
|
||||
t.Log("This doesn't do anything interesting yet.")
|
||||
}
|
123
src/logging.go
123
src/logging.go
|
@ -1,123 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
var gclog *GcLogger
|
||||
|
||||
const (
|
||||
logTimeFmt = "2006/01/02 15:04:05 "
|
||||
logFileFlags = os.O_APPEND | os.O_CREATE | os.O_RDWR
|
||||
lAccessLog = 1 << iota
|
||||
lErrorLog
|
||||
lStaffLog
|
||||
lStdLog
|
||||
lFatal
|
||||
)
|
||||
|
||||
type GcLogger struct {
|
||||
accessFile *os.File
|
||||
errorFile *os.File
|
||||
staffFile *os.File
|
||||
}
|
||||
|
||||
func (gcl *GcLogger) selectLogs(flags int) []*os.File {
|
||||
var logs []*os.File
|
||||
if flags&lAccessLog > 0 {
|
||||
logs = append(logs, gcl.accessFile)
|
||||
}
|
||||
if flags&lErrorLog > 0 {
|
||||
logs = append(logs, gcl.errorFile)
|
||||
}
|
||||
if flags&lStaffLog > 0 {
|
||||
logs = append(logs, gcl.staffFile)
|
||||
}
|
||||
if (flags&lStdLog > 0) || (config.DebugMode) {
|
||||
logs = append(logs, os.Stdout)
|
||||
}
|
||||
return logs
|
||||
}
|
||||
|
||||
func (gcl *GcLogger) getPrefix() string {
|
||||
prefix := time.Now().Format(logTimeFmt)
|
||||
_, file, line, _ := runtime.Caller(2)
|
||||
prefix += fmt.Sprint(file, ":", line, ": ")
|
||||
|
||||
return prefix
|
||||
}
|
||||
|
||||
func (gcl *GcLogger) Print(flags int, v ...interface{}) string {
|
||||
str := fmt.Sprint(v...)
|
||||
logs := gcl.selectLogs(flags)
|
||||
for _, l := range logs {
|
||||
if l == os.Stdout {
|
||||
io.WriteString(l, str+"\n")
|
||||
} else {
|
||||
io.WriteString(l, gcl.getPrefix()+str+"\n")
|
||||
}
|
||||
}
|
||||
if flags&lFatal > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func (gcl *GcLogger) Printf(flags int, format string, v ...interface{}) string {
|
||||
str := fmt.Sprintf(format, v...)
|
||||
logs := gcl.selectLogs(flags)
|
||||
for _, l := range logs {
|
||||
if l == os.Stdout {
|
||||
io.WriteString(l, str+"\n")
|
||||
} else {
|
||||
io.WriteString(l, gcl.getPrefix()+str+"\n")
|
||||
}
|
||||
}
|
||||
if flags&lFatal > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func (gcl *GcLogger) Println(flags int, v ...interface{}) string {
|
||||
str := fmt.Sprintln(v...)
|
||||
logs := gcl.selectLogs(flags)
|
||||
for _, l := range logs {
|
||||
if l == os.Stdout {
|
||||
io.WriteString(l, str+"\n")
|
||||
} else {
|
||||
io.WriteString(l, gcl.getPrefix()+str+"\n")
|
||||
}
|
||||
}
|
||||
if flags&lFatal > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func (gcl *GcLogger) Close() {
|
||||
closeHandle(gcl.accessFile)
|
||||
closeHandle(gcl.errorFile)
|
||||
closeHandle(gcl.staffFile)
|
||||
}
|
||||
|
||||
func initLogs(accessLogPath, errorLogPath, staffLogPath string) (*GcLogger, error) {
|
||||
var gcl GcLogger
|
||||
var err error
|
||||
if gcl.accessFile, err = os.OpenFile(accessLogPath, logFileFlags, 0777); err != nil {
|
||||
return nil, errors.New("Error loading " + accessLogPath + ": " + err.Error())
|
||||
}
|
||||
if gcl.errorFile, err = os.OpenFile(errorLogPath, logFileFlags, 0777); err != nil {
|
||||
return nil, errors.New("Error loading " + errorLogPath + ": " + err.Error())
|
||||
}
|
||||
if gcl.staffFile, err = os.OpenFile(staffLogPath, logFileFlags, 0777); err != nil {
|
||||
return nil, errors.New("Error loading " + staffLogPath + ": " + err.Error())
|
||||
|
||||
}
|
||||
return &gcl, nil
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGochanLog(t *testing.T) {
|
||||
gcl, err := initLogs("../access.log", "../error.log", "../staff.log")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
gcl.Print(lStdLog, "os.Stdout log")
|
||||
gcl.Print(lStdLog|lAccessLog|lErrorLog|lStaffLog, "all logs")
|
||||
gcl.Print(lAccessLog, "Access log")
|
||||
gcl.Print(lErrorLog, "Error log")
|
||||
gcl.Print(lStaffLog, "Staff log")
|
||||
gcl.Print(lAccessLog|lErrorLog, "Access and error log")
|
||||
gcl.Print(lAccessLog|lStaffLog|lFatal, "Fatal access and staff log")
|
||||
gcl.Print(lAccessLog, "This shouldn't be here")
|
||||
}
|
923
src/manage.go
923
src/manage.go
|
@ -1,923 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
chopPortNumRegex = regexp.MustCompile(`(.+|\w+):(\d+)$`)
|
||||
)
|
||||
|
||||
// ManageFunction represents the functions accessed by staff members at /manage?action=<functionname>.
|
||||
type ManageFunction struct {
|
||||
Title string
|
||||
Permissions int // 0 -> non-staff, 1 => janitor, 2 => moderator, 3 => administrator
|
||||
Callback func(writer http.ResponseWriter, request *http.Request) string `json:"-"` //return string of html output
|
||||
}
|
||||
|
||||
func getManageFunctionsJSON() string {
|
||||
var jsonStr string
|
||||
|
||||
return jsonStr
|
||||
}
|
||||
|
||||
func callManageFunction(writer http.ResponseWriter, request *http.Request) {
|
||||
var err error
|
||||
if err = request.ParseForm(); err != nil {
|
||||
serveErrorPage(writer,
|
||||
gclog.Print(lErrorLog, "Error parsing form data: ", err.Error()))
|
||||
}
|
||||
|
||||
action := request.FormValue("action")
|
||||
staffRank := getStaffRank(request)
|
||||
var managePageBuffer bytes.Buffer
|
||||
if action == "" {
|
||||
action = "announcements"
|
||||
} else if action == "postinfo" {
|
||||
writer.Header().Add("Content-Type", "application/json")
|
||||
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
|
||||
}
|
||||
|
||||
if action != "getstaffjquery" && action != "postinfo" {
|
||||
managePageBuffer.WriteString("<!DOCTYPE html><html><head>")
|
||||
if err = manageHeaderTmpl.Execute(&managePageBuffer, config); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog|lStaffLog,
|
||||
"Error executing manage page header template: ", err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := manageFunctions[action]; ok {
|
||||
if staffRank >= manageFunctions[action].Permissions {
|
||||
managePageBuffer.Write([]byte(manageFunctions[action].Callback(writer, request)))
|
||||
} else if staffRank == 0 && manageFunctions[action].Permissions == 0 {
|
||||
managePageBuffer.Write([]byte(manageFunctions[action].Callback(writer, request)))
|
||||
} else if staffRank == 0 {
|
||||
managePageBuffer.Write([]byte(manageFunctions["login"].Callback(writer, request)))
|
||||
} else {
|
||||
managePageBuffer.Write([]byte(action + " is undefined."))
|
||||
}
|
||||
} else {
|
||||
managePageBuffer.Write([]byte(action + " is undefined."))
|
||||
}
|
||||
if action != "getstaffjquery" && action != "postinfo" {
|
||||
managePageBuffer.Write([]byte("</body></html>"))
|
||||
}
|
||||
|
||||
writer.Write(managePageBuffer.Bytes())
|
||||
}
|
||||
|
||||
func getCurrentStaff(request *http.Request) (string, error) { //TODO after refactor, check if still used
|
||||
sessionCookie, err := request.Cookie("sessiondata")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name, err := GetStaffName(sessionCookie.Value)
|
||||
if err == nil {
|
||||
return "", err
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func getCurrentFullStaff(request *http.Request) (*Staff, error) {
|
||||
sessionCookie, err := request.Cookie("sessiondata")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return GetStaffBySession(sessionCookie.Value)
|
||||
}
|
||||
|
||||
func getStaffRank(request *http.Request) int {
|
||||
staff, err := getCurrentFullStaff(request)
|
||||
if err != nil {
|
||||
gclog.Print(lErrorLog, "Error getting current staff: ", err.Error())
|
||||
return 0
|
||||
}
|
||||
return staff.Rank
|
||||
}
|
||||
|
||||
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
|
||||
domain = chopPortNumRegex.Split(domain, -1)[0]
|
||||
|
||||
if !validReferrer(request) {
|
||||
gclog.Print(lStaffLog, "Rejected login from possible spambot @ "+request.RemoteAddr)
|
||||
return 2
|
||||
}
|
||||
staff, err := GetStaffByName(username)
|
||||
if err != nil {
|
||||
gclog.Print(lErrorLog, err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
success := bcrypt.CompareHashAndPassword([]byte(staff.PasswordChecksum), []byte(password))
|
||||
if success == bcrypt.ErrMismatchedHashAndPassword {
|
||||
// password mismatch
|
||||
gclog.Print(lStaffLog, "Failed login (password mismatch) from "+request.RemoteAddr+" at "+getSQLDateTime())
|
||||
return 1
|
||||
}
|
||||
|
||||
// successful login, add cookie that expires in one month
|
||||
http.SetCookie(writer, &http.Cookie{
|
||||
Name: "sessiondata",
|
||||
Value: key,
|
||||
Path: "/",
|
||||
Domain: domain,
|
||||
MaxAge: 60 * 60 * 24 * 7,
|
||||
})
|
||||
|
||||
if err = CreateSession(key, username); err != nil {
|
||||
gclog.Print(lErrorLog, "Error creating new staff session: ", err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
var manageFunctions = map[string]ManageFunction{
|
||||
"cleanup": {
|
||||
Title: "Cleanup",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
html = "<h2 class=\"manage-header\">Cleanup</h2><br />"
|
||||
var err error
|
||||
if request.FormValue("run") == "Run Cleanup" {
|
||||
html += "Removing deleted posts from the database.<hr />"
|
||||
if err = PermanentlyRemoveDeletedPosts(); err != nil {
|
||||
return html + "<tr><td>" +
|
||||
gclog.Print(lErrorLog, "Error removing deleted posts from database: ", err.Error()) +
|
||||
"</td></tr></table>"
|
||||
}
|
||||
// TODO: remove orphaned replies and uploads
|
||||
|
||||
html += "Optimizing all tables in database.<hr />"
|
||||
err = OptimizeDatabase()
|
||||
if err != nil {
|
||||
return html + "<tr><td>" +
|
||||
gclog.Print(lErrorLog, "Error optimizing SQL tables: ", err.Error()) +
|
||||
"</td></tr></table>"
|
||||
}
|
||||
|
||||
html += "Cleanup finished"
|
||||
} else {
|
||||
html += "<form action=\"/manage?action=cleanup\" method=\"post\">\n" +
|
||||
" <input name=\"run\" id=\"run\" type=\"submit\" value=\"Run Cleanup\" />\n" +
|
||||
"</form>"
|
||||
}
|
||||
return
|
||||
}},
|
||||
"config": {
|
||||
Title: "Configuration",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
do := request.FormValue("do")
|
||||
var status string
|
||||
if do == "save" {
|
||||
configJSON, err := json.MarshalIndent(config, "", "\t")
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else if err = ioutil.WriteFile("gochan.json", configJSON, 0777); err != nil {
|
||||
status += "Error backing up old gochan.json, cancelling save: " + err.Error() + "\n"
|
||||
} else {
|
||||
config.Lockdown = (request.PostFormValue("Lockdown") == "on")
|
||||
config.LockdownMessage = request.PostFormValue("LockdownMessage")
|
||||
Sillytags_arr := strings.Split(request.PostFormValue("Sillytags"), "\n")
|
||||
var Sillytags []string
|
||||
for _, tag := range Sillytags_arr {
|
||||
Sillytags = append(Sillytags, strings.Trim(tag, " \n\r"))
|
||||
}
|
||||
config.Sillytags = Sillytags
|
||||
config.UseSillytags = (request.PostFormValue("UseSillytags") == "on")
|
||||
config.Modboard = request.PostFormValue("Modboard")
|
||||
config.SiteName = request.PostFormValue("SiteName")
|
||||
config.SiteSlogan = request.PostFormValue("SiteSlogan")
|
||||
config.SiteHeaderURL = request.PostFormValue("SiteHeaderURL")
|
||||
config.SiteWebfolder = request.PostFormValue("SiteWebfolder")
|
||||
// TODO: Change this to match the new Style type in gochan.json
|
||||
/* Styles_arr := strings.Split(request.PostFormValue("Styles"), "\n")
|
||||
var Styles []string
|
||||
for _, style := range Styles_arr {
|
||||
Styles = append(Styles, strings.Trim(style, " \n\r"))
|
||||
}
|
||||
config.Styles = Styles */
|
||||
config.DefaultStyle = request.PostFormValue("DefaultStyle")
|
||||
config.AllowDuplicateImages = (request.PostFormValue("AllowDuplicateImages") == "on")
|
||||
config.AllowVideoUploads = (request.PostFormValue("AllowVideoUploads") == "on")
|
||||
NewThreadDelay, err := strconv.Atoi(request.PostFormValue("NewThreadDelay"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.NewThreadDelay = NewThreadDelay
|
||||
}
|
||||
|
||||
ReplyDelay, err := strconv.Atoi(request.PostFormValue("ReplyDelay"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.ReplyDelay = ReplyDelay
|
||||
}
|
||||
|
||||
MaxLineLength, err := strconv.Atoi(request.PostFormValue("MaxLineLength"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.MaxLineLength = MaxLineLength
|
||||
}
|
||||
|
||||
ReservedTrips_arr := strings.Split(request.PostFormValue("ReservedTrips"), "\n")
|
||||
var ReservedTrips []string
|
||||
for _, trip := range ReservedTrips_arr {
|
||||
ReservedTrips = append(ReservedTrips, strings.Trim(trip, " \n\r"))
|
||||
|
||||
}
|
||||
config.ReservedTrips = ReservedTrips
|
||||
|
||||
ThumbWidth, err := strconv.Atoi(request.PostFormValue("ThumbWidth"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.ThumbWidth = ThumbWidth
|
||||
}
|
||||
|
||||
ThumbHeight, err := strconv.Atoi(request.PostFormValue("ThumbHeight"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.ThumbHeight = ThumbHeight
|
||||
}
|
||||
|
||||
ThumbWidth_reply, err := strconv.Atoi(request.PostFormValue("ThumbWidth_reply"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.ThumbWidth_reply = ThumbWidth_reply
|
||||
}
|
||||
|
||||
ThumbHeight_reply, err := strconv.Atoi(request.PostFormValue("ThumbHeight_reply"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.ThumbHeight_reply = ThumbHeight_reply
|
||||
}
|
||||
|
||||
ThumbWidth_catalog, err := strconv.Atoi(request.PostFormValue("ThumbWidth_catalog"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.ThumbWidth_catalog = ThumbWidth_catalog
|
||||
}
|
||||
|
||||
ThumbHeight_catalog, err := strconv.Atoi(request.PostFormValue("ThumbHeight_catalog"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.ThumbHeight_catalog = ThumbHeight_catalog
|
||||
}
|
||||
|
||||
RepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("RepliesOnBoardPage"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.RepliesOnBoardPage = RepliesOnBoardPage
|
||||
}
|
||||
|
||||
StickyRepliesOnBoardPage, err := strconv.Atoi(request.PostFormValue("StickyRepliesOnBoardPage"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.StickyRepliesOnBoardPage = StickyRepliesOnBoardPage
|
||||
}
|
||||
|
||||
BanColors_arr := strings.Split(request.PostFormValue("BanColors"), "\n")
|
||||
var BanColors []string
|
||||
for _, color := range BanColors_arr {
|
||||
BanColors = append(BanColors, strings.Trim(color, " \n\r"))
|
||||
|
||||
}
|
||||
config.BanColors = BanColors
|
||||
|
||||
config.BanMsg = request.PostFormValue("BanMsg")
|
||||
EmbedWidth, err := strconv.Atoi(request.PostFormValue("EmbedWidth"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.EmbedWidth = EmbedWidth
|
||||
}
|
||||
|
||||
EmbedHeight, err := strconv.Atoi(request.PostFormValue("EmbedHeight"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.EmbedHeight = EmbedHeight
|
||||
}
|
||||
|
||||
config.ExpandButton = (request.PostFormValue("ExpandButton") == "on")
|
||||
config.ImagesOpenNewTab = (request.PostFormValue("ImagesOpenNewTab") == "on")
|
||||
config.MakeURLsHyperlinked = (request.PostFormValue("MakeURLsHyperlinked") == "on")
|
||||
config.NewTabOnOutlinks = (request.PostFormValue("NewTabOnOutlinks") == "on")
|
||||
config.MinifyHTML = (request.PostFormValue("MinifyHTML") == "on")
|
||||
config.MinifyJS = (request.PostFormValue("MinifyJS") == "on")
|
||||
config.DateTimeFormat = request.PostFormValue("DateTimeFormat")
|
||||
AkismetAPIKey := request.PostFormValue("AkismetAPIKey")
|
||||
|
||||
if err = checkAkismetAPIKey(AkismetAPIKey); err != nil {
|
||||
status += err.Error() + "<br />"
|
||||
} else {
|
||||
config.AkismetAPIKey = AkismetAPIKey
|
||||
}
|
||||
|
||||
config.UseCaptcha = (request.PostFormValue("UseCaptcha") == "on")
|
||||
CaptchaWidth, err := strconv.Atoi(request.PostFormValue("CaptchaWidth"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.CaptchaWidth = CaptchaWidth
|
||||
}
|
||||
CaptchaHeight, err := strconv.Atoi(request.PostFormValue("CaptchaHeight"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.CaptchaHeight = CaptchaHeight
|
||||
}
|
||||
|
||||
config.EnableGeoIP = (request.PostFormValue("EnableGeoIP") == "on")
|
||||
config.GeoIPDBlocation = request.PostFormValue("GeoIPDBlocation")
|
||||
|
||||
MaxRecentPosts, err := strconv.Atoi(request.PostFormValue("MaxRecentPosts"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.MaxRecentPosts = MaxRecentPosts
|
||||
}
|
||||
|
||||
config.EnableAppeals = (request.PostFormValue("EnableAppeals") == "on")
|
||||
MaxLogDays, err := strconv.Atoi(request.PostFormValue("MaxLogDays"))
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else {
|
||||
config.MaxLogDays = MaxLogDays
|
||||
}
|
||||
|
||||
configJSON, err = json.MarshalIndent(config, "", "\t")
|
||||
if err != nil {
|
||||
status += err.Error() + "<br />\n"
|
||||
} else if err = ioutil.WriteFile("gochan.json", configJSON, 0777); err != nil {
|
||||
status = "Error writing gochan.json: %s\n" + err.Error()
|
||||
} else {
|
||||
status = "Wrote gochan.json successfully <br />"
|
||||
buildJS()
|
||||
}
|
||||
}
|
||||
}
|
||||
manageConfigBuffer := bytes.NewBufferString("")
|
||||
if err := manageConfigTmpl.Execute(manageConfigBuffer,
|
||||
map[string]interface{}{"config": config, "status": status},
|
||||
); err != nil {
|
||||
return html + gclog.Print(lErrorLog, "Error executing config management page: ", err.Error())
|
||||
}
|
||||
html += manageConfigBuffer.String()
|
||||
return
|
||||
}},
|
||||
"login": {
|
||||
Title: "Login",
|
||||
Permissions: 0,
|
||||
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")
|
||||
redirect_action := request.FormValue("action")
|
||||
if redirect_action == "" {
|
||||
redirect_action = "announcements"
|
||||
}
|
||||
if username == "" || password == "" {
|
||||
//assume that they haven't logged in
|
||||
html = "\t<form method=\"POST\" action=\"" + config.SiteWebfolder + "manage?action=login\" id=\"login-box\" class=\"staff-form\">\n" +
|
||||
"\t\t<input type=\"hidden\" name=\"redirect\" value=\"" + redirect_action + "\" />\n" +
|
||||
"\t\t<input type=\"text\" name=\"username\" class=\"logindata\" /><br />\n" +
|
||||
"\t\t<input type=\"password\" name=\"password\" class=\"logindata\" /> <br />\n" +
|
||||
"\t\t<input type=\"submit\" value=\"Login\" />\n" +
|
||||
"\t</form>"
|
||||
} else {
|
||||
key := md5Sum(request.RemoteAddr + username + password + config.RandomSeed + randomString(3))[0:10]
|
||||
createSession(key, username, password, request, writer)
|
||||
http.Redirect(writer, request, path.Join(config.SiteWebfolder, "manage?action="+request.FormValue("redirect")), http.StatusFound)
|
||||
}
|
||||
return
|
||||
}},
|
||||
"logout": {
|
||||
Title: "Logout",
|
||||
Permissions: 1,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
cookie, _ := request.Cookie("sessiondata")
|
||||
cookie.MaxAge = 0
|
||||
cookie.Expires = time.Now().Add(-7 * 24 * time.Hour)
|
||||
http.SetCookie(writer, cookie)
|
||||
return "Logged out successfully"
|
||||
}},
|
||||
"announcements": {
|
||||
Title: "Announcements",
|
||||
Permissions: 1,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
html = "<h1 class=\"manage-header\">Announcements</h1><br />"
|
||||
|
||||
//get all announcements to announcement list
|
||||
//loop to html if exist, no announcement if empty
|
||||
announcements, err := GetAllAccouncements()
|
||||
if err != nil {
|
||||
return html + gclog.Print(lErrorLog, "Error getting announcements: ", err.Error())
|
||||
}
|
||||
if len(announcements) == 0 {
|
||||
html += "No announcements"
|
||||
} else {
|
||||
for _, announcement := range announcements {
|
||||
html += "<div class=\"section-block\">\n" +
|
||||
"<div class=\"section-title-block\"><b>" + announcement.Subject + "</b> by " + announcement.Poster + " at " + humanReadableTime(announcement.Timestamp) + "</div>\n" +
|
||||
"<div class=\"section-body\">" + announcement.Message + "\n</div></div>\n"
|
||||
}
|
||||
}
|
||||
return html
|
||||
}},
|
||||
"bans": {
|
||||
Title: "Bans",
|
||||
Permissions: 1,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (pageHTML string) { //TODO whatever this does idk man
|
||||
var post Post
|
||||
if request.FormValue("do") == "add" {
|
||||
ip := net.ParseIP(request.FormValue("ip"))
|
||||
name := request.FormValue("name")
|
||||
nameIsRegex := (request.FormValue("nameregex") == "on")
|
||||
checksum := request.FormValue("checksum")
|
||||
filename := request.FormValue("filename")
|
||||
durationForm := request.FormValue("duration")
|
||||
permaban := (durationForm == "" || durationForm == "0" || durationForm == "forever")
|
||||
duration, err := parseDurationString(durationForm)
|
||||
if err != nil {
|
||||
serveErrorPage(writer, err.Error())
|
||||
}
|
||||
expires := time.Now().Add(duration)
|
||||
|
||||
boards := request.FormValue("boards")
|
||||
reason := html.EscapeString(request.FormValue("reason"))
|
||||
staffNote := html.EscapeString(request.FormValue("staffnote"))
|
||||
currentStaff, _ := getCurrentStaff(request)
|
||||
|
||||
err = nil
|
||||
if filename != "" {
|
||||
err = FileNameBan(filename, nameIsRegex, currentStaff, expires, permaban, staffNote, boards)
|
||||
}
|
||||
if err != nil {
|
||||
pageHTML += err.Error()
|
||||
err = nil
|
||||
}
|
||||
if name != "" {
|
||||
err = UserNameBan(name, nameIsRegex, currentStaff, expires, permaban, staffNote, boards)
|
||||
}
|
||||
if err != nil {
|
||||
pageHTML += err.Error()
|
||||
err = nil
|
||||
}
|
||||
|
||||
if request.FormValue("fullban") == "on" {
|
||||
err = UserBan(ip, false, currentStaff, boards, expires, permaban, staffNote, reason, true, time.Now())
|
||||
if err != nil {
|
||||
pageHTML += err.Error()
|
||||
err = nil
|
||||
}
|
||||
} else {
|
||||
if request.FormValue("threadban") == "on" {
|
||||
err = UserBan(ip, true, currentStaff, boards, expires, permaban, staffNote, reason, true, time.Now())
|
||||
if err != nil {
|
||||
pageHTML += err.Error()
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if request.FormValue("imageban") == "on" {
|
||||
err = FileBan(checksum, currentStaff, expires, permaban, staffNote, boards)
|
||||
if err != nil {
|
||||
pageHTML += err.Error()
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if request.FormValue("postid") != "" {
|
||||
var err error
|
||||
post, err = GetSpecificPostByString(request.FormValue("postid"))
|
||||
if err != nil {
|
||||
return pageHTML + gclog.Print(lErrorLog, "Error getting post: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
banlist, err := GetAllBans()
|
||||
if err != nil {
|
||||
return pageHTML + gclog.Print(lErrorLog, "Error getting ban list: ", err.Error())
|
||||
}
|
||||
manageBansBuffer := bytes.NewBufferString("")
|
||||
|
||||
if err := manageBansTmpl.Execute(manageBansBuffer,
|
||||
map[string]interface{}{"config": config, "banlist": banlist, "post": post},
|
||||
); err != nil {
|
||||
return pageHTML + gclog.Print(lErrorLog, "Error executing ban management page template: ", err.Error())
|
||||
}
|
||||
pageHTML += manageBansBuffer.String()
|
||||
return
|
||||
}},
|
||||
"getstaffjquery": {
|
||||
Permissions: 0,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
staff, err := getCurrentFullStaff(request)
|
||||
if err != nil {
|
||||
html = "nobody;0;"
|
||||
return
|
||||
}
|
||||
html = staff.Username + ";" + strconv.Itoa(staff.Rank) + ";" + staff.Boards
|
||||
return
|
||||
}},
|
||||
"boards": {
|
||||
Title: "Boards",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
do := request.FormValue("do")
|
||||
var done bool
|
||||
board := new(Board)
|
||||
var boardCreationStatus string
|
||||
var err error
|
||||
for !done {
|
||||
switch {
|
||||
case do == "add":
|
||||
board.Dir = request.FormValue("dir")
|
||||
if board.Dir == "" {
|
||||
boardCreationStatus = `Error: "Directory" cannot be blank`
|
||||
do = ""
|
||||
continue
|
||||
}
|
||||
orderStr := request.FormValue("order")
|
||||
board.ListOrder, err = strconv.Atoi(orderStr)
|
||||
if err != nil {
|
||||
board.ListOrder = 0
|
||||
}
|
||||
board.Title = request.FormValue("title")
|
||||
if board.Title == "" {
|
||||
boardCreationStatus = `Error: "Title" cannot be blank`
|
||||
do = ""
|
||||
continue
|
||||
}
|
||||
board.Subtitle = request.FormValue("subtitle")
|
||||
board.Description = request.FormValue("description")
|
||||
sectionStr := request.FormValue("section")
|
||||
if sectionStr == "none" {
|
||||
sectionStr = "0"
|
||||
}
|
||||
|
||||
board.CreatedOn = time.Now()
|
||||
board.Section, err = strconv.Atoi(sectionStr)
|
||||
if err != nil {
|
||||
board.Section = 0
|
||||
}
|
||||
board.MaxFilesize, err = strconv.Atoi(request.FormValue("maximagesize"))
|
||||
if err != nil {
|
||||
board.MaxFilesize = 1024 * 4
|
||||
}
|
||||
|
||||
board.MaxPages, err = strconv.Atoi(request.FormValue("maxpages"))
|
||||
if err != nil {
|
||||
board.MaxPages = 11
|
||||
}
|
||||
|
||||
board.DefaultStyle = strings.Trim(request.FormValue("defaultstyle"), "\n")
|
||||
board.Locked = (request.FormValue("locked") == "on")
|
||||
board.ForcedAnon = (request.FormValue("forcedanon") == "on")
|
||||
|
||||
board.Anonymous = request.FormValue("anonymous")
|
||||
if board.Anonymous == "" {
|
||||
board.Anonymous = "Anonymous"
|
||||
}
|
||||
|
||||
board.MaxAge, err = strconv.Atoi(request.FormValue("maxage"))
|
||||
if err != nil {
|
||||
board.MaxAge = 0
|
||||
}
|
||||
|
||||
board.AutosageAfter, err = strconv.Atoi(request.FormValue("autosageafter"))
|
||||
if err != nil {
|
||||
board.AutosageAfter = 200
|
||||
}
|
||||
|
||||
board.NoImagesAfter, err = strconv.Atoi(request.FormValue("noimagesafter"))
|
||||
if err != nil {
|
||||
board.NoImagesAfter = 0
|
||||
}
|
||||
|
||||
board.MaxMessageLength, err = strconv.Atoi(request.FormValue("maxmessagelength"))
|
||||
if err != nil {
|
||||
board.MaxMessageLength = 1024 * 8
|
||||
}
|
||||
|
||||
board.EmbedsAllowed = (request.FormValue("embedsallowed") == "on")
|
||||
board.RedirectToThread = (request.FormValue("redirecttothread") == "on")
|
||||
board.RequireFile = (request.FormValue("require_file") == "on")
|
||||
board.EnableCatalog = (request.FormValue("enablecatalog") == "on")
|
||||
|
||||
//actually start generating stuff
|
||||
if err = os.Mkdir(path.Join(config.DocumentRoot, board.Dir), 0666); err != nil {
|
||||
do = ""
|
||||
boardCreationStatus = gclog.Printf(lStaffLog|lErrorLog, "Directory %s/%s/ already exists.",
|
||||
config.DocumentRoot, board.Dir)
|
||||
break
|
||||
}
|
||||
|
||||
if err = os.Mkdir(path.Join(config.DocumentRoot, board.Dir, "res"), 0666); err != nil {
|
||||
do = ""
|
||||
boardCreationStatus = gclog.Printf(lStaffLog|lErrorLog, "Directory %s/%s/res/ already exists.",
|
||||
config.DocumentRoot, board.Dir)
|
||||
break
|
||||
}
|
||||
|
||||
if err = os.Mkdir(path.Join(config.DocumentRoot, board.Dir, "src"), 0666); err != nil {
|
||||
do = ""
|
||||
boardCreationStatus = gclog.Printf(lStaffLog|lErrorLog, "Directory %s/%s/src/ already exists.",
|
||||
config.DocumentRoot, board.Dir)
|
||||
break
|
||||
}
|
||||
|
||||
if err = os.Mkdir(path.Join(config.DocumentRoot, board.Dir, "thumb"), 0666); err != nil {
|
||||
do = ""
|
||||
boardCreationStatus = gclog.Printf(lStaffLog|lErrorLog, "Directory %s/%s/thumb/ already exists.",
|
||||
config.DocumentRoot, board.Dir)
|
||||
break
|
||||
}
|
||||
|
||||
if err := CreateBoard(board); err != nil {
|
||||
do = ""
|
||||
boardCreationStatus = gclog.Print(lErrorLog, "Error creating board: ", err.Error())
|
||||
break
|
||||
} else {
|
||||
boardCreationStatus = "Board created successfully"
|
||||
buildBoards()
|
||||
resetBoardSectionArrays()
|
||||
gclog.Print(lStaffLog, "Boards rebuilt successfully")
|
||||
done = true
|
||||
}
|
||||
case do == "del":
|
||||
// resetBoardSectionArrays()
|
||||
case do == "edit":
|
||||
// resetBoardSectionArrays()
|
||||
default:
|
||||
// put the default column values in the text boxes
|
||||
board.Section = 1
|
||||
board.MaxFilesize = 4718592
|
||||
board.MaxPages = 11
|
||||
board.DefaultStyle = "pipes.css"
|
||||
board.Anonymous = "Anonymous"
|
||||
board.AutosageAfter = 200
|
||||
board.MaxMessageLength = 8192
|
||||
board.EmbedsAllowed = true
|
||||
board.EnableCatalog = true
|
||||
board.Worksafe = true
|
||||
board.ThreadsPerPage = config.ThreadsPerPage
|
||||
}
|
||||
|
||||
html = "<h1 class=\"manage-header\">Manage boards</h1>\n<form action=\"/manage?action=boards\" method=\"POST\">\n<input type=\"hidden\" name=\"do\" value=\"existing\" /><select name=\"boardselect\">\n<option>Select board...</option>\n"
|
||||
boards, err := GetBoardUris()
|
||||
if err != nil {
|
||||
return html + gclog.Print(lErrorLog, "Error getting board list: ", err.Error())
|
||||
}
|
||||
for _, boardDir := range boards {
|
||||
html += "<option>" + boardDir + "</option>"
|
||||
}
|
||||
|
||||
html += "</select> <input type=\"submit\" value=\"Edit\" /> <input type=\"submit\" value=\"Delete\" /></form><hr />" +
|
||||
"<h2 class=\"manage-header\">Create new board</h2>\n<span id=\"board-creation-message\">" + boardCreationStatus + "</span><br />"
|
||||
|
||||
manageBoardsBuffer := bytes.NewBufferString("")
|
||||
allSections, _ = GetAllSectionsOrCreateDefault()
|
||||
|
||||
if err := manageBoardsTmpl.Execute(manageBoardsBuffer, map[string]interface{}{
|
||||
"config": config,
|
||||
"board": board,
|
||||
"section_arr": allSections,
|
||||
}); err != nil {
|
||||
return html + gclog.Print(lErrorLog, "Error executing board management page template: ", err.Error())
|
||||
}
|
||||
html += manageBoardsBuffer.String()
|
||||
return
|
||||
}
|
||||
resetBoardSectionArrays()
|
||||
return
|
||||
}},
|
||||
"staffmenu": {
|
||||
Title: "Staff menu",
|
||||
Permissions: 1,
|
||||
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"
|
||||
if rank == 3 {
|
||||
html += "<b>Admin stuff</b><br />\n<a href=\"javascript:void(0)\" id=\"staff\" class=\"staffmenu-item\">Manage staff</a><br />\n" +
|
||||
//"<a href=\"javascript:void(0)\" id=\"purgeeverything\" class=\"staffmenu-item\">Purge everything!</a><br />\n" +
|
||||
"<a href=\"javascript:void(0)\" id=\"executesql\" class=\"staffmenu-item\">Execute SQL statement(s)</a><br />\n" +
|
||||
"<a href=\"javascript:void(0)\" id=\"cleanup\" class=\"staffmenu-item\">Run cleanup</a><br />\n" +
|
||||
"<a href=\"javascript:void(0)\" id=\"rebuildall\" class=\"staffmenu-item\">Rebuild all</a><br />\n" +
|
||||
"<a href=\"javascript:void(0)\" id=\"rebuildfront\" class=\"staffmenu-item\">Rebuild front page</a><br />\n" +
|
||||
"<a href=\"javascript:void(0)\" id=\"rebuildboards\" class=\"staffmenu-item\">Rebuild board pages</a><br />\n" +
|
||||
"<a href=\"javascript:void(0)\" id=\"reparsehtml\" class=\"staffmenu-item\">Reparse all posts</a><br />\n" +
|
||||
"<a href=\"javascript:void(0)\" id=\"boards\" class=\"staffmenu-item\">Add/edit/delete boards</a><br />\n"
|
||||
}
|
||||
if rank >= 2 {
|
||||
html += "<b>Mod stuff</b><br />\n" +
|
||||
"<a href=\"javascript:void(0)\" id=\"bans\" class=\"staffmenu-item\">Ban User(s)</a><br />\n"
|
||||
}
|
||||
|
||||
if rank >= 1 {
|
||||
html += "<a href=\"javascript:void(0)\" id=\"recentimages\" class=\"staffmenu-item\">Recently uploaded images</a><br />\n" +
|
||||
"<a href=\"javascript:void(0)\" id=\"recentposts\" class=\"staffmenu-item\">Recent posts</a><br />\n" +
|
||||
"<a href=\"javascript:void(0)\" id=\"searchip\" class=\"staffmenu-item\">Search posts by IP</a><br />\n"
|
||||
}
|
||||
return
|
||||
}},
|
||||
"rebuildfront": {
|
||||
Title: "Rebuild front page",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
initTemplates()
|
||||
return buildFrontPage()
|
||||
}},
|
||||
"rebuildall": {
|
||||
Title: "Rebuild everything",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
initTemplates()
|
||||
resetBoardSectionArrays()
|
||||
return buildFrontPage() + "<hr />" +
|
||||
buildBoardListJSON() + "<hr />" +
|
||||
buildBoards() + "<hr />" +
|
||||
buildJS() + "<hr />"
|
||||
}},
|
||||
"rebuildboards": {
|
||||
Title: "Rebuild boards",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
initTemplates()
|
||||
return buildBoards()
|
||||
}},
|
||||
"reparsehtml": {
|
||||
Title: "Reparse HTML",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
messages, err := GetAllNondeletedMessageRaw()
|
||||
if err != nil {
|
||||
html += err.Error() + "<br />"
|
||||
return
|
||||
}
|
||||
|
||||
for _, message := range messages {
|
||||
message.Message = formatMessage(message.MessageRaw)
|
||||
}
|
||||
err = SetMessages(messages)
|
||||
|
||||
if err != nil {
|
||||
return html + gclog.Printf(lErrorLog, err.Error())
|
||||
}
|
||||
html += "Done reparsing HTML<hr />" +
|
||||
buildFrontPage() + "<hr />" +
|
||||
buildBoardListJSON() + "<hr />" +
|
||||
buildBoards() + "<hr />"
|
||||
return
|
||||
}},
|
||||
"recentposts": {
|
||||
Title: "Recent posts",
|
||||
Permissions: 1,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
limit := request.FormValue("limit")
|
||||
if limit == "" {
|
||||
limit = "50"
|
||||
}
|
||||
html = "<h1 class=\"manage-header\">Recent posts</h1>\nLimit by: <select id=\"limit\"><option>25</option><option>50</option><option>100</option><option>200</option></select>\n<br />\n<table width=\"100%%d\" border=\"1\">\n<colgroup><col width=\"25%%\" /><col width=\"50%%\" /><col width=\"17%%\" /></colgroup><tr><th></th><th>Message</th><th>Time</th></tr>"
|
||||
recentposts, err := GetRecentPostsGlobal(HackyStringToInt(limit), false) //only uses boardname, boardid, postid, parentid, message, ip and timestamp
|
||||
|
||||
if err != nil {
|
||||
return html + "<tr><td>" + gclog.Print(lErrorLog, "Error getting recent posts: ",
|
||||
err.Error()) + "</td></tr></table>"
|
||||
}
|
||||
|
||||
for _, recentpost := range recentposts {
|
||||
html += fmt.Sprintf(
|
||||
`<tr><td><b>Post:</b> <a href="%s">%s/%d</a><br /><b>IP:</b> %s</td><td>%s</td><td>%s</td></tr>`,
|
||||
path.Join(config.SiteWebfolder, recentpost.BoardName, "/res/", strconv.Itoa(recentpost.ParentID)+".html#"+strconv.Itoa(recentpost.PostID)),
|
||||
recentpost.BoardName, recentpost.PostID, recentpost.IP, recentpost.Message,
|
||||
recentpost.Timestamp.Format("01/02/06, 15:04"),
|
||||
)
|
||||
}
|
||||
html += "</table>"
|
||||
return
|
||||
}},
|
||||
"postinfo": {
|
||||
Title: "Post info",
|
||||
Permissions: 2,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
errMap := map[string]interface{}{
|
||||
"action": "postInfo",
|
||||
"success": false,
|
||||
}
|
||||
post, err := GetSpecificPost(HackyStringToInt(request.FormValue("postid")), false)
|
||||
if err != nil {
|
||||
errMap["message"] = err.Error()
|
||||
jsonErr, _ := marshalJSON(errMap, false)
|
||||
return jsonErr
|
||||
}
|
||||
jsonStr, _ := marshalJSON(post, false)
|
||||
return jsonStr
|
||||
}},
|
||||
"staff": {
|
||||
Title: "Staff",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
do := request.FormValue("do")
|
||||
html = `<h1 class="manage-header">Staff</h1><br />` +
|
||||
`<table id="stafftable" border="1">` +
|
||||
"<tr><td><b>Username</b></td><td><b>Rank</b></td><td><b>Boards</b></td><td><b>Added on</b></td><td><b>Action</b></td></tr>"
|
||||
allStaff, err := GetAllStaffNopass()
|
||||
if err != nil {
|
||||
return html + gclog.Print(lErrorLog, "Error getting staff list: ", err.Error())
|
||||
}
|
||||
|
||||
for _, staff := range allStaff {
|
||||
username := request.FormValue("username")
|
||||
password := request.FormValue("password")
|
||||
rank := request.FormValue("rank")
|
||||
rankI, _ := strconv.Atoi(rank)
|
||||
if do == "add" {
|
||||
if err := newStaff(username, password, rankI); err != nil {
|
||||
serveErrorPage(writer, gclog.Printf(lErrorLog,
|
||||
"Error creating new staff account %q: %s", username, err.Error()))
|
||||
return
|
||||
}
|
||||
} else if do == "del" && username != "" {
|
||||
if err = deleteStaff(request.FormValue("username")); err != nil {
|
||||
serveErrorPage(writer, gclog.Printf(lErrorLog,
|
||||
"Error deleting staff account %q : %s", username, err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case staff.Rank == 3:
|
||||
rank = "admin"
|
||||
case staff.Rank == 2:
|
||||
rank = "mod"
|
||||
case staff.Rank == 1:
|
||||
rank = "janitor"
|
||||
}
|
||||
html += fmt.Sprintf(
|
||||
`<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td><a href="/manage?action=staff&do=del&username=%s" style="float:right;color:red;">X</a></td></tr>`,
|
||||
staff.Username, rank, staff.Boards, humanReadableTime(staff.AddedOn), staff.Username)
|
||||
}
|
||||
html += "</table>\n\n<hr />\n<h2 class=\"manage-header\">Add new staff</h2>\n\n" +
|
||||
"<form action=\"/manage?action=staff\" onsubmit=\"return makeNewStaff();\" method=\"POST\">\n" +
|
||||
"\t<input type=\"hidden\" name=\"do\" value=\"add\" />\n" +
|
||||
"\tUsername: <input id=\"username\" name=\"username\" type=\"text\" /><br />\n" +
|
||||
"\tPassword: <input id=\"password\" name=\"password\" type=\"password\" /><br />\n" +
|
||||
"\tRank: <select id=\"rank\" name=\"rank\">\n" +
|
||||
"\t\t<option value=\"3\">Admin</option>\n" +
|
||||
"\t\t<option value=\"2\">Moderator</option>\n" +
|
||||
"\t\t<option value=\"1\">Janitor</option>\n" +
|
||||
"\t\t</select><br />\n" +
|
||||
"\t\t<input id=\"submitnewstaff\" type=\"submit\" value=\"Add\" />\n" +
|
||||
"\t\t</form>"
|
||||
return
|
||||
}},
|
||||
"tempposts": {
|
||||
Title: "Temporary posts lists",
|
||||
Permissions: 3,
|
||||
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
|
||||
html += "<h1 class=\"manage-header\">Temporary posts</h1>"
|
||||
if len(tempPosts) == 0 {
|
||||
html += "No temporary posts<br />\n"
|
||||
return
|
||||
}
|
||||
for p, post := range tempPosts {
|
||||
html += fmt.Sprintf("Post[%d]: %#v<br />\n", p, post)
|
||||
}
|
||||
return
|
||||
}},
|
||||
}
|
679
src/posting.go
679
src/posting.go
|
@ -1,679 +0,0 @@
|
|||
// functions for handling posting, uploading, and bans
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/aquilax/tripcode"
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
const (
|
||||
gt = ">"
|
||||
yearInSeconds = 31536000
|
||||
)
|
||||
|
||||
var (
|
||||
allSections []BoardSection
|
||||
allBoards []Board
|
||||
tempPosts []Post
|
||||
tempCleanerTicker *time.Ticker
|
||||
)
|
||||
|
||||
// Checks check poster's name/tripcode/file checksum (from Post post) for banned status
|
||||
// returns ban table if the user is banned or sql.ErrNoRows if they aren't
|
||||
func getBannedStatus(request *http.Request) (*BanInfo, error) {
|
||||
formName := request.FormValue("postname")
|
||||
var tripcode string
|
||||
if formName != "" {
|
||||
parsedName := parseName(formName)
|
||||
tripcode += parsedName["name"]
|
||||
if tc, ok := parsedName["tripcode"]; ok {
|
||||
tripcode += "!" + tc
|
||||
}
|
||||
}
|
||||
ip := getRealIP(request)
|
||||
|
||||
var filename string
|
||||
var checksum string
|
||||
file, fileHandler, err := request.FormFile("imagefile")
|
||||
defer closeHandle(file)
|
||||
if err == nil {
|
||||
html.EscapeString(fileHandler.Filename)
|
||||
if data, err2 := ioutil.ReadAll(file); err2 == nil {
|
||||
checksum = fmt.Sprintf("%x", md5.Sum(data))
|
||||
}
|
||||
}
|
||||
return CheckBan(ip, tripcode, filename, checksum)
|
||||
}
|
||||
|
||||
func isBanned(ban *BanInfo, board string) bool {
|
||||
if ban.Boards == "" && (ban.Expires.After(time.Now()) || ban.Permaban) {
|
||||
return true
|
||||
}
|
||||
boardsArr := strings.Split(ban.Boards, ",")
|
||||
for _, b := range boardsArr {
|
||||
if b == board && (ban.Expires.After(time.Now()) || ban.Permaban) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func createImageThumbnail(imageObj image.Image, size string) image.Image {
|
||||
var thumbWidth int
|
||||
var thumbHeight int
|
||||
|
||||
switch size {
|
||||
case "op":
|
||||
thumbWidth = config.ThumbWidth
|
||||
thumbHeight = config.ThumbHeight
|
||||
case "reply":
|
||||
thumbWidth = config.ThumbWidth_reply
|
||||
thumbHeight = config.ThumbHeight_reply
|
||||
case "catalog":
|
||||
thumbWidth = config.ThumbWidth_catalog
|
||||
thumbHeight = config.ThumbHeight_catalog
|
||||
}
|
||||
oldRect := imageObj.Bounds()
|
||||
if thumbWidth >= oldRect.Max.X && thumbHeight >= oldRect.Max.Y {
|
||||
return imageObj
|
||||
}
|
||||
|
||||
thumbW, thumbH := getThumbnailSize(oldRect.Max.X, oldRect.Max.Y, size)
|
||||
imageObj = imaging.Resize(imageObj, thumbW, thumbH, imaging.CatmullRom) // resize to 600x400 px using CatmullRom cubic filter
|
||||
return imageObj
|
||||
}
|
||||
|
||||
func createVideoThumbnail(video, thumb string, size int) error {
|
||||
sizeStr := strconv.Itoa(size)
|
||||
outputBytes, err := exec.Command("ffmpeg", "-y", "-itsoffset", "-1", "-i", video, "-vframes", "1", "-filter:v", "scale='min("+sizeStr+"\\, "+sizeStr+"):-1'", thumb).CombinedOutput()
|
||||
if err != nil {
|
||||
outputStringArr := strings.Split(string(outputBytes), "\n")
|
||||
if len(outputStringArr) > 1 {
|
||||
outputString := outputStringArr[len(outputStringArr)-2]
|
||||
err = errors.New(outputString)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func getVideoInfo(path string) (map[string]int, error) {
|
||||
vidInfo := make(map[string]int)
|
||||
|
||||
outputBytes, err := exec.Command("ffprobe", "-v quiet", "-show_format", "-show_streams", path).CombinedOutput()
|
||||
if err == nil && outputBytes != nil {
|
||||
outputStringArr := strings.Split(string(outputBytes), "\n")
|
||||
for _, line := range outputStringArr {
|
||||
lineArr := strings.Split(line, "=")
|
||||
if len(lineArr) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if lineArr[0] == "width" || lineArr[0] == "height" || lineArr[0] == "size" {
|
||||
value, _ := strconv.Atoi(lineArr[1])
|
||||
vidInfo[lineArr[0]] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return vidInfo, err
|
||||
}
|
||||
|
||||
func getNewFilename() string {
|
||||
now := time.Now().Unix()
|
||||
rand.Seed(now)
|
||||
return strconv.Itoa(int(now)) + strconv.Itoa(rand.Intn(98)+1)
|
||||
}
|
||||
|
||||
// find out what out thumbnail's width and height should be, partially ripped from Kusaba X
|
||||
func getThumbnailSize(w int, h int, size string) (newWidth int, newHeight int) {
|
||||
var thumbWidth int
|
||||
var thumbHeight int
|
||||
|
||||
switch {
|
||||
case size == "op":
|
||||
thumbWidth = config.ThumbWidth
|
||||
thumbHeight = config.ThumbHeight
|
||||
case size == "reply":
|
||||
thumbWidth = config.ThumbWidth_reply
|
||||
thumbHeight = config.ThumbHeight_reply
|
||||
case size == "catalog":
|
||||
thumbWidth = config.ThumbWidth_catalog
|
||||
thumbHeight = config.ThumbHeight_catalog
|
||||
}
|
||||
if w == h {
|
||||
newWidth = thumbWidth
|
||||
newHeight = thumbHeight
|
||||
} else {
|
||||
var percent float32
|
||||
if w > h {
|
||||
percent = float32(thumbWidth) / float32(w)
|
||||
} else {
|
||||
percent = float32(thumbHeight) / float32(h)
|
||||
}
|
||||
newWidth = int(float32(w) * percent)
|
||||
newHeight = int(float32(h) * percent)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// called when a user accesses /post. Parse form data, then insert and build
|
||||
func makePost(writer http.ResponseWriter, request *http.Request) {
|
||||
var maxMessageLength int
|
||||
var post Post
|
||||
// domain := request.Host
|
||||
var formName string
|
||||
var nameCookie string
|
||||
var formEmail string
|
||||
|
||||
if request.Method == "GET" {
|
||||
http.Redirect(writer, request, config.SiteWebfolder, http.StatusFound)
|
||||
return
|
||||
}
|
||||
// fix new cookie domain for when you use a port number
|
||||
// domain = chopPortNumRegex.Split(domain, -1)[0]
|
||||
|
||||
post.ParentID, _ = strconv.Atoi(request.FormValue("threadid"))
|
||||
post.BoardID, _ = strconv.Atoi(request.FormValue("boardid"))
|
||||
|
||||
var emailCommand string
|
||||
formName = request.FormValue("postname")
|
||||
parsedName := parseName(formName)
|
||||
post.Name = parsedName["name"]
|
||||
post.Tripcode = parsedName["tripcode"]
|
||||
|
||||
formEmail = request.FormValue("postemail")
|
||||
|
||||
http.SetCookie(writer, &http.Cookie{Name: "email", Value: formEmail, MaxAge: yearInSeconds})
|
||||
|
||||
if !strings.Contains(formEmail, "noko") && !strings.Contains(formEmail, "sage") {
|
||||
post.Email = formEmail
|
||||
} else if strings.Index(formEmail, "#") > 1 {
|
||||
formEmailArr := strings.SplitN(formEmail, "#", 2)
|
||||
post.Email = formEmailArr[0]
|
||||
emailCommand = formEmailArr[1]
|
||||
} else if formEmail == "noko" || formEmail == "sage" {
|
||||
emailCommand = formEmail
|
||||
post.Email = ""
|
||||
}
|
||||
|
||||
post.Subject = request.FormValue("postsubject")
|
||||
post.MessageText = strings.Trim(request.FormValue("postmsg"), "\r\n")
|
||||
var err error
|
||||
if maxMessageLength, err = GetMaxMessageLength(post.BoardID); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Error getting board info: ", err.Error()))
|
||||
}
|
||||
|
||||
if len(post.MessageText) > maxMessageLength {
|
||||
serveErrorPage(writer, "Post body is too long")
|
||||
return
|
||||
}
|
||||
post.MessageHTML = formatMessage(post.MessageText)
|
||||
password := request.FormValue("postpassword")
|
||||
if password == "" {
|
||||
password = randomString(8)
|
||||
}
|
||||
post.Password = md5Sum(password)
|
||||
|
||||
// Reverse escapes
|
||||
nameCookie = strings.Replace(formName, "&", "&", -1)
|
||||
nameCookie = strings.Replace(nameCookie, "\\'", "'", -1)
|
||||
nameCookie = strings.Replace(url.QueryEscape(nameCookie), "+", "%20", -1)
|
||||
|
||||
// add name and email cookies that will expire in a year (31536000 seconds)
|
||||
http.SetCookie(writer, &http.Cookie{Name: "name", Value: nameCookie, MaxAge: yearInSeconds})
|
||||
http.SetCookie(writer, &http.Cookie{Name: "password", Value: password, MaxAge: yearInSeconds})
|
||||
|
||||
post.IP = getRealIP(request)
|
||||
post.Timestamp = time.Now()
|
||||
// post.PosterAuthority = getStaffRank(request)
|
||||
post.Bumped = time.Now()
|
||||
post.Stickied = request.FormValue("modstickied") == "on"
|
||||
post.Locked = request.FormValue("modlocked") == "on"
|
||||
|
||||
//post has no referrer, or has a referrer from a different domain, probably a spambot
|
||||
if !validReferrer(request) {
|
||||
gclog.Print(lAccessLog, "Rejected post from possible spambot @ "+post.IP)
|
||||
return
|
||||
}
|
||||
|
||||
switch checkPostForSpam(post.IP, request.Header["User-Agent"][0], request.Referer(),
|
||||
post.Name, post.Email, post.MessageText) {
|
||||
case "discard":
|
||||
serveErrorPage(writer, "Your post looks like spam.")
|
||||
gclog.Print(lAccessLog, "Akismet recommended discarding post from: "+post.IP)
|
||||
return
|
||||
case "spam":
|
||||
serveErrorPage(writer, "Your post looks like spam.")
|
||||
gclog.Print(lAccessLog, "Akismet suggested post is spam from "+post.IP)
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
file, handler, err := request.FormFile("imagefile")
|
||||
defer closeHandle(file)
|
||||
|
||||
if err != nil || handler.Size == 0 {
|
||||
// no file was uploaded
|
||||
post.Filename = ""
|
||||
gclog.Printf(lAccessLog, "Receiving post from %s, referred from: %s", post.IP, request.Referer())
|
||||
} else {
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog, "Error while trying to read file: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
post.FilenameOriginal = html.EscapeString(handler.Filename)
|
||||
filetype := getFileExtension(post.FilenameOriginal)
|
||||
thumbFiletype := strings.ToLower(filetype)
|
||||
if thumbFiletype == "gif" || thumbFiletype == "webm" {
|
||||
thumbFiletype = "jpg"
|
||||
}
|
||||
|
||||
post.Filename = getNewFilename() + "." + getFileExtension(post.FilenameOriginal)
|
||||
boardExists, err := DoesBoardExistByID(HackyStringToInt(request.FormValue("boardid")))
|
||||
if err != nil {
|
||||
serveErrorPage(writer, "Server error: "+err.Error())
|
||||
return
|
||||
}
|
||||
if !boardExists {
|
||||
serveErrorPage(writer, "No boards have been created yet")
|
||||
return
|
||||
}
|
||||
var _board = Board{}
|
||||
err = _board.PopulateData(HackyStringToInt(request.FormValue("boardid")))
|
||||
if err != nil {
|
||||
serveErrorPage(writer, "Server error: "+err.Error())
|
||||
return
|
||||
}
|
||||
boardDir := _board.Dir
|
||||
filePath := path.Join(config.DocumentRoot, "/"+boardDir+"/src/", post.Filename)
|
||||
thumbPath := path.Join(config.DocumentRoot, "/"+boardDir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "t."+thumbFiletype, -1))
|
||||
catalogThumbPath := path.Join(config.DocumentRoot, "/"+boardDir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "c."+thumbFiletype, -1))
|
||||
|
||||
if err = ioutil.WriteFile(filePath, data, 0777); err != nil {
|
||||
gclog.Printf(lErrorLog, "Couldn't write file %q: %s", post.Filename, err.Error())
|
||||
serveErrorPage(writer, `Couldn't write file "`+post.FilenameOriginal+`"`)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate image checksum
|
||||
post.FileChecksum = fmt.Sprintf("%x", md5.Sum(data))
|
||||
|
||||
var allowsVids bool
|
||||
if allowsVids, err = GetEmbedsAllowed(post.BoardID); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Couldn't get board info: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if filetype == "webm" {
|
||||
if !allowsVids || !config.AllowVideoUploads {
|
||||
serveErrorPage(writer, gclog.Print(lAccessLog,
|
||||
"Video uploading is not currently enabled for this board."))
|
||||
os.Remove(filePath)
|
||||
return
|
||||
}
|
||||
|
||||
gclog.Printf(lAccessLog, "Receiving post with video: %s from %s, referrer: %s",
|
||||
handler.Filename, post.IP, request.Referer())
|
||||
if post.ParentID == 0 {
|
||||
if err := createVideoThumbnail(filePath, thumbPath, config.ThumbWidth); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Error creating video thumbnail: ", err.Error()))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := createVideoThumbnail(filePath, thumbPath, config.ThumbWidth_reply); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Error creating video thumbnail: ", err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := createVideoThumbnail(filePath, catalogThumbPath, config.ThumbWidth_catalog); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Error creating video thumbnail: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
outputBytes, err := exec.Command("ffprobe", "-v", "quiet", "-show_format", "-show_streams", filePath).CombinedOutput()
|
||||
if err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Error getting video info: ", err.Error()))
|
||||
return
|
||||
}
|
||||
if outputBytes != nil {
|
||||
outputStringArr := strings.Split(string(outputBytes), "\n")
|
||||
for _, line := range outputStringArr {
|
||||
lineArr := strings.Split(line, "=")
|
||||
if len(lineArr) < 2 {
|
||||
continue
|
||||
}
|
||||
value, _ := strconv.Atoi(lineArr[1])
|
||||
switch lineArr[0] {
|
||||
case "width":
|
||||
post.ImageW = value
|
||||
case "height":
|
||||
post.ImageH = value
|
||||
case "size":
|
||||
post.Filesize = value
|
||||
}
|
||||
}
|
||||
if post.ParentID == 0 {
|
||||
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "op")
|
||||
} else {
|
||||
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "reply")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Attempt to load uploaded file with imaging library
|
||||
img, err := imaging.Open(filePath)
|
||||
if err != nil {
|
||||
os.Remove(filePath)
|
||||
gclog.Printf(lErrorLog, "Couldn't open uploaded file %q: %s", post.Filename, err.Error())
|
||||
serveErrorPage(writer, "Upload filetype not supported")
|
||||
return
|
||||
}
|
||||
// Get image filesize
|
||||
stat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Couldn't get image filesize: "+err.Error()))
|
||||
return
|
||||
}
|
||||
post.Filesize = int(stat.Size())
|
||||
|
||||
// Get image width and height, as well as thumbnail width and height
|
||||
post.ImageW = img.Bounds().Max.X
|
||||
post.ImageH = img.Bounds().Max.Y
|
||||
if post.ParentID == 0 {
|
||||
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "op")
|
||||
} else {
|
||||
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "reply")
|
||||
}
|
||||
|
||||
gclog.Printf(lAccessLog, "Receiving post with image: %q from %s, referrer: %s",
|
||||
handler.Filename, post.IP, request.Referer())
|
||||
|
||||
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(writer, "missing /spoiler.png")
|
||||
return
|
||||
}
|
||||
if err = syscall.Symlink(path.Join(config.DocumentRoot, "spoiler.png"), thumbPath); err != nil {
|
||||
serveErrorPage(writer, err.Error())
|
||||
return
|
||||
}
|
||||
} else if config.ThumbWidth >= post.ImageW && config.ThumbHeight >= post.ImageH {
|
||||
// If image fits in thumbnail size, symlink thumbnail to original
|
||||
post.ThumbW = img.Bounds().Max.X
|
||||
post.ThumbH = img.Bounds().Max.Y
|
||||
if err := syscall.Symlink(filePath, thumbPath); err != nil {
|
||||
serveErrorPage(writer, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var thumbnail image.Image
|
||||
var catalogThumbnail image.Image
|
||||
if post.ParentID == 0 {
|
||||
// If this is a new thread, generate thumbnail and catalog thumbnail
|
||||
thumbnail = createImageThumbnail(img, "op")
|
||||
catalogThumbnail = createImageThumbnail(img, "catalog")
|
||||
if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Couldn't generate catalog thumbnail: ", err.Error()))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
thumbnail = createImageThumbnail(img, "reply")
|
||||
}
|
||||
if err = imaging.Save(thumbnail, thumbPath); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Couldn't save thumbnail: ", err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(post.MessageText) == "" && post.Filename == "" {
|
||||
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(writer, "Please wait before making a new thread.")
|
||||
return
|
||||
} else if post.ParentID > 0 && postDelay < config.ReplyDelay {
|
||||
serveErrorPage(writer, "Please wait before making a reply.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
banStatus, err := getBannedStatus(request)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Error getting banned status: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
boards, _ := GetAllBoards()
|
||||
|
||||
postBoard, _ := GetBoardFromID(post.BoardID)
|
||||
|
||||
if isBanned(banStatus, postBoard.Dir) {
|
||||
var banpageBuffer bytes.Buffer
|
||||
|
||||
if err = minifyTemplate(banpageTmpl, map[string]interface{}{
|
||||
"config": config, "ban": banStatus, "banBoards": boards[post.BoardID-1].Dir,
|
||||
}, writer, "text/html"); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog, "Error minifying page: ", err.Error()))
|
||||
return
|
||||
}
|
||||
writer.Write(banpageBuffer.Bytes())
|
||||
return
|
||||
}
|
||||
|
||||
post.Sanitize()
|
||||
|
||||
if config.UseCaptcha {
|
||||
captchaID := request.FormValue("captchaid")
|
||||
captchaAnswer := request.FormValue("captchaanswer")
|
||||
if captchaID == "" && captchaAnswer == "" {
|
||||
// browser isn't using JS, save post data to tempPosts and show captcha
|
||||
request.Form.Add("temppostindex", strconv.Itoa(len(tempPosts)))
|
||||
request.Form.Add("emailcmd", emailCommand)
|
||||
tempPosts = append(tempPosts, post)
|
||||
serveCaptcha(writer, request)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = InsertPost(&post, emailCommand != "sage"); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog, "Error inserting post: ", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// rebuild the board page
|
||||
buildBoards(post.BoardID)
|
||||
buildFrontPage()
|
||||
|
||||
if emailCommand == "noko" {
|
||||
if post.ParentID < 1 {
|
||||
http.Redirect(writer, request, config.SiteWebfolder+postBoard.Dir+"/res/"+strconv.Itoa(post.ID)+".html", http.StatusFound)
|
||||
} else {
|
||||
http.Redirect(writer, request, config.SiteWebfolder+postBoard.Dir+"/res/"+strconv.Itoa(post.ParentID)+".html#"+strconv.Itoa(post.ID), http.StatusFound)
|
||||
}
|
||||
} else {
|
||||
http.Redirect(writer, request, config.SiteWebfolder+postBoard.Dir+"/", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func tempCleaner() {
|
||||
for {
|
||||
select {
|
||||
case <-tempCleanerTicker.C:
|
||||
for p, post := range tempPosts {
|
||||
if !time.Now().After(post.Timestamp.Add(time.Minute * 5)) {
|
||||
continue
|
||||
}
|
||||
// temporary post is >= 5 minutes, time to prune it
|
||||
tempPosts[p] = tempPosts[len(tempPosts)-1]
|
||||
tempPosts = tempPosts[:len(tempPosts)-1]
|
||||
if post.FilenameOriginal == "" {
|
||||
continue
|
||||
}
|
||||
var board Board
|
||||
err := board.PopulateData(post.BoardID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fileSrc := path.Join(config.DocumentRoot, board.Dir, "src", post.FilenameOriginal)
|
||||
if err = os.Remove(fileSrc); err != nil {
|
||||
gclog.Printf(lErrorLog|lStdLog,
|
||||
"Error pruning temporary upload for %q: %s", fileSrc, err.Error())
|
||||
}
|
||||
|
||||
thumbSrc := getThumbnailPath("thread", fileSrc)
|
||||
if err = os.Remove(thumbSrc); err != nil {
|
||||
gclog.Printf(lErrorLog|lStdLog,
|
||||
"Error pruning temporary upload for %q: %s", thumbSrc, err.Error())
|
||||
}
|
||||
|
||||
if post.ParentID == 0 {
|
||||
catalogSrc := getThumbnailPath("catalog", fileSrc)
|
||||
if err = os.Remove(catalogSrc); err != nil {
|
||||
gclog.Printf(lErrorLog|lStdLog,
|
||||
"Error pruning temporary upload for %s: %s", catalogSrc, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatMessage(message string) string {
|
||||
message = msgfmtr.Compile(message)
|
||||
// prepare each line to be formatted
|
||||
postLines := strings.Split(message, "<br>")
|
||||
for i, line := range postLines {
|
||||
trimmedLine := strings.TrimSpace(line)
|
||||
lineWords := strings.Split(trimmedLine, " ")
|
||||
isGreentext := false // if true, append </span> to end of line
|
||||
for w, word := range lineWords {
|
||||
if strings.LastIndex(word, gt+gt) == 0 {
|
||||
//word is a backlink
|
||||
if postID, err := strconv.Atoi(word[8:]); err == nil {
|
||||
// the link is in fact, a valid int
|
||||
var boardDir string
|
||||
var linkParent int
|
||||
|
||||
if boardDir, err = GetBoardFromPostID(postID); err != nil {
|
||||
gclog.Print(lErrorLog, "Error getting board dir for backlink: ", err.Error())
|
||||
}
|
||||
if linkParent, err = GetThreadIDZeroIfTopPost(postID); err != nil {
|
||||
gclog.Print(lErrorLog, "Error getting post parent for backlink: ", err.Error())
|
||||
}
|
||||
|
||||
// get post board dir
|
||||
if boardDir == "" {
|
||||
lineWords[w] = "<a href=\"javascript:;\"><strike>" + word + "</strike></a>"
|
||||
} else if linkParent == 0 {
|
||||
lineWords[w] = "<a href=\"" + config.SiteWebfolder + boardDir + "/res/" + word[8:] + ".html\" class=\"postref\">" + word + "</a>"
|
||||
} else {
|
||||
lineWords[w] = "<a href=\"" + config.SiteWebfolder + boardDir + "/res/" + strconv.Itoa(linkParent) + ".html#" + word[8:] + "\" class=\"postref\">" + word + "</a>"
|
||||
}
|
||||
}
|
||||
} else if strings.Index(word, gt) == 0 && w == 0 {
|
||||
// word is at the beginning of a line, and is greentext
|
||||
isGreentext = true
|
||||
lineWords[w] = "<span class=\"greentext\">" + word
|
||||
}
|
||||
}
|
||||
line = strings.Join(lineWords, " ")
|
||||
if isGreentext {
|
||||
line += "</span>"
|
||||
}
|
||||
postLines[i] = line
|
||||
}
|
||||
return strings.Join(postLines, "<br />")
|
||||
}
|
||||
|
||||
func bannedForever(ban *BanInfo) bool {
|
||||
return ban.Permaban && !ban.CanAppeal && ban.Type == 3 && ban.Boards == ""
|
||||
}
|
||||
|
||||
func banHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
appealMsg := request.FormValue("appealmsg")
|
||||
banStatus, err := getBannedStatus(request)
|
||||
|
||||
if appealMsg != "" {
|
||||
if bannedForever(banStatus) {
|
||||
fmt.Fprint(writer, "No.")
|
||||
return
|
||||
}
|
||||
escapedMsg := html.EscapeString(appealMsg)
|
||||
if err = AddBanAppeal(banStatus.ID, escapedMsg); err != nil {
|
||||
serveErrorPage(writer, err.Error())
|
||||
}
|
||||
fmt.Fprint(writer,
|
||||
"Appeal sent. It will (hopefully) be read by a staff member. check "+config.SiteWebfolder+"banned occasionally for a response",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Error getting banned status:", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err = minifyTemplate(banpageTmpl, map[string]interface{}{
|
||||
"config": config, "ban": banStatus, "banBoards": banStatus.Boards, "post": Post{},
|
||||
}, writer, "text/html"); err != nil {
|
||||
serveErrorPage(writer, gclog.Print(lErrorLog,
|
||||
"Error minifying page template: ", err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
246
src/sql.go
246
src/sql.go
|
@ -1,246 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
const (
|
||||
mysqlDatetimeFormat = "2006-01-02 15:04:05"
|
||||
unsupportedSQLVersionMsg = `Received syntax error while preparing a SQL string.
|
||||
This means that either there is a bug in gochan's code (hopefully not) or that you are using an unsupported My/Postgre/SQLite version.
|
||||
Before reporting an error, make sure that you are using the up to date version of your selected SQL server.
|
||||
Error text: %s
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
db *sql.DB
|
||||
nilTimestamp string
|
||||
sqlReplacer *strings.Replacer // used during SQL string preparation
|
||||
)
|
||||
|
||||
func connectToSQLServer() {
|
||||
var err error
|
||||
var connStr string
|
||||
sqlReplacer = strings.NewReplacer(
|
||||
"DBNAME", config.DBname,
|
||||
"DBPREFIX", config.DBprefix,
|
||||
"\n", " ")
|
||||
gclog.Print(lStdLog|lErrorLog, "Initializing server...")
|
||||
|
||||
switch config.DBtype {
|
||||
case "mysql":
|
||||
connStr = fmt.Sprintf("%s:%s@%s/%s?parseTime=true&collation=utf8mb4_unicode_ci",
|
||||
config.DBusername, config.DBpassword, config.DBhost, config.DBname)
|
||||
nilTimestamp = "0000-00-00 00:00:00"
|
||||
case "postgres":
|
||||
connStr = fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable",
|
||||
config.DBusername, config.DBpassword, config.DBhost, config.DBname)
|
||||
nilTimestamp = "0001-01-01 00:00:00"
|
||||
case "sqlite3":
|
||||
gclog.Print(lErrorLog|lStdLog, "sqlite3 support is still flaky, consider using mysql or postgres")
|
||||
connStr = fmt.Sprintf("file:%s?mode=rwc&_auth&_auth_user=%s&_auth_pass=%s&cache=shared",
|
||||
config.DBhost, config.DBusername, config.DBpassword)
|
||||
nilTimestamp = "0001-01-01 00:00:00+00:00"
|
||||
default:
|
||||
gclog.Printf(lErrorLog|lStdLog|lFatal,
|
||||
`Invalid DBtype %q in gochan.json, valid values are "mysql", "postgres", and "sqlite3"`, config.DBtype)
|
||||
}
|
||||
|
||||
if db, err = sql.Open(config.DBtype, connStr); err != nil {
|
||||
gclog.Print(lErrorLog|lStdLog|lFatal, "Failed to connect to the database: ", err.Error())
|
||||
}
|
||||
|
||||
if err = initDB("initdb_" + config.DBtype + ".sql"); err != nil {
|
||||
gclog.Print(lErrorLog|lStdLog|lFatal, "Failed initializing DB: ", err.Error())
|
||||
}
|
||||
|
||||
//Not needed anymore
|
||||
// var truncateStr string
|
||||
// switch config.DBtype {
|
||||
// case "mysql":
|
||||
// fallthrough
|
||||
// case "postgres":
|
||||
// truncateStr = "TRUNCATE TABLE DBPREFIXsessions"
|
||||
// case "sqlite3":
|
||||
// truncateStr = "DELETE FROM DBPREFIXsessions"
|
||||
// }
|
||||
|
||||
// if _, err = execSQL(truncateStr); err != nil {
|
||||
// gclog.Print(lErrorLog|lStdLog|lFatal, "Failed initializing DB: ", err.Error())
|
||||
// }
|
||||
|
||||
// Create generic "Main" section if one doesn't already exist
|
||||
if err = CreateDefaultSectionIfNotExist(); err != nil {
|
||||
gclog.Print(lErrorLog|lStdLog|lFatal, "Failed initializing DB: ", err.Error())
|
||||
}
|
||||
|
||||
//TODO fix new install thing once it works with existing database
|
||||
// var sqlVersionStr string
|
||||
// isNewInstall := false
|
||||
// if err = queryRowSQL("SELECT value FROM DBPREFIXinfo WHERE name = 'version'",
|
||||
// []interface{}{}, []interface{}{&sqlVersionStr},
|
||||
// ); err == sql.ErrNoRows {
|
||||
// isNewInstall = true
|
||||
// } else if err != nil {
|
||||
// gclog.Print(lErrorLog|lStdLog|lFatal, "Failed initializing DB: ", err.Error())
|
||||
// }
|
||||
err = CreateDefaultBoardIfNoneExist()
|
||||
if err != nil {
|
||||
gclog.Print(lErrorLog|lStdLog|lFatal, "Failed creating default board: ", err.Error())
|
||||
}
|
||||
err = CreateDefaultAdminIfNoStaff()
|
||||
if err != nil {
|
||||
gclog.Print(lErrorLog|lStdLog|lFatal, "Failed creating default admin account: ", err.Error())
|
||||
}
|
||||
//fix versioning thing
|
||||
}
|
||||
|
||||
func initDB(initFile string) error {
|
||||
var err error
|
||||
filePath := findResource(initFile,
|
||||
"/usr/local/share/gochan/"+initFile,
|
||||
"/usr/share/gochan/"+initFile)
|
||||
if filePath == "" {
|
||||
return fmt.Errorf("SQL database initialization file (%s) missing. Please reinstall gochan", initFile)
|
||||
}
|
||||
|
||||
sqlBytes, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sqlStr := regexp.MustCompile("--.*\n?").ReplaceAllString(string(sqlBytes), " ")
|
||||
sqlArr := strings.Split(sqlReplacer.Replace(sqlStr), ";")
|
||||
|
||||
for _, statement := range sqlArr {
|
||||
if statement != "" && statement != " " {
|
||||
if _, err = db.Exec(statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checks to see if the given error is a syntax error (used for built-in strings)
|
||||
func sqlVersionErr(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errText := err.Error()
|
||||
switch config.DBtype {
|
||||
case "mysql":
|
||||
if !strings.Contains(errText, "You have an error in your SQL syntax") {
|
||||
return err
|
||||
}
|
||||
case "postgres":
|
||||
if !strings.Contains(errText, "syntax error at or near") {
|
||||
return err
|
||||
}
|
||||
case "sqlite3":
|
||||
if !strings.Contains(errText, "Error: near ") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf(unsupportedSQLVersionMsg, errText)
|
||||
}
|
||||
|
||||
// used for generating a prepared SQL statement formatted according to config.DBtype
|
||||
func prepareSQL(query string) (*sql.Stmt, error) {
|
||||
var preparedStr string
|
||||
switch config.DBtype {
|
||||
case "mysql":
|
||||
fallthrough
|
||||
case "sqlite3":
|
||||
preparedStr = query
|
||||
case "postgres":
|
||||
arr := strings.Split(query, "?")
|
||||
for i := range arr {
|
||||
if i == len(arr)-1 {
|
||||
break
|
||||
}
|
||||
arr[i] += fmt.Sprintf("$%d", i+1)
|
||||
}
|
||||
preparedStr = strings.Join(arr, "")
|
||||
}
|
||||
stmt, err := db.Prepare(sqlReplacer.Replace(preparedStr))
|
||||
return stmt, sqlVersionErr(err)
|
||||
}
|
||||
|
||||
/*
|
||||
* Automatically escapes the given values and caches the statement
|
||||
* Example:
|
||||
* var intVal int
|
||||
* var stringVal string
|
||||
* result, err := execSQL("INSERT INTO tablename (intval,stringval) VALUES(?,?)", intVal, stringVal)
|
||||
*/
|
||||
func execSQL(query string, values ...interface{}) (sql.Result, error) {
|
||||
stmt, err := prepareSQL(query)
|
||||
defer closeHandle(stmt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stmt.Exec(values...)
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets a row from the db with the values in values[] and fills the respective pointers in out[]
|
||||
* Automatically escapes the given values and caches the query
|
||||
* Example:
|
||||
* id := 32
|
||||
* var intVal int
|
||||
* var stringVal string
|
||||
* err := queryRowSQL("SELECT intval,stringval FROM table WHERE id = ?",
|
||||
* []interface{}{&id},
|
||||
* []interface{}{&intVal, &stringVal}
|
||||
* )
|
||||
*/
|
||||
func queryRowSQL(query string, values []interface{}, out []interface{}) error {
|
||||
stmt, err := prepareSQL(query)
|
||||
defer closeHandle(stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return stmt.QueryRow(values...).Scan(out...)
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets all rows from the db with the values in values[] and fills the respective pointers in out[]
|
||||
* Automatically escapes the given values and caches the query
|
||||
* Example:
|
||||
* rows, err := querySQL("SELECT * FROM table")
|
||||
* if err == nil {
|
||||
* for rows.Next() {
|
||||
* var intVal int
|
||||
* var stringVal string
|
||||
* rows.Scan(&intVal, &stringVal)
|
||||
* // do something with intVal and stringVal
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
func querySQL(query string, a ...interface{}) (*sql.Rows, error) {
|
||||
stmt, err := prepareSQL(query)
|
||||
defer closeHandle(stmt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stmt.Query(a...)
|
||||
}
|
||||
|
||||
func getSQLDateTime() string {
|
||||
return time.Now().Format(mysqlDatetimeFormat)
|
||||
}
|
||||
|
||||
func getSpecificSQLDateTime(t time.Time) string {
|
||||
return t.Format(mysqlDatetimeFormat)
|
||||
}
|
713
src/types.go
713
src/types.go
|
@ -1,713 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/frustra/bbcode"
|
||||
)
|
||||
|
||||
const (
|
||||
dirIsAFileStr = "unable to create \"%s\", path exists and is a file"
|
||||
pathExistsStr = "unable to create \"%s\", path already exists"
|
||||
genericErrStr = "unable to create \"%s\": %s"
|
||||
)
|
||||
|
||||
var (
|
||||
config GochanConfig
|
||||
readBannedIPs []string
|
||||
msgfmtr MsgFormatter
|
||||
version *GochanVersion
|
||||
)
|
||||
|
||||
type MsgFormatter struct {
|
||||
// Go's garbage collection does weird things with bbcode's internal tag map.
|
||||
// Moving the bbcode compiler isntance (and eventually a Markdown compiler) to a struct
|
||||
// appears to fix this
|
||||
bbCompiler bbcode.Compiler
|
||||
}
|
||||
|
||||
func (mf *MsgFormatter) InitBBcode() {
|
||||
if config.DisableBBcode {
|
||||
return
|
||||
}
|
||||
mf.bbCompiler = bbcode.NewCompiler(true, true)
|
||||
mf.bbCompiler.SetTag("center", nil)
|
||||
mf.bbCompiler.SetTag("code", nil)
|
||||
mf.bbCompiler.SetTag("color", nil)
|
||||
mf.bbCompiler.SetTag("img", nil)
|
||||
mf.bbCompiler.SetTag("quote", nil)
|
||||
mf.bbCompiler.SetTag("size", nil)
|
||||
}
|
||||
|
||||
func (mf *MsgFormatter) Compile(msg string) string {
|
||||
if config.DisableBBcode {
|
||||
return msg
|
||||
}
|
||||
return mf.bbCompiler.Compile(msg)
|
||||
}
|
||||
|
||||
type RecentPost struct {
|
||||
BoardName string
|
||||
BoardID int
|
||||
PostID int
|
||||
ParentID int
|
||||
Name string
|
||||
Tripcode string
|
||||
Message string
|
||||
Filename string
|
||||
ThumbW int
|
||||
ThumbH int
|
||||
IP string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func (p *RecentPost) GetURL(includeDomain bool) string {
|
||||
postURL := ""
|
||||
if includeDomain {
|
||||
postURL += config.SiteDomain
|
||||
}
|
||||
idStr := strconv.Itoa(p.PostID)
|
||||
postURL += config.SiteWebfolder + p.BoardName + "/res/"
|
||||
if p.ParentID == 0 {
|
||||
postURL += idStr + ".html#" + idStr
|
||||
} else {
|
||||
postURL += strconv.Itoa(p.ParentID) + ".html#" + idStr
|
||||
}
|
||||
return postURL
|
||||
}
|
||||
|
||||
type Thread struct {
|
||||
OP Post `json:"-"`
|
||||
NumReplies int `json:"replies"`
|
||||
NumImages int `json:"images"`
|
||||
OmittedPosts int `json:"omitted_posts"`
|
||||
OmittedImages int `json:"omitted_images"`
|
||||
BoardReplies []Post `json:"-"`
|
||||
Sticky int `json:"sticky"`
|
||||
Locked int `json:"locked"`
|
||||
ThreadPage int `json:"-"`
|
||||
}
|
||||
|
||||
// SQL Table structs
|
||||
|
||||
type Announcement struct {
|
||||
ID uint `json:"no"`
|
||||
Subject string `json:"sub"`
|
||||
Message string `json:"com"`
|
||||
Poster string `json:"name"`
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type BanAppeal struct {
|
||||
ID int
|
||||
Ban int
|
||||
Message string
|
||||
Denied bool
|
||||
StaffResponse string
|
||||
}
|
||||
|
||||
type BanInfo struct {
|
||||
ID uint
|
||||
AllowRead bool
|
||||
IP string
|
||||
Name string
|
||||
NameIsRegex bool
|
||||
SilentBan uint8
|
||||
Boards string
|
||||
Staff string
|
||||
Timestamp time.Time
|
||||
Expires time.Time
|
||||
Permaban bool
|
||||
Reason string
|
||||
Type int
|
||||
StaffNote string
|
||||
AppealAt time.Time
|
||||
CanAppeal bool
|
||||
}
|
||||
|
||||
type BannedHash struct {
|
||||
ID uint
|
||||
Checksum string
|
||||
Description string
|
||||
}
|
||||
|
||||
type Board struct {
|
||||
ID int `json:"-"`
|
||||
CurrentPage int `json:"-"`
|
||||
NumPages int `json:"pages"`
|
||||
ListOrder int `json:"-"`
|
||||
Dir string `json:"board"`
|
||||
Type int `json:"-"`
|
||||
UploadType int `json:"-"`
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"meta_description"`
|
||||
Description string `json:"-"`
|
||||
Section int `json:"-"`
|
||||
MaxFilesize int `json:"max_filesize"`
|
||||
MaxPages int `json:"max_pages"`
|
||||
DefaultStyle string `json:"-"`
|
||||
Locked bool `json:"is_archived"`
|
||||
CreatedOn time.Time `json:"-"`
|
||||
Anonymous string `json:"-"`
|
||||
ForcedAnon bool `json:"-"`
|
||||
MaxAge int `json:"-"`
|
||||
AutosageAfter int `json:"bump_limit"`
|
||||
NoImagesAfter int `json:"image_limit"`
|
||||
MaxMessageLength int `json:"max_comment_chars"`
|
||||
EmbedsAllowed bool `json:"-"`
|
||||
RedirectToThread bool `json:"-"`
|
||||
ShowID bool `json:"-"`
|
||||
RequireFile bool `json:"-"`
|
||||
EnableCatalog bool `json:"-"`
|
||||
EnableSpoileredImages bool `json:"-"`
|
||||
EnableSpoileredThreads bool `json:"-"`
|
||||
Worksafe bool `json:"ws_board"`
|
||||
ThreadPage int `json:"-"`
|
||||
Cooldowns BoardCooldowns `json:"cooldowns"`
|
||||
ThreadsPerPage int `json:"per_page"`
|
||||
}
|
||||
|
||||
// AbsolutePath returns the full filepath of the board directory
|
||||
func (board *Board) AbsolutePath(subpath ...string) string {
|
||||
return path.Join(config.DocumentRoot, board.Dir, path.Join(subpath...))
|
||||
}
|
||||
|
||||
// WebPath returns a string that represents the file's path as accessible by a browser
|
||||
// fileType should be "boardPage", "threadPage", "upload", or "thumb"
|
||||
func (board *Board) WebPath(fileName string, fileType string) string {
|
||||
var filePath string
|
||||
switch fileType {
|
||||
case "":
|
||||
fallthrough
|
||||
case "boardPage":
|
||||
filePath = path.Join(config.SiteWebfolder, board.Dir, fileName)
|
||||
case "threadPage":
|
||||
filePath = path.Join(config.SiteWebfolder, board.Dir, "res", fileName)
|
||||
case "upload":
|
||||
filePath = path.Join(config.SiteWebfolder, board.Dir, "src", fileName)
|
||||
case "thumb":
|
||||
filePath = path.Join(config.SiteWebfolder, board.Dir, "thumb", fileName)
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
||||
func (board *Board) PagePath(pageNum interface{}) string {
|
||||
var page string
|
||||
pageNumStr := fmt.Sprintf("%v", pageNum)
|
||||
if pageNumStr == "prev" {
|
||||
if board.CurrentPage < 2 {
|
||||
page = "1"
|
||||
} else {
|
||||
page = strconv.Itoa(board.CurrentPage - 1)
|
||||
}
|
||||
} else if pageNumStr == "next" {
|
||||
if board.CurrentPage >= board.NumPages {
|
||||
page = strconv.Itoa(board.NumPages)
|
||||
} else {
|
||||
page = strconv.Itoa(board.CurrentPage + 1)
|
||||
}
|
||||
} else {
|
||||
page = pageNumStr
|
||||
}
|
||||
return board.WebPath(page+".html", "boardPage")
|
||||
}
|
||||
|
||||
// Build builds the board and its thread files
|
||||
// if newBoard is true, it adds a row to DBPREFIXboards and fails if it exists
|
||||
// if force is true, it doesn't fail if the directories exist but does fail if it is a file
|
||||
func (board *Board) Build(newBoard bool, force bool) error {
|
||||
var err error
|
||||
if board.Dir == "" {
|
||||
return errors.New("board must have a directory before it is built")
|
||||
}
|
||||
if board.Title == "" {
|
||||
return errors.New("board must have a title before it is built")
|
||||
}
|
||||
|
||||
dirPath := board.AbsolutePath()
|
||||
resPath := board.AbsolutePath("res")
|
||||
srcPath := board.AbsolutePath("src")
|
||||
thumbPath := board.AbsolutePath("thumb")
|
||||
dirInfo, _ := os.Stat(dirPath)
|
||||
resInfo, _ := os.Stat(resPath)
|
||||
srcInfo, _ := os.Stat(srcPath)
|
||||
thumbInfo, _ := os.Stat(thumbPath)
|
||||
if dirInfo != nil {
|
||||
if !force {
|
||||
return fmt.Errorf(pathExistsStr, dirPath)
|
||||
}
|
||||
if !dirInfo.IsDir() {
|
||||
return fmt.Errorf(dirIsAFileStr, dirPath)
|
||||
}
|
||||
} else {
|
||||
if err = os.Mkdir(dirPath, 0666); err != nil {
|
||||
return fmt.Errorf(genericErrStr, dirPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if resInfo != nil {
|
||||
if !force {
|
||||
return fmt.Errorf(pathExistsStr, resPath)
|
||||
}
|
||||
if !resInfo.IsDir() {
|
||||
return fmt.Errorf(dirIsAFileStr, resPath)
|
||||
}
|
||||
} else {
|
||||
if err = os.Mkdir(resPath, 0666); err != nil {
|
||||
return fmt.Errorf(genericErrStr, resPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if srcInfo != nil {
|
||||
if !force {
|
||||
return fmt.Errorf(pathExistsStr, srcPath)
|
||||
}
|
||||
if !srcInfo.IsDir() {
|
||||
return fmt.Errorf(dirIsAFileStr, srcPath)
|
||||
}
|
||||
} else {
|
||||
if err = os.Mkdir(srcPath, 0666); err != nil {
|
||||
return fmt.Errorf(genericErrStr, srcPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if thumbInfo != nil {
|
||||
if !force {
|
||||
return fmt.Errorf(pathExistsStr, thumbPath)
|
||||
}
|
||||
if !thumbInfo.IsDir() {
|
||||
return fmt.Errorf(dirIsAFileStr, thumbPath)
|
||||
}
|
||||
} else {
|
||||
if err = os.Mkdir(thumbPath, 0666); err != nil {
|
||||
return fmt.Errorf(genericErrStr, thumbPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if newBoard {
|
||||
board.CreatedOn = time.Now()
|
||||
err := CreateBoard(board)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = board.UpdateID(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
buildBoardPages(board)
|
||||
buildThreads(true, board.ID, 0)
|
||||
resetBoardSectionArrays()
|
||||
buildFrontPage()
|
||||
if board.EnableCatalog {
|
||||
buildCatalog(board.ID)
|
||||
}
|
||||
buildBoardListJSON()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (board *Board) SetDefaults() {
|
||||
board.ListOrder = 0
|
||||
board.Section = 1
|
||||
board.MaxFilesize = 4096
|
||||
board.MaxPages = 11
|
||||
board.DefaultStyle = config.DefaultStyle
|
||||
board.Locked = false
|
||||
board.Anonymous = "Anonymous"
|
||||
board.ForcedAnon = false
|
||||
board.MaxAge = 0
|
||||
board.AutosageAfter = 200
|
||||
board.NoImagesAfter = 0
|
||||
board.MaxMessageLength = 8192
|
||||
board.EmbedsAllowed = true
|
||||
board.RedirectToThread = false
|
||||
board.ShowID = false
|
||||
board.RequireFile = false
|
||||
board.EnableCatalog = true
|
||||
board.EnableSpoileredImages = true
|
||||
board.EnableSpoileredThreads = true
|
||||
board.Worksafe = true
|
||||
board.ThreadsPerPage = 10
|
||||
}
|
||||
|
||||
type BoardSection struct {
|
||||
ID int
|
||||
ListOrder int
|
||||
Hidden bool
|
||||
Name string
|
||||
Abbreviation string
|
||||
}
|
||||
|
||||
// Post represents each post in the database
|
||||
type Post struct {
|
||||
ID int `json:"no"`
|
||||
ParentID int `json:"resto"`
|
||||
CurrentPage int `json:"-"`
|
||||
BoardID int `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Tripcode string `json:"trip"`
|
||||
Email string `json:"email"`
|
||||
Subject string `json:"sub"`
|
||||
MessageHTML string `json:"com"`
|
||||
MessageText string `json:"-"`
|
||||
Password string `json:"-"`
|
||||
Filename string `json:"tim"`
|
||||
FilenameOriginal string `json:"filename"`
|
||||
FileChecksum string `json:"md5"`
|
||||
FileExt string `json:"extension"`
|
||||
Filesize int `json:"fsize"`
|
||||
ImageW int `json:"w"`
|
||||
ImageH int `json:"h"`
|
||||
ThumbW int `json:"tn_w"`
|
||||
ThumbH int `json:"tn_h"`
|
||||
IP string `json:"-"`
|
||||
Capcode string `json:"capcode"`
|
||||
Timestamp time.Time `json:"time"`
|
||||
Autosage bool `json:"-"`
|
||||
DeletedTimestamp time.Time `json:"-"`
|
||||
Bumped time.Time `json:"last_modified"`
|
||||
Stickied bool `json:"-"`
|
||||
Locked bool `json:"-"`
|
||||
Reviewed bool `json:"-"`
|
||||
}
|
||||
|
||||
func (p *Post) GetURL(includeDomain bool) string {
|
||||
postURL := ""
|
||||
if includeDomain {
|
||||
postURL += config.SiteDomain
|
||||
}
|
||||
var board Board
|
||||
if err := board.PopulateData(p.BoardID); err != nil {
|
||||
return postURL
|
||||
}
|
||||
|
||||
idStr := strconv.Itoa(p.ID)
|
||||
postURL += config.SiteWebfolder + board.Dir + "/res/"
|
||||
if p.ParentID == 0 {
|
||||
postURL += idStr + ".html#" + idStr
|
||||
} else {
|
||||
postURL += strconv.Itoa(p.ParentID) + ".html#" + idStr
|
||||
}
|
||||
return postURL
|
||||
}
|
||||
|
||||
// Sanitize escapes HTML strings in a post. This should be run immediately before
|
||||
// the post is inserted into the database
|
||||
func (p *Post) Sanitize() {
|
||||
p.Name = html.EscapeString(p.Name)
|
||||
p.Email = html.EscapeString(p.Email)
|
||||
p.Subject = html.EscapeString(p.Subject)
|
||||
p.Password = html.EscapeString(p.Password)
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
ID uint
|
||||
Board string
|
||||
PostID uint
|
||||
Timestamp time.Time
|
||||
IP string
|
||||
Reason string
|
||||
Cleared bool
|
||||
IsTemp bool
|
||||
}
|
||||
|
||||
type LoginSession struct {
|
||||
ID uint
|
||||
Name string
|
||||
Data string
|
||||
Expires string
|
||||
}
|
||||
|
||||
// Staff represents a single staff member's info stored in the database
|
||||
type Staff struct {
|
||||
ID int
|
||||
Username string
|
||||
PasswordChecksum string
|
||||
Rank int
|
||||
Boards string
|
||||
AddedOn time.Time
|
||||
LastActive time.Time
|
||||
}
|
||||
|
||||
type WordFilter struct {
|
||||
ID int
|
||||
From string
|
||||
To string
|
||||
Boards string
|
||||
RegEx bool
|
||||
}
|
||||
|
||||
type BoardCooldowns struct {
|
||||
NewThread int `json:"threads"`
|
||||
Reply int `json:"replies"`
|
||||
ImageReply int `json:"images"`
|
||||
}
|
||||
|
||||
type Style struct {
|
||||
Name string
|
||||
Filename string
|
||||
}
|
||||
|
||||
// GochanConfig stores crucial info and is read from/written to gochan.json
|
||||
type GochanConfig struct {
|
||||
ListenIP string
|
||||
Port int
|
||||
FirstPage []string
|
||||
Username string
|
||||
UseFastCGI bool
|
||||
DebugMode bool `description:"Disables several spam/browser checks that can cause problems when hosting an instance locally."`
|
||||
|
||||
DocumentRoot string
|
||||
TemplateDir string
|
||||
LogDir string
|
||||
|
||||
DBtype string
|
||||
DBhost string
|
||||
DBname string
|
||||
DBusername string
|
||||
DBpassword string
|
||||
DBprefix string
|
||||
|
||||
Lockdown bool `description:"Disables posting." default:"unchecked"`
|
||||
LockdownMessage string `description:"Message displayed when someone tries to post while the site is on lockdown."`
|
||||
Sillytags []string `description:"List of randomly selected staff tags separated by line, e.g. <span style=\"color: red;\">## Mod</span>, to be randomly assigned to posts if UseSillytags is checked. Don't include the \"## \""`
|
||||
UseSillytags bool `description:"Use Sillytags" default:"unchecked"`
|
||||
Modboard string `description:"A super secret clubhouse board that only staff can view/post to." default:"staff"`
|
||||
|
||||
SiteName string `description:"The name of the site that appears in the header of the front page." default:"Gochan"`
|
||||
SiteSlogan string `description:"The text that appears below SiteName on the home page"`
|
||||
SiteHeaderURL string `description:"To be honest, I'm not even sure what this does. It'll probably be removed later."`
|
||||
SiteWebfolder string `description:"The HTTP root appearing in the browser (e.g. https://gochan.org/<SiteWebFolder>" default:"/"`
|
||||
SiteDomain string `description:"The server's domain (duh). Do not edit this unless you know what you are doing or BAD THINGS WILL HAPPEN!" default:"127.0.0.1" critical:"true"`
|
||||
DomainRegex string `description:"Regular expression used for incoming request validation. Do not edit this unless you know what you are doing or BAD THINGS WILL HAPPEN!" default:"(https|http):\\\\/\\\\/(gochan\\\\.lunachan\\.net|gochan\\\\.org)\\/(.*)" critical:"true"`
|
||||
|
||||
Styles []Style `description:"List of styles (one per line) that should be accessed online at <SiteWebFolder>/css/<Style>/"`
|
||||
DefaultStyle string `description:"Filename of the default Style. This should appear in the list above or bad things might happen."`
|
||||
|
||||
AllowDuplicateImages bool `description:"Disabling this will cause gochan to reject a post if the image has already been uploaded for another post.<br />This may end up being removed or being made board-specific in the future." default:"checked"`
|
||||
AllowVideoUploads bool `description:"Allows users to upload .webm videos. <br />This may end up being removed or being made board-specific in the future."`
|
||||
NewThreadDelay int `description:"The amount of time in seconds that is required before an IP can make a new thread.<br />This may end up being removed or being made board-specific in the future." default:"30"`
|
||||
ReplyDelay int `description:"Same as the above, but for replies." default:"7"`
|
||||
MaxLineLength int `description:"Any line in a post that exceeds this will be split into two (or more) lines.<br />I'm not really sure why this is here, so it may end up being removed." default:"150"`
|
||||
ReservedTrips []string `description:"Secure tripcodes (!!Something) can be reserved here.<br />Each reservation should go on its own line and should look like this:<br />TripPassword1##Tripcode1<br />TripPassword2##Tripcode2"`
|
||||
|
||||
ThumbWidth int `description:"OP thumbnails use this as their max width.<br />To keep the aspect ratio, the image will be scaled down to the ThumbWidth or ThumbHeight, whichever is larger." default:"200"`
|
||||
ThumbHeight int `description:"OP thumbnails use this as their max height.<br />To keep the aspect ratio, the image will be scaled down to the ThumbWidth or ThumbHeight, whichever is larger." default:"200"`
|
||||
ThumbWidth_reply int `description:"Same as ThumbWidth and ThumbHeight but for reply images." default:"125"`
|
||||
ThumbHeight_reply int `description:"Same as ThumbWidth and ThumbHeight but for reply images." default:"125"`
|
||||
ThumbWidth_catalog int `description:"Same as ThumbWidth and ThumbHeight but for catalog images." default:"50"`
|
||||
ThumbHeight_catalog int `description:"Same as ThumbWidth and ThumbHeight but for catalog images." default:"50"`
|
||||
|
||||
ThreadsPerPage int `default:"15"`
|
||||
RepliesOnBoardPage int `description:"Number of replies to a thread to show on the board page." default:"3"`
|
||||
StickyRepliesOnBoardPage int `description:"Same as above for stickied threads." default:"1"`
|
||||
BanColors []string `description:"Colors to be used for public ban messages (e.g. USER WAS BANNED FOR THIS POST).<br />Each entry should be on its own line, and should look something like this:<br />username1:#FF0000<br />username2:#FAF00F<br />username3:blue<br />Invalid entries/nonexistent usernames will show a warning and use the default red."`
|
||||
BanMsg string `description:"The default public ban message." default:"USER WAS BANNED FOR THIS POST"`
|
||||
EmbedWidth int `description:"The width for inline/expanded webm videos." default:"200"`
|
||||
EmbedHeight int `description:"The height for inline/expanded webm videos." default:"164"`
|
||||
ExpandButton bool `description:"If checked, adds [Embed] after a Youtube, Vimeo, etc link to toggle an inline video frame." default:"checked"`
|
||||
ImagesOpenNewTab bool `description:"If checked, thumbnails will open the respective image/video in a new tab instead of expanding them." default:"unchecked"`
|
||||
MakeURLsHyperlinked bool `description:"If checked, URLs in posts will be turned into a hyperlink. If unchecked, ExpandButton and NewTabOnOutlinks are ignored." default:"checked"`
|
||||
NewTabOnOutlinks bool `description:"If checked, links to external sites will open in a new tab." default:"checked"`
|
||||
DisableBBcode bool `description:"If checked, gochan will not compile bbcode into HTML" default:"unchecked"`
|
||||
|
||||
MinifyHTML bool `description:"If checked, gochan will minify html files when building" default:"checked"`
|
||||
MinifyJS bool `description:"If checked, gochan will minify js and json files when building" default:"checked"`
|
||||
UseMinifiedGochanJS bool `json:"-"`
|
||||
|
||||
DateTimeFormat string `description:"The format used for dates. See <a href=\"https://golang.org/pkg/time/#Time.Format\">here</a> for more info." default:"Mon, January 02, 2006 15:04 PM"`
|
||||
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
|
||||
UseCaptcha bool `description:"If checked, a captcha will be generated"`
|
||||
CaptchaWidth int `description:"Width of the generated captcha image" default:"240"`
|
||||
CaptchaHeight int `description:"Height of the generated captcha image" default:"80"`
|
||||
CaptchaMinutesExpire int `description:"Number of minutes before a user has to enter a new CAPTCHA before posting. If <1 they have to submit one for every post." default:"15"`
|
||||
EnableGeoIP bool `description:"If checked, this enables the usage of GeoIP for posts." default:"checked"`
|
||||
GeoIPDBlocation string `description:"Specifies the location of the GeoIP database file. If you're using CloudFlare, you can set it to cf to rely on CloudFlare for GeoIP information." default:"/usr/share/GeoIP/GeoIP.dat"`
|
||||
MaxRecentPosts int `description:"The maximum number of posts to show on the Recent Posts list on the front page." default:"3"`
|
||||
RecentPostsWithNoFile bool `description:"If checked, recent posts with no image/upload are shown on the front page (as well as those with images" default:"unchecked"`
|
||||
EnableAppeals bool `description:"If checked, allow banned users to appeal their bans.<br />This will likely be removed (permanently allowing appeals) or made board-specific in the future." default:"checked"`
|
||||
MaxLogDays int `description:"The maximum number of days to keep messages in the moderation/staff log file."`
|
||||
RandomSeed string `critical:"true"`
|
||||
TimeZone int `json:"-"`
|
||||
}
|
||||
|
||||
type MessagePostContainer struct {
|
||||
ID int
|
||||
MessageRaw string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (cfg *GochanConfig) CheckString(val, defaultVal string, critical bool, msg string) string {
|
||||
if val == "" {
|
||||
val = defaultVal
|
||||
flags := lStdLog | lErrorLog
|
||||
if critical {
|
||||
flags |= lFatal
|
||||
}
|
||||
gclog.Print(flags, msg)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (cfg *GochanConfig) CheckInt(val, defaultVal int, critical bool, msg string) int {
|
||||
if val == 0 {
|
||||
val = defaultVal
|
||||
flags := lStdLog | lErrorLog
|
||||
if critical {
|
||||
flags |= lFatal
|
||||
}
|
||||
gclog.Print(flags, msg)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
cfgPath := findResource("gochan.json", "/etc/gochan/gochan.json")
|
||||
if cfgPath == "" {
|
||||
fmt.Println("gochan.json not found")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
jfile, err := ioutil.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading %s: %s\n", cfgPath, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(jfile, &config); err != nil {
|
||||
fmt.Printf("Error parsing %s: %s\n", cfgPath, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config.LogDir = findResource(config.LogDir, "log", "/var/log/gochan/")
|
||||
if gclog, err = initLogs(
|
||||
path.Join(config.LogDir, "access.log"),
|
||||
path.Join(config.LogDir, "error.log"),
|
||||
path.Join(config.LogDir, "staff.log"),
|
||||
); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config.CheckString(config.ListenIP, "", true, "ListenIP not set in gochan.json, halting.")
|
||||
|
||||
if config.Port == 0 {
|
||||
config.Port = 80
|
||||
}
|
||||
|
||||
if len(config.FirstPage) == 0 {
|
||||
config.FirstPage = []string{"index.html", "board.html", "firstrun.html"}
|
||||
}
|
||||
|
||||
config.Username = config.CheckString(config.Username, "gochan", false, "Username not set in gochan.json, using 'gochan' as default")
|
||||
config.DocumentRoot = config.CheckString(config.DocumentRoot, "gochan", true, "DocumentRoot not set in gochan.json, halting.")
|
||||
|
||||
wd, wderr := os.Getwd()
|
||||
if wderr == nil {
|
||||
_, staterr := os.Stat(path.Join(wd, config.DocumentRoot, "css"))
|
||||
if staterr == nil {
|
||||
config.DocumentRoot = path.Join(wd, config.DocumentRoot)
|
||||
}
|
||||
}
|
||||
|
||||
config.TemplateDir = config.CheckString(
|
||||
findResource(config.TemplateDir, "templates", "/usr/local/share/gochan/templates/", "/usr/share/gochan/templates/"),
|
||||
"", true, "Unable to locate template directory, halting.")
|
||||
|
||||
config.CheckString(config.DBtype, "", true, "DBtype not set in gochan.json, halting (currently supported values: mysql,postgresql,sqlite3)")
|
||||
config.CheckString(config.DBhost, "", true, "DBhost not set in gochan.json, halting.")
|
||||
config.DBname = config.CheckString(config.DBname, "gochan", false,
|
||||
"DBname not set in gochan.json, setting to 'gochan'")
|
||||
|
||||
config.CheckString(config.DBusername, "", true, "DBusername not set in gochan, halting.")
|
||||
config.CheckString(config.DBpassword, "", true, "DBpassword not set in gochan, halting.")
|
||||
config.LockdownMessage = config.CheckString(config.LockdownMessage,
|
||||
"The administrator has temporarily disabled posting. We apologize for the inconvenience", false, "")
|
||||
|
||||
config.CheckString(config.SiteName, "", true, "SiteName not set in gochan.json, halting.")
|
||||
config.CheckString(config.SiteDomain, "", true, "SiteName not set in gochan.json, halting.")
|
||||
|
||||
if config.SiteWebfolder == "" {
|
||||
gclog.Print(lErrorLog|lStdLog, "SiteWebFolder not set in gochan.json, using / as default.")
|
||||
} else if string(config.SiteWebfolder[0]) != "/" {
|
||||
config.SiteWebfolder = "/" + config.SiteWebfolder
|
||||
}
|
||||
if config.SiteWebfolder[len(config.SiteWebfolder)-1:] != "/" {
|
||||
config.SiteWebfolder += "/"
|
||||
}
|
||||
config.CheckString(config.DomainRegex, "", true, `DomainRegex not set in gochan.json. Consider using something like "(https|http):\\/\\/(gochan\\.org)\\/(.*)"`)
|
||||
//config.DomainRegex = "(https|http):\\/\\/(" + config.SiteDomain + ")\\/(.*)"
|
||||
|
||||
if config.Styles == nil {
|
||||
gclog.Print(lErrorLog|lStdLog|lFatal, "Styles not set in gochan.json, halting.")
|
||||
}
|
||||
|
||||
config.DefaultStyle = config.CheckString(config.DefaultStyle, config.Styles[0].Filename, false, "")
|
||||
|
||||
config.NewThreadDelay = config.CheckInt(config.NewThreadDelay, 30, false, "")
|
||||
config.ReplyDelay = config.CheckInt(config.ReplyDelay, 7, false, "")
|
||||
config.MaxLineLength = config.CheckInt(config.MaxLineLength, 150, false, "")
|
||||
//ReservedTrips string //eventually this will be map[string]string
|
||||
|
||||
config.ThumbWidth = config.CheckInt(config.ThumbWidth, 200, false, "")
|
||||
config.ThumbHeight = config.CheckInt(config.ThumbHeight, 200, false, "")
|
||||
config.ThumbWidth_reply = config.CheckInt(config.ThumbWidth_reply, 125, false, "")
|
||||
config.ThumbHeight_reply = config.CheckInt(config.ThumbHeight_reply, 125, false, "")
|
||||
config.ThumbWidth_catalog = config.CheckInt(config.ThumbWidth_catalog, 50, false, "")
|
||||
config.ThumbHeight_catalog = config.CheckInt(config.ThumbHeight_catalog, 50, false, "")
|
||||
|
||||
config.ThreadsPerPage = config.CheckInt(config.ThreadsPerPage, 10, false, "")
|
||||
config.RepliesOnBoardPage = config.CheckInt(config.RepliesOnBoardPage, 3, false, "")
|
||||
config.StickyRepliesOnBoardPage = config.CheckInt(config.StickyRepliesOnBoardPage, 1, false, "")
|
||||
|
||||
/*config.BanColors, err = c.GetString("threads", "ban_colors") //eventually this will be map[string] string
|
||||
if err != nil {
|
||||
config.BanColors = "admin:#CC0000"
|
||||
}*/
|
||||
|
||||
config.BanMsg = config.CheckString(config.BanMsg, "(USER WAS BANNED FOR THIS POST)", false, "")
|
||||
config.DateTimeFormat = config.CheckString(config.DateTimeFormat, "Mon, January 02, 2006 15:04 PM", false, "")
|
||||
|
||||
config.CaptchaWidth = config.CheckInt(config.CaptchaWidth, 240, false, "")
|
||||
config.CaptchaHeight = config.CheckInt(config.CaptchaHeight, 80, false, "")
|
||||
|
||||
if config.EnableGeoIP {
|
||||
if config.GeoIPDBlocation == "" {
|
||||
gclog.Print(lErrorLog|lStdLog, "GeoIPDBlocation not set in gochan.json, disabling EnableGeoIP")
|
||||
config.EnableGeoIP = false
|
||||
}
|
||||
}
|
||||
|
||||
if config.MaxLogDays == 0 {
|
||||
config.MaxLogDays = 15
|
||||
}
|
||||
|
||||
if config.RandomSeed == "" {
|
||||
gclog.Print(lErrorLog|lStdLog, "RandomSeed not set in gochan.json, Generating a random one.")
|
||||
for i := 0; i < 8; i++ {
|
||||
num := rand.Intn(127-32) + 32
|
||||
config.RandomSeed += fmt.Sprintf("%c", num)
|
||||
}
|
||||
configJSON, _ := json.MarshalIndent(config, "", "\t")
|
||||
if err = ioutil.WriteFile(cfgPath, configJSON, 0777); err != nil {
|
||||
gclog.Printf(lErrorLog|lStdLog|lFatal, "Unable to write %s with randomly generated seed: %s", cfgPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
_, zoneOffset := time.Now().Zone()
|
||||
config.TimeZone = zoneOffset / 60 / 60
|
||||
|
||||
msgfmtr.InitBBcode()
|
||||
|
||||
version = ParseVersion(versionStr)
|
||||
version.Normalize()
|
||||
}
|
367
src/util.go
367
src/util.go
|
@ -1,367 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
libgeo "github.com/nranchev/go-libGeoIP"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
errEmptyDurationString = errors.New("Empty Duration string")
|
||||
errInvalidDurationString = errors.New("Invalid Duration string")
|
||||
durationRegexp = regexp.MustCompile(`^((\d+)\s?ye?a?r?s?)?\s?((\d+)\s?mon?t?h?s?)?\s?((\d+)\s?we?e?k?s?)?\s?((\d+)\s?da?y?s?)?\s?((\d+)\s?ho?u?r?s?)?\s?((\d+)\s?mi?n?u?t?e?s?)?\s?((\d+)\s?s?e?c?o?n?d?s?)?$`)
|
||||
)
|
||||
|
||||
func arrToString(arr []string) string {
|
||||
var out string
|
||||
for i, val := range arr {
|
||||
out += val
|
||||
if i < len(arr)-1 {
|
||||
out += ","
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func md5Sum(str string) string {
|
||||
hash := md5.New()
|
||||
io.WriteString(hash, str)
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
|
||||
func sha1Sum(str string) string {
|
||||
hash := sha1.New()
|
||||
io.WriteString(hash, str)
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
|
||||
func bcryptSum(str string) string {
|
||||
digest, err := bcrypt.GenerateFromPassword([]byte(str), 4)
|
||||
if err == nil {
|
||||
return string(digest)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func byteByByteReplace(input, from, to string) string {
|
||||
if len(from) != len(to) {
|
||||
return ""
|
||||
}
|
||||
for i := 0; i < len(from); i++ {
|
||||
input = strings.Replace(input, from[i:i+1], to[i:i+1], -1)
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func closeHandle(handle io.Closer) {
|
||||
if handle != nil {
|
||||
handle.Close()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Deletes files in a folder (root) that match a given regular expression.
|
||||
* Returns the number of files that were deleted, and any error encountered.
|
||||
*/
|
||||
func deleteMatchingFiles(root, match string) (filesDeleted int, err error) {
|
||||
files, err := ioutil.ReadDir(root)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, f := range files {
|
||||
match, _ := regexp.MatchString(match, f.Name())
|
||||
if match {
|
||||
os.Remove(filepath.Join(root, f.Name()))
|
||||
filesDeleted++
|
||||
}
|
||||
}
|
||||
return filesDeleted, err
|
||||
}
|
||||
|
||||
func getCountryCode(ip string) (string, error) {
|
||||
if config.EnableGeoIP && config.GeoIPDBlocation != "" {
|
||||
gi, err := libgeo.Load(config.GeoIPDBlocation)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return gi.GetLocationByIP(ip).CountryCode, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func randomString(length int) string {
|
||||
var str string
|
||||
for i := 0; i < length; i++ {
|
||||
num := rand.Intn(127)
|
||||
if num < 32 {
|
||||
num += 32
|
||||
}
|
||||
str += string(num)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func getFileExtension(filename string) (extension string) {
|
||||
if !strings.Contains(filename, ".") {
|
||||
extension = ""
|
||||
} else {
|
||||
extension = filename[strings.LastIndex(filename, ".")+1:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getFormattedFilesize(size float64) string {
|
||||
if size < 1000 {
|
||||
return fmt.Sprintf("%dB", int(size))
|
||||
} else if size <= 100000 {
|
||||
return fmt.Sprintf("%fKB", size/1024)
|
||||
} else if size <= 100000000 {
|
||||
return fmt.Sprintf("%fMB", size/1024.0/1024.0)
|
||||
}
|
||||
return fmt.Sprintf("%0.2fGB", size/1024.0/1024.0/1024.0)
|
||||
}
|
||||
|
||||
func humanReadableTime(t time.Time) string {
|
||||
return t.Format(config.DateTimeFormat)
|
||||
}
|
||||
|
||||
func getThumbnailPath(thumbType string, img string) string {
|
||||
filetype := strings.ToLower(img[strings.LastIndex(img, ".")+1:])
|
||||
if filetype == "gif" || filetype == "webm" {
|
||||
filetype = "jpg"
|
||||
}
|
||||
index := strings.LastIndex(img, ".")
|
||||
if index < 0 || index > len(img) {
|
||||
return ""
|
||||
}
|
||||
thumbSuffix := "t." + filetype
|
||||
if thumbType == "catalog" {
|
||||
thumbSuffix = "c." + filetype
|
||||
}
|
||||
return img[0:index] + thumbSuffix
|
||||
}
|
||||
|
||||
// findResource searches for a file in the given paths and returns the first one it finds
|
||||
// or a blank string if none of the paths exist
|
||||
func findResource(paths ...string) string {
|
||||
var err error
|
||||
for _, filepath := range paths {
|
||||
if _, err = os.Stat(filepath); err == nil {
|
||||
return filepath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// paginate returns a 2d array of a specified interface from a 1d array passed in,
|
||||
// with a specified number of values per array in the 2d array.
|
||||
// interfaceLength is the number of interfaces per array in the 2d array (e.g, threads per page)
|
||||
// interf is the array of interfaces to be split up.
|
||||
func paginate(interfaceLength int, interf []interface{}) [][]interface{} {
|
||||
// paginatedInterfaces = the finished interface array
|
||||
// numArrays = the current number of arrays (before remainder overflow)
|
||||
// interfacesRemaining = if greater than 0, these are the remaining interfaces
|
||||
// that will be added to the super-interface
|
||||
|
||||
var paginatedInterfaces [][]interface{}
|
||||
numArrays := len(interf) / interfaceLength
|
||||
interfacesRemaining := len(interf) % interfaceLength
|
||||
currentInterface := 0
|
||||
for l := 0; l < numArrays; l++ {
|
||||
paginatedInterfaces = append(paginatedInterfaces,
|
||||
interf[currentInterface:currentInterface+interfaceLength])
|
||||
currentInterface += interfaceLength
|
||||
}
|
||||
if interfacesRemaining > 0 {
|
||||
paginatedInterfaces = append(paginatedInterfaces, interf[len(interf)-interfacesRemaining:])
|
||||
}
|
||||
return paginatedInterfaces
|
||||
}
|
||||
|
||||
func resetBoardSectionArrays() {
|
||||
// run when the board list needs to be changed (board/section is added, deleted, etc)
|
||||
allBoards = nil
|
||||
allSections = nil
|
||||
|
||||
allBoardsArr, _ := GetAllBoards()
|
||||
allBoards = append(allBoards, allBoardsArr...)
|
||||
|
||||
allSectionsArr, _ := GetAllSections()
|
||||
allSections = append(allSections, allSectionsArr...)
|
||||
}
|
||||
|
||||
func searchStrings(item string, arr []string, permissive bool) int {
|
||||
for i, str := range arr {
|
||||
if item == str {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Checks the validity of the Akismet API key given in the config file.
|
||||
func checkAkismetAPIKey(key string) error {
|
||||
if key == "" {
|
||||
return errors.New("blank key given, Akismet spam checking won't be used")
|
||||
}
|
||||
resp, err := http.PostForm("https://rest.akismet.com/1.1/verify-key", url.Values{"key": {key}, "blog": {"http://" + config.SiteDomain}})
|
||||
if resp != nil {
|
||||
defer closeHandle(resp.Body)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(body) == "invalid" {
|
||||
// This should disable the Akismet checks if the API key is not valid.
|
||||
errmsg := "Akismet API key is invalid, Akismet spam protection will be disabled."
|
||||
gclog.Print(lErrorLog, errmsg)
|
||||
return errors.New(errmsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checks a given post for spam with Akismet. Only checks if Akismet API key is set.
|
||||
func checkPostForSpam(userIP string, userAgent string, referrer string,
|
||||
author string, email string, postContent string) string {
|
||||
if config.AkismetAPIKey != "" {
|
||||
client := &http.Client{}
|
||||
data := url.Values{"blog": {"http://" + config.SiteDomain}, "user_ip": {userIP}, "user_agent": {userAgent}, "referrer": {referrer},
|
||||
"comment_type": {"forum-post"}, "comment_author": {author}, "comment_author_email": {email},
|
||||
"comment_content": {postContent}}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://"+config.AkismetAPIKey+".rest.akismet.com/1.1/comment-check",
|
||||
strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
gclog.Print(lErrorLog, err.Error())
|
||||
return "other_failure"
|
||||
}
|
||||
req.Header.Set("User-Agent", "gochan/1.0 | Akismet/0.1")
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err := client.Do(req)
|
||||
if resp != nil {
|
||||
closeHandle(resp.Body)
|
||||
}
|
||||
if err != nil {
|
||||
gclog.Print(lErrorLog, err.Error())
|
||||
return "other_failure"
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
gclog.Print(lErrorLog, err.Error())
|
||||
return "other_failure"
|
||||
}
|
||||
gclog.Print(lErrorLog, "Response from Akismet: ", string(body))
|
||||
|
||||
if string(body) == "true" {
|
||||
if proTip, ok := resp.Header["X-akismet-pro-tip"]; ok && proTip[0] == "discard" {
|
||||
return "discard"
|
||||
}
|
||||
return "spam"
|
||||
} else if string(body) == "invalid" {
|
||||
return "invalid"
|
||||
} else if string(body) == "false" {
|
||||
return "ham"
|
||||
}
|
||||
}
|
||||
return "other_failure"
|
||||
}
|
||||
|
||||
func marshalJSON(data interface{}, indent bool) (string, error) {
|
||||
var jsonBytes []byte
|
||||
var err error
|
||||
|
||||
if indent {
|
||||
jsonBytes, err = json.MarshalIndent(data, "", " ")
|
||||
} else {
|
||||
jsonBytes, err = json.Marshal(data)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
jsonBytes, _ = json.Marshal(map[string]string{"error": err.Error()})
|
||||
}
|
||||
return string(jsonBytes), err
|
||||
}
|
||||
|
||||
func limitArraySize(arr []string, maxSize int) []string {
|
||||
if maxSize > len(arr)-1 || maxSize < 0 {
|
||||
return arr
|
||||
}
|
||||
return arr[:maxSize]
|
||||
}
|
||||
|
||||
func numReplies(boardid, threadid int) int {
|
||||
num, err := GetReplyCount(threadid)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
// based on TinyBoard's parse_time function
|
||||
func parseDurationString(str string) (time.Duration, error) {
|
||||
if str == "" {
|
||||
return 0, errEmptyDurationString
|
||||
}
|
||||
|
||||
matches := durationRegexp.FindAllStringSubmatch(str, -1)
|
||||
if len(matches) == 0 {
|
||||
return 0, errInvalidDurationString
|
||||
}
|
||||
|
||||
var expire int
|
||||
if matches[0][2] != "" {
|
||||
years, _ := strconv.Atoi(matches[0][2])
|
||||
expire += years * 60 * 60 * 24 * 365
|
||||
}
|
||||
if matches[0][4] != "" {
|
||||
months, _ := strconv.Atoi(matches[0][4])
|
||||
expire += months * 60 * 60 * 24 * 30
|
||||
}
|
||||
if matches[0][6] != "" {
|
||||
weeks, _ := strconv.Atoi(matches[0][6])
|
||||
expire += weeks * 60 * 60 * 24 * 7
|
||||
}
|
||||
if matches[0][8] != "" {
|
||||
days, _ := strconv.Atoi(matches[0][8])
|
||||
expire += days * 60 * 60 * 24
|
||||
}
|
||||
if matches[0][10] != "" {
|
||||
hours, _ := strconv.Atoi(matches[0][10])
|
||||
expire += hours * 60 * 60
|
||||
}
|
||||
if matches[0][12] != "" {
|
||||
minutes, _ := strconv.Atoi(matches[0][12])
|
||||
expire += minutes * 60
|
||||
}
|
||||
if matches[0][14] != "" {
|
||||
seconds, _ := strconv.Atoi(matches[0][14])
|
||||
expire += seconds
|
||||
}
|
||||
return time.ParseDuration(strconv.Itoa(expire) + "s")
|
||||
}
|
||||
|
||||
func timezone() int {
|
||||
_, offset := time.Now().Zone()
|
||||
return offset / 60 / 60
|
||||
}
|
2
vagrant/Vagrantfile
vendored
2
vagrant/Vagrantfile
vendored
|
@ -21,7 +21,7 @@ Vagrant.configure("2") do |config|
|
|||
|
||||
config.vm.provision :shell, path: "bootstrap.sh", env: {
|
||||
:DBTYPE => DBTYPE,
|
||||
:GOPATH => "/vagrant/lib",
|
||||
:GOPATH => "/home/vagrant/go",
|
||||
:FROMDOCKER => ""
|
||||
}, args: "install"
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ if [ -z "$DBTYPE" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
add-apt-repository -y ppa:gophers/archive
|
||||
apt-get -y update && apt-get -y upgrade
|
||||
|
||||
if [ "$DBTYPE" == "mysql" ]; then
|
||||
|
@ -63,11 +64,9 @@ else
|
|||
exit 1
|
||||
fi
|
||||
|
||||
apt-get -y install git subversion mercurial nginx ffmpeg golang-1.10
|
||||
mkdir -p /root/bin
|
||||
ln -s /usr/lib/go-1.10/bin/* /root/bin/
|
||||
export PATH=$PATH:/root/bin
|
||||
echo "export PATH=$PATH:/root/bin" >> /root/.bashrc
|
||||
apt-get -y install git subversion mercurial nginx ffmpeg make golang-1.11
|
||||
|
||||
ln -s /usr/lib/go-1.11/bin/* /usr/local/bin/
|
||||
|
||||
rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/*
|
||||
ln -sf /vagrant/sample-configs/gochan-fastcgi.nginx /etc/nginx/sites-available/gochan.nginx
|
||||
|
@ -90,7 +89,6 @@ cp /vagrant/sample-configs/gochan.example.json /etc/gochan/gochan.json
|
|||
sed -i /etc/gochan/gochan.json \
|
||||
-e 's/"Port": 8080/"Port": 9000/' \
|
||||
-e 's/"UseFastCGI": false/"UseFastCGI": true/' \
|
||||
-e 's/"DomainRegex": ".*"/"DomainRegex": "(https|http):\\\/\\\/(.*)\\\/(.*)"/' \
|
||||
-e 's#"DocumentRoot": "html"#"DocumentRoot": "/srv/gochan"#' \
|
||||
-e 's#"TemplateDir": "templates"#"TemplateDir": "/usr/local/share/gochan/templates"#' \
|
||||
-e 's#"LogDir": "log"#"LogDir": "/var/log/gochan"#' \
|
||||
|
@ -124,27 +122,22 @@ EOF
|
|||
chmod +x /home/vagrant/dbconnect.sh
|
||||
|
||||
cat <<EOF >>/home/vagrant/.bashrc
|
||||
export PATH=$PATH:/home/vagrant/bin
|
||||
export DBTYPE=$DBTYPE
|
||||
export GOPATH=/vagrant/lib
|
||||
export GOPATH=/home/vagrant/go
|
||||
EOF
|
||||
|
||||
cat <<EOF >>/root.bashrc
|
||||
export GOPATH=/vagrant/lib
|
||||
EOF
|
||||
export GOPATH=/vagrant/lib
|
||||
|
||||
ln -s /usr/lib/go-1.10/bin/* /usr/local/bin/
|
||||
cd /vagrant
|
||||
su - vagrant <<EOF
|
||||
mkdir -p /vagrant/lib
|
||||
mkdir -p /home/vagrant/go
|
||||
source /home/vagrant/.bashrc
|
||||
export GOPATH=/vagrant/lib
|
||||
cd /vagrant
|
||||
cd /vagrant/devtools
|
||||
python build_initdb.py
|
||||
cd ..
|
||||
mkdir -p $GOPATH/src/github.com/gochan-org/gochan
|
||||
cp -r pkg $GOPATH/src/github.com/gochan-org/gochan
|
||||
make dependencies
|
||||
make
|
||||
EOF
|
||||
make install
|
||||
# make install
|
||||
|
||||
# if [ -d /lib/systemd ]; then
|
||||
# systemctl start gochan.service
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue