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/socialgroup_*
backups/*.sql
bin/*
out/*
*.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.ActivationGroup = 5 // Should be a setting in the database
config.StaffCSS = "staff_post"
config.UncategorisedForumVisible = true
config.DefaultForum = 2
config.MinifyTemplates = false
config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features

View File

@ -39,6 +39,20 @@ type ForumSimple struct {
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 {
if slug == "" {
return "/forum/" + strconv.Itoa(fid)

View File

@ -9,6 +9,7 @@ package main
import (
"database/sql"
"log"
"sort"
"sync"
"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()
if err != nil {
return err
@ -148,16 +147,16 @@ func (mfs *MemoryForumStore) rebuildView() {
}
return true
})
sort.Sort(SortForum(forumView))
mfs.forumView.Store(forumView)
}
func (mfs *MemoryForumStore) DirtyGet(id int) *Forum {
fint, ok := mfs.forums.Load(id)
forum := fint.(*Forum)
if !ok || forum.Name == "" {
if !ok || fint.(*Forum).Name == "" {
return &Forum{ID: -1, Name: ""}
}
return forum
return fint.(*Forum)
}
func (mfs *MemoryForumStore) CacheGet(id int) (*Forum, error) {
@ -225,24 +224,29 @@ func (mfs *MemoryForumStore) CacheSet(forum *Forum) error {
return nil
}
// ! Has a randomised order
func (mfs *MemoryForumStore) GetAll() (forumView []*Forum, err error) {
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
forumView = append(forumView, value.(*Forum))
return true
})
sort.Sort(SortForum(forumView))
return forumView, nil
}
// ? - Can we optimise the sorting?
func (mfs *MemoryForumStore) GetAllIDs() (ids []int, err error) {
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
ids = append(ids, value.(*Forum).ID)
return true
})
sort.Ints(ids)
return ids, nil
}
func (mfs *MemoryForumStore) GetAllVisible() ([]*Forum, error) {
return mfs.forumView.Load().([]*Forum), nil
func (mfs *MemoryForumStore) GetAllVisible() (forumView []*Forum, err error) {
forumView = mfs.forumView.Load().([]*Forum)
return forumView, nil
}
func (mfs *MemoryForumStore) GetAllVisibleIDs() ([]int, error) {
@ -285,6 +289,7 @@ func (mfs *MemoryForumStore) Delete(id int) error {
return nil
}
// ! Is this racey?
func (mfs *MemoryForumStore) IncrementTopicCount(id int) error {
forum, err := mfs.Get(id)
if err != nil {
@ -298,6 +303,7 @@ func (mfs *MemoryForumStore) IncrementTopicCount(id int) error {
return nil
}
// ! Is this racey?
func (mfs *MemoryForumStore) DecrementTopicCount(id int) error {
forum, err := mfs.Get(id)
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
// ! Is this racey?
func (mfs *MemoryForumStore) UpdateLastTopic(topicName string, tid int, username string, uid int, time string, fid int) error {
forum, err := mfs.Get(fid)
if err != nil {

View File

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

View File

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

17
main.go
View File

@ -12,7 +12,6 @@ import (
"log"
"net/http"
"os"
"strings"
"time"
//"runtime/pprof"
)
@ -41,16 +40,6 @@ var externalSites = map[string]string{
var staticFiles = make(map[string]SFile)
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() {
// TODO: Have a file for each run with the time/date the server started as the file name?
// TODO: Log panics with recover()
@ -121,6 +110,11 @@ func main() {
log.Fatal(err)
}
err = verifyConfig()
if err != nil {
log.Fatal(err)
}
// Run this goroutine once a second
secondTicker := time.NewTicker(1 * time.Second)
fifteenMinuteTicker := time.NewTicker(15 * time.Minute)
@ -149,6 +143,7 @@ func main() {
// 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: 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
case <-fifteenMinuteTicker.C:

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import (
"strconv"
"strings"
"sync"
"time"
)
type HeaderVars struct {
@ -227,7 +228,23 @@ type PanelEditGroupPermsPage struct {
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
IPAddress string
DoneAt string
@ -238,7 +255,7 @@ type PanelLogsPage struct {
CurrentUser User
Header *HeaderVars
Stats PanelStats
Logs []Log
Logs []logItem
PageList []int
Page int
LastPage int
@ -283,6 +300,7 @@ func init() {
urlReg = regexp.MustCompile(urlpattern)
}
// TODO: Write a test for this
func shortcodeToUnicode(msg string) string {
//re := regexp.MustCompile(":(.):")
msg = strings.Replace(msg, ":grinning:", "😀", -1)
@ -421,6 +439,7 @@ func preparseMessage(msg string) string {
return shortcodeToUnicode(msg)
}
// TODO: Write a test for this
func parseMessage(msg string /*, user User*/) string {
msg = strings.Replace(msg, ":)", "😀", -1)
msg = strings.Replace(msg, ":(", "😞", -1)
@ -634,6 +653,7 @@ func parseMessage(msg string /*, user User*/) string {
return msg
}
// TODO: Write a test for this
func regexParseMessage(msg string) string {
msg = strings.Replace(msg, ":)", "😀", -1)
msg = strings.Replace(msg, ":D", "😃", -1)
@ -648,6 +668,7 @@ func regexParseMessage(msg string) string {
// 6, 7, 8, 6, 7
// 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 {
datalen := len(data)
i := 0
@ -670,6 +691,7 @@ func validateURLBytes(data []byte) bool {
return true
}
// TODO: Write a test for this
func validatedURLBytes(data []byte) (url []byte) {
datalen := len(data)
i := 0
@ -694,6 +716,7 @@ func validatedURLBytes(data []byte) (url []byte) {
return url
}
// TODO: Write a test for this
func partialURLBytes(data []byte) (url []byte) {
datalen := len(data)
i := 0
@ -719,6 +742,7 @@ func partialURLBytes(data []byte) (url []byte) {
return url
}
// TODO: Write a test for this
func partialURLBytesLen(data []byte) int {
datalen := len(data)
i := 0
@ -745,6 +769,7 @@ func partialURLBytesLen(data []byte) int {
return datalen
}
// TODO: Write a test for this
func parseMediaBytes(data []byte) (protocol []byte, url []byte) {
datalen := len(data)
i := 0
@ -774,6 +799,7 @@ func parseMediaBytes(data []byte) (protocol []byte, url []byte) {
return protocol, data[i:]
}
// TODO: Write a test for this
func coerceIntBytes(data []byte) (res int, length int) {
if !(data[0] > 47 && data[0] < 58) {
return 0, 1

View File

@ -6,8 +6,11 @@ import (
"fmt"
"html"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
@ -190,6 +193,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user User) {
return
}
// TODO: Paginate this?
var forumList []interface{}
forums, err := fstore.GetAll()
if err != nil {
@ -197,6 +201,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user User) {
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 {
if forum.Name != "" && forum.ParentID == 0 {
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)
}
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) {
headerVars, stats, ok := PanelUserCheck(w, r, &user)
if !ok {
@ -1964,7 +2022,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) {
}
defer rows.Close()
var logs []Log
var logs []logItem
var action, elementType, ipaddress, doneAt string
var elementID, actorID int
for rows.Next() {
@ -2035,7 +2093,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) {
default:
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()
if err != nil {

View File

@ -81,6 +81,7 @@ func routes() {
Route{"routePanelGroupsEditPermsSubmit", "/panel/groups/edit/perms/submit/", "", []string{"extra_data"}},
Route{"routePanelGroupsCreateSubmit", "/panel/groups/create/", "", []string{}},
Route{"routePanelBackups", "/panel/backups/", "", []string{"extra_data"}},
Route{"routePanelLogsMod", "/panel/logs/mod/", "", []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 + "'")
file, ok := staticFiles[r.URL.Path]
if !ok {
if dev.DebugMode {
log.Print("Failed to find '" + r.URL.Path + "'")
}
w.WriteHeader(http.StatusNotFound)
return
}
@ -75,14 +78,9 @@ func routeStatic(w http.ResponseWriter, r *http.Request) {
}
// 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()
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?

39
site.go
View File

@ -1,6 +1,10 @@
package main
import "net/http"
import (
"errors"
"net/http"
"strings"
)
var site = &Site{Name: "Magical Fairy Land", Language: "english"}
var dbConfig = DBConfig{Host: "localhost"}
@ -8,7 +12,7 @@ var config Config
var dev DevConfig
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?
URL string
Port string
@ -40,13 +44,13 @@ type Config struct {
SMTPPassword string
SMTPPort string
DefaultRoute func(http.ResponseWriter, *http.Request, User)
DefaultGroup int
ActivationGroup int
StaffCSS string // ? - Move this into the settings table? Might be better to implement this as Group CSS
UncategorisedForumVisible bool
MinifyTemplates bool
MultiServer bool
DefaultRoute func(http.ResponseWriter, *http.Request, User)
DefaultGroup int
ActivationGroup int
StaffCSS string // ? - Move this into the settings table? Might be better to implement this as Group CSS
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
MultiServer bool
Noavatar string // ? - Move this into the settings table?
ItemsPerPage int // ? - Move this into the settings table?
@ -57,3 +61,20 @@ type DevConfig struct {
SuperDebug 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_30 = []byte(`" class="mod_button" title="Love it" style="color:#202020;">
<button class="username like_label" style="`)
var topic_31 = []byte(`background-color:/*#eaffea*/#D6FFD6;`)
var topic_32 = []byte(`"></button></a>`)
<button class="username like_label"`)
var topic_31 = []byte(` style="background-color:#D6FFD6;"`)
var topic_32 = []byte(`></button></a>`)
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_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_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_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_47 = []byte(`
<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_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_72 = []byte(`background-color:/*#eaffea*/#D6FFD6;`)
var topic_73 = []byte(`"></button></a>`)
var topic_71 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`)
var topic_72 = []byte(` style="background-color:#D6FFD6;"`)
var topic_73 = []byte(`></button></a>`)
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_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_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_80 = []byte(`
<a href="/report/submit/`)

View File

@ -1,18 +1,28 @@
{{template "header.html" . }}
<main>
<div class="rowblock opthead">
<div class="rowitem"><a>IP Search</a></div>
<div class="rowblock rowhead">
<div class="rowitem">
<h1>IP Search</h1>
</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 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>
</div>
{{else}}<div class="rowitem passive">No users found.</div>{{end}}
{{else}}<div class="rowitem">No users found.</div>{{end}}
</div>
{{end}}
</main>
{{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 id="panel_forums" class="colstack_item rowlist">
{{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 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 class="panel_buttons">
{{if gt .ID 0}}<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_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>
{{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>
</span>

View File

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

View File

@ -30,7 +30,7 @@
<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;">
<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}}
@ -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.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>
{{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">
<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.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>
{{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
}
func init() {
defaultThemeBox.Store(fallbackTheme)
}
// ? - Delete themes which no longer exist in the themes folder from the database?
func LoadThemes() error {
changeDefaultThemeMutex.Lock()
rows, err := getThemesStmt.Query()
@ -92,32 +97,16 @@ func LoadThemes() error {
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 {
log.Print("Loading the theme '" + theme.Name + "'")
log.Print("Loading the default theme '" + theme.Name + "'")
theme.Active = true
defaultThemeBox.Store(uname)
defaultThemeBox.Store(theme.Name)
mapThemeTemplates(theme)
} else {
log.Print("Loading the theme '" + theme.Name + "'")
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
}
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
}
return nil

View File

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

View File

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