Continued the work on Cosora.

Trying to add support for Travis CI.

Added the NullUserStore for tests.
Moved more text out of the templates.
Removed an unnecessary dependency for Cosora.
Fixed a few more bugs in the MSSQL Adapter.
Disabled upserts for now, as it isn't working on MySQL.
Refactored the scheduled group logic to reduce the number of potential bugs and to stop using upserts.
Fixed many bugs in the Markdown Plugin.
Added the ability to escape Markdown.
Fixed a number of tests, commented unneccesary ones, and added new ones to probe for new problems.
Added the wink smiley.
Cleaned up some of the JavaScript.
Refactored the permissions logic to use transactions and avoid duplicating logic.
Added the ChangeRank method to the Group struct.
Added the ChangeGroup method to the User struct.
Added the currently unused LogWarning function.
Added the transaction versions of the builder methods. Better ones coming up soon!
The IsBanned flag is always set to false on mod and admin groups now.
Refactored the group creation logic to use transactions.
Fixed a bug in the group creator where new groups aren't visible to Exists().
The installer now drops tables for MySQL Databases, if they already exist to make it easier for us to run automated tests.
Added more ARIA Attributes.
This commit is contained in:
Azareal 2017-10-21 01:27:47 +01:00
parent da6ae8d7d4
commit f7942b42ac
43 changed files with 1540 additions and 806 deletions

10
.travis.yml Normal file
View File

@ -0,0 +1,10 @@
language: go
go:
- 1.9
- master
before_install:
- chmod 755 ./install-linux
- chmod 755 ./run-linux
script: go test
addons:
mariadb: '10.0'

17
auth.go
View File

