1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-09-16 07:56:24 -07:00

Replace JSON struct wrappers with struct tags

move (My)SQL initialization stuff to sql script to be run every time
This commit is contained in:
Eggbertx 2019-05-14 15:58:52 -07:00
parent bb9cb5a96c
commit fa72398c0c
16 changed files with 438 additions and 694 deletions

View file

@ -1,24 +0,0 @@
# Gochan Development Roadmap
This is a rough roadmap to map out what I plan to focus on with the versions
1.x
----
* Improve posting stability (done)
* Add management functions to make things simpler
* Add some kind of database schema to handle any possible changes in the database structure with new versions (if there are any)
* Add PostgreSQL (and possibly SQLite) support
* Add functionality to aid new admins in transferring boards from other imageboard systems (TinyBoard, Kusaba, etc) to Gochan. This would largely be a database issue (see above)
* Add a manage function to modify the configuration without having to directly modify gochan.json directly (mostly done)
2.x
----
* Add functionality to go above and beyond what you would expect from an imageboard, without worrying about feature creep. Go's speed would allow us to do this without causing Gochan to slow down
* Add a plugin system, likely using Lua to make it lightweight but still powerful. Go's package system would make this pretty easy and straightforward
* Add a mange function to make it easier to update gochan. Not just the database schema but with the system in general.
3.x
----
* ???
* I'll figure this out when we get to 2.x but feel free to put suggestions in the [issue page](https://github.com/Eggbertx/gochan/issues), or make a post at http://gochan.org/

View file

@ -7,6 +7,6 @@
<h1>404: File not found</h1>
<img src="/error/lol 404.gif" border="0" alt="">
<p>The requested file could not be found on this server. Are you just typing random stuff in the address bar? If you followed a link from this site here, then post <a href="/site">here</a></p>
<hr><address>http://gochan.org powered by Gochan v2.6.1</address>
<hr><address>http://gochan.org powered by Gochan v2.7.0</address>
</body>
</html>

View file

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

View file

