You can now search for whatever IP you want in the IP Searcher.

Removed the Uncategorised Forum.
Added the Backup Page for super admins. Not quite functional yet.
Forums are now sorted properly again.
Fixed a bug in DirtyGet() where invalid IDs would trigger a panic.
Fixed a bug where alternate themes wouldn't work without setting them as default first and restarting Gosora.
This commit is contained in:
Azareal 2017-09-23 20:57:13 +01:00
parent d869b87aa1
commit 11c60b3cbe
24 changed files with 307 additions and 91 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@ brun.bat
uploads/avatar_* uploads/avatar_*
uploads/socialgroup_* uploads/socialgroup_*
backups/*.sql
bin/* bin/*
out/* out/*
*.exe *.exe

1
backups/filler.txt Normal file
View File

@ -0,0 +1 @@
This file is here so that Git will include this folder in the repository.

View File

@ -39,7 +39,7 @@ func init() {
config.DefaultGroup = 3 // Should be a setting in the database config.DefaultGroup = 3 // Should be a setting in the database
config.ActivationGroup = 5 // Should be a setting in the database config.ActivationGroup = 5 // Should be a setting in the database
config.StaffCSS = "staff_post" config.StaffCSS = "staff_post"
config.UncategorisedForumVisible = true config.DefaultForum = 2
config.MinifyTemplates = false config.MinifyTemplates = false
config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features

View File

@ -39,6 +39,20 @@ type ForumSimple struct {
Preset string Preset string
} }
// TODO: Replace this sorting mechanism with something a lot more efficient
// ? - Use sort.Slice instead?
type SortForum []*Forum
func (sf SortForum) Len() int {
return len(sf)
}
func (sf SortForum) Swap(i, j int) {
sf[i], sf[j] = sf[j], sf[i]
}
func (sf SortForum) Less(i, j int) bool {
return sf[i].ID < sf[j].ID
}
func buildForumURL(slug string, fid int) string { func buildForumURL(slug string, fid int) string {
if slug == "" { if slug == "" {
return "/forum/" + strconv.Itoa(fid) return "/forum/" + strconv.Itoa(fid)

View File

@ -9,6 +9,7 @@ package main
import ( import (
"database/sql" "database/sql"
"log" "log"
"sort"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -104,8 +105,6 @@ func (mfs *MemoryForumStore) LoadForums() error {
} }
} }
addForum(&Forum{0, buildForumURL(nameToSlug("Uncategorised"), 0), "Uncategorised", "", config.UncategorisedForumVisible, "all", 0, "", 0, "", "", 0, "", 0, ""})
rows, err := getForumsStmt.Query() rows, err := getForumsStmt.Query()
if err != nil { if err != nil {
return err return err
@ -148,16 +147,16 @@ func (mfs *MemoryForumStore) rebuildView() {
} }
return true return true
}) })
sort.Sort(SortForum(forumView))
mfs.forumView.Store(forumView) mfs.forumView.Store(forumView)
} }
func (mfs *MemoryForumStore) DirtyGet(id int) *Forum { func (mfs *MemoryForumStore) DirtyGet(id int) *Forum {
fint, ok := mfs.forums.Load(id) fint, ok := mfs.forums.Load(id)
forum := fint.(*Forum) if !ok || fint.(*Forum).Name == "" {
if !ok || forum.Name == "" {
return &Forum{ID: -1, Name: ""} return &Forum{ID: -1, Name: ""}
} }
return forum return fint.(*Forum)
} }
func (mfs *MemoryForumStore) CacheGet(id int) (*Forum, error) { func (mfs *MemoryForumStore) CacheGet(id int) (*Forum, error) {
@ -225,24 +224,29 @@ func (mfs *MemoryForumStore) CacheSet(forum *Forum) error {
return nil return nil
} }
// ! Has a randomised order
func (mfs *MemoryForumStore) GetAll() (forumView []*Forum, err error) { func (mfs *MemoryForumStore) GetAll() (forumView []*Forum, err error) {
mfs.forums.Range(func(_ interface{}, value interface{}) bool { mfs.forums.Range(func(_ interface{}, value interface{}) bool {
forumView = append(forumView, value.(*Forum)) forumView = append(forumView, value.(*Forum))
return true return true
}) })
sort.Sort(SortForum(forumView))
return forumView, nil return forumView, nil
} }
// ? - Can we optimise the sorting?
func (mfs *MemoryForumStore) GetAllIDs() (ids []int, err error) { func (mfs *MemoryForumStore) GetAllIDs() (ids []int, err error) {
mfs.forums.Range(func(_ interface{}, value interface{}) bool { mfs.forums.Range(func(_ interface{}, value interface{}) bool {
ids = append(ids, value.(*Forum).ID) ids = append(ids, value.(*Forum).ID)
return true return true
}) })
sort.Ints(ids)
return ids, nil return ids, nil
} }
func (mfs *MemoryForumStore) GetAllVisible() ([]*Forum, error) { func (mfs *MemoryForumStore) GetAllVisible() (forumView []*Forum, err error) {
return mfs.forumView.Load().([]*Forum), nil forumView = mfs.forumView.Load().([]*Forum)
return forumView, nil
} }
func (mfs *MemoryForumStore) GetAllVisibleIDs() ([]int, error) { func (mfs *MemoryForumStore) GetAllVisibleIDs() ([]int, error) {
@ -285,6 +289,7 @@ func (mfs *MemoryForumStore) Delete(id int) error {
return nil return nil
} }
// ! Is this racey?
func (mfs *MemoryForumStore) IncrementTopicCount(id int) error { func (mfs *MemoryForumStore) IncrementTopicCount(id int) error {
forum, err := mfs.Get(id) forum, err := mfs.Get(id)
if err != nil { if err != nil {
@ -298,6 +303,7 @@ func (mfs *MemoryForumStore) IncrementTopicCount(id int) error {
return nil return nil
} }
// ! Is this racey?
func (mfs *MemoryForumStore) DecrementTopicCount(id int) error { func (mfs *MemoryForumStore) DecrementTopicCount(id int) error {
forum, err := mfs.Get(id) forum, err := mfs.Get(id)
if err != nil { if err != nil {
@ -312,6 +318,7 @@ func (mfs *MemoryForumStore) DecrementTopicCount(id int) error {
} }
// TODO: Have a pointer to the last topic rather than storing it on the forum itself // TODO: Have a pointer to the last topic rather than storing it on the forum itself
// ! Is this racey?
func (mfs *MemoryForumStore) UpdateLastTopic(topicName string, tid int, username string, uid int, time string, fid int) error { func (mfs *MemoryForumStore) UpdateLastTopic(topicName string, tid int, username string, uid int, time string, fid int) error {
forum, err := mfs.Get(fid) forum, err := mfs.Get(fid)
if err != nil { if err != nil {

View File

@ -214,6 +214,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
case "/panel/groups/create/": case "/panel/groups/create/":
routePanelGroupsCreateSubmit(w,req,user) routePanelGroupsCreateSubmit(w,req,user)
return return
case "/panel/backups/":
routePanelBackups(w,req,user,extra_data)
return
case "/panel/logs/mod/": case "/panel/logs/mod/":
routePanelLogsMod(w,req,user) routePanelLogsMod(w,req,user)
return return

View File

@ -183,7 +183,7 @@ config.DefaultRoute = routeTopics
config.DefaultGroup = 3 // Should be a setting in the database config.DefaultGroup = 3 // Should be a setting in the database
config.ActivationGroup = 5 // Should be a setting in the database config.ActivationGroup = 5 // Should be a setting in the database
config.StaffCSS = "staff_post" config.StaffCSS = "staff_post"
config.UncategorisedForumVisible = true config.DefaultForum = 2
config.MinifyTemplates = true config.MinifyTemplates = true
config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features

17
main.go
View File

@ -12,7 +12,6 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
//"runtime/pprof" //"runtime/pprof"
) )
@ -41,16 +40,6 @@ var externalSites = map[string]string{
var staticFiles = make(map[string]SFile) var staticFiles = make(map[string]SFile)
var logWriter = io.MultiWriter(os.Stderr) var logWriter = io.MultiWriter(os.Stderr)
func processConfig() {
config.Noavatar = strings.Replace(config.Noavatar, "{site_url}", site.URL, -1)
if site.Port != "80" && site.Port != "443" {
site.URL = strings.TrimSuffix(site.URL, "/")
site.URL = strings.TrimSuffix(site.URL, "\\")
site.URL = strings.TrimSuffix(site.URL, ":")
site.URL = site.URL + ":" + site.Port
}
}
func main() { func main() {
// TODO: Have a file for each run with the time/date the server started as the file name? // TODO: Have a file for each run with the time/date the server started as the file name?
// TODO: Log panics with recover() // TODO: Log panics with recover()
@ -121,6 +110,11 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
err = verifyConfig()
if err != nil {
log.Fatal(err)
}
// Run this goroutine once a second // Run this goroutine once a second
secondTicker := time.NewTicker(1 * time.Second) secondTicker := time.NewTicker(1 * time.Second)
fifteenMinuteTicker := time.NewTicker(15 * time.Minute) fifteenMinuteTicker := time.NewTicker(15 * time.Minute)
@ -149,6 +143,7 @@ func main() {
// TODO: Manage the TopicStore, UserStore, and ForumStore // TODO: Manage the TopicStore, UserStore, and ForumStore
// TODO: Alert the admin, if CPU usage, RAM usage, or the number of posts in the past second are too high // TODO: Alert the admin, if CPU usage, RAM usage, or the number of posts in the past second are too high
// TODO: Clean-up alerts with no unread matches which are over two weeks old. Move this to a 24 hour task? // TODO: Clean-up alerts with no unread matches which are over two weeks old. Move this to a 24 hour task?
// TODO: Rescan the static files for changes
// TODO: Add a plugin hook here // TODO: Add a plugin hook here
case <-fifteenMinuteTicker.C: case <-fifteenMinuteTicker.C:

View File

@ -28,6 +28,9 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid st
return return
} }
} }
if fid == 0 {
fid = config.DefaultForum
}
headerVars, ok := ForumUserCheck(w, r, &user, fid) headerVars, ok := ForumUserCheck(w, r, &user, fid)
if !ok { if !ok {

View File

@ -574,7 +574,7 @@ func routeIps(w http.ResponseWriter, r *http.Request, user User) {
return return
} }
ip := html.EscapeString(r.URL.Path[len("/users/ips/"):]) ip := r.FormValue("ip")
var uid int var uid int
var reqUserList = make(map[int]bool) var reqUserList = make(map[int]bool)

View File

@ -8,6 +8,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
) )
type HeaderVars struct { type HeaderVars struct {
@ -227,7 +228,23 @@ type PanelEditGroupPermsPage struct {
GlobalPerms []NameLangToggle GlobalPerms []NameLangToggle
} }
type Log struct { type backupItem struct {
SQLURL string
// TODO: Add an easier to parse format here for Gosora to be able to more easily reimport portions of the dump and to strip unneccesary data (e.g. table defs and parsed post data)
Timestamp time.Time
}
type PanelBackupPage struct {
Title string
CurrentUser User
Header *HeaderVars
Stats PanelStats
Backups []backupItem
}
type logItem struct {
Action template.HTML Action template.HTML
IPAddress string IPAddress string
DoneAt string DoneAt string
@ -238,7 +255,7 @@ type PanelLogsPage struct {
CurrentUser User CurrentUser User
Header *HeaderVars Header *HeaderVars
Stats PanelStats Stats PanelStats
Logs []Log Logs []logItem
PageList []int PageList []int
Page int Page int
LastPage int LastPage int
@ -283,6 +300,7 @@ func init() {
urlReg = regexp.MustCompile(urlpattern) urlReg = regexp.MustCompile(urlpattern)
} }
// TODO: Write a test for this
func shortcodeToUnicode(msg string) string { func shortcodeToUnicode(msg string) string {
//re := regexp.MustCompile(":(.):") //re := regexp.MustCompile(":(.):")
msg = strings.Replace(msg, ":grinning:", "😀", -1) msg = strings.Replace(msg, ":grinning:", "😀", -1)
@ -421,6 +439,7 @@ func preparseMessage(msg string) string {
return shortcodeToUnicode(msg) return shortcodeToUnicode(msg)
} }
// TODO: Write a test for this
func parseMessage(msg string /*, user User*/) string { func parseMessage(msg string /*, user User*/) string {
msg = strings.Replace(msg, ":)", "😀", -1) msg = strings.Replace(msg, ":)", "😀", -1)
msg = strings.Replace(msg, ":(", "😞", -1) msg = strings.Replace(msg, ":(", "😞", -1)
@ -634,6 +653,7 @@ func parseMessage(msg string /*, user User*/) string {
return msg return msg
} }
// TODO: Write a test for this
func regexParseMessage(msg string) string { func regexParseMessage(msg string) string {
msg = strings.Replace(msg, ":)", "😀", -1) msg = strings.Replace(msg, ":)", "😀", -1)
msg = strings.Replace(msg, ":D", "😃", -1) msg = strings.Replace(msg, ":D", "😃", -1)
@ -648,6 +668,7 @@ func regexParseMessage(msg string) string {
// 6, 7, 8, 6, 7 // 6, 7, 8, 6, 7
// ftp://, http://, https:// git://, mailto: (not a URL, just here for length comparison purposes) // ftp://, http://, https:// git://, mailto: (not a URL, just here for length comparison purposes)
// TODO: Write a test for this
func validateURLBytes(data []byte) bool { func validateURLBytes(data []byte) bool {
datalen := len(data) datalen := len(data)
i := 0 i := 0
@ -670,6 +691,7 @@ func validateURLBytes(data []byte) bool {
return true return true
} }
// TODO: Write a test for this
func validatedURLBytes(data []byte) (url []byte) { func validatedURLBytes(data []byte) (url []byte) {
datalen := len(data) datalen := len(data)
i := 0 i := 0
@ -694,6 +716,7 @@ func validatedURLBytes(data []byte) (url []byte) {
return url return url
} }
// TODO: Write a test for this
func partialURLBytes(data []byte) (url []byte) { func partialURLBytes(data []byte) (url []byte) {
datalen := len(data) datalen := len(data)
i := 0 i := 0
@ -719,6 +742,7 @@ func partialURLBytes(data []byte) (url []byte) {
return url return url
} }
// TODO: Write a test for this
func partialURLBytesLen(data []byte) int { func partialURLBytesLen(data []byte) int {
datalen := len(data) datalen := len(data)
i := 0 i := 0
@ -745,6 +769,7 @@ func partialURLBytesLen(data []byte) int {
return datalen return datalen
} }
// TODO: Write a test for this
func parseMediaBytes(data []byte) (protocol []byte, url []byte) { func parseMediaBytes(data []byte) (protocol []byte, url []byte) {
datalen := len(data) datalen := len(data)
i := 0 i := 0
@ -774,6 +799,7 @@ func parseMediaBytes(data []byte) (protocol []byte, url []byte) {
return protocol, data[i:] return protocol, data[i:]
} }
// TODO: Write a test for this
func coerceIntBytes(data []byte) (res int, length int) { func coerceIntBytes(data []byte) (res int, length int) {
if !(data[0] > 47 && data[0] < 58) { if !(data[0] > 47 && data[0] < 58) {
return 0, 1 return 0, 1

View File

@ -6,8 +6,11 @@ import (
"fmt" "fmt"
"html" "html"
"html/template" "html/template"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -190,6 +193,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user User) {
return return
} }
// TODO: Paginate this?
var forumList []interface{} var forumList []interface{}
forums, err := fstore.GetAll() forums, err := fstore.GetAll()
if err != nil { if err != nil {
@ -197,6 +201,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user User) {
return return
} }
// ? - Should we generate something similar to the forumView? It might be a little overkill for a page which is rarely loaded in comparison to /forums/
for _, forum := range forums { for _, forum := range forums {
if forum.Name != "" && forum.ParentID == 0 { if forum.Name != "" && forum.ParentID == 0 {
fadmin := ForumAdmin{forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, forum.TopicCount, presetToLang(forum.Preset)} fadmin := ForumAdmin{forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, forum.TopicCount, presetToLang(forum.Preset)}
@ -1940,6 +1945,59 @@ func routePanelThemesSetDefault(w http.ResponseWriter, r *http.Request, user Use
http.Redirect(w, r, "/panel/themes/", http.StatusSeeOther) http.Redirect(w, r, "/panel/themes/", http.StatusSeeOther)
} }
func routePanelBackups(w http.ResponseWriter, r *http.Request, user User, backupURL string) {
headerVars, stats, ok := PanelUserCheck(w, r, &user)
if !ok {
return
}
if !user.IsSuperAdmin {
NoPermissions(w, r, user)
return
}
if backupURL != "" {
// We don't want them trying to break out of this directory, it shouldn't hurt since it's a super admin, but it's always good to practice good security hygiene, especially if this is one of many instances on a managed server not controlled by the superadmin/s
backupURL = Stripslashes(backupURL)
var ext = filepath.Ext("./backups/" + backupURL)
if ext == ".sql" {
info, err := os.Stat("./backups/" + backupURL)
if err != nil {
NotFound(w, r)
return
}
// TODO: Change the served filename to gosora_backup_%timestamp%.sql, the time the file was generated, not when it was modified aka what the name of it should be
w.Header().Set("Content-Disposition", "attachment; filename=gosora_backup.sql")
w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10))
// TODO: Fix the problem where non-existent files aren't greeted with custom 404s on ServeFile()'s side
http.ServeFile(w, r, "./backups/"+backupURL)
return
}
NotFound(w, r)
return
}
var backupList []backupItem
backupFiles, err := ioutil.ReadDir("./backups")
if err != nil {
InternalError(err, w)
return
}
for _, backupFile := range backupFiles {
var ext = filepath.Ext(backupFile.Name())
if ext != ".sql" {
continue
}
backupList = append(backupList, backupItem{backupFile.Name(), backupFile.ModTime()})
}
pi := PanelBackupPage{"Backups", user, headerVars, stats, backupList}
err = templates.ExecuteTemplate(w, "panel-backups.html", pi)
if err != nil {
InternalError(err, w)
}
}
func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) { func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) {
headerVars, stats, ok := PanelUserCheck(w, r, &user) headerVars, stats, ok := PanelUserCheck(w, r, &user)
if !ok { if !ok {
@ -1964,7 +2022,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) {
} }
defer rows.Close() defer rows.Close()
var logs []Log var logs []logItem
var action, elementType, ipaddress, doneAt string var action, elementType, ipaddress, doneAt string
var elementID, actorID int var elementID, actorID int
for rows.Next() { for rows.Next() {
@ -2035,7 +2093,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) {
default: default:
action = "Unknown action '" + action + "' by <a href='" + actor.Link + "'>" + actor.Name + "</a>" action = "Unknown action '" + action + "' by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
} }
logs = append(logs, Log{Action: template.HTML(action), IPAddress: ipaddress, DoneAt: doneAt}) logs = append(logs, logItem{Action: template.HTML(action), IPAddress: ipaddress, DoneAt: doneAt})
} }
err = rows.Err() err = rows.Err()
if err != nil { if err != nil {

View File

@ -81,6 +81,7 @@ func routes() {
Route{"routePanelGroupsEditPermsSubmit", "/panel/groups/edit/perms/submit/", "", []string{"extra_data"}}, Route{"routePanelGroupsEditPermsSubmit", "/panel/groups/edit/perms/submit/", "", []string{"extra_data"}},
Route{"routePanelGroupsCreateSubmit", "/panel/groups/create/", "", []string{}}, Route{"routePanelGroupsCreateSubmit", "/panel/groups/create/", "", []string{}},
Route{"routePanelBackups", "/panel/backups/", "", []string{"extra_data"}},
Route{"routePanelLogsMod", "/panel/logs/mod/", "", []string{}}, Route{"routePanelLogsMod", "/panel/logs/mod/", "", []string{}},
Route{"routePanelDebug", "/panel/debug/", "", []string{}}, Route{"routePanelDebug", "/panel/debug/", "", []string{}},
) )

View File

@ -45,6 +45,9 @@ func routeStatic(w http.ResponseWriter, r *http.Request) {
//log.Print("Outputting static file '" + r.URL.Path + "'") //log.Print("Outputting static file '" + r.URL.Path + "'")
file, ok := staticFiles[r.URL.Path] file, ok := staticFiles[r.URL.Path]
if !ok { if !ok {
if dev.DebugMode {
log.Print("Failed to find '" + r.URL.Path + "'")
}
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
@ -75,14 +78,9 @@ func routeStatic(w http.ResponseWriter, r *http.Request) {
} }
// Deprecated: Test route for stopping the server during a performance analysis // Deprecated: Test route for stopping the server during a performance analysis
/*func route_exit(w http.ResponseWriter, r *http.Request){ /*func routeExit(w http.ResponseWriter, r *http.Request, user User){
db.Close() db.Close()
os.Exit(0) os.Exit(0)
}
// Deprecated: Test route to see which file serving method is faster
func route_fstatic(w http.ResponseWriter, r *http.Request){
http.ServeFile(w,r,r.URL.Path)
}*/ }*/
// TODO: Make this a static file somehow? Is it possible for us to put this file somewhere else? // TODO: Make this a static file somehow? Is it possible for us to put this file somewhere else?

39
site.go
View File

@ -1,6 +1,10 @@
package main package main
import "net/http" import (
"errors"
"net/http"
"strings"
)
var site = &Site{Name: "Magical Fairy Land", Language: "english"} var site = &Site{Name: "Magical Fairy Land", Language: "english"}
var dbConfig = DBConfig{Host: "localhost"} var dbConfig = DBConfig{Host: "localhost"}
@ -8,7 +12,7 @@ var config Config
var dev DevConfig var dev DevConfig
type Site struct { type Site struct {
Name string // ? - Move this into the settings table? Name string // ? - Move this into the settings table? Should we make a second version of this for the abbreviation shown in the navbar?
Email string // ? - Move this into the settings table? Email string // ? - Move this into the settings table?
URL string URL string
Port string Port string
@ -40,13 +44,13 @@ type Config struct {
SMTPPassword string SMTPPassword string
SMTPPort string SMTPPort string
DefaultRoute func(http.ResponseWriter, *http.Request, User) DefaultRoute func(http.ResponseWriter, *http.Request, User)
DefaultGroup int DefaultGroup int
ActivationGroup int ActivationGroup int
StaffCSS string // ? - Move this into the settings table? Might be better to implement this as Group CSS StaffCSS string // ? - Move this into the settings table? Might be better to implement this as Group CSS
UncategorisedForumVisible bool DefaultForum int // The forum posts go in by default, this used to be covered by the Uncategorised Forum, but we want to replace it with a more robust solution. Make this a setting?
MinifyTemplates bool MinifyTemplates bool
MultiServer bool MultiServer bool
Noavatar string // ? - Move this into the settings table? Noavatar string // ? - Move this into the settings table?
ItemsPerPage int // ? - Move this into the settings table? ItemsPerPage int // ? - Move this into the settings table?
@ -57,3 +61,20 @@ type DevConfig struct {
SuperDebug bool SuperDebug bool
Profiling bool Profiling bool
} }
func processConfig() {
config.Noavatar = strings.Replace(config.Noavatar, "{site_url}", site.URL, -1)
if site.Port != "80" && site.Port != "443" {
site.URL = strings.TrimSuffix(site.URL, "/")
site.URL = strings.TrimSuffix(site.URL, "\\")
site.URL = strings.TrimSuffix(site.URL, ":")
site.URL = site.URL + ":" + site.Port
}
}
func verifyConfig() error {
if !fstore.Exists(config.DefaultForum) {
return errors.New("Invalid default forum")
}
return nil
}

View File

@ -134,9 +134,9 @@ var topic_28 = []byte(`</a>&nbsp;&nbsp;
`) `)
var topic_29 = []byte(`<a href="/topic/like/submit/`) var topic_29 = []byte(`<a href="/topic/like/submit/`)
var topic_30 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"> var topic_30 = []byte(`" class="mod_button" title="Love it" style="color:#202020;">
<button class="username like_label" style="`) <button class="username like_label"`)
var topic_31 = []byte(`background-color:/*#eaffea*/#D6FFD6;`) var topic_31 = []byte(` style="background-color:#D6FFD6;"`)
var topic_32 = []byte(`"></button></a>`) var topic_32 = []byte(`></button></a>`)
var topic_33 = []byte(`<a href='/topic/edit/`) var topic_33 = []byte(`<a href='/topic/edit/`)
var topic_34 = []byte(`' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic"><button class="username edit_label"></button></a>`) var topic_34 = []byte(`' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic"><button class="username edit_label"></button></a>`)
var topic_35 = []byte(`<a href='/topic/delete/submit/`) var topic_35 = []byte(`<a href='/topic/delete/submit/`)
@ -149,7 +149,7 @@ var topic_41 = []byte(`<a class="mod_button" href='/topic/unstick/submit/`)
var topic_42 = []byte(`' style="font-weight:normal;" title="Unpin Topic"><button class="username unpin_label"></button></a>`) var topic_42 = []byte(`' style="font-weight:normal;" title="Unpin Topic"><button class="username unpin_label"></button></a>`)
var topic_43 = []byte(`<a href='/topic/stick/submit/`) var topic_43 = []byte(`<a href='/topic/stick/submit/`)
var topic_44 = []byte(`' class="mod_button" style="font-weight:normal;" title="Pin Topic"><button class="username pin_label"></button></a>`) var topic_44 = []byte(`' class="mod_button" style="font-weight:normal;" title="Pin Topic"><button class="username pin_label"></button></a>`)
var topic_45 = []byte(`<a class="mod_button" href='/users/ips/`) var topic_45 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_46 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`) var topic_46 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_47 = []byte(` var topic_47 = []byte(`
<a href="/report/submit/`) <a href="/report/submit/`)
@ -195,14 +195,14 @@ var topic_68 = []byte(`" class="username real_username">`)
var topic_69 = []byte(`</a>&nbsp;&nbsp; var topic_69 = []byte(`</a>&nbsp;&nbsp;
`) `)
var topic_70 = []byte(`<a href="/reply/like/submit/`) var topic_70 = []byte(`<a href="/reply/like/submit/`)
var topic_71 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label" style="`) var topic_71 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`)
var topic_72 = []byte(`background-color:/*#eaffea*/#D6FFD6;`) var topic_72 = []byte(` style="background-color:#D6FFD6;"`)
var topic_73 = []byte(`"></button></a>`) var topic_73 = []byte(`></button></a>`)
var topic_74 = []byte(`<a href="/reply/edit/submit/`) var topic_74 = []byte(`<a href="/reply/edit/submit/`)
var topic_75 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`) var topic_75 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`)
var topic_76 = []byte(`<a href="/reply/delete/submit/`) var topic_76 = []byte(`<a href="/reply/delete/submit/`)
var topic_77 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`) var topic_77 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`)
var topic_78 = []byte(`<a class="mod_button" href='/users/ips/`) var topic_78 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_79 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`) var topic_79 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_80 = []byte(` var topic_80 = []byte(`
<a href="/report/submit/`) <a href="/report/submit/`)

View File

@ -1,18 +1,28 @@
{{template "header.html" . }} {{template "header.html" . }}
<main> <main>
<div class="rowblock opthead"> <div class="rowblock rowhead">
<div class="rowitem"><a>IP Search</a></div> <div class="rowitem">
<h1>IP Search</h1>
</div>
</div> </div>
<div class="rowblock">
<div class="rowitem passive">Searching for {{.IP}}</div> <form action="/users/ips/" method="get" id="ip-search-form"></form>
<div class="rowblock ip_search_block">
<div class="rowitem passive">
<input form="ip-search-form" name="ip" class="ip_search_input" type="search" placeholder="🔍︎" {{if .IP}}value="{{.IP}}"{{end}}/>
<input form="ip-search-form" class="ip_search_search" type="submit" value="Search" />
</div>
</div> </div>
<div class="rowblock rowlist bgavatars">
{{range .ItemList}}<div class="rowitem" style="{{if .Avatar}}background-image: url('{{.Avatar}}');{{end}}"> {{if .IP}}
<div class="rowblock bgavatars">
{{range .ItemList}}<div class="rowitem"{{if .Avatar}} style="background-image: url('{{.Avatar}}');"{{end}}>
<a href="{{.Link}}">{{.Name}}</a> <a href="{{.Link}}">{{.Name}}</a>
</div> </div>
{{else}}<div class="rowitem passive">No users found.</div>{{end}} {{else}}<div class="rowitem">No users found.</div>{{end}}
</div> </div>
{{end}}
</main> </main>
{{template "footer.html" . }} {{template "footer.html" . }}

View File

@ -0,0 +1,20 @@
{{template "header.html" . }}
{{template "panel-menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>Backups</h1></div>
</div>
<div id="panel_backups" class="colstack_item rowlist">
{{range .Backups}}
<div class="rowitem panel_compactrow">
<span>{{.SQLURL}}</span>
<span class="panel_floater">
<a href="/panel/backups/{{.SQLURL}}" class="panel_tag panel_right_button">Download</a>
</span>
</div>
{{else}}
<div class="rowitem">There aren't any backups available at this time.</div>
{{end}}
</div>
</main>
{{template "footer.html" . }}

View File

@ -11,14 +11,14 @@
</div> </div>
<div id="panel_forums" class="colstack_item rowlist"> <div id="panel_forums" class="colstack_item rowlist">
{{range .ItemList}} {{range .ItemList}}
<div class="rowitem editable_parent{{if eq .ID 1}} builtin_forum_divider{{end}}"> <div class="rowitem editable_parent">
<span class="panel_floater"> <span class="panel_floater">
<span data-field="forum_active" data-type="list" class="panel_tag editable_block forum_active {{if .Active}}forum_active_Show" data-value="1{{else}}forum_active_Hide" data-value="0{{end}}" title="Hidden"></span> <span data-field="forum_active" data-type="list" class="panel_tag editable_block forum_active {{if .Active}}forum_active_Show" data-value="1{{else}}forum_active_Hide" data-value="0{{end}}" title="Hidden"></span>
<span data-field="forum_preset" data-type="list" data-value="{{.Preset}}" class="panel_tag editable_block forum_preset forum_preset_{{.Preset}}" title="{{.PresetLang}}"></span> <span data-field="forum_preset" data-type="list" data-value="{{.Preset}}" class="panel_tag editable_block forum_preset forum_preset_{{.Preset}}" title="{{.PresetLang}}"></span>
<span class="panel_buttons"> <span class="panel_buttons">
{{if gt .ID 0}}<a class="panel_tag edit_fields hide_on_edit panel_right_button">Edit</a> <a class="panel_tag edit_fields hide_on_edit panel_right_button">Edit</a>
<a class="panel_right_button" href="/panel/forums/edit/submit/{{.ID}}"><button class='panel_tag submit_edit show_on_edit' type='submit'>Update</button></a>{{end}} <a class="panel_right_button" href="/panel/forums/edit/submit/{{.ID}}"><button class='panel_tag submit_edit show_on_edit' type='submit'>Update</button></a>
{{if gt .ID 1}}<a href="/panel/forums/delete/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button hide_on_edit">Delete</a>{{end}} {{if gt .ID 1}}<a href="/panel/forums/delete/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button hide_on_edit">Delete</a>{{end}}
<a href="/panel/forums/edit/{{.ID}}" class="panel_tag panel_right_button show_on_edit">Full Edit</a> <a href="/panel/forums/edit/{{.ID}}" class="panel_tag panel_right_button show_on_edit">Full Edit</a>
</span> </span>

View File

@ -31,6 +31,9 @@
{{if .CurrentUser.Perms.ManagePlugins}}<div class="rowitem passive"> {{if .CurrentUser.Perms.ManagePlugins}}<div class="rowitem passive">
<a href="/panel/plugins/">Plugins</a> <a href="/panel/plugins/">Plugins</a>
</div>{{end}} </div>{{end}}
{{if .CurrentUser.IsSuperAdmin}}<div class="rowitem passive">
<a href="/panel/backups/">Backups</a>
</div>{{end}}
<div class="rowitem passive"> <div class="rowitem passive">
<a href="/panel/logs/mod/">Logs</a> <a href="/panel/logs/mod/">Logs</a>
</div> </div>

View File

@ -30,7 +30,7 @@
<a href="{{.Topic.UserLink}}" class="username real_username">{{.Topic.CreatedByName}}</a>&nbsp;&nbsp; <a href="{{.Topic.UserLink}}" class="username real_username">{{.Topic.CreatedByName}}</a>&nbsp;&nbsp;
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}" class="mod_button" title="Love it" style="color:#202020;"> {{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}" class="mod_button" title="Love it" style="color:#202020;">
<button class="username like_label" style="{{if .Topic.Liked}}background-color:/*#eaffea*/#D6FFD6;{{end}}"></button></a>{{end}} <button class="username like_label"{{if .Topic.Liked}} style="background-color:#D6FFD6;"{{end}}></button></a>{{end}}
{{if .CurrentUser.Perms.EditTopic}}<a href='/topic/edit/{{.Topic.ID}}' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic"><button class="username edit_label"></button></a>{{end}} {{if .CurrentUser.Perms.EditTopic}}<a href='/topic/edit/{{.Topic.ID}}' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic"><button class="username edit_label"></button></a>{{end}}
@ -39,7 +39,7 @@
{{if .CurrentUser.Perms.CloseTopic}}{{if .Topic.IsClosed}}<a class="mod_button" href='/topic/unlock/submit/{{.Topic.ID}}' style="font-weight:normal;" title="Unlock Topic"><button class="username unlock_label"></button></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Lock Topic"><button class="username lock_label"></button></a>{{end}}{{end}} {{if .CurrentUser.Perms.CloseTopic}}{{if .Topic.IsClosed}}<a class="mod_button" href='/topic/unlock/submit/{{.Topic.ID}}' style="font-weight:normal;" title="Unlock Topic"><button class="username unlock_label"></button></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Lock Topic"><button class="username lock_label"></button></a>{{end}}{{end}}
{{if .CurrentUser.Perms.PinTopic}}{{if .Topic.Sticky}}<a class="mod_button" href='/topic/unstick/submit/{{.Topic.ID}}' style="font-weight:normal;" title="Unpin Topic"><button class="username unpin_label"></button></a>{{else}}<a href='/topic/stick/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Pin Topic"><button class="username pin_label"></button></a>{{end}}{{end}} {{if .CurrentUser.Perms.PinTopic}}{{if .Topic.Sticky}}<a class="mod_button" href='/topic/unstick/submit/{{.Topic.ID}}' style="font-weight:normal;" title="Unpin Topic"><button class="username unpin_label"></button></a>{{else}}<a href='/topic/stick/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Pin Topic"><button class="username pin_label"></button></a>{{end}}{{end}}
{{if .CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/{{.Topic.IPAddress}}' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>{{end}} {{if .CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.Topic.IPAddress}}' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>{{end}}
<a href="/report/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag Topic"><button class="username flag_label"></button></a> <a href="/report/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag Topic"><button class="username flag_label"></button></a>
{{if .Topic.LikeCount}}<a class="username hide_on_micro like_count">{{.Topic.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>{{end}} {{if .Topic.LikeCount}}<a class="username hide_on_micro like_count">{{.Topic.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>{{end}}
@ -61,12 +61,12 @@
<span class="controls"> <span class="controls">
<a href="{{.UserLink}}" class="username real_username">{{.CreatedByName}}</a>&nbsp;&nbsp; <a href="{{.UserLink}}" class="username real_username">{{.CreatedByName}}</a>&nbsp;&nbsp;
{{if $.CurrentUser.Perms.LikeItem}}<a href="/reply/like/submit/{{.ID}}" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label" style="{{if .Liked}}background-color:/*#eaffea*/#D6FFD6;{{end}}"></button></a>{{end}} {{if $.CurrentUser.Perms.LikeItem}}<a href="/reply/like/submit/{{.ID}}" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"{{if .Liked}} style="background-color:#D6FFD6;"{{end}}></button></a>{{end}}
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>{{end}} {{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>{{end}}
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>{{end}} {{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>{{end}}
{{if $.CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/{{.IPAddress}}' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>{{end}} {{if $.CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.IPAddress}}' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>{{end}}
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="mod_button report_item" title="Flag Reply"><button class="username report_item flag_label"></button></a> <a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="mod_button report_item" title="Flag Reply"><button class="username report_item flag_label"></button></a>
{{if .LikeCount}}<a class="username hide_on_micro like_count">{{.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>{{end}} {{if .LikeCount}}<a class="username hide_on_micro like_count">{{.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>{{end}}

View File

@ -70,6 +70,11 @@ type ThemeResource struct {
Location string Location string
} }
func init() {
defaultThemeBox.Store(fallbackTheme)
}
// ? - Delete themes which no longer exist in the themes folder from the database?
func LoadThemes() error { func LoadThemes() error {
changeDefaultThemeMutex.Lock() changeDefaultThemeMutex.Lock()
rows, err := getThemesStmt.Query() rows, err := getThemesStmt.Query()
@ -92,32 +97,16 @@ func LoadThemes() error {
continue continue
} }
theme.TemplatesMap = make(map[string]string)
theme.TmplPtr = make(map[string]interface{})
if theme.Templates != nil {
for _, themeTmpl := range theme.Templates {
theme.TemplatesMap[themeTmpl.Name] = themeTmpl.Source
theme.TmplPtr[themeTmpl.Name] = tmplPtrMap["o_"+themeTmpl.Source]
}
}
theme.ResourceTemplates = template.New("")
template.Must(theme.ResourceTemplates.ParseGlob("./themes/" + uname + "/public/*.css"))
if defaultThemeSwitch { if defaultThemeSwitch {
log.Print("Loading the theme '" + theme.Name + "'") log.Print("Loading the default theme '" + theme.Name + "'")
theme.Active = true theme.Active = true
defaultThemeBox.Store(uname) defaultThemeBox.Store(theme.Name)
mapThemeTemplates(theme) mapThemeTemplates(theme)
} else { } else {
log.Print("Loading the theme '" + theme.Name + "'")
theme.Active = false theme.Active = false
} }
// It should be safe for us to load the files for all the themes in memory, as-long as the admin hasn't setup a ridiculous number of themes
err = addThemeStaticFiles(theme)
if err != nil {
return err
}
themes[uname] = theme themes[uname] = theme
} }
changeDefaultThemeMutex.Unlock() changeDefaultThemeMutex.Unlock()
@ -160,6 +149,24 @@ func initThemes() error {
} }
} }
theme.TemplatesMap = make(map[string]string)
theme.TmplPtr = make(map[string]interface{})
if theme.Templates != nil {
for _, themeTmpl := range theme.Templates {
theme.TemplatesMap[themeTmpl.Name] = themeTmpl.Source
theme.TmplPtr[themeTmpl.Name] = tmplPtrMap["o_"+themeTmpl.Source]
}
}
theme.ResourceTemplates = template.New("")
template.Must(theme.ResourceTemplates.ParseGlob("./themes/" + theme.Name + "/public/*.css"))
// It should be safe for us to load the files for all the themes in memory, as-long as the admin hasn't setup a ridiculous number of themes
err = addThemeStaticFiles(theme)
if err != nil {
return err
}
themes[theme.Name] = theme themes[theme.Name] = theme
} }
return nil return nil

View File

@ -146,7 +146,6 @@ a {
margin-top: 8px; margin-top: 8px;
padding: 12px; padding: 12px;
} }
.rowitem h1 { .rowitem h1 {
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
@ -154,7 +153,6 @@ a {
-webkit-margin-after: 0; -webkit-margin-after: 0;
display: inline; display: inline;
} }
.rowsmall { .rowsmall {
font-size: 12px; font-size: 12px;
} }
@ -181,6 +179,11 @@ a {
padding: 10px; padding: 10px;
} }
/* Algin to right in a flex head */
.to_right {
margin-left: auto;
}
/* Topic View */ /* Topic View */
/* TODO: How should we handle the sticky headers? */ /* TODO: How should we handle the sticky headers? */
@ -223,7 +226,6 @@ a {
display: block; display: block;
float: left; float: left;
} }
.mod_button button { .mod_button button {
border: none; border: none;
background: none; background: none;
@ -288,7 +290,6 @@ a {
color: rgb(205,205,205); color: rgb(205,205,205);
float: right; float: right;
} }
.level { .level {
margin-left: 3px; margin-left: 3px;
} }
@ -408,7 +409,11 @@ textarea.large {
background-size: 40px; background-size: 40px;
padding-left: 46px; padding-left: 46px;
} }
.bgavatars:not(.rowlist) .rowitem {
background-repeat: no-repeat;
background-size: 40px;
padding-left: 46px;
}
.rowlist .formrow, .rowlist .formrow:first-child { .rowlist .formrow, .rowlist .formrow:first-child {
margin-top: 0px; margin-top: 0px;
} }
@ -475,7 +480,7 @@ input, select, textarea {
} }
/* Forum View */ /* Forum View */
.rowhead { .rowhead, .opthead, .colstack_head {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
@ -533,9 +538,6 @@ input, select, textarea {
white-space: nowrap; white-space: nowrap;
} }
.topic_item {
display: flex;
}
.topic_name_input { .topic_name_input {
width: 100%; width: 100%;
margin-right: 10px; margin-right: 10px;
@ -596,6 +598,32 @@ input, select, textarea {
padding-left: 136px; padding-left: 136px;
} }
.ip_search_block .rowitem {
display: flex;
flex-direction: row;
}
.ip_search_block input {
background-color: #444444;
border: 1px solid #555555;
color: #999999;
margin-top: -3px;
margin-bottom: -3px;
padding: 4px;
padding-bottom: 3px;
}
.ip_search_input {
font-size: 15px;
width: 100%;
margin-left: 0px;
}
.ip_search_search {
font-size: 14px;
margin-left: 8px;
}
.colstack_grid { .colstack_grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);

View File

@ -29,6 +29,7 @@ type Version struct {
TagID int TagID int
} }
// TODO: Write a test for this
func (version *Version) String() (out string) { func (version *Version) String() (out string) {
out = strconv.Itoa(version.Major) + "." + strconv.Itoa(version.Minor) + "." + strconv.Itoa(version.Patch) out = strconv.Itoa(version.Major) + "." + strconv.Itoa(version.Minor) + "." + strconv.Itoa(version.Patch)
if version.Tag != "" { if version.Tag != "" {
@ -40,7 +41,8 @@ func (version *Version) String() (out string) {
return return
} }
// GenerateSafeString is for generating a cryptographically secure set of random bytes.. // GenerateSafeString is for generating a cryptographically secure set of random bytes...
// TODO: Write a test for this
func GenerateSafeString(length int) (string, error) { func GenerateSafeString(length int) (string, error) {
rb := make([]byte, length) rb := make([]byte, length)
_, err := rand.Read(rb) _, err := rand.Read(rb)
@ -50,6 +52,7 @@ func GenerateSafeString(length int) (string, error) {
return base64.URLEncoding.EncodeToString(rb), nil return base64.URLEncoding.EncodeToString(rb), nil
} }
// TODO: Write a test for this
func relativeTime(in string) (string, error) { func relativeTime(in string) (string, error) {
if in == "" { if in == "" {
return "", nil return "", nil
@ -97,6 +100,7 @@ func relativeTime(in string) (string, error) {
} }
} }
// TODO: Write a test for this
func convertByteUnit(bytes float64) (float64, string) { func convertByteUnit(bytes float64) (float64, string) {
switch { switch {
case bytes >= float64(terabyte): case bytes >= float64(terabyte):
@ -112,6 +116,7 @@ func convertByteUnit(bytes float64) (float64, string) {
} }
} }
// TODO: Write a test for this
func convertByteInUnit(bytes float64, unit string) (count float64) { func convertByteInUnit(bytes float64, unit string) (count float64) {
switch unit { switch unit {
case "TB": case "TB":
@ -132,6 +137,7 @@ func convertByteInUnit(bytes float64, unit string) (count float64) {
return return
} }
// TODO: Write a test for this
func convertUnit(num int) (int, string) { func convertUnit(num int) (int, string) {
switch { switch {
case num >= 1000000000000: case num >= 1000000000000:
@ -147,6 +153,7 @@ func convertUnit(num int) (int, string) {
} }
} }
// TODO: Write a test for this
func convertFriendlyUnit(num int) (int, string) { func convertFriendlyUnit(num int) (int, string) {
switch { switch {
case num >= 1000000000000: case num >= 1000000000000:
@ -231,6 +238,7 @@ func SendEmail(email string, subject string, msg string) (res bool) {
return true return true
} }
// TODO: Write a test for this
func weakPassword(password string) error { func weakPassword(password string) error {
if len(password) < 8 { if len(password) < 8 {
return errors.New("your password needs to be at-least eight characters long") return errors.New("your password needs to be at-least eight characters long")
@ -283,6 +291,7 @@ func weakPassword(password string) error {
return nil return nil
} }
// TODO: Write a test for this
func createFile(name string) error { func createFile(name string) error {
f, err := os.Create(name) f, err := os.Create(name)
if err != nil { if err != nil {
@ -291,6 +300,7 @@ func createFile(name string) error {
return f.Close() return f.Close()
} }
// TODO: Write a test for this
func writeFile(name string, content string) (err error) { func writeFile(name string, content string) (err error) {
f, err := os.Create(name) f, err := os.Create(name)
if err != nil { if err != nil {
@ -307,6 +317,13 @@ func writeFile(name string, content string) (err error) {
return f.Close() return f.Close()
} }
// TODO: Write a test for this
func Stripslashes(text string) string {
text = strings.Replace(text, "/", "", -1)
return strings.Replace(text, "\\", "", -1)
}
// TODO: Write a test for this
func wordCount(input string) (count int) { func wordCount(input string) (count int) {
input = strings.TrimSpace(input) input = strings.TrimSpace(input)
if input == "" { if input == "" {
@ -326,6 +343,7 @@ func wordCount(input string) (count int) {
return count + 1 return count + 1
} }
// TODO: Write a test for this
func getLevel(score int) (level int) { func getLevel(score int) (level int) {
var base float64 = 25 var base float64 = 25
var current, prev float64 var current, prev float64
@ -346,6 +364,7 @@ func getLevel(score int) (level int) {
return level return level
} }
// TODO: Write a test for this
func getLevelScore(getLevel int) (score int) { func getLevelScore(getLevel int) (score int) {
var base float64 = 25 var base float64 = 25
var current, prev float64 var current, prev float64
@ -367,6 +386,7 @@ func getLevelScore(getLevel int) (score int) {
return int(math.Ceil(current)) return int(math.Ceil(current))
} }
// TODO: Write a test for this
func getLevels(maxLevel int) []float64 { func getLevels(maxLevel int) []float64 {
var base float64 = 25 var base float64 = 25
var current, prev float64 // = 0 var current, prev float64 // = 0