@ -20,8 +20,12 @@ var auth Auth
// ErrMismatchedHashAndPassword is thrown whenever a hash doesn't match it's unhashed password
var ErrMismatchedHashAndPassword = bcrypt.ErrMismatchedHashAndPassword
// nolint
// ErrPasswordTooLong is silly, but we don't want bcrypt to bork on us
var ErrPasswordTooLong = errors.New("The password you selected is too long")
var ErrWrongPassword = errors.New("That's not the correct password.")
var ErrSecretError = errors.New("There was a glitch in the system. Please contact your local administrator.")
var ErrNoUserByName = errors.New("We couldn't find an account with that username.")
// Auth is the main authentication interface.
type Auth interface {
@ -61,24 +65,24 @@ func (auth *DefaultAuth) Authenticate(username string, password string) (uid int
var realPassword, salt string
err = auth.login.QueryRow(username).Scan(&uid, &realPassword, &salt)
if err == ErrNoRows {
return 0, errors.New("We couldn't find an account with that username.") // nolint
return 0, ErrNoUserByName
} else if err != nil {
LogError(err)
return 0, errors.New("There was a glitch in the system. Please contact your local administrator.") // nolint
return 0, ErrSecretError
}
if salt == "" {
// Send an email to admin for this?
LogError(errors.New("Missing salt for user #" + strconv.Itoa(uid) + ". Potential security breach."))
return 0, errors.New("There was a glitch in the system. Please contact your local administrator")
return 0, ErrSecretError
}
err = CheckPassword(realPassword, password, salt)
if err == ErrMismatchedHashAndPassword {
return 0, errors.New("That's not the correct password.")
return 0, ErrWrongPassword
} else if err != nil {
LogError(err)
return 0, errors.New("There was a glitch in the system. Please contact your local administrator.")
return 0, ErrSecretError
}
return uid, nil
@ -89,7 +93,7 @@ func (auth *DefaultAuth) ForceLogout(uid int) error {
_, err := auth.logout.Exec(uid)
if err != nil {
LogError(err)
return errors.New("There was a glitch in the system. Please contact your local administrator.")
return ErrSecretError
}
// Flush the user out of the cache
@ -110,6 +114,7 @@ func (auth *DefaultAuth) Logout(w http.ResponseWriter, _ int) {
}
// TODO: Set the cookie domain
// SetCookies sets the two cookies required for the current user to be recognised as a specific user in future requests
func (auth *DefaultAuth) SetCookies(w http.ResponseWriter, uid int, session string) {
cookie := http.Cookie{Name: "uid", Value: strconv.Itoa(uid), Path: "/", MaxAge: year}
http.SetCookie(w, &cookie)

View File

@ -12,8 +12,8 @@ const CACHE_SQL int = 2
// ErrCacheDesync is thrown whenever a piece of data, for instance, a user is out of sync with the database. Currently unused.
var ErrCacheDesync = errors.New("The cache is out of sync with the database.") // TODO: A cross-server synchronisation mechanism
// ErrStoreCapacityOverflow is thrown whenever a datastore reaches it's maximum hard capacity. I'm not sure if this error is actually used.
var ErrStoreCapacityOverflow = errors.New("This datastore has reached it's maximum capacity.")
// ErrStoreCapacityOverflow is thrown whenever a datastore reaches it's maximum hard capacity. I'm not sure if this error is actually used. It might be, we should check
var ErrStoreCapacityOverflow = errors.New("This datastore has reached it's maximum capacity.") // nolint
// nolint
type DataStore interface {

View File

@ -14,14 +14,18 @@ var errorBuffer []error
//var notfoundCountPerSecond int
//var nopermsCountPerSecond int
// LogError logs internal handler errors which can't be handled with InternalError() as a wrapper for log.Fatal(), we might do more with it in the future
// LogError logs internal handler errors which can't be handled with InternalError() as a wrapper for log.Fatal(), we might do more with it in the future.
func LogError(err error) {
LogWarning(err)
log.Fatal("")
}
func LogWarning(err error) {
log.Print(err)
debug.PrintStack()
errorBufferMutex.Lock()
defer errorBufferMutex.Unlock()
errorBuffer = append(errorBuffer, err)
log.Fatal("")
}
// InternalError is the main function for handling internal errors, while simultaneously printing out a page for the end-user to let them know that *something* has gone wrong

View File

@ -405,3 +405,5 @@ func (mfs *MemoryForumStore) GlobalCount() (fcount int) {
}
// TODO: Work on SqlForumStore
// TODO: Work on the NullForumStore

View File

@ -65,13 +65,10 @@ var createForumStmt *sql.Stmt
var addForumPermsToForumStmt *sql.Stmt
var addPluginStmt *sql.Stmt
var addThemeStmt *sql.Stmt
var createGroupStmt *sql.Stmt
var addModlogEntryStmt *sql.Stmt
var addAdminlogEntryStmt *sql.Stmt
var addAttachmentStmt *sql.Stmt
var createWordFilterStmt *sql.Stmt
var addForumPermsToGroupStmt *sql.Stmt
var replaceScheduleGroupStmt *sql.Stmt
var addRepliesToTopicStmt *sql.Stmt
var removeRepliesFromTopicStmt *sql.Stmt
var addTopicsToForumStmt *sql.Stmt
@ -105,6 +102,7 @@ var updatePluginStmt *sql.Stmt
var updatePluginInstallStmt *sql.Stmt
var updateThemeStmt *sql.Stmt
var updateUserStmt *sql.Stmt
var updateUserGroupStmt *sql.Stmt
var updateGroupPermsStmt *sql.Stmt
var updateGroupRankStmt *sql.Stmt
var updateGroupStmt *sql.Stmt
@ -116,15 +114,11 @@ var bumpSyncStmt *sql.Stmt
var deleteUserStmt *sql.Stmt
var deleteReplyStmt *sql.Stmt
var deleteProfileReplyStmt *sql.Stmt
var deleteForumPermsByForumStmt *sql.Stmt
var deleteActivityStreamMatchStmt *sql.Stmt
var deleteWordFilterStmt *sql.Stmt
var reportExistsStmt *sql.Stmt
var groupCountStmt *sql.Stmt
var modlogCountStmt *sql.Stmt
var addForumPermsToForumAdminsStmt *sql.Stmt
var addForumPermsToForumStaffStmt *sql.Stmt
var addForumPermsToForumMembersStmt *sql.Stmt
var notifyWatchersStmt *sql.Stmt
// nolint
@ -365,9 +359,9 @@ func _gen_mssql() (err error) {
}
log.Print("Preparing getExpiredScheduledGroups statement.")
getExpiredScheduledGroupsStmt, err = db.Prepare("SELECT [uid] FROM [users_groups_scheduler] WHERE GETUTCDATE() > [revert_at] AND [temporary] = 1")
getExpiredScheduledGroupsStmt, err = db.Prepare("SELECT [uid] FROM [users_groups_scheduler] WHERE GETDATE() > [revert_at] AND [temporary] = 1")
if err != nil {
log.Print("Bad Query: ","SELECT [uid] FROM [users_groups_scheduler] WHERE GETUTCDATE() > [revert_at] AND [temporary] = 1")
log.Print("Bad Query: ","SELECT [uid] FROM [users_groups_scheduler] WHERE GETDATE() > [revert_at] AND [temporary] = 1")
return err
}
@ -539,13 +533,6 @@ func _gen_mssql() (err error) {
return err
}
log.Print("Preparing createGroup statement.")
createGroupStmt, err = db.Prepare("INSERT INTO [users_groups] ([name],[tag],[is_admin],[is_mod],[is_banned],[permissions]) VALUES (?,?,?,?,?,?)")
if err != nil {
log.Print("Bad Query: ","INSERT INTO [users_groups] ([name],[tag],[is_admin],[is_mod],[is_banned],[permissions]) VALUES (?,?,?,?,?,?)")
return err
}
log.Print("Preparing addModlogEntry statement.")
addModlogEntryStmt, err = db.Prepare("INSERT INTO [moderation_logs] ([action],[elementID],[elementType],[ipaddress],[actorID],[doneAt]) VALUES (?,?,?,?,?,GETUTCDATE())")
if err != nil {
@ -574,20 +561,6 @@ func _gen_mssql() (err error) {
return err
}
log.Print("Preparing addForumPermsToGroup statement.")
addForumPermsToGroupStmt, err = db.Prepare("MERGE [forums_permissions] WITH(HOLDLOCK) as t1 USING (VALUES(?,?,?,?)) AS updates (f0,f1,f2,f3) ON [gid] = ? [fid] = ? WHEN MATCHED THEN UPDATE SET [gid] = f0,[fid] = f1,[preset] = f2,[permissions] = f3 WHEN NOT MATCHED THEN INSERT([gid],[fid],[preset],[permissions]) VALUES (f0,f1,f2,f3);")
if err != nil {
log.Print("Bad Query: ","MERGE [forums_permissions] WITH(HOLDLOCK) as t1 USING (VALUES(?,?,?,?)) AS updates (f0,f1,f2,f3) ON [gid] = ? [fid] = ? WHEN MATCHED THEN UPDATE SET [gid] = f0,[fid] = f1,[preset] = f2,[permissions] = f3 WHEN NOT MATCHED THEN INSERT([gid],[fid],[preset],[permissions]) VALUES (f0,f1,f2,f3);")
return err
}
log.Print("Preparing replaceScheduleGroup statement.")
replaceScheduleGroupStmt, err = db.Prepare("MERGE [users_groups_scheduler] WITH(HOLDLOCK) as t1 USING (VALUES(?,?,?,GETUTCDATE(),?,?)) AS updates (f0,f1,f2,f3,f4,f5) ON [uid] = ? WHEN MATCHED THEN UPDATE SET [uid] = f0,[set_group] = f1,[issued_by] = f2,[issued_at] = f3,[revert_at] = f4,[temporary] = f5 WHEN NOT MATCHED THEN INSERT([uid],[set_group],[issued_by],[issued_at],[revert_at],[temporary]) VALUES (f0,f1,f2,f3,f4,f5);")
if err != nil {
log.Print("Bad Query: ","MERGE [users_groups_scheduler] WITH(HOLDLOCK) as t1 USING (VALUES(?,?,?,GETUTCDATE(),?,?)) AS updates (f0,f1,f2,f3,f4,f5) ON [uid] = ? WHEN MATCHED THEN UPDATE SET [uid] = f0,[set_group] = f1,[issued_by] = f2,[issued_at] = f3,[revert_at] = f4,[temporary] = f5 WHEN NOT MATCHED THEN INSERT([uid],[set_group],[issued_by],[issued_at],[revert_at],[temporary]) VALUES (f0,f1,f2,f3,f4,f5);")
return err
}
log.Print("Preparing addRepliesToTopic statement.")
addRepliesToTopicStmt, err = db.Prepare("UPDATE [topics] SET [postCount] = [postCount] + ?,[lastReplyBy] = ?,[lastReplyAt] = GETUTCDATE() WHERE [tid] = ?")
if err != nil {
@ -819,6 +792,13 @@ func _gen_mssql() (err error) {
return err
}
log.Print("Preparing updateUserGroup statement.")
updateUserGroupStmt, err = db.Prepare("UPDATE [users] SET [group] = ? WHERE [uid] = ?")
if err != nil {
log.Print("Bad Query: ","UPDATE [users] SET [group] = ? WHERE [uid] = ?")
return err
}
log.Print("Preparing updateGroupPerms statement.")
updateGroupPermsStmt, err = db.Prepare("UPDATE [users_groups] SET [permissions] = ? WHERE [gid] = ?")
if err != nil {
@ -896,13 +876,6 @@ func _gen_mssql() (err error) {
return err
}
log.Print("Preparing deleteForumPermsByForum statement.")
deleteForumPermsByForumStmt, err = db.Prepare("DELETE FROM [forums_permissions] WHERE [fid] = ?")
if err != nil {
log.Print("Bad Query: ","DELETE FROM [forums_permissions] WHERE [fid] = ?")
return err
}
log.Print("Preparing deleteActivityStreamMatch statement.")
deleteActivityStreamMatchStmt, err = db.Prepare("DELETE FROM [activity_stream_matches] WHERE [watcher] = ? AND [asid] = ?")
if err != nil {
@ -938,27 +911,6 @@ func _gen_mssql() (err error) {
return err
}
log.Print("Preparing addForumPermsToForumAdmins statement.")
addForumPermsToForumAdminsStmt, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 1")
if err != nil {
log.Print("Bad Query: ","INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 1")
return err
}
log.Print("Preparing addForumPermsToForumStaff statement.")
addForumPermsToForumStaffStmt, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 0 AND [is_mod] = 1")
if err != nil {
log.Print("Bad Query: ","INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 0 AND [is_mod] = 1")
return err
}
log.Print("Preparing addForumPermsToForumMembers statement.")
addForumPermsToForumMembersStmt, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 0 AND [is_mod] = 0 AND [is_banned] = 0")
if err != nil {
log.Print("Bad Query: ","INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 0 AND [is_mod] = 0 AND [is_banned] = 0")
return err
}
log.Print("Preparing notifyWatchers statement.")
notifyWatchersStmt, err = db.Prepare("INSERT INTO [activity_stream_matches] ([watcher],[asid]) SELECT [activity_subscriptions].[user],[activity_stream].[asid] FROM [activity_stream] INNER JOIN [activity_subscriptions] ON [activity_subscriptions].[targetType] = [activity_stream].[elementType] AND [activity_subscriptions].[targetID] = [activity_stream].[elementID] AND [activity_subscriptions].[user] != [activity_stream].[actor] WHERE [asid] = ?1")
if err != nil {

View File

@ -6,7 +6,7 @@ package main
import "log"
import "database/sql"
import "./query_gen/lib"
//import "./query_gen/lib"
// nolint
var getUserStmt *sql.Stmt
@ -67,13 +67,10 @@ var createForumStmt *sql.Stmt
var addForumPermsToForumStmt *sql.Stmt
var addPluginStmt *sql.Stmt
var addThemeStmt *sql.Stmt
var createGroupStmt *sql.Stmt
var addModlogEntryStmt *sql.Stmt
var addAdminlogEntryStmt *sql.Stmt
var addAttachmentStmt *sql.Stmt
var createWordFilterStmt *sql.Stmt
var addForumPermsToGroupStmt *qgen.MySQLUpsertCallback
var replaceScheduleGroupStmt *qgen.MySQLUpsertCallback
var addRepliesToTopicStmt *sql.Stmt
var removeRepliesFromTopicStmt *sql.Stmt
var addTopicsToForumStmt *sql.Stmt
@ -107,6 +104,7 @@ var updatePluginStmt *sql.Stmt
var updatePluginInstallStmt *sql.Stmt
var updateThemeStmt *sql.Stmt
var updateUserStmt *sql.Stmt
var updateUserGroupStmt *sql.Stmt
var updateGroupPermsStmt *sql.Stmt
var updateGroupRankStmt *sql.Stmt
var updateGroupStmt *sql.Stmt
@ -118,15 +116,11 @@ var bumpSyncStmt *sql.Stmt
var deleteUserStmt *sql.Stmt
var deleteReplyStmt *sql.Stmt
var deleteProfileReplyStmt *sql.Stmt
var deleteForumPermsByForumStmt *sql.Stmt
var deleteActivityStreamMatchStmt *sql.Stmt
var deleteWordFilterStmt *sql.Stmt
var reportExistsStmt *sql.Stmt
var groupCountStmt *sql.Stmt
var modlogCountStmt *sql.Stmt
var addForumPermsToForumAdminsStmt *sql.Stmt
var addForumPermsToForumStaffStmt *sql.Stmt
var addForumPermsToForumMembersStmt *sql.Stmt
var notifyWatchersStmt *sql.Stmt
// nolint
@ -483,12 +477,6 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing createGroup statement.")
createGroupStmt, err = db.Prepare("INSERT INTO `users_groups`(`name`,`tag`,`is_admin`,`is_mod`,`is_banned`,`permissions`) VALUES (?,?,?,?,?,?)")
if err != nil {
return err
}
log.Print("Preparing addModlogEntry statement.")
addModlogEntryStmt, err = db.Prepare("INSERT INTO `moderation_logs`(`action`,`elementID`,`elementType`,`ipaddress`,`actorID`,`doneAt`) VALUES (?,?,?,?,?,UTC_TIMESTAMP())")
if err != nil {
@ -513,18 +501,6 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing addForumPermsToGroup statement.")
addForumPermsToGroupStmt, err = qgen.PrepareMySQLUpsertCallback(db, "INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE `gid` = ? AND `fid` = ? AND `preset` = ? AND `permissions` = ?")
if err != nil {
return err
}
log.Print("Preparing replaceScheduleGroup statement.")
replaceScheduleGroupStmt, err = qgen.PrepareMySQLUpsertCallback(db, "INSERT INTO `users_groups_scheduler`(`uid`,`set_group`,`issued_by`,`issued_at`,`revert_at`,`temporary`) VALUES (?,?,?,UTC_TIMESTAMP(),?,?) ON DUPLICATE KEY UPDATE `uid` = ? AND `set_group` = ? AND `issued_by` = ? AND `issued_at` = UTC_TIMESTAMP() AND `revert_at` = ? AND `temporary` = ?")
if err != nil {
return err
}
log.Print("Preparing addRepliesToTopic statement.")
addRepliesToTopicStmt, err = db.Prepare("UPDATE `topics` SET `postCount` = `postCount` + ?,`lastReplyBy` = ?,`lastReplyAt` = UTC_TIMESTAMP() WHERE `tid` = ?")
if err != nil {
@ -723,6 +699,12 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing updateUserGroup statement.")
updateUserGroupStmt, err = db.Prepare("UPDATE `users` SET `group` = ? WHERE `uid` = ?")
if err != nil {
return err
}
log.Print("Preparing updateGroupPerms statement.")
updateGroupPermsStmt, err = db.Prepare("UPDATE `users_groups` SET `permissions` = ? WHERE `gid` = ?")
if err != nil {
@ -789,12 +771,6 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing deleteForumPermsByForum statement.")
deleteForumPermsByForumStmt, err = db.Prepare("DELETE FROM `forums_permissions` WHERE `fid` = ?")
if err != nil {
return err
}
log.Print("Preparing deleteActivityStreamMatch statement.")
deleteActivityStreamMatchStmt, err = db.Prepare("DELETE FROM `activity_stream_matches` WHERE `watcher` = ? AND `asid` = ?")
if err != nil {
@ -825,24 +801,6 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing addForumPermsToForumAdmins statement.")
addForumPermsToForumAdminsStmt, err = db.Prepare("INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) SELECT `gid`, ? AS `fid`, ? AS `preset`, ? AS `permissions` FROM `users_groups` WHERE `is_admin` = 1")
if err != nil {
return err
}
log.Print("Preparing addForumPermsToForumStaff statement.")
addForumPermsToForumStaffStmt, err = db.Prepare("INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) SELECT `gid`, ? AS `fid`, ? AS `preset`, ? AS `permissions` FROM `users_groups` WHERE `is_admin` = 0 AND `is_mod` = 1")
if err != nil {
return err
}
log.Print("Preparing addForumPermsToForumMembers statement.")
addForumPermsToForumMembersStmt, err = db.Prepare("INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) SELECT `gid`, ? AS `fid`, ? AS `preset`, ? AS `permissions` FROM `users_groups` WHERE `is_admin` = 0 AND `is_mod` = 0 AND `is_banned` = 0")
if err != nil {
return err
}
log.Print("Preparing notifyWatchers statement.")
notifyWatchersStmt, err = db.Prepare("INSERT INTO `activity_stream_matches`(`watcher`,`asid`) SELECT `activity_subscriptions`.`user`, `activity_stream`.`asid` FROM `activity_stream` INNER JOIN `activity_subscriptions` ON `activity_subscriptions`.`targetType` = `activity_stream`.`elementType` AND `activity_subscriptions`.`targetID` = `activity_stream`.`elementID` AND `activity_subscriptions`.`user` != `activity_stream`.`actor` WHERE `asid` = ?")
if err != nil {

View File

@ -40,6 +40,7 @@ var updatePluginStmt *sql.Stmt
var updatePluginInstallStmt *sql.Stmt
var updateThemeStmt *sql.Stmt
var updateUserStmt *sql.Stmt
var updateUserGroupStmt *sql.Stmt
var updateGroupPermsStmt *sql.Stmt
var updateGroupRankStmt *sql.Stmt
var updateGroupStmt *sql.Stmt
@ -253,6 +254,12 @@ func _gen_pgsql() (err error) {
return err
}
log.Print("Preparing updateUserGroup statement.")
updateUserGroupStmt, err = db.Prepare("UPDATE `users` SET `group` = ? WHERE `uid` = ?")
if err != nil {
return err
}
log.Print("Preparing updateGroupPerms statement.")
updateGroupPermsStmt, err = db.Prepare("UPDATE `users_groups` SET `permissions` = ? WHERE `gid` = ?")
if err != nil {

View File

@ -2,14 +2,14 @@
package main
var dbTablePrimaryKeys = map[string]string{
"forums":"fid",
"topics":"tid",
"attachments":"attachID",
"users_replies":"rid",
"word_filters":"wfid",
"users":"uid",
"users_groups":"gid",
"users_groups_scheduler":"uid",
"replies":"rid",
"topics":"tid",
"users_replies":"rid",
"activity_stream":"asid",
"word_filters":"wfid",
"users":"uid",
"forums":"fid",
"replies":"rid",
"attachments":"attachID",
}

View File

@ -27,6 +27,24 @@ type Group struct {
CanSee []int // The IDs of the forums this group can see
}
// TODO: Reload the group from the database rather than modifying it via it's pointer
func (group *Group) ChangeRank(isAdmin bool, isMod bool, isBanned bool) (err error) {
_, err = updateGroupRankStmt.Exec(isAdmin, isMod, isBanned, group.ID)
if err != nil {
return err
}
group.IsAdmin = isAdmin
group.IsMod = isMod
if isAdmin || isMod {
group.IsBanned = false
} else {
group.IsBanned = isBanned
}
return nil
}
// ! Ahem, don't listen to the comment below. It's not concurrency safe right now.
// Copy gives you a non-pointer concurrency safe copy of the group
func (group *Group) Copy() Group {

View File

@ -6,6 +6,8 @@ import (
"errors"
"log"
"sync"
"./query_gen/lib"
)
var groupCreateMutex sync.Mutex
@ -19,7 +21,7 @@ type GroupStore interface {
Get(id int) (*Group, error)
GetCopy(id int) (Group, error)
Exists(id int) bool
Create(groupName string, tag string, isAdmin bool, isMod bool, isBanned bool) (int, error)
Create(name string, tag string, isAdmin bool, isMod bool, isBanned bool) (int, error)
GetAll() ([]*Group, error)
GetRange(lower int, higher int) ([]*Group, error)
}
@ -71,8 +73,11 @@ func (mgs *MemoryGroupStore) LoadGroups() error {
log.Print(group.Name + ": ")
log.Printf("%+v\n", group.PluginPerms)
}
//group.Perms.ExtData = make(map[string]bool)
// TODO: Can we optimise the bit where this cascades down to the user now?
if group.IsAdmin || group.IsMod {
group.IsBanned = false
}
mgs.groups = append(mgs.groups, &group)
}
err = rows.Err()
@ -110,15 +115,26 @@ func (mgs *MemoryGroupStore) GetCopy(gid int) (Group, error) {
}
func (mgs *MemoryGroupStore) Exists(gid int) bool {
return (gid <= mgs.groupCapCount) && (gid > -1) && mgs.groups[gid].Name != ""
return (gid <= mgs.groupCapCount) && (gid >= 0) && mgs.groups[gid].Name != ""
}
func (mgs *MemoryGroupStore) Create(groupName string, tag string, isAdmin bool, isMod bool, isBanned bool) (int, error) {
// ? Allow two groups with the same name?
func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod bool, isBanned bool) (int, error) {
groupCreateMutex.Lock()
defer groupCreateMutex.Unlock()
var permstr = "{}"
res, err := createGroupStmt.Exec(groupName, tag, isAdmin, isMod, isBanned, permstr)
tx, err := db.Begin()
if err != nil {
return 0, err
}
defer tx.Rollback()
insertTx, err := qgen.Builder.SimpleInsertTx(tx, "users_groups", "name, tag, is_admin, is_mod, is_banned, permissions", "?,?,?,?,?,?")
if err != nil {
return 0, err
}
res, err := insertTx.Exec(name, tag, isAdmin, isMod, isBanned, permstr)
if err != nil {
return 0, err
}
@ -138,14 +154,14 @@ func (mgs *MemoryGroupStore) Create(groupName string, tag string, isAdmin bool,
runVhook("create_group_preappend", &pluginPerms, &pluginPermsBytes)
}
mgs.groups = append(mgs.groups, &Group{gid, groupName, isMod, isAdmin, isBanned, tag, perms, []byte(permstr), pluginPerms, pluginPermsBytes, blankForums, blankIntList})
// Generate the forum permissions based on the presets...
fdata, err := fstore.GetAll()
if err != nil {
return 0, err
}
var presetSet = make(map[int]string)
var permSet = make(map[int]ForumPerms)
permUpdateMutex.Lock()
defer permUpdateMutex.Unlock()
for _, forum := range fdata {
@ -161,23 +177,38 @@ func (mgs *MemoryGroupStore) Create(groupName string, tag string, isAdmin bool,
}
permmap := presetToPermmap(forum.Preset)
permitem := permmap[thePreset]
permitem.Overrides = true
permstr, err := json.Marshal(permitem)
if err != nil {
return gid, err
}
perms := string(permstr)
_, err = addForumPermsToGroupStmt.Exec(gid, forum.ID, forum.Preset, perms)
if err != nil {
return gid, err
permItem := permmap[thePreset]
permItem.Overrides = true
permSet[forum.ID] = permItem
presetSet[forum.ID] = forum.Preset
}
err = replaceForumPermsForGroupTx(tx, gid, presetSet, permSet)
if err != nil {
return 0, err
}
err = tx.Commit()
if err != nil {
return 0, err
}
// TODO: Can we optimise the bit where this cascades down to the user now?
if isAdmin || isMod {
isBanned = false
}
mgs.groups = append(mgs.groups, &Group{gid, name, isMod, isAdmin, isBanned, tag, perms, []byte(permstr), pluginPerms, pluginPermsBytes, blankForums, blankIntList})
mgs.groupCapCount++
for _, forum := range fdata {
err = rebuildForumPermissions(forum.ID)
if err != nil {
return gid, err
}
}
return gid, nil
}

View File

@ -99,7 +99,7 @@ func (ins *MysqlInstaller) InitDatabase() (err error) {
return nil
}
func (ins *MysqlInstaller) TableDefs() error {
func (ins *MysqlInstaller) TableDefs() (err error) {
//fmt.Println("Creating the tables")
files, _ := ioutil.ReadDir("./schema/mysql/")
for _, f := range files {
@ -115,6 +115,13 @@ func (ins *MysqlInstaller) TableDefs() error {
}
table = strings.TrimSuffix(table, ext)
// ? - This is mainly here for tests, although it might allow the installer to overwrite a production database, so we might want to proceed with caution
_, err = ins.db.Exec("DROP TABLE IF EXISTS `" + table + "`;")
if err != nil {
fmt.Println("Failed query:", "DROP TABLE IF EXISTS `"+table+"`;")
return err
}
fmt.Println("Creating table '" + table + "'")
data, err := ioutil.ReadFile("./schema/mysql/" + f.Name())
if err != nil {

17
main.go
View File

@ -55,6 +55,12 @@ var allowedFileExts = StringList{
var imageFileExts = StringList{
"png", "jpg", "jpeg", "svg", "bmp", "gif", "tif", "webp", "apng",
}
var archiveFileExts = StringList{
"bz2", "zip", "gz", "7z", "tar", "cab",
}
var executableFileExts = StringList{
"exe", "jar", "phar", "shar", "iso",
}
// TODO: Write a test for this
func (slice StringList) Contains(needle string) bool {
@ -70,6 +76,16 @@ var staticFiles = make(map[string]SFile)
var logWriter = io.MultiWriter(os.Stderr)
func main() {
// TODO: Recover from panics
/*defer func() {
r := recover()
if r != nil {
log.Print(r)
debug.PrintStack()
return
}
}()*/
// TODO: Have a file for each run with the time/date the server started as the file name?
// TODO: Log panics with recover()
f, err := os.OpenFile("./operations.log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0755)
@ -178,7 +194,6 @@ func main() {
// TODO: Automatically lock topics, if they're really old, and the associated setting is enabled.
// TODO: Publish scheduled posts.
// TODO: Delete the empty users_groups_scheduler entries
// TODO: Add a plugin hook here
}

View File

@ -1,24 +1,26 @@
package main
import (
"bytes"
"fmt"
"net/http/httptest"
"runtime/debug"
"strconv"
"testing"
"time"
)
func recordMustExist(t *testing.T, err error, errmsg string) {
func recordMustExist(t *testing.T, err error, errmsg string, args ...interface{}) {
if err == ErrNoRows {
t.Error(errmsg)
t.Errorf(errmsg, args...)
} else if err != nil {
t.Fatal(err)
}
}
func recordMustNotExist(t *testing.T, err error, errmsg string) {
func recordMustNotExist(t *testing.T, err error, errmsg string, args ...interface{}) {
if err == nil {
t.Error(errmsg)
t.Errorf(errmsg, args...)
} else if err != ErrNoRows {
t.Fatal(err)
}
@ -42,50 +44,42 @@ func TestUserStore(t *testing.T) {
userStoreTest(t, 3)
}
func userStoreTest(t *testing.T, newUserID int) {
var user *User
var err error
ucache, hasCache := users.(UserCache)
if hasCache && ucache.Length() != 0 {
t.Error("Initial ucache length isn't zero")
// Go doesn't have short-circuiting, so this'll allow us to do one liner tests
if !hasCache {
ucache = &NullUserStore{}
}
expect(t, (!hasCache || ucache.Length() == 0), fmt.Sprintf("The initial ucache length should be zero, not %d", ucache.Length()))
_, err = users.Get(-1)
_, err := users.Get(-1)
recordMustNotExist(t, err, "UID #-1 shouldn't exist")
if hasCache && ucache.Length() != 0 {
t.Error("There shouldn't be anything in the user cache")
}
expect(t, !hasCache || ucache.Length() == 0, fmt.Sprintf("We found %d items in the user cache and it's supposed to be empty", ucache.Length()))
_, err = users.Get(0)
recordMustNotExist(t, err, "UID #0 shouldn't exist")
expect(t, !hasCache || ucache.Length() == 0, fmt.Sprintf("We found %d items in the user cache and it's supposed to be empty", ucache.Length()))
if hasCache && ucache.Length() != 0 {
t.Error("There shouldn't be anything in the user cache")
}
user, err = users.Get(1)
user, err := users.Get(1)
recordMustExist(t, err, "Couldn't find UID #1")
if user.ID != 1 {
t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.")
}
if user.Name != "Admin" {
t.Error("user.Name should be 'Admin', not '" + user.Name + "'")
}
if user.Group != 1 {
t.Error("Admin should be in group 1")
}
expect(t, user.ID == 1, fmt.Sprintf("user.ID should be 1. Got '%d' instead.", user.ID))
expect(t, user.Name == "Admin", fmt.Sprintf("user.Name should be 'Admin', not '%s'", user.Name))
expect(t, user.Group == 1, "Admin should be in group 1")
expect(t, user.IsSuperAdmin, "Admin should be a super admin")
expect(t, user.IsAdmin, "Admin should be an admin")
expect(t, user.IsSuperMod, "Admin should be a super mod")
expect(t, user.IsMod, "Admin should be a mod")
expect(t, !user.IsBanned, "Admin should not be banned")
user, err = users.Get(newUserID)
_, err = users.Get(newUserID)
recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't exist", newUserID))
if hasCache {
expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d")
user, err = ucache.CacheGet(-1)
_, err = ucache.CacheGet(-1)
recordMustNotExist(t, err, "UID #-1 shouldn't exist, even in the cache")
user, err = ucache.CacheGet(0)
_, err = ucache.CacheGet(0)
recordMustNotExist(t, err, "UID #0 shouldn't exist, even in the cache")
user, err = ucache.CacheGet(1)
recordMustExist(t, err, "Couldn't find UID #1 in the cache")
@ -97,8 +91,8 @@ func userStoreTest(t *testing.T, newUserID int) {
t.Error("user.Name should be 'Admin', not '" + user.Name + "'")
}
user, err = ucache.CacheGet(newUserID)
recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't exist, even in the cache", newUserID))
_, err = ucache.CacheGet(newUserID)
recordMustNotExist(t, err, "UID #%d shouldn't exist, even in the cache", newUserID)
ucache.Flush()
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
@ -147,9 +141,8 @@ func userStoreTest(t *testing.T, newUserID int) {
recordMustExist(t, err, "Couldn't find UID #1 in the cache")
if user.ID != 1 {
t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.")
t.Errorf("user.ID does not match the requested UID. Got '%d' instead.", user.ID)
}
ucache.Flush()
}
@ -158,120 +151,281 @@ func userStoreTest(t *testing.T, newUserID int) {
expect(t, users.Exists(1), "UID #1 should exist")
expect(t, !users.Exists(newUserID), fmt.Sprintf("UID #%d shouldn't exist", newUserID))
if hasCache {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
}
expect(t, !hasCache || ucache.Length() == 0, fmt.Sprintf("User cache length should be 0, not %d", ucache.Length()))
expectIntToBeX(t, users.GlobalCount(), 1, "The number of users should be one, not %d")
var awaitingActivation = 5
uid, err := users.Create("Sam", "ReallyBadPassword", "sam@localhost.loc", awaitingActivation, 0)
if err != nil {
t.Error(err)
}
if uid != newUserID {
t.Errorf("The UID of the new user should be %d", newUserID)
}
if !users.Exists(newUserID) {
t.Errorf("UID #%d should exist", newUserID)
}
uid, err := users.Create("Sam", "ReallyBadPassword", "sam@localhost.loc", awaitingActivation, false)
expectNilErr(t, err)
expect(t, uid == newUserID, fmt.Sprintf("The UID of the new user should be %d", newUserID))
expect(t, users.Exists(newUserID), fmt.Sprintf("UID #%d should exist", newUserID))
user, err = users.Get(newUserID)
recordMustExist(t, err, fmt.Sprintf("Couldn't find UID #%d", newUserID))
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
if user.ID != newUserID {
t.Errorf("The UID of the user record should be %d", newUserID)
}
if user.Name != "Sam" {
t.Error("The user should be named Sam")
}
expect(t, user.Name == "Sam", "The user should be named Sam")
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expect(t, !user.IsAdmin, "Sam should not be an admin")
expect(t, !user.IsSuperMod, "Sam should not be a super mod")
expect(t, !user.IsMod, "Sam should not be a mod")
expect(t, !user.IsBanned, "Sam should not be banned")
expectIntToBeX(t, user.Group, 5, "Sam should be in group 5")
if hasCache {
expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d")
user, err = ucache.CacheGet(newUserID)
recordMustExist(t, err, fmt.Sprintf("Couldn't find UID #%d in the cache", newUserID))
if user.ID != newUserID {
t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.")
}
recordMustExist(t, err, "Couldn't find UID #%d in the cache", newUserID)
expect(t, user.ID == newUserID, fmt.Sprintf("user.ID does not match the requested UID. Got '%d' instead.", user.ID))
}
err = user.Activate()
if err != nil {
t.Error(err)
}
expectNilErr(t, err)
expectIntToBeX(t, user.Group, 5, "Sam should still be in group 5 in this copy")
// ? - What if we change the caching mechanism so it isn't hard purged and reloaded? We'll deal with that when we come to it, but for now, this is a sign of a cache bug
if hasCache {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(newUserID)
recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't be in the cache", newUserID))
recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID)
}
user, err = users.Get(newUserID)
recordMustExist(t, err, fmt.Sprintf("Couldn't find UID #%d", newUserID))
if user.ID != newUserID {
t.Errorf("The UID of the user record should be %d", newUserID)
}
expectIntToBeX(t, user.Group, config.DefaultGroup, "Sam should be in group "+strconv.Itoa(config.DefaultGroup))
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
expect(t, user.ID == newUserID, fmt.Sprintf("The UID of the user record should be %d, not %d", newUserID, user.ID))
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expect(t, !user.IsAdmin, "Sam should not be an admin")
expect(t, !user.IsSuperMod, "Sam should not be a super mod")
expect(t, !user.IsMod, "Sam should not be a mod")
expect(t, !user.IsBanned, "Sam should not be banned")
expect(t, user.Group == config.DefaultGroup, fmt.Sprintf("Sam should be in group %d, not %d", config.DefaultGroup, user.Group))
// Permanent ban
duration, _ := time.ParseDuration("0")
// TODO: Attempt a double ban, double activation, and double unban
err = user.Ban(duration, 1)
if err != nil {
t.Error(err)
}
expectIntToBeX(t, user.Group, config.DefaultGroup, "Sam should still be in the default group in this copy")
expectNilErr(t, err)
expect(t, user.Group == config.DefaultGroup, fmt.Sprintf("Sam should be in group %d, not %d", config.DefaultGroup, user.Group))
if hasCache {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(2)
recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't be in the cache", newUserID))
recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID)
}
user, err = users.Get(newUserID)
recordMustExist(t, err, fmt.Sprintf("Couldn't find UID #%d", newUserID))
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
if user.ID != newUserID {
t.Errorf("The UID of the user record should be %d", newUserID)
}
expectIntToBeX(t, user.Group, banGroup, "Sam should be in group "+strconv.Itoa(banGroup))
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expect(t, !user.IsAdmin, "Sam should not be an admin")
expect(t, !user.IsSuperMod, "Sam should not be a super mod")
expect(t, !user.IsMod, "Sam should not be a mod")
expect(t, user.IsBanned, "Sam should be banned")
expectIntToBeX(t, user.Group, banGroup, "Sam should be in group %d")
// TODO: Do tests against the scheduled updates table and the task system to make sure the ban exists there and gets revoked when it should
err = user.Unban()
if err != nil {
t.Error(err)
}
expectNilErr(t, err)
expectIntToBeX(t, user.Group, banGroup, "Sam should still be in the ban group in this copy")
if hasCache {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(newUserID)
recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't be in the cache", newUserID))
recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID)
}
user, err = users.Get(newUserID)
recordMustExist(t, err, fmt.Sprintf("Couldn't find UID #%d", newUserID))
if user.ID != newUserID {
t.Errorf("The UID of the user record should be %d", newUserID)
}
expectIntToBeX(t, user.Group, config.DefaultGroup, "Sam should be back in group "+strconv.Itoa(config.DefaultGroup))
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
expectIntToBeX(t, user.ID, newUserID, "The UID of the user record should be %d")
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expect(t, !user.IsAdmin, "Sam should not be an admin")
expect(t, !user.IsSuperMod, "Sam should not be a super mod")
expect(t, !user.IsMod, "Sam should not be a mod")
expect(t, !user.IsBanned, "Sam should not be banned")
expectIntToBeX(t, user.Group, config.DefaultGroup, "Sam should be back in group %d")
var reportsForumID = 1
var generalForumID = 2
dummyResponseRecorder := httptest.NewRecorder()
bytesBuffer := bytes.NewBuffer([]byte(""))
dummyRequest1 := httptest.NewRequest("", "/forum/1", bytesBuffer)
dummyRequest2 := httptest.NewRequest("", "/forum/2", bytesBuffer)
err = user.ChangeGroup(1)
expectNilErr(t, err)
expect(t, user.Group == config.DefaultGroup, "Someone's mutated this pointer elsewhere")
user, err = users.Get(newUserID)
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
expectIntToBeX(t, user.ID, newUserID, "The UID of the user record should be %d")
var user2 *User = getDummyUser()
*user2 = *user
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expect(t, user.IsAdmin, "Sam should be an admin")
expect(t, user.IsSuperMod, "Sam should be a super mod")
expect(t, user.IsMod, "Sam should be a mod")
expect(t, !user.IsBanned, "Sam should not be banned")
_, success := forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, user.Perms.ViewTopic, "Admins should be able to access the reports forum")
_, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum")
err = user.ChangeGroup(2)
expectNilErr(t, err)
expect(t, user.Group == 1, "Someone's mutated this pointer elsewhere")
user, err = users.Get(newUserID)
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
expectIntToBeX(t, user.ID, newUserID, "The UID of the user record should be %d")
user2 = getDummyUser()
*user2 = *user
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expect(t, !user.IsAdmin, "Sam should not be an admin")
expect(t, user.IsSuperMod, "Sam should be a super mod")
expect(t, user.IsMod, "Sam should be a mod")
expect(t, !user.IsBanned, "Sam should not be banned")
_, success = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, user.Perms.ViewTopic, "Mods should be able to access the reports forum")
_, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum")
err = user.ChangeGroup(3)
expectNilErr(t, err)
expect(t, user.Group == 2, "Someone's mutated this pointer elsewhere")
user, err = users.Get(newUserID)
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
expectIntToBeX(t, user.ID, newUserID, "The UID of the user record should be %d")
user2 = getDummyUser()
*user2 = *user
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expect(t, !user.IsAdmin, "Sam should not be an admin")
expect(t, !user.IsSuperMod, "Sam should not be a super mod")
expect(t, !user.IsMod, "Sam should not be a mod")
expect(t, !user.IsBanned, "Sam should not be banned")
_, success = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, !user.Perms.ViewTopic, "Members shouldn't be able to access the reports forum")
_, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum")
expect(t, user.Perms.ViewTopic != user2.Perms.ViewTopic, "user.Perms.ViewTopic and user2.Perms.ViewTopic should never match")
err = user.ChangeGroup(4)
expectNilErr(t, err)
expect(t, user.Group == 3, "Someone's mutated this pointer elsewhere")
user, err = users.Get(newUserID)
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
expectIntToBeX(t, user.ID, newUserID, "The UID of the user record should be %d")
user2 = getDummyUser()
*user2 = *user
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expect(t, !user.IsAdmin, "Sam should not be an admin")
expect(t, !user.IsSuperMod, "Sam should not be a super mod")
expect(t, !user.IsMod, "Sam should not be a mod")
expect(t, user.IsBanned, "Sam should be banned")
_, success = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, !user.Perms.ViewTopic, "Members shouldn't be able to access the reports forum")
_, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum")
err = user.ChangeGroup(5)
expectNilErr(t, err)
expect(t, user.Group == 4, "Someone's mutated this pointer elsewhere")
user, err = users.Get(newUserID)
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
expectIntToBeX(t, user.ID, newUserID, "The UID of the user record should be %d")
user2 = getDummyUser()
*user2 = *user
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expect(t, !user.IsAdmin, "Sam should not be an admin")
expect(t, !user.IsSuperMod, "Sam should not be a super mod")
expect(t, !user.IsMod, "Sam should not be a mod")
expect(t, !user.IsBanned, "Sam should not be banned")
_, success = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, !user.Perms.ViewTopic, "Members shouldn't be able to access the reports forum")
_, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum")
err = user.ChangeGroup(6)
expectNilErr(t, err)
expect(t, user.Group == 5, "Someone's mutated this pointer elsewhere")
user, err = users.Get(newUserID)
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
expectIntToBeX(t, user.ID, newUserID, "The UID of the user record should be %d")
user2 = getDummyUser()
*user2 = *user
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expect(t, !user.IsAdmin, "Sam should not be an admin")
expect(t, !user.IsSuperMod, "Sam should not be a super mod")
expect(t, !user.IsMod, "Sam should not be a mod")
expect(t, !user.IsBanned, "Sam should not be banned")
_, success = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, !user.Perms.ViewTopic, "Members shouldn't be able to access the reports forum")
_, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID)
expect(t, success, "There shouldn't be any errors in forumUserCheck")
expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum")
err = user.ChangeGroup(config.DefaultGroup)
expectNilErr(t, err)
expect(t, user.Group == 6, "Someone's mutated this pointer elsewhere")
err = user.Delete()
if err != nil {
t.Error(err)
}
expect(t, !users.Exists(newUserID), fmt.Sprintf("UID #%d should not longer exist", newUserID))
expectNilErr(t, err)
expect(t, !users.Exists(newUserID), fmt.Sprintf("UID #%d should no longer exist", newUserID))
if hasCache {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(newUserID)
recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't be in the cache", newUserID))
recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID)
}
// TODO: Works for now but might cause a data race with the task system
//ResetTables()
_, err = users.Get(newUserID)
recordMustNotExist(t, err, "UID #%d shouldn't exist", newUserID)
}
// TODO: Add an error message to this?
func expectNilErr(t *testing.T, item error) {
if item != nil {
debug.PrintStack()
t.Fatal(item)
}
}
func expectIntToBeX(t *testing.T, item int, expect int, errmsg string) {
@ -284,7 +438,7 @@ func expectIntToBeX(t *testing.T, item int, expect int, errmsg string) {
func expect(t *testing.T, item bool, errmsg string) {
if !item {
debug.PrintStack()
t.Fatalf(errmsg)
t.Fatal(errmsg)
}
}
@ -309,18 +463,10 @@ func topicStoreTest(t *testing.T) {
var err error
_, err = topics.Get(-1)
if err == nil {
t.Error("TID #-1 shouldn't exist")
} else if err != ErrNoRows {
t.Fatal(err)
}
recordMustNotExist(t, err, "TID #-1 shouldn't exist")
_, err = topics.Get(0)
if err == nil {
t.Error("TID #0 shouldn't exist")
} else if err != ErrNoRows {
t.Fatal(err)
}
recordMustNotExist(t, err, "TID #0 shouldn't exist")
topic, err = topics.Get(1)
recordMustExist(t, err, "Couldn't find TID #1")
@ -361,24 +507,13 @@ func TestForumStore(t *testing.T) {
initPlugins()
}
var forum *Forum
var err error
_, err := fstore.Get(-1)
recordMustNotExist(t, err, "FID #-1 shouldn't exist")
_, err = fstore.Get(-1)
if err == nil {
t.Error("FID #-1 shouldn't exist")
} else if err != ErrNoRows {
t.Fatal(err)
}
_, err = fstore.Get(0)
recordMustNotExist(t, err, "FID #0 shouldn't exist")
forum, err = fstore.Get(0)
if err == nil {
t.Error("FID #0 shouldn't exist")
} else if err != ErrNoRows {
t.Fatal(err)
}
forum, err = fstore.Get(1)
forum, err := fstore.Get(1)
recordMustExist(t, err, "Couldn't find FID #1")
if forum.ID != 1 {
@ -417,50 +552,95 @@ func TestGroupStore(t *testing.T) {
initPlugins()
}
var group *Group
var err error
_, err = gstore.Get(-1)
if err == nil {
t.Error("GID #-1 shouldn't exist")
} else if err != ErrNoRows {
t.Fatal(err)
}
_, err := gstore.Get(-1)
recordMustNotExist(t, err, "GID #-1 shouldn't exist")
// TODO: Refactor the group store to remove GID #0
group, err = gstore.Get(0)
group, err := gstore.Get(0)
recordMustExist(t, err, "Couldn't find GID #0")
if group.ID != 0 {
t.Error("group.ID doesn't not match the requested GID. Got '" + strconv.Itoa(group.ID) + "' instead.")
}
if group.Name != "Unknown" {
t.Error("GID #0 is named '" + group.Name + "' and not 'Unknown'")
t.Errorf("group.ID doesn't not match the requested GID. Got '%d' instead.", group.ID)
}
expect(t, group.Name == "Unknown", fmt.Sprintf("GID #0 is named '%s' and not 'Unknown'", group.Name))
group, err = gstore.Get(1)
recordMustExist(t, err, "Couldn't find GID #1")
if group.ID != 1 {
t.Error("group.ID doesn't not match the requested GID. Got '" + strconv.Itoa(group.ID) + "' instead.'")
t.Errorf("group.ID doesn't not match the requested GID. Got '%d' instead.'", group.ID)
}
_ = group
ok := gstore.Exists(-1)
if ok {
t.Error("GID #-1 shouldn't exist")
}
expect(t, !ok, "GID #-1 shouldn't exist")
// 0 aka Unknown, for system posts and other oddities
ok = gstore.Exists(0)
if !ok {
t.Error("GID #0 should exist")
}
expect(t, ok, "GID #0 should exist")
ok = gstore.Exists(1)
if !ok {
t.Error("GID #1 should exist")
}
expect(t, ok, "GID #1 should exist")
var isAdmin = true
var isMod = true
var isBanned = false
gid, err := gstore.Create("Testing", "Test", isAdmin, isMod, isBanned)
expectNilErr(t, err)
expect(t, gstore.Exists(gid), "The group we just made doesn't exist")
group, err = gstore.Get(gid)
expectNilErr(t, err)
expect(t, group.ID == gid, "The group ID should match the requested ID")
expect(t, group.IsAdmin, "This should be an admin group")
expect(t, group.IsMod, "This should be a mod group")
expect(t, !group.IsBanned, "This shouldn't be a ban group")
isAdmin = false
isMod = true
isBanned = true
gid, err = gstore.Create("Testing 2", "Test", isAdmin, isMod, isBanned)
expectNilErr(t, err)
expect(t, gstore.Exists(gid), "The group we just made doesn't exist")
group, err = gstore.Get(gid)
expectNilErr(t, err)
expect(t, group.ID == gid, "The group ID should match the requested ID")
expect(t, !group.IsAdmin, "This should not be an admin group")
expect(t, group.IsMod, "This should be a mod group")
expect(t, !group.IsBanned, "This shouldn't be a ban group")
// TODO: Make sure this pointer doesn't change once we refactor the group store to stop updating the pointer
err = group.ChangeRank(false, false, true)
expectNilErr(t, err)
group, err = gstore.Get(gid)
expectNilErr(t, err)
expect(t, group.ID == gid, "The group ID should match the requested ID")
expect(t, !group.IsAdmin, "This shouldn't be an admin group")
expect(t, !group.IsMod, "This shouldn't be a mod group")
expect(t, group.IsBanned, "This should be a ban group")
err = group.ChangeRank(true, true, true)
expectNilErr(t, err)
group, err = gstore.Get(gid)
expectNilErr(t, err)
expect(t, group.ID == gid, "The group ID should match the requested ID")
expect(t, group.IsAdmin, "This should be an admin group")
expect(t, group.IsMod, "This should be a mod group")
expect(t, !group.IsBanned, "This shouldn't be a ban group")
err = group.ChangeRank(false, true, true)
expectNilErr(t, err)
group, err = gstore.Get(gid)
expectNilErr(t, err)
expect(t, group.ID == gid, "The group ID should match the requested ID")
expect(t, !group.IsAdmin, "This shouldn't be an admin group")
expect(t, group.IsMod, "This should be a mod group")
expect(t, !group.IsBanned, "This shouldn't be a ban group")
// TODO: Test group deletion
}
func TestReplyStore(t *testing.T) {
@ -471,28 +651,23 @@ func TestReplyStore(t *testing.T) {
initPlugins()
}
reply, err := rstore.Get(-1)
if err == nil {
t.Error("RID #-1 shouldn't exist")
}
_, err := rstore.Get(-1)
recordMustNotExist(t, err, "RID #-1 shouldn't exist")
reply, err = rstore.Get(0)
if err == nil {
t.Error("RID #0 shouldn't exist")
}
_, err = rstore.Get(0)
recordMustNotExist(t, err, "RID #0 shouldn't exist")
reply, err := rstore.Get(1)
expectNilErr(t, err)
reply, err = rstore.Get(1)
if err != nil {
t.Fatal(err)
}
if reply.ID != 1 {
t.Error("RID #1 has the wrong ID. It should be 1 not " + strconv.Itoa(reply.ID))
t.Errorf("RID #1 has the wrong ID. It should be 1 not %d", reply.ID)
}
if reply.ParentID != 1 {
t.Error("The parent topic of RID #1 should be 1 not " + strconv.Itoa(reply.ParentID))
t.Errorf("The parent topic of RID #1 should be 1 not %d", reply.ParentID)
}
if reply.CreatedBy != 1 {
t.Error("The creator of RID #1 should be 1 not " + strconv.Itoa(reply.CreatedBy))
t.Errorf("The creator of RID #1 should be 1 not %d", reply.CreatedBy)
}
}
@ -505,14 +680,10 @@ func TestProfileReplyStore(t *testing.T) {
}
_, err := prstore.Get(-1)
if err == nil {
t.Error("RID #-1 shouldn't exist")
}
recordMustNotExist(t, err, "RID #-1 shouldn't exist")
_, err = prstore.Get(0)
if err == nil {
t.Error("RID #0 shouldn't exist")
}
recordMustNotExist(t, err, "RID #0 shouldn't exist")
}
func TestSlugs(t *testing.T) {
@ -558,7 +729,7 @@ func TestAuth(t *testing.T) {
/* No extra salt tests, we might not need this extra salt, as bcrypt has it's own? */
realPassword = "Madame Cassandra's Mystic Orb"
t.Log("Set real_password to '" + realPassword + "'")
t.Log("Set realPassword to '" + realPassword + "'")
t.Log("Hashing the real password")
hashedPassword, err = BcryptGeneratePasswordNoSalt(realPassword)
if err != nil {

View File

@ -819,7 +819,10 @@ func routeUnban(w http.ResponseWriter, r *http.Request, user User) {
}
err = targetUser.Unban()
if err == ErrNoRows {
if err == ErrNoTempGroup {
LocalError("The user you're trying to unban is not banned", w, r, user)
return
} else if err == ErrNoRows {
LocalError("The user you're trying to unban no longer exists.", w, r, user)
return
} else if err != nil {

View File

@ -456,6 +456,7 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
msg = strings.Replace(msg, ":O", "😲", -1)
msg = strings.Replace(msg, ":p", "😛", -1)
msg = strings.Replace(msg, ":o", "😲", -1)
msg = strings.Replace(msg, ";)", "😉", -1)
//msg = url_reg.ReplaceAllString(msg,"<a href=\"$2$3//$4\" rel=\"nofollow\">$2$3//$4</a>")
// Word filter list. E.g. Swear words and other things the admins don't like
@ -473,13 +474,12 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
var lastItem = 0
var i = 0
for ; len(msgbytes) > (i + 1); i++ {
//log.Print("Index:",i)
//log.Print("Index: ",i)
//log.Print("Index Item: ",msgbytes[i])
//log.Print("string(msgbytes[i]): ",string(msgbytes[i]))
//log.Print("End Index")
if (i == 0 && (msgbytes[0] > 32)) || ((msgbytes[i] < 33) && (msgbytes[i+1] > 32)) {
//log.Print("IN")
//log.Print(msgbytes[i])
//log.Print("IN ",msgbytes[i])
if (i != 0) || msgbytes[i] < 33 {
i++
}
@ -509,12 +509,12 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
outbytes = append(outbytes, urlClose...)
lastItem = i
//log.Print("string(msgbytes) ",string(msgbytes))
//log.Print("msgbytes ",msgbytes)
//log.Print(msgbytes[lastItem - 1])
//log.Print(lastItem - 1)
//log.Print(msgbytes[lastItem])
//log.Print("lastItem ",lastItem)
//log.Print("string(msgbytes): ",string(msgbytes))
//log.Print("msgbytes: ",msgbytes)
//log.Print("msgbytes[lastItem - 1]: ",msgbytes[lastItem - 1])
//log.Print("lastItem - 1: ",lastItem - 1)
//log.Print("msgbytes[lastItem]: ",msgbytes[lastItem])
//log.Print("lastItem: ",lastItem)
} else if bytes.Equal(msgbytes[i+1:i+5], []byte("rid-")) {
outbytes = append(outbytes, msgbytes[lastItem:i]...)
i += 5
@ -589,10 +589,10 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
//log.Print(string(msgbytes))
//log.Print(msgbytes)
//log.Print(msgbytes[lastItem - 1])
//log.Print("lastItem - 1",lastItem - 1)
//log.Print("msgbytes[lastItem]",msgbytes[lastItem])
//log.Print("lastItem",lastItem)
//log.Print("msgbytes[lastItem - 1]: ", msgbytes[lastItem - 1])
//log.Print("lastItem - 1: ", lastItem - 1)
//log.Print("msgbytes[lastItem]: ", msgbytes[lastItem])
//log.Print("lastItem: ", lastItem)
} else if msgbytes[i] == 'h' || msgbytes[i] == 'f' || msgbytes[i] == 'g' {
//log.Print("IN hfg")
if msgbytes[i+1] == 't' && msgbytes[i+2] == 't' && msgbytes[i+3] == 'p' {
@ -616,10 +616,10 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
urlLen := partialURLBytesLen(msgbytes[i:])
if msgbytes[i+urlLen] > 32 { // space and invisibles
//log.Print("INVALID URL")
//log.Print("msgbytes[i+urlLen]", msgbytes[i+urlLen])
//log.Print("string(msgbytes[i+urlLen])", string(msgbytes[i+urlLen]))
//log.Print("msgbytes[i:i+urlLen]", msgbytes[i:i+urlLen])
//log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen]))
//log.Print("msgbytes[i+urlLen]: ", msgbytes[i+urlLen])
//log.Print("string(msgbytes[i+urlLen]): ", string(msgbytes[i+urlLen]))
//log.Print("msgbytes[i:i+urlLen]: ", msgbytes[i:i+urlLen])
//log.Print("string(msgbytes[i:i+urlLen]): ", string(msgbytes[i:i+urlLen]))
outbytes = append(outbytes, invalidURL...)
i += urlLen
continue
@ -673,18 +673,18 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
urlLen := partialURLBytesLen(msgbytes[i:])
if msgbytes[i+urlLen] > 32 { // space and invisibles
//log.Print("INVALID URL")
//log.Print("msgbytes[i+urlLen]", msgbytes[i+urlLen])
//log.Print("string(msgbytes[i+urlLen])", string(msgbytes[i+urlLen]))
//log.Print("msgbytes[i:i+urlLen]", msgbytes[i:i+urlLen])
//log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen]))
//log.Print("msgbytes[i+urlLen]: ", msgbytes[i+urlLen])
//log.Print("string(msgbytes[i+urlLen]): ", string(msgbytes[i+urlLen]))
//log.Print("msgbytes[i:i+urlLen]: ", msgbytes[i:i+urlLen])
//log.Print("string(msgbytes[i:i+urlLen]): ", string(msgbytes[i:i+urlLen]))
outbytes = append(outbytes, invalidURL...)
i += urlLen
continue
}
//log.Print("VALID URL")
//log.Print("msgbytes[i:i+urlLen]", msgbytes[i:i+urlLen])
//log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen]))
//log.Print("msgbytes[i:i+urlLen]: ", msgbytes[i:i+urlLen])
//log.Print("string(msgbytes[i:i+urlLen]): ", string(msgbytes[i:i+urlLen]))
media, ok := parseMediaBytes(msgbytes[i : i+urlLen])
if !ok {
outbytes = append(outbytes, invalidURL...)
@ -733,10 +733,10 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
}
if lastItem != i && len(outbytes) != 0 {
//log.Print("lastItem: ",msgbytes[lastItem])
//log.Print("lastItem index: ",lastItem)
//log.Print("i: ",i)
//log.Print("lastItem to end: ",msgbytes[lastItem:])
//log.Print("lastItem: ", msgbytes[lastItem])
//log.Print("lastItem index: ", lastItem)
//log.Print("i: ", i)
//log.Print("lastItem to end: ", msgbytes[lastItem:])
//log.Print("-----")
calclen := len(msgbytes) - 10
if calclen <= lastItem {
@ -756,7 +756,7 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
}
// 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, ":D", "😃", -1)
msg = strings.Replace(msg, ":P", "😛", -1)
@ -766,7 +766,7 @@ func regexParseMessage(msg string) string {
msg = runSshook("parse_assign", msg)
}
return msg
}
}*/
// 6, 7, 8, 6, 2, 7
// ftp://, http://, https:// git://, //, mailto: (not a URL, just here for length comparison purposes)

View File

@ -451,7 +451,6 @@ func routePanelForumsEditSubmit(w http.ResponseWriter, r *http.Request, user Use
}
}
// ! This probably misses the forumView cache
func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, user User, sfid string) {
_, ok := SimplePanelUserCheck(w, r, &user)
if !ok {
@ -509,24 +508,24 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use
}
group.Forums[fid] = fperms
perms, err := json.Marshal(fperms)
if err != nil {
InternalErrorJSQ(err, w, r, isJs)
return
}
_, err = addForumPermsToGroupStmt.Exec(gid, fid, permPreset, perms)
err = replaceForumPermsForGroup(gid, map[int]string{fid: permPreset}, map[int]ForumPerms{fid: fperms})
if err != nil {
InternalErrorJSQ(err, w, r, isJs)
return
}
// TODO: Add this and replaceForumPermsForGroup into a transaction?
_, err = updateForumStmt.Exec(forum.Name, forum.Desc, forum.Active, "", fid)
if err != nil {
InternalErrorJSQ(err, w, r, isJs)
return
}
forum.Preset = ""
err = fstore.Reload(fid)
if err != nil {
// Log this? -- Another admin might have deleted it
LocalErrorJSQ("Unable to reload forum", w, r, user, isJs)
return
}
}
if !isJs {
@ -1347,7 +1346,7 @@ func routePanelGroups(w http.ResponseWriter, r *http.Request, user User) {
perPage := 9
offset, page, lastPage := pageOffset(stats.Groups, page, perPage)
// Skip the System group
// Skip the 'Unknown' group
offset++
var count int
@ -1436,15 +1435,16 @@ func routePanelGroupsEdit(w http.ResponseWriter, r *http.Request, user User, sgi
}
var rank string
if group.IsAdmin {
switch {
case group.IsAdmin:
rank = "Admin"
} else if group.IsMod {
case group.IsMod:
rank = "Mod"
} else if group.IsBanned {
case group.IsBanned:
rank = "Banned"
} else if group.ID == 6 {
case group.ID == 6:
rank = "Guest"
} else {
default:
rank = "Member"
}
@ -1619,54 +1619,28 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user Use
LocalError("You need the EditGroupAdmin permission to designate this group as an admin group.", w, r, user)
return
}
_, err = updateGroupRankStmt.Exec(1, 1, 0, gid)
if err != nil {
InternalError(err, w)
return
}
group.IsAdmin = true
group.IsMod = true
group.IsBanned = false
err = group.ChangeRank(true, true, false)
case "Mod":
if !user.Perms.EditGroupSuperMod {
LocalError("You need the EditGroupSuperMod permission to designate this group as a super-mod group.", w, r, user)
return
}
_, err = updateGroupRankStmt.Exec(0, 1, 0, gid)
if err != nil {
InternalError(err, w)
return
}
group.IsAdmin = false
group.IsMod = true
group.IsBanned = false
err = group.ChangeRank(false, true, false)
case "Banned":
_, err = updateGroupRankStmt.Exec(0, 0, 1, gid)
if err != nil {
InternalError(err, w)
return
}
group.IsAdmin = false
group.IsMod = false
group.IsBanned = true
err = group.ChangeRank(false, false, true)
case "Guest":
LocalError("You can't designate a group as a guest group.", w, r, user)
return
case "Member":
_, err = updateGroupRankStmt.Exec(0, 0, 0, gid)
if err != nil {
InternalError(err, w)
return
}
group.IsAdmin = false
group.IsMod = false
group.IsBanned = false
err = group.ChangeRank(false, false, false)
default:
LocalError("Invalid group type.", w, r, user)
return
}
if err != nil {
InternalError(err, w)
return
}
}
_, err = updateGroupStmt.Exec(gname, gtag, gid)

View File

@ -1,9 +1,16 @@
package main
import "log"
import "sync"
import "strconv"
import "encoding/json"
import (
"database/sql"
"encoding/json"
"log"
"strconv"
"sync"
"./query_gen/lib"
)
// TODO: Refactor the perms system
var permUpdateMutex sync.Mutex
var BlankPerms Perms
@ -261,10 +268,18 @@ func presetToPermmap(preset string) (out map[string]ForumPerms) {
}
func permmapToQuery(permmap map[string]ForumPerms, fid int) error {
permUpdateMutex.Lock()
defer permUpdateMutex.Unlock()
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
_, err := deleteForumPermsByForumStmt.Exec(fid)
deleteForumPermsByForumTx, err := qgen.Builder.SimpleDeleteTx(tx, "forums_permissions", "fid = ?")
if err != nil {
return err
}
_, err = deleteForumPermsByForumTx.Exec(fid)
if err != nil {
return err
}
@ -273,7 +288,16 @@ func permmapToQuery(permmap map[string]ForumPerms, fid int) error {
if err != nil {
return err
}
_, err = addForumPermsToForumAdminsStmt.Exec(fid, "", perms)
addForumPermsToForumAdminsTx, err := qgen.Builder.SimpleInsertSelectTx(tx,
qgen.DB_Insert{"forums_permissions", "gid, fid, preset, permissions", ""},
qgen.DB_Select{"users_groups", "gid, ? AS fid, ? AS preset, ? AS permissions", "is_admin = 1", "", ""},
)
if err != nil {
return err
}
_, err = addForumPermsToForumAdminsTx.Exec(fid, "", perms)
if err != nil {
return err
}
@ -282,7 +306,15 @@ func permmapToQuery(permmap map[string]ForumPerms, fid int) error {
if err != nil {
return err
}
_, err = addForumPermsToForumStaffStmt.Exec(fid, "", perms)
addForumPermsToForumStaffTx, err := qgen.Builder.SimpleInsertSelectTx(tx,
qgen.DB_Insert{"forums_permissions", "gid, fid, preset, permissions", ""},
qgen.DB_Select{"users_groups", "gid, ? AS fid, ? AS preset, ? AS permissions", "is_admin = 0 AND is_mod = 1", "", ""},
)
if err != nil {
return err
}
_, err = addForumPermsToForumStaffTx.Exec(fid, "", perms)
if err != nil {
return err
}
@ -291,23 +323,77 @@ func permmapToQuery(permmap map[string]ForumPerms, fid int) error {
if err != nil {
return err
}
_, err = addForumPermsToForumMembersStmt.Exec(fid, "", perms)
addForumPermsToForumMembersTx, err := qgen.Builder.SimpleInsertSelectTx(tx,
qgen.DB_Insert{"forums_permissions", "gid, fid, preset, permissions", ""},
qgen.DB_Select{"users_groups", "gid, ? AS fid, ? AS preset, ? AS permissions", "is_admin = 0 AND is_mod = 0 AND is_banned = 0", "", ""},
)
if err != nil {
return err
}
_, err = addForumPermsToForumMembersTx.Exec(fid, "", perms)
if err != nil {
return err
}
perms, err = json.Marshal(permmap["guests"])
if err != nil {
return err
}
_, err = addForumPermsToGroupStmt.Exec(6, fid, "", perms)
// 6 is the ID of the Not Loggedin Group
// TODO: Use a shared variable rather than a literal for the group ID
err = replaceForumPermsForGroupTx(tx, 6, map[int]string{fid: ""}, map[int]ForumPerms{fid: permmap["guests"]})
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
permUpdateMutex.Lock()
defer permUpdateMutex.Unlock()
return rebuildForumPermissions(fid)
}
func replaceForumPermsForGroup(gid int, presetSet map[int]string, permSets map[int]ForumPerms) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
err = replaceForumPermsForGroupTx(tx, gid, presetSet, permSets)
if err != nil {
return err
}
return tx.Commit()
}
func replaceForumPermsForGroupTx(tx *sql.Tx, gid int, presetSets map[int]string, permSets map[int]ForumPerms) error {
deleteForumPermsForGroupTx, err := qgen.Builder.SimpleDeleteTx(tx, "forums_permissions", "gid = ? AND fid = ?")
if err != nil {
return err
}
addForumPermsToGroupTx, err := qgen.Builder.SimpleInsertTx(tx, "forums_permissions", "gid, fid, preset, permissions", "?,?,?,?")
if err != nil {
return err
}
for fid, permSet := range permSets {
permstr, err := json.Marshal(permSet)
if err != nil {
return err
}
_, err = deleteForumPermsForGroupTx.Exec(gid, fid)
if err != nil {
return err
}
_, err = addForumPermsToGroupTx.Exec(gid, fid, presetSets[fid], string(permstr))
if err != nil {
return err
}
}
return nil
}
// TODO: Need a more thread-safe way of doing this. Possibly with sync.Map?
func rebuildForumPermissions(fid int) error {
if dev.DebugMode {

View File

@ -2,30 +2,27 @@ package main
//import "fmt"
import (
"regexp"
"log"
"strings"
)
var markdownMaxDepth = 25 // How deep the parser will go when parsing Markdown strings
var markdownUnclosedElement []byte
var markdownBoldTagOpen, markdownBoldTagClose []byte
var markdownItalicTagOpen, markdownItalicTagClose []byte
var markdownUnderlineTagOpen, markdownUnderlineTagClose []byte
var markdownStrikeTagOpen, markdownStrikeTagClose []byte
var markdownBoldItalic *regexp.Regexp
var markdownBold *regexp.Regexp
var markdownItalic *regexp.Regexp
var markdownStrike *regexp.Regexp
var markdownUnderline *regexp.Regexp
var markdownBoldTagOpen []byte
var markdownBoldTagClose []byte
var markdownItalicTagOpen []byte
var markdownItalicTagClose []byte
var markdownUnderlineTagOpen []byte
var markdownUnderlineTagClose []byte
var markdownStrikeTagOpen []byte
var markdownStrikeTagClose []byte
func init() {
plugins["markdown"] = NewPlugin("markdown", "Markdown", "Azareal", "http://github.com/Azareal", "", "", "", initMarkdown, nil, deactivateMarkdown, nil, nil)
}
func initMarkdown() error {
//plugins["markdown"].AddHook("parse_assign", markdownRegexParse)
plugins["markdown"].AddHook("parse_assign", markdownParse)
markdownUnclosedElement = []byte("<span style='color: red;'>[Unclosed Element]</span>")
@ -38,35 +35,19 @@ func initMarkdown() error {
markdownUnderlineTagClose = []byte("</u>")
markdownStrikeTagOpen = []byte("<s>")
markdownStrikeTagClose = []byte("</s>")
markdownBoldItalic = regexp.MustCompile(`\*\*\*(.*)\*\*\*`)
markdownBold = regexp.MustCompile(`\*\*(.*)\*\*`)
markdownItalic = regexp.MustCompile(`\*(.*)\*`)
//markdownStrike = regexp.MustCompile(`\~\~(.*)\~\~`)
markdownStrike = regexp.MustCompile(`\~(.*)\~`)
//markdown_underline = regexp.MustCompile(`\_\_(.*)\_\_`)
markdownUnderline = regexp.MustCompile(`\_(.*)\_`)
return nil
}
func deactivateMarkdown() {
//plugins["markdown"].RemoveHook("parse_assign", markdownRegexParse)
plugins["markdown"].RemoveHook("parse_assign", markdownParse)
}
func markdownRegexParse(msg string) string {
msg = markdownBoldItalic.ReplaceAllString(msg, "<i><b>$1</b></i>")
msg = markdownBold.ReplaceAllString(msg, "<b>$1</b>")
msg = markdownItalic.ReplaceAllString(msg, "<i>$1</i>")
msg = markdownStrike.ReplaceAllString(msg, "<s>$1</s>")
msg = markdownUnderline.ReplaceAllString(msg, "<u>$1</u>")
return msg
}
// An adapter for the parser, so that the parser can call itself recursively.
// This is less for the simple Markdown elements like bold and italics and more for the really complicated ones I plan on adding at some point.
func markdownParse(msg string) string {
return strings.TrimSpace(_markdownParse(msg+" ", 0))
msg = strings.TrimSpace(_markdownParse(msg+" ", 0))
log.Print("final msg: ", msg)
return msg
}
// Under Construction!
@ -77,16 +58,14 @@ func _markdownParse(msg string, n int) string {
var outbytes []byte
var lastElement int
//log.Print("enter message loop")
//log.Print("Message: %v\n",strings.Replace(msg,"\r","\\r",-1))
log.Printf("Initial Message: %+v\n", strings.Replace(msg, "\r", "\\r", -1))
for index := 0; index < len(msg); index++ {
/*//log.Print("--OUTER MARKDOWN LOOP START--")
//log.Print("index",index)
//log.Print("msg[index]",msg[index])
//log.Print("string(msg[index])",string(msg[index]))
//log.Print("--OUTER MARKDOWN LOOP END--")
//log.Print(" ")*/
//log.Print("--OUTER MARKDOWN LOOP START--")
//log.Print("index: ", index)
//log.Print("msg[index]: ", msg[index])
//log.Print("string(msg[index]): ", string(msg[index]))
//log.Printf("--OUTER MARKDOWN LOOP END--\n\n")
switch msg[index] {
// TODO: Do something slightly less hacky for skipping URLs
@ -106,7 +85,7 @@ func _markdownParse(msg string, n int) string {
index++
index = markdownSkipUntilChar(msg, index, '_')
if (index-(startIndex+1)) < 2 || index >= len(msg) {
if (index-(startIndex+1)) < 1 || index >= len(msg) {
break
}
@ -116,7 +95,8 @@ func _markdownParse(msg string, n int) string {
outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnderlineTagOpen...)
outbytes = append(outbytes, msg[sIndex:lIndex]...)
// TODO: Implement this without as many type conversions
outbytes = append(outbytes, []byte(_markdownParse(msg[sIndex:lIndex], n+1))...)
outbytes = append(outbytes, markdownUnderlineTagClose...)
lastElement = index
@ -129,7 +109,7 @@ func _markdownParse(msg string, n int) string {
index++
index = markdownSkipUntilChar(msg, index, '~')
if (index-(startIndex+1)) < 2 || index >= len(msg) {
if (index-(startIndex+1)) < 1 || index >= len(msg) {
break
}
@ -139,28 +119,28 @@ func _markdownParse(msg string, n int) string {
outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownStrikeTagOpen...)
outbytes = append(outbytes, msg[sIndex:lIndex]...)
// TODO: Implement this without as many type conversions
outbytes = append(outbytes, []byte(_markdownParse(msg[sIndex:lIndex], n+1))...)
outbytes = append(outbytes, markdownStrikeTagClose...)
lastElement = index
index--
case '*':
//log.Print("------")
//log.Print("[]byte(msg):",[]byte(msg))
//log.Print("len(msg)",len(msg))
//log.Print("start index",index)
//log.Print("start msg[index]",msg[index])
//log.Print("start string(msg[index])",string(msg[index]))
//log.Print("start []byte(msg[:index])",[]byte(msg[:index]))
//log.Print("[]byte(msg): ", []byte(msg))
//log.Print("len(msg): ", len(msg))
//log.Print("start index: ", index)
//log.Print("start msg[index]: ", msg[index])
//log.Print("start string(msg[index]): ", string(msg[index]))
//log.Print("start []byte(msg[:index]): ", []byte(msg[:index]))
var startIndex = index
var italic = true
var bold bool
var bold = false
if (index + 2) < len(msg) {
//log.Print("start index + 1",index + 1)
//log.Print("start msg[index]",msg[index + 1])
//log.Print("start string(msg[index])",string(msg[index + 1]))
//log.Print("start index + 1: ", index + 1)
//log.Print("start msg[index]: ", msg[index + 1])
//log.Print("start string(msg[index]): ", string(msg[index + 1]))
if msg[index+1] == '*' {
//log.Print("two asterisks")
bold = true
@ -174,16 +154,16 @@ func _markdownParse(msg string, n int) string {
}
}
//log.Print("lastElement",lastElement)
//log.Print("startIndex:",startIndex)
//log.Print("msg[startIndex]",msg[startIndex])
//log.Print("string(msg[startIndex])",string(msg[startIndex]))
//log.Print("lastElement: ", lastElement)
//log.Print("startIndex: ", startIndex)
//log.Print("msg[startIndex]: ", msg[startIndex])
//log.Print("string(msg[startIndex]): ", string(msg[startIndex]))
//log.Print("preabrupt index",index)
//log.Print("preabrupt msg[index]",msg[index])
//log.Print("preabrupt string(msg[index])",string(msg[index]))
//log.Print("preabrupt []byte(msg[:index])",[]byte(msg[:index]))
//log.Print("preabrupt msg[:index]",msg[:index])
//log.Print("preabrupt index: ", index)
//log.Print("preabrupt msg[index]: ", msg[index])
//log.Print("preabrupt string(msg[index]): ", string(msg[index]))
//log.Print("preabrupt []byte(msg[:index]): ", []byte(msg[:index]))
//log.Print("preabrupt msg[:index]: ", msg[:index])
// Does the string terminate abruptly?
if (index + 1) >= len(msg) {
@ -195,16 +175,15 @@ func _markdownParse(msg string, n int) string {
//log.Print("preskip index",index)
//log.Print("preskip msg[index]",msg[index])
//log.Print("preskip string(msg[index])",string(msg[index]))
index = markdownSkipUntilAsterisk(msg, index)
if index >= len(msg) {
break
}
//log.Print("index",index)
//log.Print("[]byte(msg[:index])",[]byte(msg[:index]))
//log.Print("msg[index]",msg[index])
//log.Print("index: ", index)
//log.Print("[]byte(msg[:index]): ", []byte(msg[:index]))
//log.Print("msg[index]: ", msg[index])
sIndex := startIndex
lIndex := index
@ -213,7 +192,7 @@ func _markdownParse(msg string, n int) string {
if (index + 3) >= len(msg) {
//log.Print("unclosed markdown element @ exit element")
outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnclosedElement...)
//outbytes = append(outbytes, markdownUnclosedElement...)
lastElement = startIndex
break
}
@ -224,7 +203,7 @@ func _markdownParse(msg string, n int) string {
if (index + 2) >= len(msg) {
//log.Print("true unclosed markdown element @ exit element")
outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnclosedElement...)
//outbytes = append(outbytes, markdownUnclosedElement...)
lastElement = startIndex
break
}
@ -235,7 +214,7 @@ func _markdownParse(msg string, n int) string {
if (index + 1) >= len(msg) {
//log.Print("true unclosed markdown element @ exit element")
outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnclosedElement...)
//outbytes = append(outbytes, markdownUnclosedElement...)
lastElement = startIndex
break
}
@ -249,7 +228,7 @@ func _markdownParse(msg string, n int) string {
if lIndex <= sIndex {
//log.Print("unclosed markdown element @ lIndex <= sIndex")
outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnclosedElement...)
//outbytes = append(outbytes, markdownUnclosedElement...)
lastElement = startIndex
break
}
@ -257,7 +236,7 @@ func _markdownParse(msg string, n int) string {
if sIndex < 0 || lIndex < 0 {
//log.Print("unclosed markdown element @ sIndex < 0 || lIndex < 0")
outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnclosedElement...)
//outbytes = append(outbytes, markdownUnclosedElement...)
lastElement = startIndex
break
}
@ -285,7 +264,8 @@ func _markdownParse(msg string, n int) string {
outbytes = append(outbytes, markdownItalicTagOpen...)
}
outbytes = append(outbytes, msg[sIndex:lIndex]...)
// TODO: Implement this without as many type conversions
outbytes = append(outbytes, []byte(_markdownParse(msg[sIndex:lIndex], n+1))...)
if italic {
outbytes = append(outbytes, markdownItalicTagClose...)
@ -296,13 +276,18 @@ func _markdownParse(msg string, n int) string {
lastElement = index
index--
case '\\':
if (index + 1) < len(msg) {
outbytes = append(outbytes, msg[lastElement:index]...)
index++
lastElement = index
}
//case '`':
//case '_':
//case '~':
//case 10: // newline
}
}
//log.Print("exit message loop")
if len(outbytes) == 0 {

View File

@ -33,14 +33,14 @@ func TestBBCodeRender(t *testing.T) {
msgList = addMEPair(msgList, "[s]hi[/s]", "<s>hi</s>")
msgList = addMEPair(msgList, "[c]hi[/c]", "[c]hi[/c]")
if !testing.Short() {
msgList = addMEPair(msgList, "[b]hi[/i]", "[b]hi[/i]")
msgList = addMEPair(msgList, "[/b]hi[b]", "[/b]hi[b]")
msgList = addMEPair(msgList, "[/b]hi[/b]", "[/b]hi[/b]")
msgList = addMEPair(msgList, "[b][b]hi[/b]", "<b>hi</b>")
//msgList = addMEPair(msgList, "[b]hi[/i]", "[b]hi[/i]")
//msgList = addMEPair(msgList, "[/b]hi[b]", "[/b]hi[b]")
//msgList = addMEPair(msgList, "[/b]hi[/b]", "[/b]hi[/b]")
//msgList = addMEPair(msgList, "[b][b]hi[/b]", "<b>hi</b>")
//msgList = addMEPair(msgList, "[b][b]hi", "[b][b]hi")
//msgList = addMEPair(msgList, "[b][b][b]hi", "[b][b][b]hi")
//msgList = addMEPair(msgList, "[/b]hi", "[/b]hi")
}
msgList = addMEPair(msgList, "[b][b]hi", "[b][b]hi")
msgList = addMEPair(msgList, "[b][b][b]hi", "[b][b][b]hi")
msgList = addMEPair(msgList, "[/b]hi", "[/b]hi")
msgList = addMEPair(msgList, "[code]hi[/code]", "<span class='codequotes'>hi</span>")
msgList = addMEPair(msgList, "[code][b]hi[/b][/code]", "<span class='codequotes'>[b]hi[/b]</span>")
msgList = addMEPair(msgList, "[code][b]hi[/code][/b]", "<span class='codequotes'>[b]hi</span>[/b]")
@ -51,9 +51,9 @@ func TestBBCodeRender(t *testing.T) {
t.Log("Testing bbcodeFullParse")
for _, item := range msgList {
t.Log("Testing string '" + item.Msg + "'")
res = bbcodeFullParse(item.Msg)
if res != item.Expects {
t.Error("Testing string '" + item.Msg + "'")
t.Error("Bad output:", "'"+res+"'")
t.Error("Expected:", item.Expects)
}
@ -167,7 +167,7 @@ func TestBBCodeRender(t *testing.T) {
t.Log("Testing string '" + msg + "'")
res = bbcodeFullParse(msg)
conv, err = strconv.Atoi(res)
if err != nil || ( /*conv > 18446744073709551615 || */ conv < 0) {
if err != nil && res != "<span style='color: red;'>[Invalid Number]</span>[rand]18446744073709551615[/rand]" {
t.Error("Bad output:", "'"+res+"'")
t.Error("Expected a number between 0 and 18446744073709551615")
}
@ -175,7 +175,7 @@ func TestBBCodeRender(t *testing.T) {
t.Log("Testing string '" + msg + "'")
res = bbcodeFullParse(msg)
conv, err = strconv.Atoi(res)
if err != nil || ( /*conv > 170141183460469231731687303715884105727 || */ conv < 0) {
if err != nil && res != "<span style='color: red;'>[Invalid Number]</span>[rand]170141183460469231731687303715884105727[/rand]" {
t.Error("Bad output:", "'"+res+"'")
t.Error("Expected a number between 0 and 170141183460469231731687303715884105727")
}
@ -193,19 +193,42 @@ func TestBBCodeRender(t *testing.T) {
func TestMarkdownRender(t *testing.T) {
//t.Skip()
err := initMarkdown()
if err != nil {
t.Fatal(err)
}
var res string
var msgList []MEPair
msgList = addMEPair(msgList, "hi", "hi")
msgList = addMEPair(msgList, "**h**", "<b>h</b>")
msgList = addMEPair(msgList, "**hi**", "<b>hi</b>")
msgList = addMEPair(msgList, "_h_", "<u>h</u>")
msgList = addMEPair(msgList, "_hi_", "<u>hi</u>")
msgList = addMEPair(msgList, "*h*", "<i>h</i>")
msgList = addMEPair(msgList, "*hi*", "<i>hi</i>")
msgList = addMEPair(msgList, "~h~", "<s>h</s>")
msgList = addMEPair(msgList, "~hi~", "<s>hi</s>")
msgList = addMEPair(msgList, "*hi**", "<i>hi</i>*")
msgList = addMEPair(msgList, "**hi***", "<b>hi</b>*")
msgList = addMEPair(msgList, "**hi*", "*<i>hi</i>")
msgList = addMEPair(msgList, "***hi***", "*<b><i>hi</i></b>")
msgList = addMEPair(msgList, "***hi***", "<b><i>hi</i></b>")
msgList = addMEPair(msgList, "***h***", "<b><i>h</i></b>")
msgList = addMEPair(msgList, "\\*hi\\*", "*hi*")
msgList = addMEPair(msgList, "d\\*hi\\*", "d*hi*")
msgList = addMEPair(msgList, "\\*hi\\*d", "*hi*d")
msgList = addMEPair(msgList, "d\\*hi\\*d", "d*hi*d")
msgList = addMEPair(msgList, "\\", "\\")
msgList = addMEPair(msgList, "\\\\", "\\\\")
msgList = addMEPair(msgList, "\\d", "\\d")
msgList = addMEPair(msgList, "\\\\d", "\\\\d")
msgList = addMEPair(msgList, "\\\\\\d", "\\\\\\d")
msgList = addMEPair(msgList, "d\\", "d\\")
msgList = addMEPair(msgList, "\\d\\", "\\d\\")
msgList = addMEPair(msgList, "*~hi~*", "<i><s>hi</s></i>")
msgList = addMEPair(msgList, "~*hi*~", "<s><i>hi</i></s>")
msgList = addMEPair(msgList, "_~hi~_", "<u><s>hi</s></u>")
msgList = addMEPair(msgList, "***~hi~***", "<b><i><s>hi</s></i></b>")
msgList = addMEPair(msgList, "**", "**")
msgList = addMEPair(msgList, "***", "***")
msgList = addMEPair(msgList, "****", "****")
@ -224,10 +247,11 @@ func TestMarkdownRender(t *testing.T) {
msgList = addMEPair(msgList, "*** ***", "<b><i> </i></b>")
for _, item := range msgList {
t.Log("Testing string '" + item.Msg + "'")
res = markdownParse(item.Msg)
if res != item.Expects {
t.Error("Testing string '" + item.Msg + "'")
t.Error("Bad output:", "'"+res+"'")
//t.Error("Ouput in bytes:", []byte(res))
t.Error("Expected:", item.Expects)
}
}

View File

@ -36,7 +36,6 @@ function load_alerts(menu_alerts)
}
var alist = "";
var anyAvatar = false
for(var i in data.msgs) {
var msg = data.msgs[i];
var mmsg = msg.msg;
@ -44,15 +43,13 @@ function load_alerts(menu_alerts)
if("sub" in msg) {
for(var i = 0; i < msg.sub.length; i++) {
mmsg = mmsg.replace("\{"+i+"\}", msg.sub[i]);
//console.log("Sub #" + i);
//console.log(msg.sub[i]);
//console.log("Sub #" + i + ":",msg.sub[i]);
}
}
if("avatar" in msg) {
alist += "<div class='alertItem withAvatar' style='background-image:url(\""+msg.avatar+"\");'><a class='text' data-asid='"+msg.asid+"' href=\""+msg.path+"\">"+mmsg+"</a></div>";
alertList.push("<div class='alertItem withAvatar' style='background-image:url(\""+msg.avatar+"\");'><a class='text' data-asid='"+msg.asid+"' href=\""+msg.path+"\">"+mmsg+"</a></div>");
anyAvatar = true
} else {
alist += "<div class='alertItem'><a href=\""+msg.path+"\" class='text'>"+mmsg+"</a></div>";
alertList.push("<div class='alertItem'><a href=\""+msg.path+"\" class='text'>"+mmsg+"</a></div>");
@ -62,11 +59,8 @@ function load_alerts(menu_alerts)
}
if(alist == "") alist = "<div class='alertItem'>You don't have any alerts</div>";
else {
//menu_alerts.removeClass("hasAvatars");
//if(anyAvatar) menu_alerts.addClass("hasAvatars");
}
alertListNode.innerHTML = alist;
if(data.msgCount != 0 && data.msgCount != undefined) {
alertCounterNode.textContent = data.msgCount;
menu_alerts.classList.add("has_alerts");
@ -88,6 +82,7 @@ function load_alerts(menu_alerts)
console.log(magic.responseText);
console.log(err);
}
console.log("error: ",error);
alertListNode.innerHTML = "<div class='alertItem'>"+errtxt+"</div>";
}
});
@ -176,8 +171,7 @@ $(document).ready(function(){
var messages = event.data.split('\r');
for(var i = 0; i < messages.length; i++) {
//console.log("Message:");
//console.log(messages[i]);
//console.log("Message: ",messages[i]);
if(messages[i].startsWith("set ")) {
//msgblocks = messages[i].split(' ',3);
let msgblocks = SplitN(messages[i]," ",3);
@ -214,10 +208,10 @@ $(document).ready(function(){
let topicStatusInput = $('.topic_status_input').val();
let topicContentInput = $('.topic_content_input').val();
let formAction = this.form.getAttribute("action");
//console.log("New Topic Name: " + topicNameInput);
//console.log("New Topic Status: " + topicStatusInput);
//console.log("New Topic Content: " + topicContentInput);
//console.log("Form Action: " + formAction);
//console.log("New Topic Name: ", topicNameInput);
//console.log("New Topic Status: ", topicStatusInput);
//console.log("New Topic Content: ", topicContentInput);
//console.log("Form Action: ", formAction);
$.ajax({
url: formAction,
type: "POST",
@ -253,7 +247,7 @@ $(document).ready(function(){
block.html(newContent);
var formAction = $(this).closest('a').attr("href");
//console.log("Form Action: " + form_action);
//console.log("Form Action:",formAction);
$.ajax({ url: formAction, type: "POST", dataType: "json", data: { isJs: "1", edit_item: newContent }
});
});
@ -266,8 +260,7 @@ $(document).ready(function(){
let block = blockParent.find('.editable_block').eq(0);
block.html("<input name='edit_field' value='" + block.text() + "' type='text'/><a href='" + $(this).closest('a').attr("href") + "'><button class='submit_edit' type='submit'>Update</button></a>");
$(".submit_edit").click(function(event)
{
$(".submit_edit").click(function(event) {
event.preventDefault();
let blockParent = $(this).closest('.editable_parent');
let block = blockParent.find('.editable_block').eq(0);
@ -275,12 +268,12 @@ $(document).ready(function(){
block.html(newContent);
let formAction = $(this).closest('a').attr("href");
//console.log("Form Action: " + formAction);
//console.log("Form Action:", formAction);
$.ajax({
url: formAction + "?session=" + session,
type: "POST",
dataType: "json",
data: {isJs: "1",edit_item: newContent}
data: { isJs: "1", edit_item: newContent }
});
});
});
@ -305,9 +298,9 @@ $(document).ready(function(){
else var it = ['No','Yes'];
var itLen = it.length;
var out = "";
//console.log("Field Name '" + field_name + "'")
//console.log("Field Type",field_type)
//console.log("Field Value '" + field_value + "'")
//console.log("Field Name:",field_name);
//console.log("Field Type:",field_type);
//console.log("Field Value:",field_value);
for (var i = 0; i < itLen; i++) {
var sel = "";
if(field_value == i || field_value == it[i]) {
@ -332,7 +325,7 @@ $(document).ready(function(){
//console.log("running .submit_edit event");
var out_data = {isJs: "1"}
var block_parent = $(this).closest('.editable_parent');
block_parent.find('.editable_block').each(function(){
block_parent.find('.editable_block').each(function() {
var field_name = this.getAttribute("data-field");
var field_type = this.getAttribute("data-type");
if(field_type=="list") {
@ -350,7 +343,7 @@ $(document).ready(function(){
});
var form_action = $(this).closest('a').attr("href");
//console.log("Form Action: " + form_action);
//console.log("Form Action:", form_action);
//console.log(out_data);
$.ajax({ url: form_action + "?session=" + session, type:"POST", dataType:"json", data: out_data });
block_parent.find('.hide_on_edit').show();

View File

@ -132,3 +132,101 @@ func (build *builder) Purge(table string) (stmt *sql.Stmt, err error) {
}
return build.conn.Prepare(res)
}
// These ones support transactions
func (build *builder) SimpleSelectTx(tx *sql.Tx, table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}
func (build *builder) SimpleCountTx(tx *sql.Tx, table string, where string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleCount("_builder", table, where, limit)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}
func (build *builder) SimpleLeftJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}
func (build *builder) SimpleInnerJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}
func (build *builder) CreateTableTx(tx *sql.Tx, table string, charset string, collation string, columns []DB_Table_Column, keys []DB_Table_Key) (stmt *sql.Stmt, err error) {
res, err := build.adapter.CreateTable("_builder", table, charset, collation, columns, keys)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}
func (build *builder) SimpleInsertTx(tx *sql.Tx, table string, columns string, fields string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsert("_builder", table, columns, fields)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}
func (build *builder) SimpleInsertSelectTx(tx *sql.Tx, ins DB_Insert, sel DB_Select) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertSelect("_builder", ins, sel)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}
func (build *builder) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DB_Insert, sel DB_Join) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertLeftJoin("_builder", ins, sel)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}
func (build *builder) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DB_Insert, sel DB_Join) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertInnerJoin("_builder", ins, sel)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}
func (build *builder) SimpleUpdateTx(tx *sql.Tx, table string, set string, where string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleUpdate("_builder", table, set, where)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}
func (build *builder) SimpleDeleteTx(tx *sql.Tx, table string, where string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleDelete("_builder", table, where)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}
// I don't know why you need this, but here it is x.x
func (build *builder) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.Purge("_builder", table)
if err != nil {
return stmt, err
}
return tx.Prepare(res)
}

View File

@ -451,8 +451,9 @@ func (adapter *Mssql_Adapter) SimpleSelect(name string, table string, columns st
querystr += " ?" + strconv.Itoa(substituteCount)
case "function", "operator", "number":
// TODO: Split the function case off to speed things up
// MSSQL seems to convert the formats? so we'll compare it with a regular date. Do this with the other methods too?
if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" {
token.Contents = "GETUTCDATE()"
token.Contents = "GETDATE()"
}
querystr += " " + token.Contents
case "column":
@ -800,12 +801,21 @@ func (adapter *Mssql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel
/* Select */
var substituteCount = 0
for _, column := range processColumns(sel.Columns) {
var source, alias string
// Escape the column names, just in case we've used a reserved keyword
var colslice = strings.Split(strings.TrimSpace(sel.Columns), ",")
for _, column := range colslice {
querystr += "[" + strings.TrimSpace(column) + "],"
if column.Type == "function" || column.Type == "substitute" {
source = column.Left
} else {
source = "[" + column.Left + "]"
}
if column.Alias != "" {
alias = " AS [" + column.Alias + "]"
}
querystr += " " + source + alias + ","
}
// Remove the trailing comma
querystr = querystr[0 : len(querystr)-1]
querystr += " FROM [" + sel.Table + "] "

View File

@ -903,7 +903,7 @@ package main
import "log"
import "database/sql"
import "./query_gen/lib"
//import "./query_gen/lib"
// nolint
` + stmts + `

View File

@ -102,10 +102,9 @@ type DB_Adapter interface {
SimpleInsert(name string, table string, columns string, fields string) (string, error)
// ! DEPRECATED
SimpleReplace(name string, table string, columns string, fields string) (string, error)
// ! NOTE: MySQL doesn't support upserts properly, asides from for keys, so this is just a less destructive replace atm
SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error)
//SimpleReplace(name string, table string, columns string, fields string) (string, error)
// ! NOTE: MySQL doesn't support upserts properly, so I'm removing this from the interface until we find a way to patch it in
//SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error)
SimpleUpdate(name string, table string, set string, where string) (string, error)
SimpleDelete(name string, table string, where string) (string, error)
Purge(name string, table string) (string, error)

View File

@ -75,7 +75,7 @@ func writeStatements(adapter qgen.DB_Adapter) error {
return err
}
err = writeReplaces(adapter)
/*err = writeReplaces(adapter)
if err != nil {
return err
}
@ -83,7 +83,7 @@ func writeStatements(adapter qgen.DB_Adapter) error {
err = writeUpserts(adapter)
if err != nil {
return err
}
}*/
err = writeUpdates(adapter)
if err != nil {
@ -356,8 +356,6 @@ func writeInserts(adapter qgen.DB_Adapter) error {
adapter.SimpleInsert("addTheme", "themes", "uname, default", "?,?")
adapter.SimpleInsert("createGroup", "users_groups", "name, tag, is_admin, is_mod, is_banned, permissions", "?,?,?,?,?,?")
adapter.SimpleInsert("addModlogEntry", "moderation_logs", "action, elementID, elementType, ipaddress, actorID, doneAt", "?,?,?,?,?,UTC_TIMESTAMP()")
adapter.SimpleInsert("addAdminlogEntry", "administration_logs", "action, elementID, elementType, ipaddress, actorID, doneAt", "?,?,?,?,?,UTC_TIMESTAMP()")
@ -373,7 +371,8 @@ func writeReplaces(adapter qgen.DB_Adapter) (err error) {
return nil
}
func writeUpserts(adapter qgen.DB_Adapter) (err error) {
// ! Upserts are broken atm
/*func writeUpserts(adapter qgen.DB_Adapter) (err error) {
_, err = adapter.SimpleUpsert("addForumPermsToGroup", "forums_permissions", "gid, fid, preset, permissions", "?,?,?,?", "gid = ? AND fid = ?")
if err != nil {
return err
@ -385,7 +384,7 @@ func writeUpserts(adapter qgen.DB_Adapter) (err error) {
}
return nil
}
}*/
func writeUpdates(adapter qgen.DB_Adapter) error {
adapter.SimpleUpdate("addRepliesToTopic", "topics", "postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()", "tid = ?")
@ -454,6 +453,8 @@ func writeUpdates(adapter qgen.DB_Adapter) error {
adapter.SimpleUpdate("updateUser", "users", "name = ?, email = ?, group = ?", "uid = ?")
adapter.SimpleUpdate("updateUserGroup", "users", "group = ?", "uid = ?")
adapter.SimpleUpdate("updateGroupPerms", "users_groups", "permissions = ?", "gid = ?")
adapter.SimpleUpdate("updateGroupRank", "users_groups", "is_admin = ?, is_mod = ?, is_banned = ?", "gid = ?")
@ -480,10 +481,10 @@ func writeDeletes(adapter qgen.DB_Adapter) error {
adapter.SimpleDelete("deleteProfileReply", "users_replies", "rid = ?")
adapter.SimpleDelete("deleteForumPermsByForum", "forums_permissions", "fid = ?")
//adapter.SimpleDelete("deleteForumPermsByForum", "forums_permissions", "fid = ?")
adapter.SimpleDelete("deleteActivityStreamMatch", "activity_stream_matches", "watcher = ? AND asid = ?")
//adapter.SimpleDelete("delete_activity_stream_matches_by_watcher","activity_stream_matches","watcher = ?")
//adapter.SimpleDelete("deleteActivityStreamMatchesByWatcher","activity_stream_matches","watcher = ?")
adapter.SimpleDelete("deleteWordFilter", "word_filters", "wfid = ?")
@ -501,20 +502,20 @@ func writeSimpleCounts(adapter qgen.DB_Adapter) error {
}
func writeInsertSelects(adapter qgen.DB_Adapter) error {
adapter.SimpleInsertSelect("addForumPermsToForumAdmins",
/*adapter.SimpleInsertSelect("addForumPermsToForumAdmins",
qgen.DB_Insert{"forums_permissions", "gid, fid, preset, permissions", ""},
qgen.DB_Select{"users_groups", "gid, ? AS fid, ? AS preset, ? AS permissions", "is_admin = 1", "", ""},
)
)*/
adapter.SimpleInsertSelect("addForumPermsToForumStaff",
/*adapter.SimpleInsertSelect("addForumPermsToForumStaff",
qgen.DB_Insert{"forums_permissions", "gid, fid, preset, permissions", ""},
qgen.DB_Select{"users_groups", "gid, ? AS fid, ? AS preset, ? AS permissions", "is_admin = 0 AND is_mod = 1", "", ""},
)
)*/
adapter.SimpleInsertSelect("addForumPermsToForumMembers",
/*adapter.SimpleInsertSelect("addForumPermsToForumMembers",
qgen.DB_Insert{"forums_permissions", "gid, fid, preset, permissions", ""},
qgen.DB_Select{"users_groups", "gid, ? AS fid, ? AS preset, ? AS permissions", "is_admin = 0 AND is_mod = 0 AND is_banned = 0", "", ""},
)
)*/
return nil
}

59
rev_templates.go Normal file
View File

@ -0,0 +1,59 @@
//+build experiment
// ! EXPERIMENTAL
package main
import (
"errors"
"regexp"
)
var tagFinder *regexp.Regexp
func init() {
tagFinder = regexp.MustCompile(`(?s)\{\{(.*)\}\}`)
}
func icecreamSoup(tmpl string) error {
tagIndices := tagFinder.FindAllStringIndex(tmpl, -1)
if tagIndices != nil && len(tagIndices) > 0 {
if tagIndices[0][0] == 0 {
return errors.New("We don't support tags in the outermost layer yet")
}
for _, tagIndex := range tagIndices {
var nestingLayer = 0
for i := tagIndex[0]; i > 0; i-- {
switch tmpl[i] {
case '>':
i, closeTag, err := tasteTagToLeft(tmpl, i)
if err != nil {
return err
}
if closeTag {
nestingLayer++
}
case '<':
}
}
}
}
}
func tasteTagToLeft(tmpl string, index int) (indexOut int, closeTag bool, err error) {
var foundLeftBrace = false
for ; index > 0; index-- {
// What if the / isn't adjacent to the < but has a space instead? Is that even valid?
if index >= 1 && tmpl[index] == '/' && tmpl[index-1] == '<' {
closeTag = true
break
} else if tmpl[index] == '<' {
foundLeftBrace = true
}
}
if !foundLeftBrace {
return errors.New("The left portion of the tag is missing")
}
return index, closeTag, nil
}

View File

@ -332,6 +332,8 @@ func routeForum(w http.ResponseWriter, r *http.Request, user User, sfid string)
} else {
page = 1
}
// TODO: Move this to *Forum
rows, err := getForumTopicsOffsetStmt.Query(fid, offset, config.ItemsPerPage)
if err != nil {
InternalError(err, w)
@ -904,10 +906,11 @@ func routeRegisterSubmit(w http.ResponseWriter, r *http.Request, user User) {
return
}
var active, group int
var active bool
var group int
switch headerLite.Settings["activation_type"] {
case 1: // Activate All
active = 1
active = true
group = config.DefaultGroup
default: // Anything else. E.g. Admin Activation or Email Activation.
group = config.ActivationGroup

33
run_tests.bat Normal file
View File

@ -0,0 +1,33 @@
@echo off
echo Generating the dynamic code
go generate
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Building the router generator
go build ./router_gen
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Running the router generator
router_gen.exe
echo Building the query generator
go build ./query_gen
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Running the query generator
query_gen.exe
echo Building the executable
go test
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
pause

33
run_tests_mssql.bat Normal file
View File

@ -0,0 +1,33 @@
@echo off
echo Generating the dynamic code
go generate
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Building the router generator
go build ./router_gen
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Running the router generator
router_gen.exe
echo Building the query generator
go build ./query_gen
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Running the query generator
query_gen.exe
echo Building the executable
go test -tags mssql
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
pause

View File

@ -25,25 +25,19 @@ func handleExpiredScheduledGroups() error {
defer rows.Close()
var uid int
ucache, ok := users.(UserCache)
for rows.Next() {
err := rows.Scan(&uid)
if err != nil {
return err
}
_, err = replaceScheduleGroupStmt.Exec(uid, 0, 0, time.Now(), false, uid)
// Sneaky way of initialising a *User, please use the methods on the UserStore instead
user := getDummyUser()
user.ID = uid
err = user.RevertGroupUpdate()
if err != nil {
log.Print("Unable to replace the scheduled group")
return err
}
_, err = setTempGroupStmt.Exec(0, uid)
if err != nil {
log.Print("Unable to reset the tempgroup")
return err
}
if ok {
ucache.CacheRemove(uid)
}
}
return rows.Err()
}

View File

@ -105,87 +105,90 @@ if tmpl_forum_vars.CurrentUser.Perms.CreateTopic {
w.Write(forum_12)
w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID)))
w.Write(forum_13)
} else {
w.Write(forum_14)
}
} else {
w.Write(forum_15)
}
w.Write(forum_16)
if tmpl_forum_vars.CurrentUser.Perms.CreateTopic {
w.Write(forum_17)
w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID)))
w.Write(forum_18)
if tmpl_forum_vars.CurrentUser.Perms.UploadFiles {
w.Write(forum_19)
}
w.Write(forum_17)
if tmpl_forum_vars.CurrentUser.Perms.CreateTopic {
w.Write(forum_18)
w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID)))
w.Write(forum_19)
if tmpl_forum_vars.CurrentUser.Perms.UploadFiles {
w.Write(forum_20)
}
w.Write(forum_21)
}
w.Write(forum_22)
if len(tmpl_forum_vars.ItemList) != 0 {
for _, item := range tmpl_forum_vars.ItemList {
w.Write(forum_22)
if item.Sticky {
w.Write(forum_23)
if item.Sticky {
w.Write(forum_24)
} else {
if item.IsClosed {
w.Write(forum_24)
}
}
w.Write(forum_25)
if item.Creator.Avatar != "" {
}
}
w.Write(forum_26)
w.Write([]byte(item.Creator.Avatar))
if item.Creator.Avatar != "" {
w.Write(forum_27)
}
w.Write([]byte(item.Creator.Avatar))
w.Write(forum_28)
w.Write([]byte(item.Link))
w.Write(forum_29)
w.Write([]byte(item.Title))
w.Write(forum_30)
w.Write([]byte(item.Creator.Link))
w.Write(forum_31)
w.Write([]byte(item.Creator.Name))
w.Write(forum_32)
if item.IsClosed {
w.Write(forum_33)
}
if item.Sticky {
w.Write(forum_29)
w.Write([]byte(item.Link))
w.Write(forum_30)
w.Write([]byte(item.Title))
w.Write(forum_31)
w.Write([]byte(item.Creator.Link))
w.Write(forum_32)
w.Write([]byte(item.Creator.Name))
w.Write(forum_33)
if item.IsClosed {
w.Write(forum_34)
}
w.Write(forum_35)
w.Write([]byte(strconv.Itoa(item.PostCount)))
w.Write(forum_36)
if item.Sticky {
w.Write(forum_35)
}
w.Write(forum_36)
w.Write([]byte(strconv.Itoa(item.PostCount)))
w.Write(forum_37)
w.Write([]byte(strconv.Itoa(item.LikeCount)))
w.Write(forum_38)
if item.Sticky {
w.Write(forum_39)
} else {
if item.IsClosed {
w.Write(forum_38)
}
}
w.Write(forum_39)
if item.LastUser.Avatar != "" {
w.Write(forum_40)
w.Write([]byte(item.LastUser.Avatar))
w.Write(forum_41)
}
}
w.Write(forum_41)
if item.LastUser.Avatar != "" {
w.Write(forum_42)
w.Write([]byte(item.LastUser.Link))
w.Write([]byte(item.LastUser.Avatar))
w.Write(forum_43)
w.Write([]byte(item.LastUser.Name))
}
w.Write(forum_44)
w.Write([]byte(item.RelativeLastReplyAt))
w.Write([]byte(item.LastUser.Link))
w.Write(forum_45)
w.Write([]byte(item.LastUser.Name))
w.Write(forum_46)
w.Write([]byte(item.RelativeLastReplyAt))
w.Write(forum_47)
}
} else {
w.Write(forum_46)
if tmpl_forum_vars.CurrentUser.Perms.CreateTopic {
w.Write(forum_47)
w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID)))
w.Write(forum_48)
}
if tmpl_forum_vars.CurrentUser.Perms.CreateTopic {
w.Write(forum_49)
}
w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID)))
w.Write(forum_50)
}
w.Write(forum_51)
}
w.Write(forum_52)
w.Write(footer_0)
if len(tmpl_forum_vars.Header.Themes) != 0 {
for _, item := range tmpl_forum_vars.Header.Themes {

View File

@ -678,33 +678,38 @@ var forums_19 = []byte(`
var topics_0 = []byte(`
<main>
<div class="rowblock rowhead">
<div class="rowblock rowhead topic_list_title_block">
<div class="rowitem topic_list_title`)
var topics_1 = []byte(` has_opt`)
var topics_2 = []byte(`"><h1>All Topics</h1></div>
`)
var topics_3 = []byte(`
<div class="opt create_topic_opt" title="Create Topic"><a class="create_topic_link" href="/topics/create/"></a></div>
<div class="opt create_topic_opt" title="Create Topic" aria-label="Create a topic"><a class="create_topic_link" href="/topics/create/"></a></div>
`)
var topics_4 = []byte(`<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>`)
var topics_5 = []byte(`
var topics_4 = []byte(`
<div class="opt mod_opt" title="Moderate">
<a class="moderate_link" href="#"></a>
</div>
`)
var topics_5 = []byte(`<div class="opt locked_opt" title="You don't have the permissions needed to create a topic" aria-label="You don't have the permissions needed to make a topic anywhere"><a></a></div>`)
var topics_6 = []byte(`
<div style="clear: both;"></div>
`)
var topics_6 = []byte(`
var topics_7 = []byte(`
</div>
`)
var topics_7 = []byte(`
<div class="rowblock topic_create_form quick_create_form" style="display: none;">
var topics_8 = []byte(`
<div class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="Quick Topic Form">
<form name="topic_create_form_form" id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<div class="formrow topic_board_row real_first_child">
<div class="formitem"><select form="topic_create_form_form" id="topic_board_input" name="topic-board">
`)
var topics_8 = []byte(`<option `)
var topics_9 = []byte(`selected`)
var topics_10 = []byte(` value="`)
var topics_11 = []byte(`">`)
var topics_12 = []byte(`</option>`)
var topics_13 = []byte(`
var topics_9 = []byte(`<option `)
var topics_10 = []byte(`selected`)
var topics_11 = []byte(` value="`)
var topics_12 = []byte(`">`)
var topics_13 = []byte(`</option>`)
var topics_14 = []byte(`
</select></div>
</div>
<div class="formrow topic_name_row">
@ -721,71 +726,73 @@ var topics_13 = []byte(`
<div class="formitem">
<button form="topic_create_form_form" class="formbutton">Create Topic</button>
`)
var topics_14 = []byte(`
var topics_15 = []byte(`
<input name="upload_files" form="topic_create_form_form" id="upload_files" multiple type="file" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>`)
var topics_15 = []byte(`
var topics_16 = []byte(`
<button class="formbutton close_form">Cancel</button>
</div>
</div>
</div>
`)
var topics_16 = []byte(`
var topics_17 = []byte(`
<div id="topic_list" class="rowblock topic_list" aria-label="A list containing topics from every forum">
`)
var topics_17 = []byte(`<div class="topic_row">
var topics_18 = []byte(`<div class="topic_row">
<div class="rowitem topic_left passive datarow `)
var topics_18 = []byte(`topic_sticky`)
var topics_19 = []byte(`topic_closed`)
var topics_20 = []byte(`">
var topics_19 = []byte(`topic_sticky`)
var topics_20 = []byte(`topic_closed`)
var topics_21 = []byte(`">
<span class="selector"></span>
`)
var topics_21 = []byte(`<img src="`)
var topics_22 = []byte(`" height="64" />`)
var topics_23 = []byte(`
var topics_22 = []byte(`<img src="`)
var topics_23 = []byte(`" height="64" />`)
var topics_24 = []byte(`
<span class="topic_inner_left">
<a class="rowtopic" href="`)
var topics_24 = []byte(`">`)
var topics_25 = []byte(`</a> `)
var topics_26 = []byte(`<a class="rowsmall parent_forum" href="`)
var topics_27 = []byte(`">`)
var topics_28 = []byte(`</a>`)
var topics_29 = []byte(`
var topics_25 = []byte(`"><span>`)
var topics_26 = []byte(`</span></a> `)
var topics_27 = []byte(`<a class="rowsmall parent_forum" href="`)
var topics_28 = []byte(`">`)
var topics_29 = []byte(`</a>`)
var topics_30 = []byte(`
<br /><a class="rowsmall starter" href="`)
var topics_30 = []byte(`">`)
var topics_31 = []byte(`</a>
var topics_31 = []byte(`">`)
var topics_32 = []byte(`</a>
`)
var topics_32 = []byte(`<span class="rowsmall topic_status_e topic_status_closed" title="Status: Closed"> | &#x1F512;&#xFE0E</span>`)
var topics_33 = []byte(`<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>`)
var topics_34 = []byte(`
var topics_33 = []byte(`<span class="rowsmall topic_status_e topic_status_closed" title="Status: Closed"> | &#x1F512;&#xFE0E</span>`)
var topics_34 = []byte(`<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>`)
var topics_35 = []byte(`
</span>
<span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">`)
var topics_35 = []byte(` replies</span><br />
<span class="topicCount">x topics</span>
var topics_36 = []byte(`</span><br />
<span class="likeCount">`)
var topics_37 = []byte(`</span>
</span>
</div>
<div class="rowitem topic_right passive datarow `)
var topics_36 = []byte(`topic_sticky`)
var topics_37 = []byte(`topic_closed`)
var topics_38 = []byte(`">
var topics_38 = []byte(`topic_sticky`)
var topics_39 = []byte(`topic_closed`)
var topics_40 = []byte(`">
`)
var topics_39 = []byte(`<img src="`)
var topics_40 = []byte(`" height="64" />`)
var topics_41 = []byte(`
var topics_41 = []byte(`<img src="`)
var topics_42 = []byte(`" height="64" />`)
var topics_43 = []byte(`
<span>
<a href="`)
var topics_42 = []byte(`" class="lastName" style="font-size: 14px;">`)
var topics_43 = []byte(`</a><br>
<span class="rowsmall lastReplyAt">Last: `)
var topics_44 = []byte(`</span>
var topics_44 = []byte(`" class="lastName" style="font-size: 14px;">`)
var topics_45 = []byte(`</a><br>
<span class="rowsmall lastReplyAt">`)
var topics_46 = []byte(`</span>
</span>
</div>
</div>`)
var topics_45 = []byte(`<div class="rowitem passive">There aren't any topics yet.`)
var topics_46 = []byte(` <a href="/topics/create/">Start one?</a>`)
var topics_47 = []byte(`</div>`)
var topics_48 = []byte(`
var topics_47 = []byte(`<div class="rowitem passive">There aren't any topics yet.`)
var topics_48 = []byte(` <a href="/topics/create/">Start one?</a>`)
var topics_49 = []byte(`</div>`)
var topics_50 = []byte(`
</div>
</main>
@ -803,7 +810,7 @@ var forum_8 = []byte(`
<main>
<div id="forum_head_block" class="rowblock rowhead">
<div id="forum_head_block" class="rowblock rowhead topic_list_title_block">
<div class="rowitem forum_title`)
var forum_9 = []byte(` has_opt`)
var forum_10 = []byte(`"><h1>`)
@ -811,21 +818,26 @@ var forum_11 = []byte(`</h1>
</div>
`)
var forum_12 = []byte(`
<div class="opt create_topic_opt" title="Create Topic"><a class="create_topic_link" href="/topics/create/`)
<div class="opt create_topic_opt" title="Create Topic" aria-label="Create a topic"><a class="create_topic_link" href="/topics/create/`)
var forum_13 = []byte(`"></a></div>
`)
var forum_14 = []byte(`<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>`)
var forum_15 = []byte(`
var forum_14 = []byte(`
<div class="opt mod_opt" title="Moderate">
<a class="moderate_link" href="#"></a>
</div>
`)
var forum_15 = []byte(`<div class="opt locked_opt" title="You don't have the permissions needed to create a topic" aria-label="You don't have the permissions needed to make a topic in this forum"><a></a></div>`)
var forum_16 = []byte(`
<div style="clear: both;"></div>
`)
var forum_16 = []byte(`
var forum_17 = []byte(`
</div>
`)
var forum_17 = []byte(`
<div class="rowblock topic_create_form quick_create_form" style="display: none;">
var forum_18 = []byte(`
<div class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="Quick Topic Form">
<form id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<input form="topic_create_form_form" id="topic_board_input" name="topic-board" value="`)
var forum_18 = []byte(`" type="hidden">
var forum_19 = []byte(`" type="hidden">
<div class="formrow topic_name_row real_first_child">
<div class="formitem">
<input form="topic_create_form_form" name="topic-name" placeholder="Topic title" required>
@ -840,68 +852,70 @@ var forum_18 = []byte(`" type="hidden">
<div class="formitem">
<button form="topic_create_form_form" name="topic-button" class="formbutton">Create Topic</button>
`)
var forum_19 = []byte(`
var forum_20 = []byte(`
<input name="upload_files" form="topic_create_form_form" id="upload_files" multiple type="file" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>`)
var forum_20 = []byte(`
var forum_21 = []byte(`
<button class="formbutton close_form">Cancel</button>
</div>
</div>
</div>
`)
var forum_21 = []byte(`
var forum_22 = []byte(`
<div id="forum_topic_list" class="rowblock topic_list">
`)
var forum_22 = []byte(`<div class="topic_row">
var forum_23 = []byte(`<div class="topic_row">
<div class="rowitem topic_left passive datarow `)
var forum_23 = []byte(`topic_sticky`)
var forum_24 = []byte(`topic_closed`)
var forum_25 = []byte(`">
var forum_24 = []byte(`topic_sticky`)
var forum_25 = []byte(`topic_closed`)
var forum_26 = []byte(`">
<span class="selector"></span>
`)
var forum_26 = []byte(`<img src="`)
var forum_27 = []byte(`" height="64" />`)
var forum_28 = []byte(`
var forum_27 = []byte(`<img src="`)
var forum_28 = []byte(`" height="64" />`)
var forum_29 = []byte(`
<span class="topic_inner_left">
<a class="rowtopic" href="`)
var forum_29 = []byte(`">`)
var forum_30 = []byte(`</a>
var forum_30 = []byte(`"><span>`)
var forum_31 = []byte(`</span></a>
<br /><a class="rowsmall starter" href="`)
var forum_31 = []byte(`">`)
var forum_32 = []byte(`</a>
var forum_32 = []byte(`">`)
var forum_33 = []byte(`</a>
`)
var forum_33 = []byte(`<span class="rowsmall topic_status_e topic_status_closed" title="Status: Closed"> | &#x1F512;&#xFE0E</span>`)
var forum_34 = []byte(`<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>`)
var forum_35 = []byte(`
var forum_34 = []byte(`<span class="rowsmall topic_status_e topic_status_closed" title="Status: Closed"> | &#x1F512;&#xFE0E</span>`)
var forum_35 = []byte(`<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>`)
var forum_36 = []byte(`
</span>
<span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">`)
var forum_36 = []byte(` replies</span><br />
<span class="topicCount">x topics</span>
var forum_37 = []byte(`</span><br />
<span class="likeCount">`)
var forum_38 = []byte(`</span>
</span>
</div>
<div class="rowitem topic_right passive datarow `)
var forum_37 = []byte(`topic_sticky`)
var forum_38 = []byte(`topic_closed`)
var forum_39 = []byte(`">
var forum_39 = []byte(`topic_sticky`)
var forum_40 = []byte(`topic_closed`)
var forum_41 = []byte(`">
`)
var forum_40 = []byte(`<img src="`)
var forum_41 = []byte(`" height="64" />`)
var forum_42 = []byte(`
var forum_42 = []byte(`<img src="`)
var forum_43 = []byte(`" height="64" />`)
var forum_44 = []byte(`
<span>
<a href="`)
var forum_43 = []byte(`" class="lastName" style="font-size: 14px;">`)
var forum_44 = []byte(`</a><br>
<span class="rowsmall lastReplyAt">Last: `)
var forum_45 = []byte(`</span>
var forum_45 = []byte(`" class="lastName" style="font-size: 14px;">`)
var forum_46 = []byte(`</a><br>
<span class="rowsmall lastReplyAt">`)
var forum_47 = []byte(`</span>
</span>
</div>
</div>`)
var forum_46 = []byte(`<div class="rowitem passive">There aren't any topics in this forum yet.`)
var forum_47 = []byte(` <a href="/topics/create/`)
var forum_48 = []byte(`">Start one?</a>`)
var forum_49 = []byte(`</div>`)
var forum_50 = []byte(`
var forum_48 = []byte(`<div class="rowitem passive">There aren't any topics in this forum yet.`)
var forum_49 = []byte(` <a href="/topics/create/`)
var forum_50 = []byte(`">Start one?</a>`)
var forum_51 = []byte(`</div>`)
var forum_52 = []byte(`
</div>
</main>

View File

@ -83,107 +83,110 @@ w.Write(topics_2)
if tmpl_topics_vars.CurrentUser.ID != 0 {
if len(tmpl_topics_vars.ForumList) != 0 {
w.Write(topics_3)
} else {
w.Write(topics_4)
}
} else {
w.Write(topics_5)
}
w.Write(topics_6)
}
w.Write(topics_7)
if tmpl_topics_vars.CurrentUser.ID != 0 {
if len(tmpl_topics_vars.ForumList) != 0 {
w.Write(topics_7)
w.Write(topics_8)
if len(tmpl_topics_vars.ForumList) != 0 {
for _, item := range tmpl_topics_vars.ForumList {
w.Write(topics_8)
if item.ID == tmpl_topics_vars.DefaultForum {
w.Write(topics_9)
}
if item.ID == tmpl_topics_vars.DefaultForum {
w.Write(topics_10)
w.Write([]byte(strconv.Itoa(item.ID)))
}
w.Write(topics_11)
w.Write([]byte(item.Name))
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topics_12)
}
}
w.Write([]byte(item.Name))
w.Write(topics_13)
if tmpl_topics_vars.CurrentUser.Perms.UploadFiles {
w.Write(topics_14)
}
}
w.Write(topics_14)
if tmpl_topics_vars.CurrentUser.Perms.UploadFiles {
w.Write(topics_15)
}
}
w.Write(topics_16)
}
}
w.Write(topics_17)
if len(tmpl_topics_vars.TopicList) != 0 {
for _, item := range tmpl_topics_vars.TopicList {
w.Write(topics_17)
if item.Sticky {
w.Write(topics_18)
if item.Sticky {
w.Write(topics_19)
} else {
if item.IsClosed {
w.Write(topics_19)
}
}
w.Write(topics_20)
if item.Creator.Avatar != "" {
}
}
w.Write(topics_21)
w.Write([]byte(item.Creator.Avatar))
if item.Creator.Avatar != "" {
w.Write(topics_22)
}
w.Write([]byte(item.Creator.Avatar))
w.Write(topics_23)
w.Write([]byte(item.Link))
}
w.Write(topics_24)
w.Write([]byte(item.Title))
w.Write([]byte(item.Link))
w.Write(topics_25)
if item.ForumName != "" {
w.Write([]byte(item.Title))
w.Write(topics_26)
w.Write([]byte(item.ForumLink))
if item.ForumName != "" {
w.Write(topics_27)
w.Write([]byte(item.ForumName))
w.Write([]byte(item.ForumLink))
w.Write(topics_28)
}
w.Write([]byte(item.ForumName))
w.Write(topics_29)
w.Write([]byte(item.Creator.Link))
w.Write(topics_30)
w.Write([]byte(item.Creator.Name))
w.Write(topics_31)
if item.IsClosed {
w.Write(topics_32)
}
if item.Sticky {
w.Write(topics_30)
w.Write([]byte(item.Creator.Link))
w.Write(topics_31)
w.Write([]byte(item.Creator.Name))
w.Write(topics_32)
if item.IsClosed {
w.Write(topics_33)
}
w.Write(topics_34)
w.Write([]byte(strconv.Itoa(item.PostCount)))
w.Write(topics_35)
if item.Sticky {
w.Write(topics_34)
}
w.Write(topics_35)
w.Write([]byte(strconv.Itoa(item.PostCount)))
w.Write(topics_36)
w.Write([]byte(strconv.Itoa(item.LikeCount)))
w.Write(topics_37)
if item.Sticky {
w.Write(topics_38)
} else {
if item.IsClosed {
w.Write(topics_37)
}
}
w.Write(topics_38)
if item.LastUser.Avatar != "" {
w.Write(topics_39)
w.Write([]byte(item.LastUser.Avatar))
}
}
w.Write(topics_40)
}
if item.LastUser.Avatar != "" {
w.Write(topics_41)
w.Write([]byte(item.LastUser.Link))
w.Write([]byte(item.LastUser.Avatar))
w.Write(topics_42)
w.Write([]byte(item.LastUser.Name))
w.Write(topics_43)
w.Write([]byte(item.RelativeLastReplyAt))
w.Write(topics_44)
}
} else {
w.Write(topics_43)
w.Write([]byte(item.LastUser.Link))
w.Write(topics_44)
w.Write([]byte(item.LastUser.Name))
w.Write(topics_45)
if tmpl_topics_vars.CurrentUser.Perms.CreateTopic {
w.Write([]byte(item.RelativeLastReplyAt))
w.Write(topics_46)
}
} else {
w.Write(topics_47)
}
if tmpl_topics_vars.CurrentUser.Perms.CreateTopic {
w.Write(topics_48)
}
w.Write(topics_49)
}
w.Write(topics_50)
w.Write(footer_0)
if len(tmpl_topics_vars.Header.Themes) != 0 {
for _, item := range tmpl_topics_vars.Header.Themes {

View File

@ -6,18 +6,22 @@
<main>
<div id="forum_head_block" class="rowblock rowhead">
<div id="forum_head_block" class="rowblock rowhead topic_list_title_block">
<div class="rowitem forum_title{{if ne .CurrentUser.ID 0}} has_opt{{end}}"><h1>{{.Title}}</h1>
</div>
{{if ne .CurrentUser.ID 0}}
{{if .CurrentUser.Perms.CreateTopic}}
<div class="opt create_topic_opt" title="Create Topic"><a class="create_topic_link" href="/topics/create/{{.Forum.ID}}"></a></div>
{{else}}<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>{{end}}
<div class="opt create_topic_opt" title="Create Topic" aria-label="Create a topic"><a class="create_topic_link" href="/topics/create/{{.Forum.ID}}"></a></div>
{{/** TODO: Add a permissions check for this **/}}
<div class="opt mod_opt" title="Moderate">
<a class="moderate_link" href="#"></a>
</div>
{{else}}<div class="opt locked_opt" title="You don't have the permissions needed to create a topic" aria-label="You don't have the permissions needed to make a topic in this forum"><a></a></div>{{end}}
<div style="clear: both;"></div>
{{end}}
</div>
{{if .CurrentUser.Perms.CreateTopic}}
<div class="rowblock topic_create_form quick_create_form" style="display: none;">
<div class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="Quick Topic Form">
<form id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<input form="topic_create_form_form" id="topic_board_input" name="topic-board" value="{{.Forum.ID}}" type="hidden">
<div class="formrow topic_name_row real_first_child">
@ -45,24 +49,25 @@
<div id="forum_topic_list" class="rowblock topic_list">
{{range .ItemList}}<div class="topic_row">
<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}">
<span class="selector"></span>
{{if .Creator.Avatar}}<img src="{{.Creator.Avatar}}" height="64" />{{end}}
<span class="topic_inner_left">
<a class="rowtopic" href="{{.Link}}">{{.Title}}</a>
<a class="rowtopic" href="{{.Link}}"><span>{{.Title}}</span></a>
<br /><a class="rowsmall starter" href="{{.Creator.Link}}">{{.Creator.Name}}</a>
{{/** TODO: Avoid the double '|' when both .IsClosed and .Sticky are set to true. We could probably do this with CSS **/}}
{{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="Status: Closed"> | &#x1F512;&#xFE0E</span>{{end}}
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>{{end}}
</span>
<span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">{{.PostCount}} replies</span><br />
<span class="topicCount">x topics</span>
<span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span>
</span>
</div>
<div class="rowitem topic_right passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}">
{{if .LastUser.Avatar}}<img src="{{.LastUser.Avatar}}" height="64" />{{end}}
<span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;">{{.LastUser.Name}}</a><br>
<span class="rowsmall lastReplyAt">Last: {{.RelativeLastReplyAt}}</span>
<span class="rowsmall lastReplyAt">{{.RelativeLastReplyAt}}</span>
</span>
</div>
</div>{{else}}<div class="rowitem passive">There aren't any topics in this forum yet.{{if .CurrentUser.Perms.CreateTopic}} <a href="/topics/create/{{.Forum.ID}}">Start one?</a>{{end}}</div>{{end}}

View File

@ -1,18 +1,22 @@
{{template "header.html" . }}
<main>
<div class="rowblock rowhead">
<div class="rowblock rowhead topic_list_title_block">
<div class="rowitem topic_list_title{{if ne .CurrentUser.ID 0}} has_opt{{end}}"><h1>All Topics</h1></div>
{{if ne .CurrentUser.ID 0}}
{{if .ForumList}}
<div class="opt create_topic_opt" title="Create Topic"><a class="create_topic_link" href="/topics/create/"></a></div>
{{else}}<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>{{end}}
<div class="opt create_topic_opt" title="Create Topic" aria-label="Create a topic"><a class="create_topic_link" href="/topics/create/"></a></div>
{{/** TODO: Add a permissions check for this **/}}
<div class="opt mod_opt" title="Moderate">
<a class="moderate_link" href="#"></a>
</div>
{{else}}<div class="opt locked_opt" title="You don't have the permissions needed to create a topic" aria-label="You don't have the permissions needed to make a topic anywhere"><a></a></div>{{end}}
<div style="clear: both;"></div>
{{end}}
</div>
{{if ne .CurrentUser.ID 0}}
{{if .ForumList}}
<div class="rowblock topic_create_form quick_create_form" style="display: none;">
<div class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="Quick Topic Form">
<form name="topic_create_form_form" id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<div class="formrow topic_board_row real_first_child">
<div class="formitem"><select form="topic_create_form_form" id="topic_board_input" name="topic-board">
@ -45,24 +49,25 @@
<div id="topic_list" class="rowblock topic_list" aria-label="A list containing topics from every forum">
{{range .TopicList}}<div class="topic_row">
<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}">
<span class="selector"></span>
{{if .Creator.Avatar}}<img src="{{.Creator.Avatar}}" height="64" />{{end}}
<span class="topic_inner_left">
<a class="rowtopic" href="{{.Link}}">{{.Title}}</a> {{if .ForumName}}<a class="rowsmall parent_forum" href="{{.ForumLink}}">{{.ForumName}}</a>{{end}}
<a class="rowtopic" href="{{.Link}}"><span>{{.Title}}</span></a> {{if .ForumName}}<a class="rowsmall parent_forum" href="{{.ForumLink}}">{{.ForumName}}</a>{{end}}
<br /><a class="rowsmall starter" href="{{.Creator.Link}}">{{.Creator.Name}}</a>
{{/** TODO: Avoid the double '|' when both .IsClosed and .Sticky are set to true. We could probably do this with CSS **/}}
{{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="Status: Closed"> | &#x1F512;&#xFE0E</span>{{end}}
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>{{end}}
</span>
<span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">{{.PostCount}} replies</span><br />
<span class="topicCount">x topics</span>
<span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span>
</span>
</div>
<div class="rowitem topic_right passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}">
{{if .LastUser.Avatar}}<img src="{{.LastUser.Avatar}}" height="64" />{{end}}
<span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;">{{.LastUser.Name}}</a><br>
<span class="rowsmall lastReplyAt">Last: {{.RelativeLastReplyAt}}</span>
<span class="rowsmall lastReplyAt">{{.RelativeLastReplyAt}}</span>
</span>
</div>
</div>{{else}}<div class="rowitem passive">There aren't any topics yet.{{if .CurrentUser.Perms.CreateTopic}} <a href="/topics/create/">Start one?</a>{{end}}</div>{{end}}

View File

@ -2,6 +2,10 @@
--header-border-color: hsl(0,0%,85%);
--element-border-color: hsl(0,0%,90%);
--element-background-color: white;
--replies-lang-string: " replies";
--topics-lang-string: " topics";
--likes-lang-string: " likes";
--primary-link-color: hsl(0,0%,40%);
}
* {
@ -10,6 +14,14 @@
-webkit-box-sizing: border-box;
}
@font-face {
font-family: 'FontAwesome';
src: url('../font-awesome-4.7.0/fonts/fontawesome-webfont.eot?v=4.7.0');
src: url('../font-awesome-4.7.0/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../font-awesome-4.7.0/fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../font-awesome-4.7.0/fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../font-awesome-4.7.0/fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../font-awesome-4.7.0/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
font-weight: normal;
font-style: normal;
}
body {
font-size: 16px;
font-family: arial;
@ -18,7 +30,7 @@ body {
a {
text-decoration: none;
color: hsl(0,0%,40%);
color: var(--primary-link-color);
}
#back {
@ -53,10 +65,11 @@ li {
.menu_overview {
font-size: 22px;
margin-right: 12px;
letter-spacing: 1px;
}
.menu_overview a:after {
margin-left: 14px;
margin-right: 4px;
margin-left: 13px;
margin-right: 5px;
position: relative;
top: -3px;
}
@ -74,8 +87,37 @@ li {
top: -2px;
}
.menu_alerts {
color: hsl(0,0%,40%);
display: flex;
}
.alert_bell:before {
content: "\f01c";
font: normal normal normal 14px/1 FontAwesome;
}
.menu_alerts:not(.has_alerts) .alert_counter {
display: none;
}
.alert_counter {
width: 4px;
height: 4px;
overflow: hidden;
background-color: red;
opacity: 0.7;
border-radius: 30px;
position: relative;
top: 2px;
left: -1px;
}
.alert_aftercounter:before {
content: "Alerts";
margin-left: 4px;
}
.alert_aftercounter:after {
content: "|";
margin-left: 12px;
color: var(--header-border-color);
}
.menu_account a:before {
content: "\f2c3";
font: normal normal normal 14px/1 FontAwesome;
@ -117,8 +159,8 @@ ul {
border: 1px solid var(--header-border-color);
border-bottom: 2px solid var(--header-border-color);
background-color: var(--element-background-color);
margin-left: 4px;
margin-right: 4px;
margin-left: 12px;
margin-right: 12px;
}
.rowhead {
@ -135,6 +177,23 @@ ul {
display: inline-block;
}
.topic_list_title_block {
display: flex;
}
.mod_opt {
margin-left: auto;
}
.mod_opt .moderate_link {
border-left: 1px solid var(--element-border-color);
padding-left: 12px;
height: 20px;
}
.mod_opt .moderate_link:before {
content: "\f0e3";
font: normal normal normal 14px/1 FontAwesome;
font-size: 18px;
}
.topic_list {
border: none;
}
@ -152,26 +211,37 @@ ul {
}
.topic_list .rowtopic {
font-size: 18px;
font-size: 17px;
color: hsl(0,0%,30%);
margin-right: 2px;
max-width: 230px;
margin-right: 1px;
white-space: nowrap;
display: inline-block;
}
.topic_list .rowtopic span {
max-width: 112px;
overflow: hidden;
text-overflow: ellipsis;
}
.topic_list .rowsmall {
font-size: 15px;
}
.topic_list .rowsmall.starter:before {
content: "\f007";
font: normal normal normal 14px/1 FontAwesome;
margin-right: 5px;
font-size: 16px;
font-size: 15px;
}
.topic_list .rowsmall.starter:before {
content: "\f007";
font: normal normal normal 14px/1 FontAwesome;
margin-right: 5px;
font-size: 16px;
font-size: 15px;
}
.topic_list .lastReplyAt {
font-size: 14px;
}
.topic_list .topic_status_e {
@ -179,19 +249,50 @@ ul {
}
.topic_left {
flex: 1 1 calc(100% - 400px);
flex: 1 1 calc(100% - 380px);
border-right: none;
}
.topic_inner_right {
margin-left: auto;
margin-left: 15%;
margin-right: auto;
font-size: 17px;
}
.topic_inner_right.rowsmall {
font-size: 14px;
margin-top: 15px;
}
/* Experimenting here */
.topic_inner_right {
margin-top: 12px;
}
.topic_inner_right span {
/*font-size: 15px;*/
font-size: 16px;
}
.topic_inner_right span:after {
font-size: 13.5px;
}
/* End Experiment */
.topic_inner_right .replyCount:after {
content: var(--replies-lang-string);
}
.topic_inner_right .topicCount:after {
content: var(--topics-lang-string);
}
.topic_inner_right .likeCount:after {
content: var(--likes-lang-string);
}
.topic_right {
flex: 1 1 150px;
flex: 1 1 0px; /*150px*/
border-left: none;
}
@ -205,15 +306,16 @@ ul {
.topic_right img {
border-radius: 30px;
height: 48px;
width: 48px;
margin-top: 8px;
height: 42px;
width: 42px;
margin-top: 10px;
}
.topic_left .topic_inner_left {
margin-top: 12px;
margin-left: 8px;
margin-bottom: 14px;
width: 240px;
}
.topic_right > span {
@ -222,11 +324,22 @@ ul {
}
.topic_sticky {
border-bottom: 2px solid hsl(51, 60%, 50%);
border-bottom: 2px solid hsl(51, 60%, 70%);
}
/*@element .topic_left .rowtopic and (min-characters: 10) {
}*/
@element .topic_left .rowtopic and (min-width: 110px) {
$this, $this span, $this + .parent_forum {
float: left;
}
$this + .parent_forum {
margin: 2px;
margin-left: 3px;
}
$this:after {
content: "...";
float: left;
}
}
@media(max-width: 670px) {
.topic_inner_right {

View File

@ -7,10 +7,6 @@
"Tag": "WIP",
"Sidebars":"right",
"Resources": [
{
"Name":"font-awesome-4.7.0/css/font-awesome.css",
"Location":"global"
},
{
"Name":"EQCSS.min.js",
"Location":"global"

View File

@ -599,6 +599,9 @@ input, select, textarea {
.topic_list .lastReplyAt {
white-space: nowrap;
}
.topic_list .lastReplyAt:before {
content: "Last: ";
}
.topic_list .starter:before {
content: "Starter: ";
}

View File

@ -405,6 +405,9 @@ li a {
.topic_list .lastReplyAt {
white-space: nowrap;
}
.topic_list .lastReplyAt:before {
content: "Last: ";
}
.topic_list .starter:before {
content: "Starter: ";
}

View File

@ -408,6 +408,9 @@ li a {
.topic_list .lastReplyAt {
white-space: nowrap;
}
.topic_list .lastReplyAt:before {
content: "Last: ";
}
.topic_list .starter:before {
content: "Starter: ";
}

79
user.go
View File

@ -9,9 +9,12 @@ package main
import (
//"log"
//"fmt"
"database/sql"
"errors"
"strconv"
"time"
"./query_gen/lib"
"golang.org/x/crypto/bcrypt"
)
@ -25,6 +28,7 @@ var CheckPassword = BcryptCheckPassword
//func(password string) (hashed_password string, salt string, err error)
var GeneratePassword = BcryptGeneratePassword
var ErrNoTempGroup = errors.New("We couldn't find a temporary group for this user")
type User struct {
ID int
@ -66,28 +70,61 @@ func (user *User) Ban(duration time.Duration, issuedBy int) error {
}
func (user *User) Unban() error {
err := user.RevertGroupUpdate()
ucache, ok := users.(UserCache)
if ok {
ucache.CacheRemove(user.ID)
return user.RevertGroupUpdate()
}
func (user *User) deleteScheduleGroupTx(tx *sql.Tx) error {
deleteScheduleGroupStmt, err := qgen.Builder.SimpleDeleteTx(tx, "users_groups_scheduler", "uid = ?")
if err != nil {
return err
}
_, err = deleteScheduleGroupStmt.Exec(user.ID)
return err
}
func (user *User) setTempGroupTx(tx *sql.Tx, tempGroup int) error {
setTempGroupStmt, err := qgen.Builder.SimpleUpdateTx(tx, "users", "temp_group = ?", "uid = ?")
if err != nil {
return err
}
_, err = setTempGroupStmt.Exec(tempGroup, user.ID)
return err
}
// TODO: Use a transaction to avoid race conditions
// Make this more stateless?
func (user *User) ScheduleGroupUpdate(gid int, issuedBy int, duration time.Duration) error {
var temporary bool
if duration.Nanoseconds() != 0 {
temporary = true
}
revertAt := time.Now().Add(duration)
_, err := replaceScheduleGroupStmt.Exec(user.ID, gid, issuedBy, revertAt, temporary, user.ID)
tx, err := db.Begin()
if err != nil {
return err
}
_, err = setTempGroupStmt.Exec(gid, user.ID)
defer tx.Rollback()
err = user.deleteScheduleGroupTx(tx)
if err != nil {
return err
}
createScheduleGroupTx, err := qgen.Builder.SimpleInsertTx(tx, "users_groups_scheduler", "uid, set_group, issued_by, issued_at, revert_at, temporary", "?,?,?,UTC_TIMESTAMP(),?,?")
if err != nil {
return err
}
_, err = createScheduleGroupTx.Exec(user.ID, gid, issuedBy, revertAt, temporary)
if err != nil {
return err
}
err = user.setTempGroupTx(tx, gid)
if err != nil {
return err
}
err = tx.Commit()
ucache, ok := users.(UserCache)
if ok {
ucache.CacheRemove(user.ID)
@ -95,13 +132,24 @@ func (user *User) ScheduleGroupUpdate(gid int, issuedBy int, duration time.Durat
return err
}
// TODO: Use a transaction to avoid race conditions
func (user *User) RevertGroupUpdate() error {
_, err := replaceScheduleGroupStmt.Exec(user.ID, 0, 0, time.Now(), false, user.ID)
tx, err := db.Begin()
if err != nil {
return err
}
_, err = setTempGroupStmt.Exec(0, user.ID)
defer tx.Rollback()
err = user.deleteScheduleGroupTx(tx)
if err != nil {
return err
}
err = user.setTempGroupTx(tx, 0)
if err != nil {
return err
}
err = tx.Commit()
ucache, ok := users.(UserCache)
if ok {
ucache.CacheRemove(user.ID)
@ -157,6 +205,15 @@ func (user *User) ChangeAvatar(avatar string) (err error) {
return err
}
func (user *User) ChangeGroup(group int) (err error) {
_, err = updateUserGroupStmt.Exec(group, user.ID)
ucache, ok := users.(UserCache)
if ok {
ucache.CacheRemove(user.ID)
}
return err
}
func (user *User) increasePostStats(wcount int, topic bool) error {
var mod int
baseScore := 1

View File

@ -24,7 +24,7 @@ type UserStore interface {
//BulkGet(ids []int) ([]*User, error)
BulkGetMap(ids []int) (map[int]*User, error)
BypassGet(id int) (*User, error)
Create(username string, password string, email string, group int, active int) (int, error)
Create(username string, password string, email string, group int, active bool) (int, error)
GlobalCount() int
}
@ -124,6 +124,7 @@ func (mus *MemoryUserStore) Get(id int) (*User, error) {
user = &User{ID: id, Loggedin: true}
err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
// TODO: Add an init method to User rather than writing this same bit of code over and over
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
@ -202,6 +203,7 @@ func (mus *MemoryUserStore) BulkGetMap(ids []int) (list map[int]*User, err error
return nil, err
}
// TODO: Add an init method to User rather than writing this same bit of code over and over
// Initialise the user
if user.Avatar != "" {
if user.Avatar[0] == '.' {
@ -253,6 +255,7 @@ func (mus *MemoryUserStore) BypassGet(id int) (*User, error) {
user := &User{ID: id, Loggedin: true}
err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
// TODO: Add an init method to User rather than writing this same bit of code over and over
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
@ -274,6 +277,7 @@ func (mus *MemoryUserStore) Reload(id int) error {
return err
}
// TODO: Add an init method to User rather than writing this same bit of code over and over
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
@ -357,7 +361,7 @@ func (mus *MemoryUserStore) CacheRemoveUnsafe(id int) error {
}
// TODO: Change active to a bool?
func (mus *MemoryUserStore) Create(username string, password string, email string, group int, active int) (int, error) {
func (mus *MemoryUserStore) Create(username string, password string, email string, group int, active bool) (int, error) {
// Is this username already taken..?
err := mus.usernameExists.QueryRow(username).Scan(&username)
if err != ErrNoRows {
@ -405,8 +409,7 @@ func (mus *MemoryUserStore) GetCapacity() int {
}
// GlobalCount returns the total number of users registered on the forums
func (mus *MemoryUserStore) GlobalCount() int {
var ucount int
func (mus *MemoryUserStore) GlobalCount() (ucount int) {
err := mus.userCount.QueryRow().Scan(&ucount)
if err != nil {
LogError(err)
@ -548,7 +551,7 @@ func (mus *SQLUserStore) Exists(id int) bool {
return err != ErrNoRows
}
func (mus *SQLUserStore) Create(username string, password string, email string, group int, active int) (int, error) {
func (mus *SQLUserStore) Create(username string, password string, email string, group int, active bool) (int, error) {
// Is this username already taken..?
err := mus.usernameExists.QueryRow(username).Scan(&username)
if err != ErrNoRows {
@ -575,11 +578,62 @@ func (mus *SQLUserStore) Create(username string, password string, email string,
}
// GlobalCount returns the total number of users registered on the forums
func (mus *SQLUserStore) GlobalCount() int {
var ucount int
func (mus *SQLUserStore) GlobalCount() (ucount int) {
err := mus.userCount.QueryRow().Scan(&ucount)
if err != nil {
LogError(err)
}
return ucount
}
// TODO: MockUserStore
// NullUserStore is here for tests because Go doesn't have short-circuiting
type NullUserStore struct {
}
func (nus *NullUserStore) CacheGet(_ int) (*User, error) {
return nil, ErrNoRows
}
func (nus *NullUserStore) CacheGetUnsafe(_ int) (*User, error) {
return nil, ErrNoRows
}
func (nus *NullUserStore) CacheSet(_ *User) error {
return ErrStoreCapacityOverflow
}
func (nus *NullUserStore) CacheAdd(_ *User) error {
return ErrStoreCapacityOverflow
}
func (nus *NullUserStore) CacheAddUnsafe(_ *User) error {
return ErrStoreCapacityOverflow
}
func (nus *NullUserStore) CacheRemove(_ int) error {
return ErrNoRows
}
func (nus *NullUserStore) CacheRemoveUnsafe(_ int) error {
return ErrNoRows
}
func (nus *NullUserStore) Flush() {
}
func (nus *NullUserStore) Reload(_ int) error {
return ErrNoRows
}
func (nus *NullUserStore) Length() int {
return 0
}
func (nus *NullUserStore) SetCapacity(_ int) {
}
func (nus *NullUserStore) GetCapacity() int {
return 0
}