@ -1,7 +1,5 @@
-- Initial setup file for Gochan
-- Turn off warnings in case tables are already there.
SET sql_notes=0;
-- Gochan MySQL startup/update script
-- DO NOT DELETE
CREATE TABLE IF NOT EXISTS `DBPREFIXannouncements` (
`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
@ -42,6 +40,18 @@ CREATE TABLE IF NOT EXISTS `DBPREFIXbanlist` (
`can_appeal` TINYINT(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
ALTER TABLE `DBPREFIXbanlist`
CHANGE IF EXISTS `banned_by` `staff` VARCHAR(50) NOT NULL,
CHANGE IF EXISTS `id` `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
CHANGE IF EXISTS `expires` `expires` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHANGE IF EXISTS `boards` `boards` VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS `type` TINYINT UNSIGNED NOT NULL DEFAULT '3',
ADD COLUMN IF NOT EXISTS `name_is_regex` TINYINT(1) DEFAULT '0',
ADD COLUMN IF NOT EXISTS `filename` VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS `file_checksum` VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS `permaban` TINYINT(1) DEFAULT '0',
ADD COLUMN IF NOT EXISTS `can_appeal` TINYINT(1) DEFAULT '1',
DROP COLUMN IF EXISTS `message`;
CREATE TABLE IF NOT EXISTS `DBPREFIXbannedhashes` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
@ -62,7 +72,6 @@ CREATE TABLE IF NOT EXISTS `DBPREFIXboards` (
`section` VARCHAR(45) NOT NULL,
`max_image_size` INT UNSIGNED NOT NULL DEFAULT 4718592,
`max_pages` TINYINT UNSIGNED NOT NULL DEFAULT 11,
`locale` VARCHAR(10) NOT NULL DEFAULT 'en-us',
`default_style` VARCHAR(45) NOT NULL,
`locked` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
`created_on` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -79,6 +88,8 @@ CREATE TABLE IF NOT EXISTS `DBPREFIXboards` (
PRIMARY KEY (`id`),
UNIQUE (`dir`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=0;
ALTER TABLE `DBPREFIXboards`
DROP COLUMN IF EXISTS `locale`;
CREATE TABLE IF NOT EXISTS `DBPREFIXembeds` (
`id` TINYINT UNSIGNED NOT NULL AUTO_INCREMENT,
@ -163,7 +174,6 @@ CREATE TABLE IF NOT EXISTS `DBPREFIXposts` (
`tag` VARCHAR(5) NOT NULL DEFAULT '',
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`autosage` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
`poster_authority` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
`deleted_timestamp` TIMESTAMP,
`bumped` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`stickied` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
@ -176,6 +186,9 @@ CREATE TABLE IF NOT EXISTS `DBPREFIXposts` (
KEY `file_checksum` (`file_checksum`),
KEY `stickied` (`stickied`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;
ALTER TABLE `DBPREFIXposts`
DROP COLUMN IF EXISTS `sillytag`,
DROP COLUMN IF EXISTS `poster_authority`;
CREATE TABLE IF NOT EXISTS `DBPREFIXreports` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
@ -219,12 +232,6 @@ CREATE TABLE IF NOT EXISTS `DBPREFIXstaff` (
UNIQUE (`username`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
-- create a temp table with the same columns as the posts table to be stored in memory
-- This is currently not used, and commented out.
-- CREATE TABLE IF NOT EXISTS `DBPREFIXtempposts` SELECT * FROM DBPREFIXposts;
-- ALTER TABLE `DBPREFIXtempposts` CHANGE `message` `message` VARCHAR(1024);
-- ALTER TABLE `DBPREFIXtempposts` ENGINE=MEMORY;
CREATE TABLE IF NOT EXISTS `DBPREFIXwordfilters` (
`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
`from` VARCHAR(75) NOT NULL,

45
src/api_test.go Normal file
View file

@ -0,0 +1,45 @@
package main
import (
"fmt"
"testing"
)
type Color struct {
Red int `json:"red"`
Green int `json:"green"`
Blue int `json:"blue"`
}
func TestAPI(t *testing.T) {
var api string
var err error
if api, err = marshalAPI("colorsSlice", []Color{
Color{255, 0, 0},
Color{0, 255, 0},
Color{0, 0, 255},
}, true); err != nil {
t.Fatal(err.Error())
}
fmt.Println("API slice: " + api)
if api, err = marshalAPI("colorsMap", map[string]Color{
"red": Color{255, 0, 0},
"green": Color{0, 255, 0},
"blue": Color{0, 0, 255},
}, true); err != nil {
t.Fatal(err.Error())
}
fmt.Println("API map: " + api)
if api, err = marshalAPI("color", Color{255, 0, 0}, true); err != nil {
t.Fatal(err.Error())
}
fmt.Println("API struct: " + api)
if api, err = marshalAPI("error", "Some error", false); err != nil {
t.Fatal(err.Error())
}
fmt.Println("API string: " + api)
}

View file

@ -19,7 +19,7 @@ func buildFrontPage() (html string) {
os.Remove(path.Join(config.DocumentRoot, "index.html"))
front_file, err := os.OpenFile(path.Join(config.DocumentRoot, "index.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
defer closeFile(front_file)
defer closeHandle(front_file)
if err != nil {
return handleError(1, "Failed opening front page for writing: "+err.Error()) + "<br />\n"
}
@ -39,7 +39,7 @@ func buildFrontPage() (html string) {
"ORDER BY `timestamp` DESC LIMIT ?"
rows, err := querySQL(recentQueryStr, nilTimestamp, config.MaxRecentPosts)
defer closeRows(rows)
defer closeHandle(rows)
if err != nil {
return handleError(1, err.Error())
}
@ -55,8 +55,7 @@ func buildFrontPage() (html string) {
recentPostsArr = append(recentPostsArr, recentPost)
}
for i := range allBoards {
board := allBoards[i].(BoardsTable)
for _, board := range allBoards {
if board.Section == 0 {
board.Section = 1
}
@ -75,29 +74,24 @@ func buildFrontPage() (html string) {
func buildBoardListJSON() (html string) {
board_list_file, err := os.OpenFile(path.Join(config.DocumentRoot, "boards.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
defer closeFile(board_list_file)
defer closeHandle(board_list_file)
if err != nil {
return handleError(1, "Failed opening board.json for writing: "+err.Error()) + "<br />\n"
}
board_list_wrapper := new(BoardJSONWrapper)
// Our cooldowns are site-wide currently.
cooldowns_obj := BoardCooldowns{NewThread: config.NewThreadDelay, Reply: config.ReplyDelay, ImageReply: config.ReplyDelay}
for _, board_int := range allBoards {
board := board_int.(BoardsTable)
board_obj := BoardJSON{BoardName: board.Dir, Title: board.Title, WorkSafeBoard: 1,
ThreadsPerPage: config.ThreadsPerPage, Pages: board.MaxPages, MaxFilesize: board.MaxImageSize,
MaxMessageLength: board.MaxMessageLength, BumpLimit: 200, ImageLimit: board.NoImagesAfter,
Cooldowns: cooldowns_obj, Description: board.Description, IsArchived: 0}
if board.EnableNSFW {
board_obj.WorkSafeBoard = 0
}
board_list_wrapper.Boards = append(board_list_wrapper.Boards, board_obj)
boardsMap := map[string][]Board{
"boards": []Board{},
}
boardJSON, err := json.Marshal(board_list_wrapper)
// 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 handleError(1, "Failed marshal to JSON: "+err.Error()) + "<br />\n"
}
@ -108,9 +102,9 @@ func buildBoardListJSON() (html string) {
}
// buildBoardPages builds the pages for the board archive.
// `board` is a BoardsTable object representing the board to build archive pages for.
// `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 *BoardsTable) (html string) {
func buildBoardPages(board *Board) (html string) {
start_time := benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), time.Now(), true)
var current_page_file *os.File
var threads []interface{}
@ -148,7 +142,7 @@ func buildBoardPages(board *BoardsTable) (html string) {
// For each top level post, start building a Thread struct
for _, op := range op_posts {
var thread Thread
var posts_in_thread []PostTable
var posts_in_thread []Post
// Get the number of replies to this thread.
if err = queryRowSQL("SELECT COUNT(*) FROM `"+config.DBprefix+"posts` WHERE `boardid` = ? AND `parentid` = ? AND `deleted_timestamp` = ?",
@ -188,7 +182,7 @@ func buildBoardPages(board *BoardsTable) (html string) {
html += err.Error() + "<br />"
}
var reversedPosts []PostTable
var reversedPosts []Post
for i := len(posts_in_thread); i > 0; i-- {
reversedPosts = append(reversedPosts, posts_in_thread[i-1])
}
@ -254,10 +248,10 @@ func buildBoardPages(board *BoardsTable) (html string) {
board.NumPages = len(thread_pages) - 1
// Create array of page wrapper objects, and open the file.
var pages_obj []BoardPageJSON
pagesArr := make([]map[string]interface{}, board.NumPages)
catalog_json_file, err := os.OpenFile(path.Join(config.DocumentRoot, board.Dir, "catalog.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
defer closeFile(catalog_json_file)
defer closeHandle(catalog_json_file)
if err != nil {
html += handleError(1, "Failed opening /"+board.Dir+"/catalog.json: "+err.Error())
return
@ -270,7 +264,7 @@ func buildBoardPages(board *BoardsTable) (html string) {
pageFilename := strconv.Itoa(board.CurrentPage) + ".html"
current_page_filepath = path.Join(config.DocumentRoot, board.Dir, pageFilename)
current_page_file, err = os.OpenFile(current_page_filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
defer closeFile(current_page_file)
defer closeHandle(current_page_file)
if err != nil {
html += handleError(1, "Failed opening board page: "+err.Error()) + "<br />"
continue
@ -284,7 +278,7 @@ func buildBoardPages(board *BoardsTable) (html string) {
"threads": page_threads,
"board": board,
"posts": []interface{}{
PostTable{BoardID: board.ID},
Post{BoardID: board.ID},
},
}); err != nil {
html += handleError(1, "Failed building /"+board.Dir+"/ boardpage: "+err.Error()) + "<br />"
@ -300,38 +294,14 @@ func buildBoardPages(board *BoardsTable) (html string) {
}
// Collect up threads for this page.
var page_obj BoardPageJSON
page_obj.Page = board.CurrentPage
for _, thread_int := range page_threads {
thread := thread_int.(Thread)
post_json := makePostJSON(thread.OP, board.Anonymous)
var thread_json ThreadJSON
thread_json.PostJSON = &post_json
thread_json.Replies = thread.NumReplies
thread_json.ImagesOnArchive = thread.NumImages
thread_json.OmittedImages = thread.OmittedImages
if thread.Stickied {
if thread.NumReplies > config.StickyRepliesOnBoardPage {
thread_json.OmittedPosts = thread.NumReplies - config.StickyRepliesOnBoardPage
}
thread_json.Sticky = 1
} else {
if thread.NumReplies > config.RepliesOnBoardPage {
thread_json.OmittedPosts = thread.NumReplies - config.RepliesOnBoardPage
}
}
if thread.OP.Locked {
thread_json.Locked = 1
}
page_obj.Threads = append(page_obj.Threads, thread_json)
}
pages_obj = append(pages_obj, page_obj)
pageMap := make(map[string]interface{})
pageMap["page"] = board.CurrentPage
pageMap["threads"] = page_threads
pagesArr = append(pagesArr, pageMap)
}
board.CurrentPage = currentBoardPage
catalog_json, err := json.Marshal(pages_obj)
catalog_json, err := json.Marshal(pagesArr)
if err != nil {
html += handleError(1, "Failed to marshal to JSON: "+err.Error()) + "<br />"
return
@ -346,35 +316,34 @@ func buildBoardPages(board *BoardsTable) (html string) {
return
}
// buildBoards builds one or all boards.
// If `all` == true, all boards will have their pages built and `which` is ignored
// Otherwise, the board with the id equal to the value specified as which.
// 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.
// TODO: make this a variadic function (which ...int)
func buildBoards(all bool, which int) (html string) {
if all {
boards, _ := getBoardArr(nil, "")
if len(boards) == 0 {
return html + "No boards to build.<br />\n"
}
for _, board := range boards {
html += buildBoardPages(&board) + "<br />\n"
if board.EnableCatalog {
html += buildCatalog(board.ID) + "<br />\n"
}
func buildBoards(which ...int) (html string) {
var boards []Board
html += buildThreads(true, board.ID, 0)
}
if which == nil {
boards = allBoards
} else {
boardArr, _ := getBoardArr(map[string]interface{}{"id": which}, "")
board := boardArr[0]
html += buildBoardPages(&board) + "<br />\n"
for _, b := range which {
board, err := getBoardFromID(b)
if err != nil {
html += handleError(0, err.Error()) + "<br />\n"
continue
}
boards = append(boards, *board)
}
}
if len(boards) == 0 {
return html + "No boards to build.<br />\n"
}
for _, board := range boards {
if board.EnableCatalog {
html += buildCatalog(board.ID) + "<br />\n"
}
html += buildThreads(true, board.ID, 0)
html += buildBoardPages(&board) + "<br />\n" +
buildThreads(true, board.ID, 0) + "<br />\n"
}
return
}
@ -417,9 +386,9 @@ func buildCatalog(which int) (html string) {
return
}
// buildThreadPages builds the pages for a thread given by a PostTable object.
func buildThreadPages(op *PostTable) (html string) {
var replies []PostTable
// buildThreadPages builds the pages for a thread given by a Post object.
func buildThreadPages(op *Post) (html string) {
var replies []Post
var current_page_file *os.File
board, err := getBoardFromID(op.BoardID)
if err != nil {
@ -453,6 +422,7 @@ func buildThreadPages(op *PostTable) (html string) {
html += handleError(1, "Failed opening "+current_page_filepath+": "+err.Error())
return
}
// render main page
if err = img_threadpage_tmpl.Execute(current_page_file, map[string]interface{}{
"config": config,
@ -468,25 +438,22 @@ func buildThreadPages(op *PostTable) (html string) {
// 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 closeFile(threadJSONFile)
defer closeHandle(threadJSONFile)
if err != nil {
html += handleError(1, "Failed opening /%s/res/%d.json: %s", board.Dir, op.ID, err.Error())
return
}
// Create the wrapper object
thread_json_wrapper := new(ThreadJSONWrapper)
threadMap := make(map[string][]Post)
// Handle the OP, of type *PostTable
op_post_obj := makePostJSON(*op, board.Anonymous)
thread_json_wrapper.Posts = append(thread_json_wrapper.Posts, op_post_obj)
// Handle the OP, of type *Post
threadMap["posts"] = []Post{*op}
// Iterate through each reply, which are of type PostTable
// Iterate through each reply, which are of type Post
for _, reply := range replies {
postJSON := makePostJSON(reply, board.Anonymous)
thread_json_wrapper.Posts = append(thread_json_wrapper.Posts, postJSON)
threadMap["posts"] = append(threadMap["posts"], reply)
}
threadJSON, err := json.Marshal(thread_json_wrapper)
threadJSON, err := json.Marshal(threadMap)
if err != nil {
html += handleError(1, "Failed to marshal to JSON: %s", err.Error()) + "<br />"
return
@ -527,27 +494,24 @@ func buildThreadPages(op *PostTable) (html string) {
// 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: detect which page will be built and only build that one and the board page
// TODO: make it variadic
func buildThreads(all bool, boardid, threadid int) (html string) {
if !all {
threads, _ := getPostArr(map[string]interface{}{
"boardid": boardid,
"id": threadid,
"parentid": 0,
"deleted_timestamp": nilTimestamp,
}, "")
thread := threads[0]
html += buildThreadPages(&thread) + "<br />\n"
return
}
var threads []Post
var err error
threads, _ := getPostArr(map[string]interface{}{
queryMap := map[string]interface{}{
"boardid": boardid,
"parentid": 0,
"deleted_timestamp": nilTimestamp,
}, "")
}
if !all {
queryMap["id"] = threadid
}
if threads, err = getPostArr(queryMap, ""); err != nil {
return handleError(0, err.Error()) + "<br />\n"
}
if len(threads) == 0 {
return
return "No threads to build<br />\n"
}
for _, op := range threads {

View file

@ -11,11 +11,7 @@ var versionStr string
var buildtimeString string // set in Makefile, format: YRMMDD.HHMM
func main() {
defer func() {
if db != nil {
_ = db.Close()
}
}()
defer closeHandle(db)
initConfig()
connectToSQLServer()
parseCommandLine()
@ -27,11 +23,6 @@ func main() {
os.Exit(2)
}
println(0, "Initializing server...")
if _, err := db.Exec("USE `" + config.DBname + "`"); err != nil {
handleError(0, customError(err))
os.Exit(2)
}
initServer()
}

View file

@ -84,25 +84,25 @@ func getCurrentStaff(request *http.Request) (string, error) {
return "", nil
}
key := sessionCookie.Value
current_session := new(SessionsTable)
currentSession := new(LoginSession)
if err := queryRowSQL(
"SELECT `data` FROM `"+config.DBprefix+"sessions` WHERE `key` = ?",
[]interface{}{key},
[]interface{}{&current_session.Data},
[]interface{}{&currentSession.Data},
); err != nil {
return "", err
}
return current_session.Data, nil
return currentSession.Data, nil
}
func getStaff(name string) (*StaffTable, error) {
staff_obj := new(StaffTable)
func getStaff(name string) (*Staff, error) {
staff := new(Staff)
err := queryRowSQL(
"SELECT * FROM `"+config.DBprefix+"staff` WHERE `username` = ?",
[]interface{}{name},
[]interface{}{&staff_obj.ID, &staff_obj.Username, &staff_obj.PasswordChecksum, &staff_obj.Salt, &staff_obj.Rank, &staff_obj.Boards, &staff_obj.AddedOn, &staff_obj.LastActive},
[]interface{}{&staff.ID, &staff.Username, &staff.PasswordChecksum, &staff.Salt, &staff.Rank, &staff.Boards, &staff.AddedOn, &staff.LastActive},
)
return staff_obj, err
return staff, err
}
func getStaffRank(request *http.Request) int {
@ -200,7 +200,7 @@ var manage_functions = map[string]ManageFunction{
html += "Optimizing all tables in database.<hr />"
tableRows, tablesErr := querySQL("SHOW TABLES")
defer closeRows(tableRows)
defer closeHandle(tableRows)
if tablesErr != nil {
html += "<tr><td>" + tablesErr.Error() + "</td></tr></table>"
return
@ -438,7 +438,7 @@ var manage_functions = map[string]ManageFunction{
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
html = "<img src=\"/css/purge.jpg\" />"
rows, err := querySQL("SELECT `dir` FROM `" + config.DBprefix + "boards`")
defer closeRows(rows)
defer closeHandle(rows)
if err != nil {
html += err.Error()
handleError(1, customError(err))
@ -484,7 +484,7 @@ var manage_functions = map[string]ManageFunction{
return
}
html += "<br />Everything purged, rebuilding all<br />" +
buildBoards(true, 0) + "<hr />\n" +
buildBoards() + "<hr />\n" +
buildFrontPage()
return
}},
@ -545,14 +545,14 @@ var manage_functions = map[string]ManageFunction{
html = "<h1 class=\"manage-header\">Announcements</h1><br />"
rows, err := querySQL("SELECT `subject`,`message`,`poster`,`timestamp` FROM `" + config.DBprefix + "announcements` ORDER BY `id` DESC")
defer closeRows(rows)
defer closeHandle(rows)
if err != nil {
html += handleError(1, err.Error())
return
}
iterations := 0
for rows.Next() {
announcement := new(AnnouncementsTable)
announcement := new(Announcement)
err = rows.Scan(&announcement.Subject, &announcement.Message, &announcement.Poster, &announcement.Timestamp)
if err != nil {
html += handleError(1, err.Error())
@ -572,7 +572,7 @@ var manage_functions = map[string]ManageFunction{
"bans": {
Permissions: 1,
Callback: func(writer http.ResponseWriter, request *http.Request) (pageHTML string) {
var post PostTable
var post Post
if request.FormValue("do") == "add" {
ip := net.ParseIP(request.FormValue("ip"))
name := request.FormValue("name")
@ -641,15 +641,15 @@ var manage_functions = map[string]ManageFunction{
post = posts[0]
}
rows, err := querySQL("SELECT `ip`,`name`,`reason`,`boards`,`staff`,`timestamp`,`expires`,`permaban`,`can_appeal` FROM `" + config.DBprefix + "banlist`")
defer closeRows(rows)
defer closeHandle(rows)
if err != nil {
pageHTML += handleError(1, err.Error())
return
}
var banlist []BanlistTable
var banlist []BanInfo
for rows.Next() {
var ban BanlistTable
var ban BanInfo
rows.Scan(&ban.IP, &ban.Name, &ban.Reason, &ban.Boards, &ban.Staff, &ban.Timestamp, &ban.Expires, &ban.Permaban, &ban.CanAppeal)
banlist = append(banlist, ban)
}
@ -677,7 +677,7 @@ var manage_functions = map[string]ManageFunction{
html = "nobody;0;"
return
}
staff := new(StaffTable)
staff := new(Staff)
if err := queryRowSQL("SELECT `rank`,`boards` FROM `"+config.DBprefix+"staff` WHERE `username` = ?",
[]interface{}{current_staff},
[]interface{}{&staff.Rank, &staff.Boards},
@ -693,7 +693,7 @@ var manage_functions = map[string]ManageFunction{
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
do := request.FormValue("do")
var done bool
board := new(BoardsTable)
board := new(Board)
var board_creation_status string
var err error
var rows *sql.Rows
@ -729,9 +729,9 @@ var manage_functions = map[string]ManageFunction{
if err != nil {
board.Section = 0
}
board.MaxImageSize, err = strconv.Atoi(request.FormValue("maximagesize"))
board.MaxFilesize, err = strconv.Atoi(request.FormValue("maximagesize"))
if err != nil {
board.MaxImageSize = 1024 * 4
board.MaxFilesize = 1024 * 4
}
board.MaxPages, err = strconv.Atoi(request.FormValue("maxpages"))
@ -800,17 +800,17 @@ var manage_functions = map[string]ManageFunction{
boardCreationTimestamp := getSpecificSQLDateTime(board.CreatedOn)
if _, err := execSQL(
"INSERT INTO `"+config.DBprefix+"boards` (`order`,`dir`,`type`,`upload_type`,`title`,`subtitle`,"+
"`description`,`section`,`max_image_size`,`max_pages`,`locale`,`default_style`,`locked`,`created_on`,"+
"`description`,`section`,`max_image_size`,`max_pages`,`default_style`,`locked`,`created_on`,"+
"`anonymous`,`forced_anon`,`max_age`,`autosage_after`,`no_images_after`,`max_message_length`,`embeds_allowed`,"+
"`redirect_to_thread`,`require_file`,`enable_catalog`) "+
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
&board.Order, &board.Dir, &board.Type, &board.UploadType,
&board.Title, &board.Subtitle, &board.Description, &board.Section,
&board.MaxImageSize, &board.MaxPages, &board.Locale, &board.DefaultStyle,
&board.Locked, &boardCreationTimestamp, &board.Anonymous,
&board.ForcedAnon, &board.MaxAge, &board.AutosageAfter,
&board.NoImagesAfter, &board.MaxMessageLength, &board.EmbedsAllowed,
&board.RedirectToThread, &board.RequireFile, &board.EnableCatalog,
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
board.Order, board.Dir, board.Type, board.UploadType,
board.Title, board.Subtitle, board.Description, board.Section,
board.MaxFilesize, board.MaxPages, board.DefaultStyle,
board.Locked, boardCreationTimestamp, board.Anonymous,
board.ForcedAnon, board.MaxAge, board.AutosageAfter,
board.NoImagesAfter, board.MaxMessageLength, board.EmbedsAllowed,
board.RedirectToThread, board.RequireFile, board.EnableCatalog,
); err != nil {
do = ""
board_creation_status = handleError(1, "Error creating board: "+customError(err))
@ -818,7 +818,7 @@ var manage_functions = map[string]ManageFunction{
} else {
board_creation_status = "Board created successfully"
println(2, board_creation_status)
buildBoards(true, 0)
buildBoards()
resetBoardSectionArrays()
println(2, "Boards rebuilt successfully")
done = true
@ -831,7 +831,7 @@ var manage_functions = map[string]ManageFunction{
default:
// put the default column values in the text boxes
rows, err = querySQL("SELECT `column_name`,`column_default` FROM `information_schema`.`columns` WHERE `table_name` = '" + config.DBprefix + "boards'")
defer closeRows(rows)
defer closeHandle(rows)
if err != nil {
html += handleError(1, "Error getting column names from boards table:"+err.Error())
return
@ -862,11 +862,9 @@ var manage_functions = map[string]ManageFunction{
case "section":
board.Section = columnDefaultInt
case "max_image_size":
board.MaxImageSize = columnDefaultInt
board.MaxFilesize = columnDefaultInt
case "max_pages":
board.MaxPages = columnDefaultInt
case "locale":
board.Locale = columnDefault
case "default_style":
board.DefaultStyle = columnDefault
case "locked":
@ -897,7 +895,7 @@ var manage_functions = map[string]ManageFunction{
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"
rows, err = querySQL("SELECT `dir` FROM `" + config.DBprefix + "boards`")
defer closeRows(rows)
defer closeHandle(rows)
if err != nil {
html += handleError(1, err.Error())
return
@ -976,13 +974,13 @@ var manage_functions = map[string]ManageFunction{
resetBoardSectionArrays()
return buildFrontPage() + "<hr />\n" +
buildBoardListJSON() + "<hr />\n" +
buildBoards(true, 0) + "<hr />\n"
buildBoards() + "<hr />\n"
}},
"rebuildboards": {
Permissions: 3,
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
initTemplates()
return buildBoards(true, 0)
return buildBoards()
}},
"reparsehtml": {
Permissions: 3,
@ -1007,7 +1005,7 @@ var manage_functions = map[string]ManageFunction{
html += "Done reparsing HTML<hr />" +
buildFrontPage() + "<hr />\n" +
buildBoardListJSON() + "<hr />\n" +
buildBoards(true, 0) + "<hr />\n"
buildBoards() + "<hr />\n"
return
}},
"recentposts": {
@ -1037,7 +1035,7 @@ var manage_functions = map[string]ManageFunction{
"ORDER BY `timestamp` DESC LIMIT ?",
nilTimestamp, limit,
)
defer closeRows(rows)
defer closeHandle(rows)
if err != nil {
html += "<tr><td>" + handleError(1, err.Error()) + "</td></tr></table>"
return
@ -1070,36 +1068,28 @@ var manage_functions = map[string]ManageFunction{
"dir": boardDir,
}, "")
if err != nil {
var jsonErr ErrorJSON
jsonErr.Message = err.Error()
jsonBytes, _ := json.Marshal(jsonErr)
return string(jsonBytes)
jsonErr, _ := marshalAPI("error", err.Error(), false)
return jsonErr
}
if len(boards) < 1 {
var jsonErr ErrorJSON
jsonErr.Message = "Board doesn't exist."
jsonBytes, _ := json.Marshal(jsonErr)
return string(jsonBytes)
jsonErr, _ := marshalAPI("error", "Board doesn't exist.", false)
return jsonErr
}
posts, err := getPostArr(map[string]interface{}{
"id": request.FormValue("postid"),
"boardid": boards[0].ID,
}, "")
if err != nil {
var jsonErr ErrorJSON
jsonErr.Message = err.Error()
jsonBytes, _ := json.Marshal(jsonErr)
return string(jsonBytes)
jsonErr, _ := marshalAPI("error", err.Error(), false)
return jsonErr
}
if len(posts) < 1 {
var jsonErr ErrorJSON
jsonErr.Message = "Post doesn't exist."
jsonBytes, _ := json.Marshal(jsonErr)
return string(jsonBytes)
jsonErr, _ := marshalAPI("eror", "Post doesn't exist.", false)
return jsonErr
}
jsonBytes, _ := json.Marshal(posts[0])
return string(jsonBytes)
jsonStr, _ := marshalAPI("", posts[0], false)
return jsonStr
}},
"staff": {
Permissions: 3,
@ -1109,7 +1099,7 @@ var manage_functions = map[string]ManageFunction{
"<table id=\"stafftable\" border=\"1\">\n" +
"<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>\n"
rows, err := querySQL("SELECT `username`,`rank`,`boards`,`added_on` FROM `" + config.DBprefix + "staff`")
defer closeRows(rows)
defer closeHandle(rows)
if err != nil {
html += "<tr><td>" + handleError(1, err.Error()) + "</td></tr></table>"
return
@ -1117,7 +1107,7 @@ var manage_functions = map[string]ManageFunction{
iter := 1
for rows.Next() {
staff := new(StaffTable)
staff := new(Staff)
if err = rows.Scan(&staff.Username, &staff.Rank, &staff.Boards, &staff.AddedOn); err != nil {
handleError(1, err.Error())
return err.Error()

View file

@ -34,8 +34,8 @@ const (
)
var (
allSections []interface{}
allBoards []interface{}
allSections []BoardSection
allBoards []Board
)
// bumps the given thread on the given board and returns true if there were no errors
@ -47,10 +47,10 @@ func bumpThread(postID, boardID int) error {
return err
}
// Checks check poster's name/tripcode/file checksum (from PostTable post) for banned status
// Checks check poster's name/tripcode/file checksum (from Post post) for banned status
// returns ban table if the user is banned or errNotBanned if they aren't
func getBannedStatus(request *http.Request) (BanlistTable, error) {
var banEntry BanlistTable
func getBannedStatus(request *http.Request) (BanInfo, error) {
var banEntry BanInfo
formName := request.FormValue("postname")
var tripcode string
@ -103,7 +103,7 @@ func getBannedStatus(request *http.Request) (BanlistTable, error) {
return banEntry, err
}
func isBanned(ban BanlistTable, board string) bool {
func isBanned(ban BanInfo, board string) bool {
if ban.Boards == "" && (ban.Expires.After(time.Now()) || ban.Permaban) {
return true
}
@ -117,7 +117,7 @@ func isBanned(ban BanlistTable, board string) bool {
return false
}
func sinceLastPost(post *PostTable) int {
func sinceLastPost(post *Post) int {
var lastPostTime time.Time
if err := queryRowSQL("SELECT `timestamp` FROM `"+config.DBprefix+"posts` WHERE `ip` = '?' ORDER BY `timestamp` DESC LIMIT 1",
[]interface{}{post.IP},
@ -243,17 +243,17 @@ func parseName(name string) map[string]string {
}
// inserts prepared post object into the SQL table so that it can be rendered
func insertPost(post PostTable, bump bool) (sql.Result, error) {
func insertPost(post Post, bump bool) (sql.Result, error) {
result, err := execSQL(
"INSERT INTO `"+config.DBprefix+"posts` "+
"(`boardid`,`parentid`,`name`,`tripcode`,`email`,`subject`,`message`,`message_raw`,`password`,`filename`,`filename_original`,`file_checksum`,`filesize`,`image_w`,`image_h`,`thumb_w`,`thumb_h`,`ip`,`tag`,`timestamp`,`autosage`,`poster_authority`,`deleted_timestamp`,`bumped`,`stickied`,`locked`,`reviewed`,`sillytag`)"+
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
"(`boardid`,`parentid`,`name`,`tripcode`,`email`,`subject`,`message`,`message_raw`,`password`,`filename`,`filename_original`,`file_checksum`,`filesize`,`image_w`,`image_h`,`thumb_w`,`thumb_h`,`ip`,`tag`,`timestamp`,`autosage`,`deleted_timestamp`,`bumped`,`stickied`,`locked`,`reviewed`)"+
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
post.BoardID, post.ParentID, post.Name, post.Tripcode, post.Email,
post.Subject, post.MessageHTML, post.MessageText, post.Password,
post.Filename, post.FilenameOriginal, post.FileChecksum, post.Filesize,
post.ImageW, post.ImageH, post.ThumbW, post.ThumbH, post.IP, post.Tag,
post.Timestamp, post.Autosage, post.PosterAuthority, post.DeletedTimestamp,
post.Bumped, post.Stickied, post.Locked, post.Reviewed, post.Sillytag,
post.ImageW, post.ImageH, post.ThumbW, post.ThumbH, post.IP, post.Capcode,
post.Timestamp, post.Autosage, post.DeletedTimestamp, post.Bumped,
post.Stickied, post.Locked, post.Reviewed,
)
if err != nil {
@ -274,7 +274,7 @@ func insertPost(post PostTable, bump bool) (sql.Result, error) {
func makePost(writer http.ResponseWriter, request *http.Request) {
startTime := benchmarkTimer("makePost", time.Now(), true)
var maxMessageLength int
var post PostTable
var post Post
domain := request.Host
var formName string
var nameCookie string
@ -343,7 +343,7 @@ func makePost(writer http.ResponseWriter, request *http.Request) {
post.IP = getRealIP(request)
post.Timestamp = time.Now()
post.PosterAuthority = getStaffRank(request)
// post.PosterAuthority = getStaffRank(request)
post.Bumped = time.Now()
post.Stickied = request.FormValue("modstickied") == "on"
post.Locked = request.FormValue("modlocked") == "on"
@ -597,7 +597,7 @@ func makePost(writer http.ResponseWriter, request *http.Request) {
post.ID = int(postid)
// rebuild the board page
buildBoards(false, post.BoardID)
buildBoards(post.BoardID)
buildFrontPage()
if emailCommand == "noko" {
@ -659,7 +659,7 @@ func formatMessage(message string) string {
return strings.Join(postLines, "<br />")
}
func bannedForever(ban BanlistTable) bool {
func bannedForever(ban BanInfo) bool {
return ban.Permaban && !ban.CanAppeal && ban.Type == 3 && ban.Boards == ""
}
@ -694,7 +694,7 @@ func banHandler(writer http.ResponseWriter, request *http.Request) {
banpageBuffer.Write([]byte(""))
if err = banpage_tmpl.Execute(&banpageBuffer, map[string]interface{}{
"config": config, "ban": banStatus, "banBoards": banStatus.Boards, "post": PostTable{},
"config": config, "ban": banStatus, "banBoards": banStatus.Boards, "post": Post{},
}); err != nil {
fmt.Fprintf(writer, handleError(1, err.Error())+"\n</body>\n</html>")
return

View file

@ -58,6 +58,7 @@ func (s GochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
writer.Header().Add("Content-Type", "image/gif")
writer.Header().Add("Cache-Control", "max-age=86400")
case "jpg":
fallthrough
case "jpeg":
writer.Header().Add("Content-Type", "image/jpeg")
writer.Header().Add("Cache-Control", "max-age=86400")
@ -73,8 +74,9 @@ func (s GochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
case "webm":
writer.Header().Add("Content-Type", "video/webm")
writer.Header().Add("Cache-Control", "max-age=86400")
}
if strings.HasPrefix(extension, "htm") {
case "htm":
fallthrough
case "html":
writer.Header().Add("Content-Type", "text/html")
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
}
@ -133,7 +135,7 @@ func initServer() {
// Check if Akismet API key is usable at startup.
if err = checkAkismetAPIKey(config.AkismetAPIKey); err != nil {
config.AkismetAPIKey = ""
handleError(0, err.Error())
// handleError(0, err.Error())
}
// Compile regex for checking referrers.
@ -148,7 +150,6 @@ func initServer() {
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
if config.UseFastCGI {
@ -221,7 +222,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
}
passwordMD5 := md5Sum(password)
var post PostTable
var post Post
post.ID, _ = strconv.Atoi(postsArr[0])
post.BoardID, _ = strconv.Atoi(boardid)
if err = queryRowSQL("SELECT `parentid`,`name`,`tripcode`,`email`,`subject`,`password`,`message_raw` FROM `"+config.DBprefix+"posts` WHERE `id` = ? AND `boardid` = ? AND `deleted_timestamp` = ?",
@ -292,7 +293,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
return
}
buildBoards(false, boardid)
buildBoards(boardid)
if request.FormValue("parentid") == "0" {
http.Redirect(writer, request, "/"+board.Dir+"/res/"+strconv.Itoa(postid)+".html", http.StatusFound)
} else {
@ -316,7 +317,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
for _, checkedPostID := range postsArr {
var fileType string
var thumbType string
var post PostTable
var post Post
var err error
post.ID, _ = strconv.Atoi(checkedPostID)
post.BoardID, _ = strconv.Atoi(boardid)
@ -423,7 +424,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
os.Remove(path.Join(config.DocumentRoot, board, "/thumb/", strings.Replace(deletedFilename, ".", "c.", -1)))
}
buildBoards(false, post.BoardID)
buildBoards(post.BoardID)
writer.Header().Add("refresh", "4;url="+request.Referer())
fmt.Fprintf(writer, "%d deleted successfully\n", post.ID)

View file

@ -5,6 +5,7 @@ import (
"errors"
"io/ioutil"
"os"
"regexp"
"strings"
"time"
@ -23,66 +24,72 @@ var (
func connectToSQLServer() {
var err error
var sqlVersionStr string
println(0, "Initializing server...")
db, err = sql.Open("mysql", config.DBusername+":"+config.DBpassword+"@"+config.DBhost+"/"+config.DBname+"?parseTime=true&collation=utf8mb4_unicode_ci")
if err != nil {
handleError(0, "Failed to connect to the database: %s\n", customError(err))
os.Exit(2)
}
if !checkTableExists(config.DBprefix+"info") {
err = sql.ErrNoRows
} else {
err = queryRowSQL("SELECT `value` FROM `"+config.DBprefix+"info` WHERE `name` = 'version'",
[]interface{}{}, []interface{}{&sqlVersionStr})
if err = initDB(); err != nil {
handleError(0, "Failed initializing DB: %s\n", customError(err))
os.Exit(2)
}
var sqlVersionStr string
err = queryRowSQL("SELECT `value` FROM `"+config.DBprefix+"info` WHERE `name` = 'version'",
[]interface{}{}, []interface{}{&sqlVersionStr})
if err == sql.ErrNoRows {
println(0, "\nThis looks like a new installation")
if err = loadInitialSQL(); err != nil {
handleError(0, "failed with error: %s\n", customError(err))
os.Exit(2)
}
if _, err = db.Exec("INSERT INTO `" + config.DBname + "`.`" + config.DBprefix + "staff` " +
"(`username`, `password_checksum`, `salt`, `rank`) " +
"VALUES ('admin', '" + bcryptSum("password") + "', 'abc', 3)",
); err != nil {
handleError(0, "failed with error: %s\n", customError(err))
handleError(0, "Failed creating admin user with error: %s\n", customError(err))
os.Exit(2)
}
_, err = execSQL("INSERT INTO `"+config.DBprefix+"info` (`name`,`value`) VALUES('version',?)", versionStr)
if err != nil && !strings.Contains(err.Error(), "Duplicate entry") {
handleError(0, "failed with error: %s\n", customError(err))
os.Exit(2)
}
return
} else if err != nil {
handleError(0, "failed: %s\n", customError(err))
os.Exit(2)
}
checkDeprecatedSchema(sqlVersionStr)
}
func loadInitialSQL() error {
var err error
if _, err = os.Stat("initialsetupdb.sql"); err != nil {
return errors.New("Initial setup file (initialsetupdb.sql) missing. Please reinstall gochan")
if err != nil && !strings.Contains(err.Error(), "Duplicate entry") {
handleError(0, "failed with error: %s\n", customError(err))
os.Exit(2)
}
if version.CompareString(sqlVersionStr) > 0 {
printf(0, "Updating version in database from %s to %s\n", sqlVersionStr, version.String())
execSQL("UPDATE `"+config.DBprefix+"info` SET `value` = ? WHERE `name` = 'version'", versionStr)
}
initialSQLBytes, err := ioutil.ReadFile("initialsetupdb.sql")
}
func initDB() error {
var err error
if _, err = os.Stat("initdb.sql"); err != nil {
return errors.New("SQL database initialization file (initdb.sql) missing. Please reinstall gochan")
}
sqlBytes, err := ioutil.ReadFile("initdb.sql")
if err != nil {
return err
}
initialSQLStr := string(initialSQLBytes)
initialSQLStr = strings.NewReplacer("DBNAME", config.DBname, "DBPREFIX", config.DBprefix).Replace(initialSQLStr)
initialSQLArr := strings.Split(initialSQLStr, ";")
for _, statement := range initialSQLArr {
if statement != "" && statement != "\n" && statement != "\r\n" && strings.Index(statement, "--") != 0 {
if _, err := db.Exec(statement); err != nil {
sqlStr := regexp.MustCompile("--.*\n?").ReplaceAllString(string(sqlBytes), " ")
sqlStr = strings.NewReplacer(
"DBNAME", config.DBname,
"DBPREFIX", config.DBprefix,
"\n", " ").Replace(sqlStr)
sqlArr := strings.Split(sqlStr, ";")
for _, statement := range sqlArr {
if statement != "" && statement != " " {
if _, err := db.Exec(statement + ";"); err != nil {
return err
}
}
@ -99,7 +106,7 @@ func loadInitialSQL() error {
*/
func execSQL(query string, values ...interface{}) (sql.Result, error) {
stmt, err := db.Prepare(query)
defer closeStatement(stmt)
defer closeHandle(stmt)
if err != nil {
return nil, err
}
@ -120,7 +127,7 @@ func execSQL(query string, values ...interface{}) (sql.Result, error) {
*/
func queryRowSQL(query string, values []interface{}, out []interface{}) error {
stmt, err := db.Prepare(query)
defer closeStatement(stmt)
defer closeHandle(stmt)
if err != nil {
return err
}
@ -143,7 +150,7 @@ func queryRowSQL(query string, values []interface{}, out []interface{}) error {
*/
func querySQL(query string, a ...interface{}) (*sql.Rows, error) {
stmt, err := db.Prepare(query)
defer closeStatement(stmt)
defer closeHandle(stmt)
if err != nil {
return nil, err
}
@ -158,63 +165,8 @@ func getSpecificSQLDateTime(t time.Time) string {
return t.Format(mysqlDatetimeFormat)
}
// checkDeprecatedSchema checks the tables for outdated columns and column values
// and causes gochan to quit with an error message specific to the needed change
func checkDeprecatedSchema(dbVersion string) {
var hasColumn int
var err error
if version.CompareString(dbVersion) < 1 {
return
}
printf(0, "Database is out of date (%s) updating for compatibility\n", dbVersion)
execSQL("ALTER TABLE `"+config.DBprefix+"banlist` CHANGE COLUMN `id` `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT", nil)
execSQL("ALTER TABLE `"+config.DBprefix+"banlist` CHANGE COLUMN `expires` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP", nil)
if err = queryRowSQL(
"SELECT COUNT(*) FROM information_schema.COlUMNS WHERE `TABLE_SCHEMA` = '"+config.DBname+"' AND TABLE_NAME = '"+config.DBprefix+"banlist' AND COLUMN_NAME = 'appeal_message'",
[]interface{}{}, []interface{}{&hasColumn},
); err != nil {
println(0, "error checking for deprecated column: "+err.Error())
os.Exit(2)
return
}
if hasColumn > 0 {
// Running them one at a time, in case we get errors from individual queries
execSQL("ALTER TABLE `"+config.DBprefix+"banlist` CHANGE COLUMN `banned_by` `staff` VARCHAR(50) NOT NULL", nil)
execSQL("ALTER TABLE `"+config.DBprefix+"banlist` ADD COLUMN `type` TINYINT UNSIGNED NOT NULL DEFAULT '3'", nil)
execSQL("ALTER TABLE `"+config.DBprefix+"banlist` ADD COLUMN `name_is_regex` TINYINT(1) DEFAULT '0'", nil)
execSQL("ALTER TABLE `"+config.DBprefix+"banlist` ADD COLUMN `filename` VARCHAR(255) NOT NULL DEFAULT ''", nil)
execSQL("ALTER TABLE `"+config.DBprefix+"banlist` ADD COLUMN `file_checksum` VARCHAR(255) NOT NULL DEFAULT ''", nil)
execSQL("ALTER TABLE `"+config.DBprefix+"banlist` ADD COLUMN `permaban` TINYINT(1) DEFAULT '0'", nil)
execSQL("ALTER TABLE `"+config.DBprefix+"banlist` ADD COLUMN `can_appeal` TINYINT(1) DEFAULT '1'", nil)
execSQL("ALTER TABLE `"+config.DBprefix+"banlist` DROP COLUMN `message`", nil)
execSQL("ALTER TABLE `"+config.DBprefix+"boards` CHANGE COLUMN `boards` VARCHAR(255) NOT NULL DEFAULT ''", nil)
println(0, "The column `appeal_message` in table "+config.DBprefix+"banlist is deprecated. A new table , `"+config.DBprefix+"appeals` has been created for it, and the banlist table will be modified accordingly.")
println(0, "Just to be safe, you may want to check both tables to make sure everything is good.")
rows, err := querySQL("SELECT `id`,`appeal_message` FROM `" + config.DBprefix + "banlist`")
if err != nil {
handleError(0, "Error updating banlist schema: %s\n", err.Error())
os.Exit(2)
return
}
for rows.Next() {
var id int
var appealMessage string
rows.Scan(&id, &appealMessage)
if appealMessage != "" {
execSQL("INSERT INTO `"+config.DBprefix+"appeals` (`ban`,`message`) VALUES(?,?)", &id, &appealMessage)
}
execSQL("ALTER TABLE `" + config.DBprefix + "banlist` DROP COLUMN `appeal_message`")
}
}
execSQL("UPDATE `"+config.DBprefix+"info` SET `value` = ? WHERE `name` = 'version'", versionStr)
}
func checkTableExists(tableName string) bool {
rows, err := querySQL("SELECT * FROM information_schema.tables WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ? LIMIT 1",
config.DBname, tableName)
return err == nil && rows.Next() == true
}
}

View file

@ -145,7 +145,7 @@ var funcMap = template.FuncMap{
return getThumbnailPath("catalog", img)
},
"getThreadID": func(post_i interface{}) (thread int) {
post, ok := post_i.(PostTable)
post, ok := post_i.(Post)
if !ok {
thread = 0
} else if post.ParentID == 0 {
@ -168,7 +168,7 @@ var funcMap = template.FuncMap{
}
postURL = post.GetURL(withDomain)
} else {
post, ok := post_i.(*PostTable)
post, ok := post_i.(*Post)
if !ok {
return
}

View file

@ -54,26 +54,28 @@ func (p *RecentPost) GetURL(includeDomain bool) string {
}
type Thread struct {
OP PostTable
NumReplies int
NumImages int
OmittedImages int
BoardReplies []PostTable
Stickied bool
ThreadPage int
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 AnnouncementsTable struct {
ID uint
Subject string
Message string
Poster string
type Announcement struct {
ID uint `json:"no"`
Subject string `json:"sub"`
Message string `json:"com"`
Poster string `json:"name"`
Timestamp time.Time
}
type AppealsTable struct {
type BanAppeal struct {
ID int
Ban int
Message string
@ -81,8 +83,8 @@ type AppealsTable struct {
StaffResponse string
}
func (a *AppealsTable) GetBan() (BanlistTable, error) {
var ban BanlistTable
func (a *BanAppeal) GetBan() (BanInfo, error) {
var ban BanInfo
var err error
err = queryRowSQL("SELECT * FROM `"+config.DBprefix+"banlist` WHERE `id` = ? LIMIT 1",
@ -94,7 +96,7 @@ func (a *AppealsTable) GetBan() (BanlistTable, error) {
return ban, err
}
type BanlistTable struct {
type BanInfo struct {
ID uint
AllowRead bool
IP string
@ -113,48 +115,49 @@ type BanlistTable struct {
CanAppeal bool
}
type BannedHashesTable struct {
type BannedHash struct {
ID uint
Checksum string
Description string
}
type BoardsTable struct {
ID int
CurrentPage int
NumPages int
Order int
Dir string
Type int
UploadType int
Title string
Subtitle string
Description string
Section int
MaxImageSize int
MaxPages int
Locale string
DefaultStyle string
Locked bool
CreatedOn time.Time
Anonymous string
ForcedAnon bool
MaxAge int
AutosageAfter int
NoImagesAfter int
MaxMessageLength int
EmbedsAllowed bool
RedirectToThread bool
ShowID bool
RequireFile bool
EnableCatalog bool
EnableSpoileredImages bool
EnableSpoileredThreads bool
EnableNSFW bool
ThreadPage int
type Board struct {
ID int `json:"-"`
CurrentPage int `json:`
NumPages int `json:"pages"`
Order 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"`
}
type BoardSectionsTable struct {
type BoardSection struct {
ID int
Order int
Hidden bool
@ -162,95 +165,41 @@ type BoardSectionsTable struct {
Abbreviation string
}
// EmbedsTable represents the embedable media on different sites.
// It's held over from Kusaba X and may be removed in the future
type EmbedsTable struct {
ID uint8
Filetype string
Name string
URL string
Width uint16
Height uint16
EmbedCode string
// Post represents each post in the database
type Post struct {
ID int `json:"no"`
ParentID int `json:"resto"`
CurrentPage int `json:"-"`
NumPages 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 int `json:"-"`
DeletedTimestamp time.Time `json:"-"`
Bumped time.Time `json:"last_modified"`
Stickied bool `json:"-"`
Locked bool `json:"-"`
Reviewed bool `json:"-"`
}
// FrontTable represents the information (News, rules, etc) on the front page
type FrontTable struct {
ID int
Page int
Order int
Subject string
Message string
Timestamp time.Time
Poster string
Email string
}
// FrontLinksTable is used for linking to sites that the admin likes
type FrontLinksTable struct {
ID uint8
Title string
URL string
}
type LoginAttemptsTable struct {
ID uint
IP string
Timestamp time.Time
}
type ModLogTable struct {
IP uint
Entry string
User string
Category uint8
Timestamp time.Time
}
// PollResultsTable may or may not be used in the future for user polls
type PollResultsTable struct {
ID uint
IP string
Selection string
Timestamp time.Time
}
// PostTable represents each post in the database
type PostTable struct {
ID int
CurrentPage int
NumPages int
BoardID int
ParentID int
Name string
Tripcode string
Email string
Subject string
MessageHTML string
MessageText string
Password string
Filename string
FilenameOriginal string
FileChecksum string
Filesize int
ImageW int
ImageH int
ThumbW int
ThumbH int
IP string
Tag string
Timestamp time.Time
Autosage int
PosterAuthority int
DeletedTimestamp time.Time
Bumped time.Time
Stickied bool
Locked bool
Reviewed bool
Sillytag bool
}
func (p *PostTable) GetURL(includeDomain bool) string {
func (p *Post) GetURL(includeDomain bool) string {
postURL := ""
if includeDomain {
postURL += config.SiteDomain
@ -273,14 +222,14 @@ func (p *PostTable) GetURL(includeDomain bool) string {
// Sanitize escapes HTML strings in a post. This should be run immediately before
// the post is inserted into the database
func (p *PostTable) Sanitize() {
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 ReportsTable struct {
type Report struct {
ID uint
Board string
PostID uint
@ -291,14 +240,14 @@ type ReportsTable struct {
IsTemp bool
}
type SessionsTable struct {
type LoginSession struct {
ID uint
Data string
Expires string
}
// StaffTable represents a single staff member's info stored in the database
type StaffTable struct {
// Staff represents a single staff member's info stored in the database
type Staff struct {
ID int
Username string
PasswordChecksum string
@ -309,7 +258,7 @@ type StaffTable struct {
LastActive time.Time
}
type WordFiltersTable struct {
type WordFilter struct {
ID int
From string
To string
@ -317,76 +266,12 @@ type WordFiltersTable struct {
RegEx bool
}
// Types for the JSON files we generate as a sort of "API"
type BoardJSONWrapper struct {
Boards []BoardJSON `json:"boards"`
}
type BoardJSON struct {
BoardName string `json:"board"`
Title string `json:"title"`
WorkSafeBoard int `json:"ws_board"`
ThreadsPerPage int `json:"per_page"`
Pages int `json:"pages"`
MaxFilesize int `json:"max_filesize"`
MaxMessageLength int `json:"max_comment_chars"`
BumpLimit int `json:"bump_limit"`
ImageLimit int `json:"image_limit"`
Cooldowns BoardCooldowns `json:"cooldowns"`
Description string `json:"meta_description"`
IsArchived int `json:"is_archived"`
}
type BoardCooldowns struct {
NewThread int `json:"threads"`
Reply int `json:"replies"`
ImageReply int `json:"images"`
}
type ThreadJSONWrapper struct {
Posts []PostJSON `json:"posts"`
}
type PostJSON struct {
ID int `json:"no"`
ParentID int `json:"resto"`
Subject string `json:"sub"`
Message string `json:"com"`
Name string `json:"name"`
Tripcode string `json:"trip"`
Timestamp int64 `json:"time"`
Bumped int64 `json:"last_modified"`
ThumbWidth int `json:"tn_w"`
ThumbHeight int `json:"tn_h"`
ImageWidth int `json:"w"`
ImageHeight int `json:"h"`
FileSize int `json:"fsize"`
OrigFilename string `json:"filename"`
Extension string `json:"ext"`
Filename string `json:"tim"`
FileChecksum string `json:"md5"`
}
type BoardPageJSON struct {
Threads []ThreadJSON `json:"threads"`
Page int `json:"page"`
}
type ThreadJSON struct {
*PostJSON
OmittedPosts int `json:"omitted_posts"`
OmittedImages int `json:"omitted_images"`
Replies int `json:"replies"`
ImagesOnArchive int `json:"images"`
Sticky int `json:"sticky"`
Locked int `json:"locked"`
}
// ErrorJSON and PostInfoJSON are mostly used for AJAX requests
type ErrorJSON struct {
Message string `json:"error"`
}
type Style struct {
Name string
Filename string

View file

@ -1,10 +1,9 @@
package main
import (
"bytes"
"crypto/md5"
"crypto/sha1"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
@ -14,13 +13,14 @@ import (
"net/url"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"github.com/nranchev/go-libGeoIP"
libgeo "github.com/nranchev/go-libGeoIP"
"golang.org/x/crypto/bcrypt"
)
@ -91,21 +91,13 @@ func byteByByteReplace(input, from, to string) string {
}
// for easier defer cleaning
func closeFile(file *os.File) {
if file != nil {
_ = file.Close()
}
type Closeable interface {
Close() error
}
func closeRows(rows *sql.Rows) {
if rows != nil {
_ = rows.Close()
}
}
func closeStatement(stmt *sql.Stmt) {
if stmt != nil {
_ = stmt.Close()
func closeHandle(handle Closeable) {
if handle != nil && !reflect.ValueOf(handle).IsNil() {
handle.Close()
}
}
@ -128,57 +120,9 @@ func deleteMatchingFiles(root, match string) (filesDeleted int, err error) {
return filesDeleted, err
}
// escapeString and escapeQuotes copied from github.com/ziutek/mymysql/native/codecs.go
func escapeString(txt string) string {
var (
esc string
buf bytes.Buffer
)
last := 0
for ii, bb := range txt {
switch bb {
case 0:
esc = `\0`
case '\n':
esc = `\n`
case '\r':
esc = `\r`
case '\\':
esc = `\\`
case '\'':
esc = `\'`
case '"':
esc = `\"`
case '\032':
esc = `\Z`
default:
continue
}
io.WriteString(&buf, txt[last:ii])
io.WriteString(&buf, esc)
last = ii + 1
}
io.WriteString(&buf, txt[last:])
return buf.String()
}
func escapeQuotes(txt string) string {
var buf bytes.Buffer
last := 0
for ii, bb := range txt {
if bb == '\'' {
io.WriteString(&buf, txt[last:ii])
io.WriteString(&buf, `''`)
last = ii + 1
}
}
io.WriteString(&buf, txt[last:])
return buf.String()
}
// getBoardArr performs a query against the database, and returns an array of BoardsTables along with an error value.
// If specified, the string where is added to the query, prefaced by WHERE. An example valid value is where = "id = 1".
func getBoardArr(parameterList map[string]interface{}, extra string) (boards []BoardsTable, err error) {
func getBoardArr(parameterList map[string]interface{}, extra string) (boards []Board, err error) {
queryString := "SELECT * FROM `" + config.DBprefix + "boards` "
numKeys := len(parameterList)
var parameterValues []interface{}
@ -199,7 +143,7 @@ func getBoardArr(parameterList map[string]interface{}, extra string) (boards []B
queryString += fmt.Sprintf(" %s ORDER BY `order`", extra)
rows, err := querySQL(queryString, parameterValues...)
defer closeRows(rows)
defer closeHandle(rows)
if err != nil {
handleError(0, "error getting board list: %s", customError(err))
return
@ -208,7 +152,7 @@ func getBoardArr(parameterList map[string]interface{}, extra string) (boards []B
// For each row in the results from the database, populate a new BoardsTable instance,
// then append it to the boards array we are going to return
for rows.Next() {
board := new(BoardsTable)
board := new(Board)
if err = rows.Scan(
&board.ID,
&board.Order,
@ -219,9 +163,8 @@ func getBoardArr(parameterList map[string]interface{}, extra string) (boards []B
&board.Subtitle,
&board.Description,
&board.Section,
&board.MaxImageSize,
&board.MaxFilesize,
&board.MaxPages,
&board.Locale,
&board.DefaultStyle,
&board.Locked,
&board.CreatedOn,
@ -244,30 +187,29 @@ func getBoardArr(parameterList map[string]interface{}, extra string) (boards []B
return
}
func getBoardFromID(id int) (*BoardsTable, error) {
board := new(BoardsTable)
func getBoardFromID(id int) (*Board, error) {
board := new(Board)
err := queryRowSQL(
"SELECT `order`,`dir`,`type`,`upload_type`,`title`,`subtitle`,`description`,`section`,"+
"`max_image_size`,`max_pages`,`locale`,`default_style`,`locked`,`created_on`,`anonymous`,`forced_anon`,`max_age`,"+
"`max_image_size`,`max_pages`,`default_style`,`locked`,`created_on`,`anonymous`,`forced_anon`,`max_age`,"+
"`autosage_after`,`no_images_after`,`max_message_length`,`embeds_allowed`,`redirect_to_thread`,`require_file`,"+
"`enable_catalog` FROM `"+config.DBprefix+"boards` WHERE `id` = ?",
[]interface{}{id},
[]interface{}{
&board.Order, &board.Dir, &board.Type, &board.UploadType, &board.Title,
&board.Subtitle, &board.Description, &board.Section, &board.MaxImageSize,
&board.MaxPages, &board.Locale, &board.DefaultStyle, &board.Locked, &board.CreatedOn,
&board.Subtitle, &board.Description, &board.Section, &board.MaxFilesize,
&board.MaxPages, &board.DefaultStyle, &board.Locked, &board.CreatedOn,
&board.Anonymous, &board.ForcedAnon, &board.MaxAge, &board.AutosageAfter,
&board.NoImagesAfter, &board.MaxMessageLength, &board.EmbedsAllowed,
&board.RedirectToThread, &board.RequireFile, &board.EnableCatalog,
},
)
board.ID = id
return board, err
}
// if parameterList is nil, ignore it and treat extra like a whole SQL query
func getPostArr(parameterList map[string]interface{}, extra string) (posts []PostTable, err error) {
func getPostArr(parameterList map[string]interface{}, extra string) (posts []Post, err error) {
queryString := "SELECT * FROM `" + config.DBprefix + "posts` "
numKeys := len(parameterList)
var parameterValues []interface{}
@ -287,22 +229,22 @@ func getPostArr(parameterList map[string]interface{}, extra string) (posts []Pos
queryString += " " + extra // " ORDER BY `order`"
rows, err := querySQL(queryString, parameterValues...)
defer closeRows(rows)
defer closeHandle(rows)
if err != nil {
handleError(1, customError(err))
return
}
// For each row in the results from the database, populate a new PostTable instance,
// then append it to the posts array we are going to return
// then append it to the posts array we are going to return
for rows.Next() {
var post PostTable
var post Post
if err = rows.Scan(&post.ID, &post.BoardID, &post.ParentID, &post.Name, &post.Tripcode,
&post.Email, &post.Subject, &post.MessageHTML, &post.MessageText, &post.Password, &post.Filename,
&post.FilenameOriginal, &post.FileChecksum, &post.Filesize, &post.ImageW,
&post.ImageH, &post.ThumbW, &post.ThumbH, &post.IP, &post.Tag, &post.Timestamp,
&post.Autosage, &post.PosterAuthority, &post.DeletedTimestamp, &post.Bumped,
&post.Stickied, &post.Locked, &post.Reviewed, &post.Sillytag,
&post.ImageH, &post.ThumbW, &post.ThumbH, &post.IP, &post.Capcode, &post.Timestamp,
&post.Autosage, &post.DeletedTimestamp, &post.Bumped, &post.Stickied, &post.Locked, &post.Reviewed,
); err != nil {
handleError(0, customError(err))
return
@ -313,19 +255,19 @@ func getPostArr(parameterList map[string]interface{}, extra string) (posts []Pos
}
// TODO: replace where with a map[string]interface{} like getBoardsArr()
func getSectionArr(where string) (sections []interface{}, err error) {
func getSectionArr(where string) (sections []BoardSection, err error) {
if where == "" {
where = "1"
}
rows, err := querySQL("SELECT * FROM `" + config.DBprefix + "sections` WHERE " + where + " ORDER BY `order`")
defer closeRows(rows)
defer closeHandle(rows)
if err != nil {
errorLog.Print(err.Error())
return
}
for rows.Next() {
section := new(BoardSectionsTable)
var section BoardSection
if err = rows.Scan(&section.ID, &section.Order, &section.Hidden, &section.Name, &section.Abbreviation); err != nil {
handleError(1, customError(err))
return
@ -392,6 +334,7 @@ func customError(err error) string {
func handleError(verbosity int, format string, a ...interface{}) string {
out := fmt.Sprintf(format, a...)
panic(out)
println(verbosity, out)
errorLog.Print(out)
return out
@ -566,36 +509,33 @@ func checkPostForSpam(userIP string, userAgent string, referrer string,
return "other_failure"
}
func makePostJSON(post PostTable, anonymous string) (postObj PostJSON) {
var filename string
var fileExt string
var origFilename string
// Separate out the extension from the filenames
if post.Filename != "deleted" && post.Filename != "" {
extStart := strings.LastIndex(post.Filename, ".")
fileExt = post.Filename[extStart:]
origExtStart := strings.LastIndex(post.FilenameOriginal, fileExt)
origFilename = post.FilenameOriginal[:origExtStart]
filename = post.Filename[:extStart]
func marshalAPI(tag string, data interface{}, indent bool) (string, error) {
var apiMap map[string]interface{}
var apiBytes []byte
var err error
if indent {
if tag == "" {
apiBytes, err = json.MarshalIndent(data, "", " ")
} else {
apiMap = make(map[string]interface{})
apiMap[tag] = data
apiBytes, err = json.MarshalIndent(apiMap, "", " ")
}
} else {
if tag == "" {
apiBytes, err = json.Marshal(data)
} else {
apiMap := make(map[string]interface{})
apiMap[tag] = data
apiBytes, err = json.Marshal(apiMap)
}
}
postObj = PostJSON{ID: post.ID, ParentID: post.ParentID, Subject: post.Subject, Message: post.MessageHTML,
Name: post.Name, Timestamp: post.Timestamp.Unix(), Bumped: post.Bumped.Unix(),
ThumbWidth: post.ThumbW, ThumbHeight: post.ThumbH, ImageWidth: post.ImageW, ImageHeight: post.ImageH,
FileSize: post.Filesize, OrigFilename: origFilename, Extension: fileExt, Filename: filename, FileChecksum: post.FileChecksum}
// Handle Anonymous
if post.Name == "" {
postObj.Name = anonymous
if err != nil {
apiBytes, _ = json.Marshal(map[string]string{
"error": err.Error(),
})
}
// If we have a Tripcode, prepend a !
if post.Tripcode != "" {
postObj.Tripcode = "!" + post.Tripcode
}
return
return string(apiBytes), err
}
func limitArraySize(arr []string, maxSize int) []string {

View file

@ -1,24 +1,19 @@
#!/bin/bash
# Vagrant provision script
# I guess I should give credit where credit is due since a fair amount
# of this script was was originally written by Macil (as far as I know) for Ponychan
# https://github.com/MacilPrime/ponychan
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
export GOCHAN_PATH=/home/vagrant/gochan
export GOPATH=/vagrant/lib
export DBTYPE=mysql
apt-get update
apt-get -y update && apt-get -y upgrade
if [ "$DBTYPE" == "mysql" ]; then
apt-get -y install mariadb-server mariadb-client
# Make sure any imported database is utf8mb4
# http://mathiasbynens.be/notes/mysql-utf8mb4
# Put in /etc/mysql/conf.d/local.cnf
cat - <<EOF123 >/etc/mysql/conf.d/local.cnf
cat << EOF >/etc/mysql/conf.d/local.cnf
[client]
default-character-set = utf8mb4
@ -30,7 +25,7 @@ character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
default-storage-engine = innodb
EOF123
EOF
mysql -uroot -e "CREATE DATABASE IF NOT EXISTS gochan; \
GRANT USAGE ON *.* TO gochan IDENTIFIED BY ''; \
@ -38,10 +33,10 @@ GRANT ALL PRIVILEGES ON gochan.* TO gochan; \
SET PASSWORD FOR 'gochan'@'%' = PASSWORD('gochan');
FLUSH PRIVILEGES;"
cat - <<EOF123 >/etc/mysql/conf.d/open.cnf
cat << EOF >/etc/mysql/conf.d/open.cnf
[mysqld]
bind-address = 0.0.0.0
EOF123
EOF
elif [ "$DBTYPE" == "postgresql" ]; then
# apt-get -y install postgresql postgresql-contrib
# useradd gochan
@ -61,7 +56,6 @@ else
fi
apt-get -y install git subversion mercurial golang-1.10 nginx ffmpeg
apt-get -y upgrade
rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/*
ln -sf /vagrant/gochan-fastcgi.nginx /etc/nginx/sites-available/gochan.nginx
@ -76,11 +70,10 @@ sed -i 's/WantedBy=multi-user.target/WantedBy=vagrant.mount/' /lib/systemd/syste
systemctl daemon-reload
systemctl enable nginx
systemctl restart nginx mysql &
wait
mkdir -p /vagrant/lib || true
mkdir -p /vagrant/lib
cd /vagrant
su - vagrant
export GOCHAN_PATH=/home/vagrant/gochan
@ -98,64 +91,64 @@ function makeLink {
ln -sf /vagrant/$1 $GOCHAN_PATH/$1
}
cat - <<EOF >>/home/vagrant/.bashrc
cat << EOF >>/home/vagrant/.bashrc
export GOPATH=/vagrant/lib
export GOCHAN_PATH=/home/vagrant/gochan
EOF
# a couple convenience shell scripts, since they're nice to have
cat - <<EOF >/home/vagrant/dbconnect.sh
cat << EOF >/home/vagrant/dbconnect.sh
#!/usr/bin/env bash
mysql -s -t -u gochan -D gochan -pgochan
EOF
cat - <<EOF >/home/vagrant/buildgochan.sh
cat << EOF >/home/vagrant/buildgochan.sh
#!/usr/bin/env bash
cd /vagrant && make debug && cd ~/gochan && ./gochan
EOF
chmod +x /home/vagrant/dbconnect.sh
chmod +x /home/vagrant/buildgochan.sh
sudo chown vagrant:vagrant /home/vagrant/dbconnect.sh /home/vagrant/buildgochan.sh
sudo chown -R vagrant:vagrant /home/vagrant/bin
go get github.com/disintegration/imaging
go get github.com/nranchev/go-libGeoIP
#go get github.com/nyarla/go-crypt
go get github.com/go-sql-driver/mysql
go get github.com/lib/pq
go get golang.org/x/crypto/bcrypt
go get github.com/frustra/bbcode
go get \
github.com/disintegration/imaging \
github.com/nranchev/go-libGeoIP \
github.com/go-sql-driver/mysql \
github.com/lib/pq \
golang.org/x/net/html \
github.com/aquilax/tripcode \
golang.org/x/crypto/bcrypt \
github.com/frustra/bbcode
make debug
rm -f $GOCHAN_PATH/gochan
rm -f $GOCHAN_PATH/initialsetupdb.sql
rm -f $GOCHAN_PATH/initdb.sql
install -m 775 -o vagrant -g vagrant -d $GOCHAN_PATH
makeLink html
makeLink log
makeLink gochan
makeLink templates
makeLink initialsetupdb.sql
makeLink initdb.sql
changePerms $GOCHAN_PATH
if [ ! -e "$GOCHAN_PATH/gochan.json" ]; then
install -m 775 -o vagrant -g vagrant -T /vagrant/gochan.example.json $GOCHAN_PATH/gochan.json
fi
mkdir -p /home/vagrant/.config/systemd/user/
ln -s /vagrant/gochan.service /home/vagrant/.config/systemd/user/gochan.service
sed -e 's/"Port": 8080,/"Port": 9000,/' -i $GOCHAN_PATH/gochan.json
sed -e 's/"UseFastCGI": false,/"UseFastCGI": true,/' -i /$GOCHAN_PATH/gochan.json
sed -e 's/"DomainRegex": ".*",/"DomainRegex": "(https|http):\\\/\\\/(.*)\\\/(.*)",/' -i $GOCHAN_PATH/gochan.json
sed -e 's/"DBpassword": ""/"DBpassword": "gochan"/' -i /home/vagrant/gochan/gochan.json
sed -e 's/"RandomSeed": ""/"RandomSeed": "abc123"/' -i $GOCHAN_PATH/gochan.json
sed /vagrant/gochan.example.json \
-e 's/"Port": 8080,/"Port": 9000,/' \
-e 's/"UseFastCGI": false,/"UseFastCGI": true,/' \
-e 's/"DomainRegex": ".*",/"DomainRegex": "(https|http):\\\/\\\/(.*)\\\/(.*)",/' \
-e 's/"DBpassword": ""/"DBpassword": "gochan"/' \
-e 's/"RandomSeed": ""/"RandomSeed": "abc123"/' \
-e 's/"Verbosity": 0/"Verbosity": 1/' \
-e w\ $GOCHAN_PATH/gochan.json
echo
echo "Server set up, please run \"vagrant ssh\" on your host machine, and"
echo "Server set up, please run \"vagrant ssh\" on your host machine and"
echo "(optionally) \"systemctl --user start gochan\" in the guest."
echo "Then browse to http://172.27.0.3/manage to complete installation"
echo "If you want gochan to run on system startup run \"systemctl --user enable gochan\""
echo "Then browse to http://172.27.0.3/manage to complete installation."
# echo "If you want gochan to run on system startup run \"systemctl --user enable gochan\""
# TODO: add further instructions as default initial announcement or /manage?action=firstrun

View file

@ -1 +1 @@
2.6.1
2.7.0