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

View File

@ -14,14 +14,18 @@ var errorBuffer []error
//var notfoundCountPerSecond int //var notfoundCountPerSecond int
//var nopermsCountPerSecond 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) { func LogError(err error) {
LogWarning(err)
log.Fatal("")
}
func LogWarning(err error) {
log.Print(err) log.Print(err)
debug.PrintStack() debug.PrintStack()
errorBufferMutex.Lock() errorBufferMutex.Lock()
defer errorBufferMutex.Unlock() defer errorBufferMutex.Unlock()
errorBuffer = append(errorBuffer, err) 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 // 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 SqlForumStore
// TODO: Work on the NullForumStore

View File

@ -65,13 +65,10 @@ var createForumStmt *sql.Stmt
var addForumPermsToForumStmt *sql.Stmt var addForumPermsToForumStmt *sql.Stmt
var addPluginStmt *sql.Stmt var addPluginStmt *sql.Stmt
var addThemeStmt *sql.Stmt var addThemeStmt *sql.Stmt
var createGroupStmt *sql.Stmt
var addModlogEntryStmt *sql.Stmt var addModlogEntryStmt *sql.Stmt
var addAdminlogEntryStmt *sql.Stmt var addAdminlogEntryStmt *sql.Stmt
var addAttachmentStmt *sql.Stmt var addAttachmentStmt *sql.Stmt
var createWordFilterStmt *sql.Stmt var createWordFilterStmt *sql.Stmt
var addForumPermsToGroupStmt *sql.Stmt
var replaceScheduleGroupStmt *sql.Stmt
var addRepliesToTopicStmt *sql.Stmt var addRepliesToTopicStmt *sql.Stmt
var removeRepliesFromTopicStmt *sql.Stmt var removeRepliesFromTopicStmt *sql.Stmt
var addTopicsToForumStmt *sql.Stmt var addTopicsToForumStmt *sql.Stmt
@ -105,6 +102,7 @@ var updatePluginStmt *sql.Stmt
var updatePluginInstallStmt *sql.Stmt var updatePluginInstallStmt *sql.Stmt
var updateThemeStmt *sql.Stmt var updateThemeStmt *sql.Stmt
var updateUserStmt *sql.Stmt var updateUserStmt *sql.Stmt
var updateUserGroupStmt *sql.Stmt
var updateGroupPermsStmt *sql.Stmt var updateGroupPermsStmt *sql.Stmt
var updateGroupRankStmt *sql.Stmt var updateGroupRankStmt *sql.Stmt
var updateGroupStmt *sql.Stmt var updateGroupStmt *sql.Stmt
@ -116,15 +114,11 @@ var bumpSyncStmt *sql.Stmt
var deleteUserStmt *sql.Stmt var deleteUserStmt *sql.Stmt
var deleteReplyStmt *sql.Stmt var deleteReplyStmt *sql.Stmt
var deleteProfileReplyStmt *sql.Stmt var deleteProfileReplyStmt *sql.Stmt
var deleteForumPermsByForumStmt *sql.Stmt
var deleteActivityStreamMatchStmt *sql.Stmt var deleteActivityStreamMatchStmt *sql.Stmt
var deleteWordFilterStmt *sql.Stmt var deleteWordFilterStmt *sql.Stmt
var reportExistsStmt *sql.Stmt var reportExistsStmt *sql.Stmt
var groupCountStmt *sql.Stmt var groupCountStmt *sql.Stmt
var modlogCountStmt *sql.Stmt var modlogCountStmt *sql.Stmt
var addForumPermsToForumAdminsStmt *sql.Stmt
var addForumPermsToForumStaffStmt *sql.Stmt
var addForumPermsToForumMembersStmt *sql.Stmt
var notifyWatchersStmt *sql.Stmt var notifyWatchersStmt *sql.Stmt
// nolint // nolint
@ -365,9 +359,9 @@ func _gen_mssql() (err error) {
} }
log.Print("Preparing getExpiredScheduledGroups statement.") 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 { 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 return err
} }
@ -539,13 +533,6 @@ func _gen_mssql() (err error) {
return err 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.") log.Print("Preparing addModlogEntry statement.")
addModlogEntryStmt, err = db.Prepare("INSERT INTO [moderation_logs] ([action],[elementID],[elementType],[ipaddress],[actorID],[doneAt]) VALUES (?,?,?,?,?,GETUTCDATE())") addModlogEntryStmt, err = db.Prepare("INSERT INTO [moderation_logs] ([action],[elementID],[elementType],[ipaddress],[actorID],[doneAt]) VALUES (?,?,?,?,?,GETUTCDATE())")
if err != nil { if err != nil {
@ -574,20 +561,6 @@ func _gen_mssql() (err error) {
return err 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.") log.Print("Preparing addRepliesToTopic statement.")
addRepliesToTopicStmt, err = db.Prepare("UPDATE [topics] SET [postCount] = [postCount] + ?,[lastReplyBy] = ?,[lastReplyAt] = GETUTCDATE() WHERE [tid] = ?") addRepliesToTopicStmt, err = db.Prepare("UPDATE [topics] SET [postCount] = [postCount] + ?,[lastReplyBy] = ?,[lastReplyAt] = GETUTCDATE() WHERE [tid] = ?")
if err != nil { if err != nil {
@ -819,6 +792,13 @@ func _gen_mssql() (err error) {
return err 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.") log.Print("Preparing updateGroupPerms statement.")
updateGroupPermsStmt, err = db.Prepare("UPDATE [users_groups] SET [permissions] = ? WHERE [gid] = ?") updateGroupPermsStmt, err = db.Prepare("UPDATE [users_groups] SET [permissions] = ? WHERE [gid] = ?")
if err != nil { if err != nil {
@ -896,13 +876,6 @@ func _gen_mssql() (err error) {
return err 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.") log.Print("Preparing deleteActivityStreamMatch statement.")
deleteActivityStreamMatchStmt, err = db.Prepare("DELETE FROM [activity_stream_matches] WHERE [watcher] = ? AND [asid] = ?") deleteActivityStreamMatchStmt, err = db.Prepare("DELETE FROM [activity_stream_matches] WHERE [watcher] = ? AND [asid] = ?")
if err != nil { if err != nil {
@ -938,27 +911,6 @@ func _gen_mssql() (err error) {
return err 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.") 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") 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 { if err != nil {

View File

@ -6,7 +6,7 @@ package main
import "log" import "log"
import "database/sql" import "database/sql"
import "./query_gen/lib" //import "./query_gen/lib"
// nolint // nolint
var getUserStmt *sql.Stmt var getUserStmt *sql.Stmt
@ -67,13 +67,10 @@ var createForumStmt *sql.Stmt
var addForumPermsToForumStmt *sql.Stmt var addForumPermsToForumStmt *sql.Stmt
var addPluginStmt *sql.Stmt var addPluginStmt *sql.Stmt
var addThemeStmt *sql.Stmt var addThemeStmt *sql.Stmt
var createGroupStmt *sql.Stmt
var addModlogEntryStmt *sql.Stmt var addModlogEntryStmt *sql.Stmt
var addAdminlogEntryStmt *sql.Stmt var addAdminlogEntryStmt *sql.Stmt
var addAttachmentStmt *sql.Stmt var addAttachmentStmt *sql.Stmt
var createWordFilterStmt *sql.Stmt var createWordFilterStmt *sql.Stmt
var addForumPermsToGroupStmt *qgen.MySQLUpsertCallback
var replaceScheduleGroupStmt *qgen.MySQLUpsertCallback
var addRepliesToTopicStmt *sql.Stmt var addRepliesToTopicStmt *sql.Stmt
var removeRepliesFromTopicStmt *sql.Stmt var removeRepliesFromTopicStmt *sql.Stmt
var addTopicsToForumStmt *sql.Stmt var addTopicsToForumStmt *sql.Stmt
@ -107,6 +104,7 @@ var updatePluginStmt *sql.Stmt
var updatePluginInstallStmt *sql.Stmt var updatePluginInstallStmt *sql.Stmt
var updateThemeStmt *sql.Stmt var updateThemeStmt *sql.Stmt
var updateUserStmt *sql.Stmt var updateUserStmt *sql.Stmt
var updateUserGroupStmt *sql.Stmt
var updateGroupPermsStmt *sql.Stmt var updateGroupPermsStmt *sql.Stmt
var updateGroupRankStmt *sql.Stmt var updateGroupRankStmt *sql.Stmt
var updateGroupStmt *sql.Stmt var updateGroupStmt *sql.Stmt
@ -118,15 +116,11 @@ var bumpSyncStmt *sql.Stmt
var deleteUserStmt *sql.Stmt var deleteUserStmt *sql.Stmt
var deleteReplyStmt *sql.Stmt var deleteReplyStmt *sql.Stmt
var deleteProfileReplyStmt *sql.Stmt var deleteProfileReplyStmt *sql.Stmt
var deleteForumPermsByForumStmt *sql.Stmt
var deleteActivityStreamMatchStmt *sql.Stmt var deleteActivityStreamMatchStmt *sql.Stmt
var deleteWordFilterStmt *sql.Stmt var deleteWordFilterStmt *sql.Stmt
var reportExistsStmt *sql.Stmt var reportExistsStmt *sql.Stmt
var groupCountStmt *sql.Stmt var groupCountStmt *sql.Stmt
var modlogCountStmt *sql.Stmt var modlogCountStmt *sql.Stmt
var addForumPermsToForumAdminsStmt *sql.Stmt
var addForumPermsToForumStaffStmt *sql.Stmt
var addForumPermsToForumMembersStmt *sql.Stmt
var notifyWatchersStmt *sql.Stmt var notifyWatchersStmt *sql.Stmt
// nolint // nolint
@ -483,12 +477,6 @@ func _gen_mysql() (err error) {
return err 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.") log.Print("Preparing addModlogEntry statement.")
addModlogEntryStmt, err = db.Prepare("INSERT INTO `moderation_logs`(`action`,`elementID`,`elementType`,`ipaddress`,`actorID`,`doneAt`) VALUES (?,?,?,?,?,UTC_TIMESTAMP())") addModlogEntryStmt, err = db.Prepare("INSERT INTO `moderation_logs`(`action`,`elementID`,`elementType`,`ipaddress`,`actorID`,`doneAt`) VALUES (?,?,?,?,?,UTC_TIMESTAMP())")
if err != nil { if err != nil {
@ -513,18 +501,6 @@ func _gen_mysql() (err error) {
return err 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.") log.Print("Preparing addRepliesToTopic statement.")
addRepliesToTopicStmt, err = db.Prepare("UPDATE `topics` SET `postCount` = `postCount` + ?,`lastReplyBy` = ?,`lastReplyAt` = UTC_TIMESTAMP() WHERE `tid` = ?") addRepliesToTopicStmt, err = db.Prepare("UPDATE `topics` SET `postCount` = `postCount` + ?,`lastReplyBy` = ?,`lastReplyAt` = UTC_TIMESTAMP() WHERE `tid` = ?")
if err != nil { if err != nil {
@ -723,6 +699,12 @@ func _gen_mysql() (err error) {
return err 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.") log.Print("Preparing updateGroupPerms statement.")
updateGroupPermsStmt, err = db.Prepare("UPDATE `users_groups` SET `permissions` = ? WHERE `gid` = ?") updateGroupPermsStmt, err = db.Prepare("UPDATE `users_groups` SET `permissions` = ? WHERE `gid` = ?")
if err != nil { if err != nil {
@ -789,12 +771,6 @@ func _gen_mysql() (err error) {
return err 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.") log.Print("Preparing deleteActivityStreamMatch statement.")
deleteActivityStreamMatchStmt, err = db.Prepare("DELETE FROM `activity_stream_matches` WHERE `watcher` = ? AND `asid` = ?") deleteActivityStreamMatchStmt, err = db.Prepare("DELETE FROM `activity_stream_matches` WHERE `watcher` = ? AND `asid` = ?")
if err != nil { if err != nil {
@ -825,24 +801,6 @@ func _gen_mysql() (err error) {
return err 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.") 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` = ?") 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 { if err != nil {

View File

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

View File

@ -2,14 +2,14 @@
package main package main
var dbTablePrimaryKeys = map[string]string{ var dbTablePrimaryKeys = map[string]string{
"forums":"fid",
"topics":"tid",
"attachments":"attachID",
"users_replies":"rid",
"word_filters":"wfid",
"users":"uid",
"users_groups":"gid", "users_groups":"gid",
"users_groups_scheduler":"uid", "users_groups_scheduler":"uid",
"replies":"rid", "topics":"tid",
"users_replies":"rid",
"activity_stream":"asid", "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 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. // ! 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 // Copy gives you a non-pointer concurrency safe copy of the group
func (group *Group) Copy() Group { func (group *Group) Copy() Group {

View File

@ -6,6 +6,8 @@ import (
"errors" "errors"
"log" "log"
"sync" "sync"
"./query_gen/lib"
) )
var groupCreateMutex sync.Mutex var groupCreateMutex sync.Mutex
@ -19,7 +21,7 @@ type GroupStore interface {
Get(id int) (*Group, error) Get(id int) (*Group, error)
GetCopy(id int) (Group, error) GetCopy(id int) (Group, error)
Exists(id int) bool 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) GetAll() ([]*Group, error)
GetRange(lower int, higher int) ([]*Group, error) GetRange(lower int, higher int) ([]*Group, error)
} }
@ -71,8 +73,11 @@ func (mgs *MemoryGroupStore) LoadGroups() error {
log.Print(group.Name + ": ") log.Print(group.Name + ": ")
log.Printf("%+v\n", group.PluginPerms) log.Printf("%+v\n", group.PluginPerms)
} }
//group.Perms.ExtData = make(map[string]bool) //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) mgs.groups = append(mgs.groups, &group)
} }
err = rows.Err() err = rows.Err()
@ -110,15 +115,26 @@ func (mgs *MemoryGroupStore) GetCopy(gid int) (Group, error) {
} }
func (mgs *MemoryGroupStore) Exists(gid int) bool { 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() groupCreateMutex.Lock()
defer groupCreateMutex.Unlock() defer groupCreateMutex.Unlock()
var permstr = "{}" 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 { if err != nil {
return 0, err return 0, err
} }
@ -138,14 +154,14 @@ func (mgs *MemoryGroupStore) Create(groupName string, tag string, isAdmin bool,
runVhook("create_group_preappend", &pluginPerms, &pluginPermsBytes) 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... // Generate the forum permissions based on the presets...
fdata, err := fstore.GetAll() fdata, err := fstore.GetAll()
if err != nil { if err != nil {
return 0, err return 0, err
} }
var presetSet = make(map[int]string)
var permSet = make(map[int]ForumPerms)
permUpdateMutex.Lock() permUpdateMutex.Lock()
defer permUpdateMutex.Unlock() defer permUpdateMutex.Unlock()
for _, forum := range fdata { for _, forum := range fdata {
@ -161,23 +177,38 @@ func (mgs *MemoryGroupStore) Create(groupName string, tag string, isAdmin bool,
} }
permmap := presetToPermmap(forum.Preset) permmap := presetToPermmap(forum.Preset)
permitem := permmap[thePreset] permItem := permmap[thePreset]
permitem.Overrides = true 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
}
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) err = rebuildForumPermissions(forum.ID)
if err != nil { if err != nil {
return gid, err return gid, err
} }
} }
return gid, nil return gid, nil
} }

View File

@ -99,7 +99,7 @@ func (ins *MysqlInstaller) InitDatabase() (err error) {
return nil return nil
} }
func (ins *MysqlInstaller) TableDefs() error { func (ins *MysqlInstaller) TableDefs() (err error) {
//fmt.Println("Creating the tables") //fmt.Println("Creating the tables")
files, _ := ioutil.ReadDir("./schema/mysql/") files, _ := ioutil.ReadDir("./schema/mysql/")
for _, f := range files { for _, f := range files {
@ -115,6 +115,13 @@ func (ins *MysqlInstaller) TableDefs() error {
} }
table = strings.TrimSuffix(table, ext) 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 + "'") fmt.Println("Creating table '" + table + "'")
data, err := ioutil.ReadFile("./schema/mysql/" + f.Name()) data, err := ioutil.ReadFile("./schema/mysql/" + f.Name())
if err != nil { if err != nil {

17
main.go
View File

@ -55,6 +55,12 @@ var allowedFileExts = StringList{
var imageFileExts = StringList{ var imageFileExts = StringList{
"png", "jpg", "jpeg", "svg", "bmp", "gif", "tif", "webp", "apng", "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 // TODO: Write a test for this
func (slice StringList) Contains(needle string) bool { func (slice StringList) Contains(needle string) bool {
@ -70,6 +76,16 @@ var staticFiles = make(map[string]SFile)
var logWriter = io.MultiWriter(os.Stderr) var logWriter = io.MultiWriter(os.Stderr)
func main() { 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: Have a file for each run with the time/date the server started as the file name?
// TODO: Log panics with recover() // TODO: Log panics with recover()
f, err := os.OpenFile("./operations.log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0755) 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: Automatically lock topics, if they're really old, and the associated setting is enabled.
// TODO: Publish scheduled posts. // TODO: Publish scheduled posts.
// TODO: Delete the empty users_groups_scheduler entries
// TODO: Add a plugin hook here // TODO: Add a plugin hook here
} }

View File

@ -1,24 +1,26 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"net/http/httptest"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
"testing" "testing"
"time" "time"
) )
func recordMustExist(t *testing.T, err error, errmsg string) { func recordMustExist(t *testing.T, err error, errmsg string, args ...interface{}) {
if err == ErrNoRows { if err == ErrNoRows {
t.Error(errmsg) t.Errorf(errmsg, args...)
} else if err != nil { } else if err != nil {
t.Fatal(err) 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 { if err == nil {
t.Error(errmsg) t.Errorf(errmsg, args...)
} else if err != ErrNoRows { } else if err != ErrNoRows {
t.Fatal(err) t.Fatal(err)
} }
@ -42,50 +44,42 @@ func TestUserStore(t *testing.T) {
userStoreTest(t, 3) userStoreTest(t, 3)
} }
func userStoreTest(t *testing.T, newUserID int) { func userStoreTest(t *testing.T, newUserID int) {
var user *User
var err error
ucache, hasCache := users.(UserCache) ucache, hasCache := users.(UserCache)
if hasCache && ucache.Length() != 0 { // Go doesn't have short-circuiting, so this'll allow us to do one liner tests
t.Error("Initial ucache length isn't zero") 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") recordMustNotExist(t, err, "UID #-1 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")
}
_, err = users.Get(0) _, err = users.Get(0)
recordMustNotExist(t, err, "UID #0 shouldn't exist") 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 { user, err := users.Get(1)
t.Error("There shouldn't be anything in the user cache")
}
user, err = users.Get(1)
recordMustExist(t, err, "Couldn't find UID #1") recordMustExist(t, err, "Couldn't find UID #1")
if user.ID != 1 { expect(t, user.ID == 1, fmt.Sprintf("user.ID should be 1. Got '%d' instead.", user.ID))
t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.") 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")
if user.Name != "Admin" { expect(t, user.IsSuperAdmin, "Admin should be a super admin")
t.Error("user.Name should be 'Admin', not '" + user.Name + "'") expect(t, user.IsAdmin, "Admin should be an admin")
} expect(t, user.IsSuperMod, "Admin should be a super mod")
if user.Group != 1 { expect(t, user.IsMod, "Admin should be a mod")
t.Error("Admin should be in group 1") 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)) recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't exist", newUserID))
if hasCache { if hasCache {
expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d") 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") 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") recordMustNotExist(t, err, "UID #0 shouldn't exist, even in the cache")
user, err = ucache.CacheGet(1) user, err = ucache.CacheGet(1)
recordMustExist(t, err, "Couldn't find UID #1 in the cache") 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 + "'") t.Error("user.Name should be 'Admin', not '" + user.Name + "'")
} }
user, err = ucache.CacheGet(newUserID) _, err = ucache.CacheGet(newUserID)
recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't exist, even in the cache", newUserID)) recordMustNotExist(t, err, "UID #%d shouldn't exist, even in the cache", newUserID)
ucache.Flush() ucache.Flush()
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") 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") recordMustExist(t, err, "Couldn't find UID #1 in the cache")
if user.ID != 1 { 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() 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(1), "UID #1 should exist")
expect(t, !users.Exists(newUserID), fmt.Sprintf("UID #%d shouldn't exist", newUserID)) expect(t, !users.Exists(newUserID), fmt.Sprintf("UID #%d shouldn't exist", newUserID))
if hasCache { expect(t, !hasCache || ucache.Length() == 0, fmt.Sprintf("User cache length should be 0, not %d", ucache.Length()))
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
}
expectIntToBeX(t, users.GlobalCount(), 1, "The number of users should be one, not %d") expectIntToBeX(t, users.GlobalCount(), 1, "The number of users should be one, not %d")
var awaitingActivation = 5 var awaitingActivation = 5
uid, err := users.Create("Sam", "ReallyBadPassword", "sam@localhost.loc", awaitingActivation, 0) uid, err := users.Create("Sam", "ReallyBadPassword", "sam@localhost.loc", awaitingActivation, false)
if err != nil { expectNilErr(t, err)
t.Error(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))
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)
}
user, err = users.Get(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 { if user.ID != newUserID {
t.Errorf("The UID of the user record should be %d", 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") expectIntToBeX(t, user.Group, 5, "Sam should be in group 5")
if hasCache { if hasCache {
expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d") expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d")
user, err = ucache.CacheGet(newUserID) user, err = ucache.CacheGet(newUserID)
recordMustExist(t, err, fmt.Sprintf("Couldn't find UID #%d in the cache", newUserID)) recordMustExist(t, err, "Couldn't find UID #%d in the cache", newUserID)
if user.ID != newUserID { expect(t, user.ID == newUserID, fmt.Sprintf("user.ID does not match the requested UID. Got '%d' instead.", user.ID))
t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.")
}
} }
err = user.Activate() err = user.Activate()
if err != nil { expectNilErr(t, err)
t.Error(err)
}
expectIntToBeX(t, user.Group, 5, "Sam should still be in group 5 in this copy") 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 // ? - 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 { if hasCache {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(newUserID) _, 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) 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) 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")
expectIntToBeX(t, user.Group, config.DefaultGroup, "Sam should be in group "+strconv.Itoa(config.DefaultGroup)) 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 // Permanent ban
duration, _ := time.ParseDuration("0") duration, _ := time.ParseDuration("0")
// TODO: Attempt a double ban, double activation, and double unban // TODO: Attempt a double ban, double activation, and double unban
err = user.Ban(duration, 1) err = user.Ban(duration, 1)
if err != nil { expectNilErr(t, err)
t.Error(err) expect(t, user.Group == config.DefaultGroup, fmt.Sprintf("Sam should be in group %d, not %d", config.DefaultGroup, user.Group))
}
expectIntToBeX(t, user.Group, config.DefaultGroup, "Sam should still be in the default group in this copy")
if hasCache { if hasCache {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(2) _, 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) 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 { if user.ID != newUserID {
t.Errorf("The UID of the user record should be %d", 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 // 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() err = user.Unban()
if err != nil { expectNilErr(t, err)
t.Error(err)
}
expectIntToBeX(t, user.Group, banGroup, "Sam should still be in the ban group in this copy") expectIntToBeX(t, user.Group, banGroup, "Sam should still be in the ban group in this copy")
if hasCache { if hasCache {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(newUserID) _, 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) 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 { expectIntToBeX(t, user.ID, newUserID, "The UID of the user record should be %d")
t.Errorf("The UID of the user record should be %d", newUserID)
} expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expectIntToBeX(t, user.Group, config.DefaultGroup, "Sam should be back in group "+strconv.Itoa(config.DefaultGroup)) 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() err = user.Delete()
if err != nil { expectNilErr(t, err)
t.Error(err) expect(t, !users.Exists(newUserID), fmt.Sprintf("UID #%d should no longer exist", newUserID))
}
expect(t, !users.Exists(newUserID), fmt.Sprintf("UID #%d should not longer exist", newUserID))
if hasCache { if hasCache {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(newUserID) _, 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 _, err = users.Get(newUserID)
//ResetTables() 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) { 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) { func expect(t *testing.T, item bool, errmsg string) {
if !item { if !item {
debug.PrintStack() debug.PrintStack()
t.Fatalf(errmsg) t.Fatal(errmsg)
} }
} }
@ -309,18 +463,10 @@ func topicStoreTest(t *testing.T) {
var err error var err error
_, err = topics.Get(-1) _, err = topics.Get(-1)
if err == nil { recordMustNotExist(t, err, "TID #-1 shouldn't exist")
t.Error("TID #-1 shouldn't exist")
} else if err != ErrNoRows {
t.Fatal(err)
}
_, err = topics.Get(0) _, err = topics.Get(0)
if err == nil { recordMustNotExist(t, err, "TID #0 shouldn't exist")
t.Error("TID #0 shouldn't exist")
} else if err != ErrNoRows {
t.Fatal(err)
}
topic, err = topics.Get(1) topic, err = topics.Get(1)
recordMustExist(t, err, "Couldn't find TID #1") recordMustExist(t, err, "Couldn't find TID #1")
@ -361,24 +507,13 @@ func TestForumStore(t *testing.T) {
initPlugins() initPlugins()
} }
var forum *Forum _, err := fstore.Get(-1)
var err error recordMustNotExist(t, err, "FID #-1 shouldn't exist")
_, err = fstore.Get(-1) _, err = fstore.Get(0)
if err == nil { recordMustNotExist(t, err, "FID #0 shouldn't exist")
t.Error("FID #-1 shouldn't exist")
} else if err != ErrNoRows {
t.Fatal(err)
}
forum, err = fstore.Get(0) forum, err := fstore.Get(1)
if err == nil {
t.Error("FID #0 shouldn't exist")
} else if err != ErrNoRows {
t.Fatal(err)
}
forum, err = fstore.Get(1)
recordMustExist(t, err, "Couldn't find FID #1") recordMustExist(t, err, "Couldn't find FID #1")
if forum.ID != 1 { if forum.ID != 1 {
@ -417,50 +552,95 @@ func TestGroupStore(t *testing.T) {
initPlugins() initPlugins()
} }
var group *Group _, err := gstore.Get(-1)
var err error recordMustNotExist(t, err, "GID #-1 shouldn't exist")
_, err = gstore.Get(-1)
if err == nil {
t.Error("GID #-1 shouldn't exist")
} else if err != ErrNoRows {
t.Fatal(err)
}
// TODO: Refactor the group store to remove GID #0 // 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") recordMustExist(t, err, "Couldn't find GID #0")
if group.ID != 0 { if group.ID != 0 {
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)
}
if group.Name != "Unknown" {
t.Error("GID #0 is named '" + group.Name + "' and not 'Unknown'")
} }
expect(t, group.Name == "Unknown", fmt.Sprintf("GID #0 is named '%s' and not 'Unknown'", group.Name))
group, err = gstore.Get(1) group, err = gstore.Get(1)
recordMustExist(t, err, "Couldn't find GID #1") recordMustExist(t, err, "Couldn't find GID #1")
if group.ID != 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) ok := gstore.Exists(-1)
if ok { expect(t, !ok, "GID #-1 shouldn't exist")
t.Error("GID #-1 shouldn't exist")
}
// 0 aka Unknown, for system posts and other oddities
ok = gstore.Exists(0) ok = gstore.Exists(0)
if !ok { expect(t, ok, "GID #0 should exist")
t.Error("GID #0 should exist")
}
ok = gstore.Exists(1) ok = gstore.Exists(1)
if !ok { expect(t, ok, "GID #1 should exist")
t.Error("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) { func TestReplyStore(t *testing.T) {
@ -471,28 +651,23 @@ func TestReplyStore(t *testing.T) {
initPlugins() initPlugins()
} }
reply, err := rstore.Get(-1) _, err := rstore.Get(-1)
if err == nil { recordMustNotExist(t, err, "RID #-1 shouldn't exist")
t.Error("RID #-1 shouldn't exist")
}
reply, err = rstore.Get(0) _, err = rstore.Get(0)
if err == nil { recordMustNotExist(t, err, "RID #0 shouldn't exist")
t.Error("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 { 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 { 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 { 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) _, err := prstore.Get(-1)
if err == nil { recordMustNotExist(t, err, "RID #-1 shouldn't exist")
t.Error("RID #-1 shouldn't exist")
}
_, err = prstore.Get(0) _, err = prstore.Get(0)
if err == nil { recordMustNotExist(t, err, "RID #0 shouldn't exist")
t.Error("RID #0 shouldn't exist")
}
} }
func TestSlugs(t *testing.T) { 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? */ /* No extra salt tests, we might not need this extra salt, as bcrypt has it's own? */
realPassword = "Madame Cassandra's Mystic Orb" 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") t.Log("Hashing the real password")
hashedPassword, err = BcryptGeneratePasswordNoSalt(realPassword) hashedPassword, err = BcryptGeneratePasswordNoSalt(realPassword)
if err != nil { if err != nil {

View File

@ -819,7 +819,10 @@ func routeUnban(w http.ResponseWriter, r *http.Request, user User) {
} }
err = targetUser.Unban() 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) LocalError("The user you're trying to unban no longer exists.", w, r, user)
return return
} else if err != nil { } 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, ":O", "😲", -1)
msg = strings.Replace(msg, ":p", "😛", -1) msg = strings.Replace(msg, ":p", "😛", -1)
msg = strings.Replace(msg, ":o", "😲", -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>") //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 // 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 lastItem = 0
var i = 0 var i = 0
for ; len(msgbytes) > (i + 1); i++ { for ; len(msgbytes) > (i + 1); i++ {
//log.Print("Index:",i) //log.Print("Index: ",i)
//log.Print("Index Item: ",msgbytes[i]) //log.Print("Index Item: ",msgbytes[i])
//log.Print("string(msgbytes[i]): ",string(msgbytes[i])) //log.Print("string(msgbytes[i]): ",string(msgbytes[i]))
//log.Print("End Index") //log.Print("End Index")
if (i == 0 && (msgbytes[0] > 32)) || ((msgbytes[i] < 33) && (msgbytes[i+1] > 32)) { if (i == 0 && (msgbytes[0] > 32)) || ((msgbytes[i] < 33) && (msgbytes[i+1] > 32)) {
//log.Print("IN") //log.Print("IN ",msgbytes[i])
//log.Print(msgbytes[i])
if (i != 0) || msgbytes[i] < 33 { if (i != 0) || msgbytes[i] < 33 {
i++ i++
} }
@ -509,12 +509,12 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
outbytes = append(outbytes, urlClose...) outbytes = append(outbytes, urlClose...)
lastItem = i lastItem = i
//log.Print("string(msgbytes) ",string(msgbytes)) //log.Print("string(msgbytes): ",string(msgbytes))
//log.Print("msgbytes ",msgbytes) //log.Print("msgbytes: ",msgbytes)
//log.Print(msgbytes[lastItem - 1]) //log.Print("msgbytes[lastItem - 1]: ",msgbytes[lastItem - 1])
//log.Print(lastItem - 1) //log.Print("lastItem - 1: ",lastItem - 1)
//log.Print(msgbytes[lastItem]) //log.Print("msgbytes[lastItem]: ",msgbytes[lastItem])
//log.Print("lastItem ",lastItem) //log.Print("lastItem: ",lastItem)
} else if bytes.Equal(msgbytes[i+1:i+5], []byte("rid-")) { } else if bytes.Equal(msgbytes[i+1:i+5], []byte("rid-")) {
outbytes = append(outbytes, msgbytes[lastItem:i]...) outbytes = append(outbytes, msgbytes[lastItem:i]...)
i += 5 i += 5
@ -589,10 +589,10 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
//log.Print(string(msgbytes)) //log.Print(string(msgbytes))
//log.Print(msgbytes) //log.Print(msgbytes)
//log.Print(msgbytes[lastItem - 1]) //log.Print("msgbytes[lastItem - 1]: ", msgbytes[lastItem - 1])
//log.Print("lastItem - 1",lastItem - 1) //log.Print("lastItem - 1: ", lastItem - 1)
//log.Print("msgbytes[lastItem]",msgbytes[lastItem]) //log.Print("msgbytes[lastItem]: ", msgbytes[lastItem])
//log.Print("lastItem",lastItem) //log.Print("lastItem: ", lastItem)
} else if msgbytes[i] == 'h' || msgbytes[i] == 'f' || msgbytes[i] == 'g' { } else if msgbytes[i] == 'h' || msgbytes[i] == 'f' || msgbytes[i] == 'g' {
//log.Print("IN hfg") //log.Print("IN hfg")
if msgbytes[i+1] == 't' && msgbytes[i+2] == 't' && msgbytes[i+3] == 'p' { 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:]) urlLen := partialURLBytesLen(msgbytes[i:])
if msgbytes[i+urlLen] > 32 { // space and invisibles if msgbytes[i+urlLen] > 32 { // space and invisibles
//log.Print("INVALID URL") //log.Print("INVALID URL")
//log.Print("msgbytes[i+urlLen]", msgbytes[i+urlLen]) //log.Print("msgbytes[i+urlLen]: ", msgbytes[i+urlLen])
//log.Print("string(msgbytes[i+urlLen])", string(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("msgbytes[i:i+urlLen]: ", msgbytes[i:i+urlLen])
//log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen])) //log.Print("string(msgbytes[i:i+urlLen]): ", string(msgbytes[i:i+urlLen]))
outbytes = append(outbytes, invalidURL...) outbytes = append(outbytes, invalidURL...)
i += urlLen i += urlLen
continue continue
@ -673,18 +673,18 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
urlLen := partialURLBytesLen(msgbytes[i:]) urlLen := partialURLBytesLen(msgbytes[i:])
if msgbytes[i+urlLen] > 32 { // space and invisibles if msgbytes[i+urlLen] > 32 { // space and invisibles
//log.Print("INVALID URL") //log.Print("INVALID URL")
//log.Print("msgbytes[i+urlLen]", msgbytes[i+urlLen]) //log.Print("msgbytes[i+urlLen]: ", msgbytes[i+urlLen])
//log.Print("string(msgbytes[i+urlLen])", string(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("msgbytes[i:i+urlLen]: ", msgbytes[i:i+urlLen])
//log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen])) //log.Print("string(msgbytes[i:i+urlLen]): ", string(msgbytes[i:i+urlLen]))
outbytes = append(outbytes, invalidURL...) outbytes = append(outbytes, invalidURL...)
i += urlLen i += urlLen
continue continue
} }
//log.Print("VALID URL") //log.Print("VALID URL")
//log.Print("msgbytes[i:i+urlLen]", 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])) //log.Print("string(msgbytes[i:i+urlLen]): ", string(msgbytes[i:i+urlLen]))
media, ok := parseMediaBytes(msgbytes[i : i+urlLen]) media, ok := parseMediaBytes(msgbytes[i : i+urlLen])
if !ok { if !ok {
outbytes = append(outbytes, invalidURL...) outbytes = append(outbytes, invalidURL...)
@ -733,10 +733,10 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
} }
if lastItem != i && len(outbytes) != 0 { if lastItem != i && len(outbytes) != 0 {
//log.Print("lastItem: ",msgbytes[lastItem]) //log.Print("lastItem: ", msgbytes[lastItem])
//log.Print("lastItem index: ",lastItem) //log.Print("lastItem index: ", lastItem)
//log.Print("i: ",i) //log.Print("i: ", i)
//log.Print("lastItem to end: ",msgbytes[lastItem:]) //log.Print("lastItem to end: ", msgbytes[lastItem:])
//log.Print("-----") //log.Print("-----")
calclen := len(msgbytes) - 10 calclen := len(msgbytes) - 10
if calclen <= lastItem { if calclen <= lastItem {
@ -756,7 +756,7 @@ func parseMessage(msg string, sectionID int, sectionType string /*, user User*/)
} }
// TODO: Write a test for this // TODO: Write a test for this
func regexParseMessage(msg string) string { /*func regexParseMessage(msg string) string {
msg = strings.Replace(msg, ":)", "😀", -1) msg = strings.Replace(msg, ":)", "😀", -1)
msg = strings.Replace(msg, ":D", "😃", -1) msg = strings.Replace(msg, ":D", "😃", -1)
msg = strings.Replace(msg, ":P", "😛", -1) msg = strings.Replace(msg, ":P", "😛", -1)
@ -766,7 +766,7 @@ func regexParseMessage(msg string) string {
msg = runSshook("parse_assign", msg) msg = runSshook("parse_assign", msg)
} }
return msg return msg
} }*/
// 6, 7, 8, 6, 2, 7 // 6, 7, 8, 6, 2, 7
// ftp://, http://, https:// git://, //, mailto: (not a URL, just here for length comparison purposes) // ftp://, http://, https:// git://, //, mailto: (not a URL, just here for length comparison purposes)

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

View File

@ -1,9 +1,16 @@
package main package main
import "log" import (
import "sync" "database/sql"
import "strconv" "encoding/json"
import "encoding/json" "log"
"strconv"
"sync"
"./query_gen/lib"
)
// TODO: Refactor the perms system
var permUpdateMutex sync.Mutex var permUpdateMutex sync.Mutex
var BlankPerms Perms var BlankPerms Perms
@ -261,10 +268,18 @@ func presetToPermmap(preset string) (out map[string]ForumPerms) {
} }
func permmapToQuery(permmap map[string]ForumPerms, fid int) error { func permmapToQuery(permmap map[string]ForumPerms, fid int) error {
permUpdateMutex.Lock() tx, err := db.Begin()
defer permUpdateMutex.Unlock() 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 { if err != nil {
return err return err
} }
@ -273,7 +288,16 @@ func permmapToQuery(permmap map[string]ForumPerms, fid int) error {
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -282,7 +306,15 @@ func permmapToQuery(permmap map[string]ForumPerms, fid int) error {
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -291,23 +323,77 @@ func permmapToQuery(permmap map[string]ForumPerms, fid int) error {
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
perms, err = json.Marshal(permmap["guests"]) // 6 is the ID of the Not Loggedin Group
if err != nil { // TODO: Use a shared variable rather than a literal for the group ID
return err err = replaceForumPermsForGroupTx(tx, 6, map[int]string{fid: ""}, map[int]ForumPerms{fid: permmap["guests"]})
}
_, err = addForumPermsToGroupStmt.Exec(6, fid, "", perms)
if err != nil { if err != nil {
return err return err
} }
err = tx.Commit()
if err != nil {
return err
}
permUpdateMutex.Lock()
defer permUpdateMutex.Unlock()
return rebuildForumPermissions(fid) 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? // TODO: Need a more thread-safe way of doing this. Possibly with sync.Map?
func rebuildForumPermissions(fid int) error { func rebuildForumPermissions(fid int) error {
if dev.DebugMode { if dev.DebugMode {

View File

@ -2,30 +2,27 @@ package main
//import "fmt" //import "fmt"
import ( import (
"regexp" "log"
"strings" "strings"
) )
var markdownMaxDepth = 25 // How deep the parser will go when parsing Markdown strings var markdownMaxDepth = 25 // How deep the parser will go when parsing Markdown strings
var markdownUnclosedElement []byte var markdownUnclosedElement []byte
var markdownBoldTagOpen, markdownBoldTagClose []byte var markdownBoldTagOpen []byte
var markdownItalicTagOpen, markdownItalicTagClose []byte var markdownBoldTagClose []byte
var markdownUnderlineTagOpen, markdownUnderlineTagClose []byte var markdownItalicTagOpen []byte
var markdownStrikeTagOpen, markdownStrikeTagClose []byte var markdownItalicTagClose []byte
var markdownUnderlineTagOpen []byte
var markdownBoldItalic *regexp.Regexp var markdownUnderlineTagClose []byte
var markdownBold *regexp.Regexp var markdownStrikeTagOpen []byte
var markdownItalic *regexp.Regexp var markdownStrikeTagClose []byte
var markdownStrike *regexp.Regexp
var markdownUnderline *regexp.Regexp
func init() { func init() {
plugins["markdown"] = NewPlugin("markdown", "Markdown", "Azareal", "http://github.com/Azareal", "", "", "", initMarkdown, nil, deactivateMarkdown, nil, nil) plugins["markdown"] = NewPlugin("markdown", "Markdown", "Azareal", "http://github.com/Azareal", "", "", "", initMarkdown, nil, deactivateMarkdown, nil, nil)
} }
func initMarkdown() error { func initMarkdown() error {
//plugins["markdown"].AddHook("parse_assign", markdownRegexParse)
plugins["markdown"].AddHook("parse_assign", markdownParse) plugins["markdown"].AddHook("parse_assign", markdownParse)
markdownUnclosedElement = []byte("<span style='color: red;'>[Unclosed Element]</span>") markdownUnclosedElement = []byte("<span style='color: red;'>[Unclosed Element]</span>")
@ -38,35 +35,19 @@ func initMarkdown() error {
markdownUnderlineTagClose = []byte("</u>") markdownUnderlineTagClose = []byte("</u>")
markdownStrikeTagOpen = []byte("<s>") markdownStrikeTagOpen = []byte("<s>")
markdownStrikeTagClose = []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 return nil
} }
func deactivateMarkdown() { func deactivateMarkdown() {
//plugins["markdown"].RemoveHook("parse_assign", markdownRegexParse)
plugins["markdown"].RemoveHook("parse_assign", markdownParse) 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. // 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. // 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 { 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! // Under Construction!
@ -77,16 +58,14 @@ func _markdownParse(msg string, n int) string {
var outbytes []byte var outbytes []byte
var lastElement int var lastElement int
//log.Print("enter message loop") log.Printf("Initial Message: %+v\n", strings.Replace(msg, "\r", "\\r", -1))
//log.Print("Message: %v\n",strings.Replace(msg,"\r","\\r",-1))
for index := 0; index < len(msg); index++ { for index := 0; index < len(msg); index++ {
/*//log.Print("--OUTER MARKDOWN LOOP START--") //log.Print("--OUTER MARKDOWN LOOP START--")
//log.Print("index",index) //log.Print("index: ", index)
//log.Print("msg[index]",msg[index]) //log.Print("msg[index]: ", msg[index])
//log.Print("string(msg[index])",string(msg[index])) //log.Print("string(msg[index]): ", string(msg[index]))
//log.Print("--OUTER MARKDOWN LOOP END--") //log.Printf("--OUTER MARKDOWN LOOP END--\n\n")
//log.Print(" ")*/
switch msg[index] { switch msg[index] {
// TODO: Do something slightly less hacky for skipping URLs // TODO: Do something slightly less hacky for skipping URLs
@ -106,7 +85,7 @@ func _markdownParse(msg string, n int) string {
index++ index++
index = markdownSkipUntilChar(msg, index, '_') index = markdownSkipUntilChar(msg, index, '_')
if (index-(startIndex+1)) < 2 || index >= len(msg) { if (index-(startIndex+1)) < 1 || index >= len(msg) {
break break
} }
@ -116,7 +95,8 @@ func _markdownParse(msg string, n int) string {
outbytes = append(outbytes, msg[lastElement:startIndex]...) outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnderlineTagOpen...) 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...) outbytes = append(outbytes, markdownUnderlineTagClose...)
lastElement = index lastElement = index
@ -129,7 +109,7 @@ func _markdownParse(msg string, n int) string {
index++ index++
index = markdownSkipUntilChar(msg, index, '~') index = markdownSkipUntilChar(msg, index, '~')
if (index-(startIndex+1)) < 2 || index >= len(msg) { if (index-(startIndex+1)) < 1 || index >= len(msg) {
break break
} }
@ -139,28 +119,28 @@ func _markdownParse(msg string, n int) string {
outbytes = append(outbytes, msg[lastElement:startIndex]...) outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownStrikeTagOpen...) 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...) outbytes = append(outbytes, markdownStrikeTagClose...)
lastElement = index lastElement = index
index-- index--
case '*': case '*':
//log.Print("------") //log.Print("------")
//log.Print("[]byte(msg):",[]byte(msg)) //log.Print("[]byte(msg): ", []byte(msg))
//log.Print("len(msg)",len(msg)) //log.Print("len(msg): ", len(msg))
//log.Print("start index",index) //log.Print("start index: ", index)
//log.Print("start msg[index]",msg[index]) //log.Print("start msg[index]: ", msg[index])
//log.Print("start string(msg[index])",string(msg[index])) //log.Print("start string(msg[index]): ", string(msg[index]))
//log.Print("start []byte(msg[:index])",[]byte(msg[:index])) //log.Print("start []byte(msg[:index]): ", []byte(msg[:index]))
var startIndex = index var startIndex = index
var italic = true var italic = true
var bold bool var bold = false
if (index + 2) < len(msg) { if (index + 2) < len(msg) {
//log.Print("start index + 1",index + 1) //log.Print("start index + 1: ", index + 1)
//log.Print("start msg[index]",msg[index + 1]) //log.Print("start msg[index]: ", msg[index + 1])
//log.Print("start string(msg[index])",string(msg[index + 1])) //log.Print("start string(msg[index]): ", string(msg[index + 1]))
if msg[index+1] == '*' { if msg[index+1] == '*' {
//log.Print("two asterisks") //log.Print("two asterisks")
bold = true bold = true
@ -174,16 +154,16 @@ func _markdownParse(msg string, n int) string {
} }
} }
//log.Print("lastElement",lastElement) //log.Print("lastElement: ", lastElement)
//log.Print("startIndex:",startIndex) //log.Print("startIndex: ", startIndex)
//log.Print("msg[startIndex]",msg[startIndex]) //log.Print("msg[startIndex]: ", msg[startIndex])
//log.Print("string(msg[startIndex])",string(msg[startIndex])) //log.Print("string(msg[startIndex]): ", string(msg[startIndex]))
//log.Print("preabrupt index",index) //log.Print("preabrupt index: ", index)
//log.Print("preabrupt msg[index]",msg[index]) //log.Print("preabrupt msg[index]: ", msg[index])
//log.Print("preabrupt string(msg[index])",string(msg[index])) //log.Print("preabrupt string(msg[index]): ", string(msg[index]))
//log.Print("preabrupt []byte(msg[:index])",[]byte(msg[:index])) //log.Print("preabrupt []byte(msg[:index]): ", []byte(msg[:index]))
//log.Print("preabrupt msg[:index]",msg[:index]) //log.Print("preabrupt msg[:index]: ", msg[:index])
// Does the string terminate abruptly? // Does the string terminate abruptly?
if (index + 1) >= len(msg) { if (index + 1) >= len(msg) {
@ -195,16 +175,15 @@ func _markdownParse(msg string, n int) string {
//log.Print("preskip index",index) //log.Print("preskip index",index)
//log.Print("preskip msg[index]",msg[index]) //log.Print("preskip msg[index]",msg[index])
//log.Print("preskip string(msg[index])",string(msg[index])) //log.Print("preskip string(msg[index])",string(msg[index]))
index = markdownSkipUntilAsterisk(msg, index) index = markdownSkipUntilAsterisk(msg, index)
if index >= len(msg) { if index >= len(msg) {
break break
} }
//log.Print("index",index) //log.Print("index: ", index)
//log.Print("[]byte(msg[:index])",[]byte(msg[:index])) //log.Print("[]byte(msg[:index]): ", []byte(msg[:index]))
//log.Print("msg[index]",msg[index]) //log.Print("msg[index]: ", msg[index])
sIndex := startIndex sIndex := startIndex
lIndex := index lIndex := index
@ -213,7 +192,7 @@ func _markdownParse(msg string, n int) string {
if (index + 3) >= len(msg) { if (index + 3) >= len(msg) {
//log.Print("unclosed markdown element @ exit element") //log.Print("unclosed markdown element @ exit element")
outbytes = append(outbytes, msg[lastElement:startIndex]...) outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnclosedElement...) //outbytes = append(outbytes, markdownUnclosedElement...)
lastElement = startIndex lastElement = startIndex
break break
} }
@ -224,7 +203,7 @@ func _markdownParse(msg string, n int) string {
if (index + 2) >= len(msg) { if (index + 2) >= len(msg) {
//log.Print("true unclosed markdown element @ exit element") //log.Print("true unclosed markdown element @ exit element")
outbytes = append(outbytes, msg[lastElement:startIndex]...) outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnclosedElement...) //outbytes = append(outbytes, markdownUnclosedElement...)
lastElement = startIndex lastElement = startIndex
break break
} }
@ -235,7 +214,7 @@ func _markdownParse(msg string, n int) string {
if (index + 1) >= len(msg) { if (index + 1) >= len(msg) {
//log.Print("true unclosed markdown element @ exit element") //log.Print("true unclosed markdown element @ exit element")
outbytes = append(outbytes, msg[lastElement:startIndex]...) outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnclosedElement...) //outbytes = append(outbytes, markdownUnclosedElement...)
lastElement = startIndex lastElement = startIndex
break break
} }
@ -249,7 +228,7 @@ func _markdownParse(msg string, n int) string {
if lIndex <= sIndex { if lIndex <= sIndex {
//log.Print("unclosed markdown element @ lIndex <= sIndex") //log.Print("unclosed markdown element @ lIndex <= sIndex")
outbytes = append(outbytes, msg[lastElement:startIndex]...) outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnclosedElement...) //outbytes = append(outbytes, markdownUnclosedElement...)
lastElement = startIndex lastElement = startIndex
break break
} }
@ -257,7 +236,7 @@ func _markdownParse(msg string, n int) string {
if sIndex < 0 || lIndex < 0 { if sIndex < 0 || lIndex < 0 {
//log.Print("unclosed markdown element @ sIndex < 0 || lIndex < 0") //log.Print("unclosed markdown element @ sIndex < 0 || lIndex < 0")
outbytes = append(outbytes, msg[lastElement:startIndex]...) outbytes = append(outbytes, msg[lastElement:startIndex]...)
outbytes = append(outbytes, markdownUnclosedElement...) //outbytes = append(outbytes, markdownUnclosedElement...)
lastElement = startIndex lastElement = startIndex
break break
} }
@ -285,7 +264,8 @@ func _markdownParse(msg string, n int) string {
outbytes = append(outbytes, markdownItalicTagOpen...) 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 { if italic {
outbytes = append(outbytes, markdownItalicTagClose...) outbytes = append(outbytes, markdownItalicTagClose...)
@ -296,13 +276,18 @@ func _markdownParse(msg string, n int) string {
lastElement = index lastElement = index
index-- index--
case '\\':
if (index + 1) < len(msg) {
outbytes = append(outbytes, msg[lastElement:index]...)
index++
lastElement = index
}
//case '`': //case '`':
//case '_': //case '_':
//case '~': //case '~':
//case 10: // newline //case 10: // newline
} }
} }
//log.Print("exit message loop") //log.Print("exit message loop")
if len(outbytes) == 0 { 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, "[s]hi[/s]", "<s>hi</s>")
msgList = addMEPair(msgList, "[c]hi[/c]", "[c]hi[/c]") msgList = addMEPair(msgList, "[c]hi[/c]", "[c]hi[/c]")
if !testing.Short() { if !testing.Short() {
msgList = addMEPair(msgList, "[b]hi[/i]", "[b]hi[/i]") //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]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</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]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[/b][/code]", "<span class='codequotes'>[b]hi[/b]</span>")
msgList = addMEPair(msgList, "[code][b]hi[/code][/b]", "<span class='codequotes'>[b]hi</span>[/b]") 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") t.Log("Testing bbcodeFullParse")
for _, item := range msgList { for _, item := range msgList {
t.Log("Testing string '" + item.Msg + "'")
res = bbcodeFullParse(item.Msg) res = bbcodeFullParse(item.Msg)
if res != item.Expects { if res != item.Expects {
t.Error("Testing string '" + item.Msg + "'")
t.Error("Bad output:", "'"+res+"'") t.Error("Bad output:", "'"+res+"'")
t.Error("Expected:", item.Expects) t.Error("Expected:", item.Expects)
} }
@ -167,7 +167,7 @@ func TestBBCodeRender(t *testing.T) {
t.Log("Testing string '" + msg + "'") t.Log("Testing string '" + msg + "'")
res = bbcodeFullParse(msg) res = bbcodeFullParse(msg)
conv, err = strconv.Atoi(res) 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("Bad output:", "'"+res+"'")
t.Error("Expected a number between 0 and 18446744073709551615") t.Error("Expected a number between 0 and 18446744073709551615")
} }
@ -175,7 +175,7 @@ func TestBBCodeRender(t *testing.T) {
t.Log("Testing string '" + msg + "'") t.Log("Testing string '" + msg + "'")
res = bbcodeFullParse(msg) res = bbcodeFullParse(msg)
conv, err = strconv.Atoi(res) 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("Bad output:", "'"+res+"'")
t.Error("Expected a number between 0 and 170141183460469231731687303715884105727") t.Error("Expected a number between 0 and 170141183460469231731687303715884105727")
} }
@ -193,19 +193,42 @@ func TestBBCodeRender(t *testing.T) {
func TestMarkdownRender(t *testing.T) { func TestMarkdownRender(t *testing.T) {
//t.Skip() //t.Skip()
err := initMarkdown()
if err != nil {
t.Fatal(err)
}
var res string var res string
var msgList []MEPair var msgList []MEPair
msgList = addMEPair(msgList, "hi", "hi") msgList = addMEPair(msgList, "hi", "hi")
msgList = addMEPair(msgList, "**h**", "<b>h</b>")
msgList = addMEPair(msgList, "**hi**", "<b>hi</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, "_hi_", "<u>hi</u>")
msgList = addMEPair(msgList, "*h*", "<i>h</i>")
msgList = addMEPair(msgList, "*hi*", "<i>hi</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~", "<s>hi</s>")
msgList = addMEPair(msgList, "*hi**", "<i>hi</i>*") msgList = addMEPair(msgList, "*hi**", "<i>hi</i>*")
msgList = addMEPair(msgList, "**hi***", "<b>hi</b>*") msgList = addMEPair(msgList, "**hi***", "<b>hi</b>*")
msgList = addMEPair(msgList, "**hi*", "*<i>hi</i>") 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, "\\*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~*", "<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, "***", "***") 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>") msgList = addMEPair(msgList, "*** ***", "<b><i> </i></b>")
for _, item := range msgList { for _, item := range msgList {
t.Log("Testing string '" + item.Msg + "'")
res = markdownParse(item.Msg) res = markdownParse(item.Msg)
if res != item.Expects { if res != item.Expects {
t.Error("Testing string '" + item.Msg + "'")
t.Error("Bad output:", "'"+res+"'") t.Error("Bad output:", "'"+res+"'")
//t.Error("Ouput in bytes:", []byte(res))
t.Error("Expected:", item.Expects) t.Error("Expected:", item.Expects)
} }
} }

View File

@ -36,7 +36,6 @@ function load_alerts(menu_alerts)
} }
var alist = ""; var alist = "";
var anyAvatar = false
for(var i in data.msgs) { for(var i in data.msgs) {
var msg = data.msgs[i]; var msg = data.msgs[i];
var mmsg = msg.msg; var mmsg = msg.msg;
@ -44,15 +43,13 @@ function load_alerts(menu_alerts)
if("sub" in msg) { if("sub" in msg) {
for(var i = 0; i < msg.sub.length; i++) { for(var i = 0; i < msg.sub.length; i++) {
mmsg = mmsg.replace("\{"+i+"\}", msg.sub[i]); mmsg = mmsg.replace("\{"+i+"\}", msg.sub[i]);
//console.log("Sub #" + i); //console.log("Sub #" + i + ":",msg.sub[i]);
//console.log(msg.sub[i]);
} }
} }
if("avatar" in msg) { 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>"; 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>"); 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 { } else {
alist += "<div class='alertItem'><a href=\""+msg.path+"\" class='text'>"+mmsg+"</a></div>"; 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>"); 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>"; 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; alertListNode.innerHTML = alist;
if(data.msgCount != 0 && data.msgCount != undefined) { if(data.msgCount != 0 && data.msgCount != undefined) {
alertCounterNode.textContent = data.msgCount; alertCounterNode.textContent = data.msgCount;
menu_alerts.classList.add("has_alerts"); menu_alerts.classList.add("has_alerts");
@ -88,6 +82,7 @@ function load_alerts(menu_alerts)
console.log(magic.responseText); console.log(magic.responseText);
console.log(err); console.log(err);
} }
console.log("error: ",error);
alertListNode.innerHTML = "<div class='alertItem'>"+errtxt+"</div>"; alertListNode.innerHTML = "<div class='alertItem'>"+errtxt+"</div>";
} }
}); });
@ -176,8 +171,7 @@ $(document).ready(function(){
var messages = event.data.split('\r'); var messages = event.data.split('\r');
for(var i = 0; i < messages.length; i++) { for(var i = 0; i < messages.length; i++) {
//console.log("Message:"); //console.log("Message: ",messages[i]);
//console.log(messages[i]);
if(messages[i].startsWith("set ")) { if(messages[i].startsWith("set ")) {
//msgblocks = messages[i].split(' ',3); //msgblocks = messages[i].split(' ',3);
let msgblocks = SplitN(messages[i]," ",3); let msgblocks = SplitN(messages[i]," ",3);
@ -214,10 +208,10 @@ $(document).ready(function(){
let topicStatusInput = $('.topic_status_input').val(); let topicStatusInput = $('.topic_status_input').val();
let topicContentInput = $('.topic_content_input').val(); let topicContentInput = $('.topic_content_input').val();
let formAction = this.form.getAttribute("action"); let formAction = this.form.getAttribute("action");
//console.log("New Topic Name: " + topicNameInput); //console.log("New Topic Name: ", topicNameInput);
//console.log("New Topic Status: " + topicStatusInput); //console.log("New Topic Status: ", topicStatusInput);
//console.log("New Topic Content: " + topicContentInput); //console.log("New Topic Content: ", topicContentInput);
//console.log("Form Action: " + formAction); //console.log("Form Action: ", formAction);
$.ajax({ $.ajax({
url: formAction, url: formAction,
type: "POST", type: "POST",
@ -253,7 +247,7 @@ $(document).ready(function(){
block.html(newContent); block.html(newContent);
var formAction = $(this).closest('a').attr("href"); 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 } $.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); 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>"); 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(); event.preventDefault();
let blockParent = $(this).closest('.editable_parent'); let blockParent = $(this).closest('.editable_parent');
let block = blockParent.find('.editable_block').eq(0); let block = blockParent.find('.editable_block').eq(0);
@ -275,12 +268,12 @@ $(document).ready(function(){
block.html(newContent); block.html(newContent);
let formAction = $(this).closest('a').attr("href"); let formAction = $(this).closest('a').attr("href");
//console.log("Form Action: " + formAction); //console.log("Form Action:", formAction);
$.ajax({ $.ajax({
url: formAction + "?session=" + session, url: formAction + "?session=" + session,
type: "POST", type: "POST",
dataType: "json", 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']; else var it = ['No','Yes'];
var itLen = it.length; var itLen = it.length;
var out = ""; var out = "";
//console.log("Field Name '" + field_name + "'") //console.log("Field Name:",field_name);
//console.log("Field Type",field_type) //console.log("Field Type:",field_type);
//console.log("Field Value '" + field_value + "'") //console.log("Field Value:",field_value);
for (var i = 0; i < itLen; i++) { for (var i = 0; i < itLen; i++) {
var sel = ""; var sel = "";
if(field_value == i || field_value == it[i]) { if(field_value == i || field_value == it[i]) {
@ -332,7 +325,7 @@ $(document).ready(function(){
//console.log("running .submit_edit event"); //console.log("running .submit_edit event");
var out_data = {isJs: "1"} var out_data = {isJs: "1"}
var block_parent = $(this).closest('.editable_parent'); 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_name = this.getAttribute("data-field");
var field_type = this.getAttribute("data-type"); var field_type = this.getAttribute("data-type");
if(field_type=="list") { if(field_type=="list") {
@ -350,7 +343,7 @@ $(document).ready(function(){
}); });
var form_action = $(this).closest('a').attr("href"); var form_action = $(this).closest('a').attr("href");
//console.log("Form Action: " + form_action); //console.log("Form Action:", form_action);
//console.log(out_data); //console.log(out_data);
$.ajax({ url: form_action + "?session=" + session, type:"POST", dataType:"json", data: out_data }); $.ajax({ url: form_action + "?session=" + session, type:"POST", dataType:"json", data: out_data });
block_parent.find('.hide_on_edit').show(); 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) 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) querystr += " ?" + strconv.Itoa(substituteCount)
case "function", "operator", "number": case "function", "operator", "number":
// TODO: Split the function case off to speed things up // 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()" { if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" {
token.Contents = "GETUTCDATE()" token.Contents = "GETDATE()"
} }
querystr += " " + token.Contents querystr += " " + token.Contents
case "column": case "column":
@ -800,12 +801,21 @@ func (adapter *Mssql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel
/* Select */ /* Select */
var substituteCount = 0 var substituteCount = 0
// Escape the column names, just in case we've used a reserved keyword for _, column := range processColumns(sel.Columns) {
var colslice = strings.Split(strings.TrimSpace(sel.Columns), ",") var source, alias string
for _, column := range colslice {
querystr += "[" + strings.TrimSpace(column) + "]," // Escape the column names, just in case we've used a reserved keyword
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 = querystr[0 : len(querystr)-1]
querystr += " FROM [" + sel.Table + "] " querystr += " FROM [" + sel.Table + "] "

View File

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

View File

@ -102,10 +102,9 @@ type DB_Adapter interface {
SimpleInsert(name string, table string, columns string, fields string) (string, error) SimpleInsert(name string, table string, columns string, fields string) (string, error)
// ! DEPRECATED // ! DEPRECATED
SimpleReplace(name string, table string, columns string, fields 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
// ! 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)
SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error)
SimpleUpdate(name string, table string, set 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) SimpleDelete(name string, table string, where string) (string, error)
Purge(name string, table 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 return err
} }
err = writeReplaces(adapter) /*err = writeReplaces(adapter)
if err != nil { if err != nil {
return err return err
} }
@ -83,7 +83,7 @@ func writeStatements(adapter qgen.DB_Adapter) error {
err = writeUpserts(adapter) err = writeUpserts(adapter)
if err != nil { if err != nil {
return err return err
} }*/
err = writeUpdates(adapter) err = writeUpdates(adapter)
if err != nil { if err != nil {
@ -356,8 +356,6 @@ func writeInserts(adapter qgen.DB_Adapter) error {
adapter.SimpleInsert("addTheme", "themes", "uname, default", "?,?") 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("addModlogEntry", "moderation_logs", "action, elementID, elementType, ipaddress, actorID, doneAt", "?,?,?,?,?,UTC_TIMESTAMP()")
adapter.SimpleInsert("addAdminlogEntry", "administration_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 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 = ?") _, err = adapter.SimpleUpsert("addForumPermsToGroup", "forums_permissions", "gid, fid, preset, permissions", "?,?,?,?", "gid = ? AND fid = ?")
if err != nil { if err != nil {
return err return err
@ -385,7 +384,7 @@ func writeUpserts(adapter qgen.DB_Adapter) (err error) {
} }
return nil return nil
} }*/
func writeUpdates(adapter qgen.DB_Adapter) error { func writeUpdates(adapter qgen.DB_Adapter) error {
adapter.SimpleUpdate("addRepliesToTopic", "topics", "postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()", "tid = ?") 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("updateUser", "users", "name = ?, email = ?, group = ?", "uid = ?")
adapter.SimpleUpdate("updateUserGroup", "users", "group = ?", "uid = ?")
adapter.SimpleUpdate("updateGroupPerms", "users_groups", "permissions = ?", "gid = ?") adapter.SimpleUpdate("updateGroupPerms", "users_groups", "permissions = ?", "gid = ?")
adapter.SimpleUpdate("updateGroupRank", "users_groups", "is_admin = ?, is_mod = ?, is_banned = ?", "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("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("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 = ?") adapter.SimpleDelete("deleteWordFilter", "word_filters", "wfid = ?")
@ -501,20 +502,20 @@ func writeSimpleCounts(adapter qgen.DB_Adapter) error {
} }
func writeInsertSelects(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_Insert{"forums_permissions", "gid, fid, preset, permissions", ""},
qgen.DB_Select{"users_groups", "gid, ? AS fid, ? AS preset, ? AS permissions", "is_admin = 1", "", ""}, 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_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", "", ""}, 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_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", "", ""}, 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 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 { } else {
page = 1 page = 1
} }
// TODO: Move this to *Forum
rows, err := getForumTopicsOffsetStmt.Query(fid, offset, config.ItemsPerPage) rows, err := getForumTopicsOffsetStmt.Query(fid, offset, config.ItemsPerPage)
if err != nil { if err != nil {
InternalError(err, w) InternalError(err, w)
@ -904,10 +906,11 @@ func routeRegisterSubmit(w http.ResponseWriter, r *http.Request, user User) {
return return
} }
var active, group int var active bool
var group int
switch headerLite.Settings["activation_type"] { switch headerLite.Settings["activation_type"] {
case 1: // Activate All case 1: // Activate All
active = 1 active = true
group = config.DefaultGroup group = config.DefaultGroup
default: // Anything else. E.g. Admin Activation or Email Activation. default: // Anything else. E.g. Admin Activation or Email Activation.
group = config.ActivationGroup 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() defer rows.Close()
var uid int var uid int
ucache, ok := users.(UserCache)
for rows.Next() { for rows.Next() {
err := rows.Scan(&uid) err := rows.Scan(&uid)
if err != nil { if err != nil {
return err 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 { if err != nil {
log.Print("Unable to replace the scheduled group")
return err 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() return rows.Err()
} }

View File

@ -105,87 +105,90 @@ if tmpl_forum_vars.CurrentUser.Perms.CreateTopic {
w.Write(forum_12) w.Write(forum_12)
w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID)))
w.Write(forum_13) w.Write(forum_13)
} else {
w.Write(forum_14) w.Write(forum_14)
} } else {
w.Write(forum_15) w.Write(forum_15)
} }
w.Write(forum_16) 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_20)
} }
w.Write(forum_21) w.Write(forum_21)
}
w.Write(forum_22)
if len(tmpl_forum_vars.ItemList) != 0 { if len(tmpl_forum_vars.ItemList) != 0 {
for _, item := range tmpl_forum_vars.ItemList { for _, item := range tmpl_forum_vars.ItemList {
w.Write(forum_22)
if item.Sticky {
w.Write(forum_23) w.Write(forum_23)
if item.Sticky {
w.Write(forum_24)
} else { } else {
if item.IsClosed { if item.IsClosed {
w.Write(forum_24)
}
}
w.Write(forum_25) w.Write(forum_25)
if item.Creator.Avatar != "" { }
}
w.Write(forum_26) w.Write(forum_26)
w.Write([]byte(item.Creator.Avatar)) if item.Creator.Avatar != "" {
w.Write(forum_27) w.Write(forum_27)
} w.Write([]byte(item.Creator.Avatar))
w.Write(forum_28) 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_34)
} }
w.Write(forum_35)
w.Write([]byte(strconv.Itoa(item.PostCount)))
w.Write(forum_36)
if item.Sticky { if item.Sticky {
w.Write(forum_35)
}
w.Write(forum_36)
w.Write([]byte(strconv.Itoa(item.PostCount)))
w.Write(forum_37) w.Write(forum_37)
w.Write([]byte(strconv.Itoa(item.LikeCount)))
w.Write(forum_38)
if item.Sticky {
w.Write(forum_39)
} else { } else {
if item.IsClosed { if item.IsClosed {
w.Write(forum_38)
}
}
w.Write(forum_39)
if item.LastUser.Avatar != "" {
w.Write(forum_40) 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(forum_42)
w.Write([]byte(item.LastUser.Link)) w.Write([]byte(item.LastUser.Avatar))
w.Write(forum_43) w.Write(forum_43)
w.Write([]byte(item.LastUser.Name)) }
w.Write(forum_44) w.Write(forum_44)
w.Write([]byte(item.RelativeLastReplyAt)) w.Write([]byte(item.LastUser.Link))
w.Write(forum_45) w.Write(forum_45)
w.Write([]byte(item.LastUser.Name))
w.Write(forum_46)
w.Write([]byte(item.RelativeLastReplyAt))
w.Write(forum_47)
} }
} else { } 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) w.Write(forum_48)
} if tmpl_forum_vars.CurrentUser.Perms.CreateTopic {
w.Write(forum_49) w.Write(forum_49)
} w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID)))
w.Write(forum_50) w.Write(forum_50)
}
w.Write(forum_51)
}
w.Write(forum_52)
w.Write(footer_0) w.Write(footer_0)
if len(tmpl_forum_vars.Header.Themes) != 0 { if len(tmpl_forum_vars.Header.Themes) != 0 {
for _, item := range tmpl_forum_vars.Header.Themes { for _, item := range tmpl_forum_vars.Header.Themes {

View File

@ -678,33 +678,38 @@ var forums_19 = []byte(`
var topics_0 = []byte(` var topics_0 = []byte(`
<main> <main>
<div class="rowblock rowhead"> <div class="rowblock rowhead topic_list_title_block">
<div class="rowitem topic_list_title`) <div class="rowitem topic_list_title`)
var topics_1 = []byte(` has_opt`) var topics_1 = []byte(` has_opt`)
var topics_2 = []byte(`"><h1>All Topics</h1></div> var topics_2 = []byte(`"><h1>All Topics</h1></div>
`) `)
var topics_3 = []byte(` 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_4 = []byte(`
var topics_5 = []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> <div style="clear: both;"></div>
`) `)
var topics_6 = []byte(` var topics_7 = []byte(`
</div> </div>
`) `)
var topics_7 = []byte(` var topics_8 = []byte(`
<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> <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="formrow topic_board_row real_first_child">
<div class="formitem"><select form="topic_create_form_form" id="topic_board_input" name="topic-board"> <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(`<option `)
var topics_9 = []byte(`selected`) var topics_10 = []byte(`selected`)
var topics_10 = []byte(` value="`) var topics_11 = []byte(` value="`)
var topics_11 = []byte(`">`) var topics_12 = []byte(`">`)
var topics_12 = []byte(`</option>`) var topics_13 = []byte(`</option>`)
var topics_13 = []byte(` var topics_14 = []byte(`
</select></div> </select></div>
</div> </div>
<div class="formrow topic_name_row"> <div class="formrow topic_name_row">
@ -721,71 +726,73 @@ var topics_13 = []byte(`
<div class="formitem"> <div class="formitem">
<button form="topic_create_form_form" class="formbutton">Create Topic</button> <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;" /> <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> <label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>`) <div id="upload_file_dock"></div>`)
var topics_15 = []byte(` var topics_16 = []byte(`
<button class="formbutton close_form">Cancel</button> <button class="formbutton close_form">Cancel</button>
</div> </div>
</div> </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"> <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 `) <div class="rowitem topic_left passive datarow `)
var topics_18 = []byte(`topic_sticky`) var topics_19 = []byte(`topic_sticky`)
var topics_19 = []byte(`topic_closed`) var topics_20 = []byte(`topic_closed`)
var topics_20 = []byte(`"> var topics_21 = []byte(`">
<span class="selector"></span>
`) `)
var topics_21 = []byte(`<img src="`) var topics_22 = []byte(`<img src="`)
var topics_22 = []byte(`" height="64" />`) var topics_23 = []byte(`" height="64" />`)
var topics_23 = []byte(` var topics_24 = []byte(`
<span class="topic_inner_left"> <span class="topic_inner_left">
<a class="rowtopic" href="`) <a class="rowtopic" href="`)
var topics_24 = []byte(`">`) var topics_25 = []byte(`"><span>`)
var topics_25 = []byte(`</a> `) var topics_26 = []byte(`</span></a> `)
var topics_26 = []byte(`<a class="rowsmall parent_forum" href="`) var topics_27 = []byte(`<a class="rowsmall parent_forum" href="`)
var topics_27 = []byte(`">`) var topics_28 = []byte(`">`)
var topics_28 = []byte(`</a>`) var topics_29 = []byte(`</a>`)
var topics_29 = []byte(` var topics_30 = []byte(`
<br /><a class="rowsmall starter" href="`) <br /><a class="rowsmall starter" href="`)
var topics_30 = []byte(`">`) var topics_31 = []byte(`">`)
var topics_31 = []byte(`</a> 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_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(`<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>`)
var topics_34 = []byte(` var topics_35 = []byte(`
</span> </span>
<span class="topic_inner_right rowsmall" style="float: right;"> <span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">`) <span class="replyCount">`)
var topics_35 = []byte(` replies</span><br /> var topics_36 = []byte(`</span><br />
<span class="topicCount">x topics</span> <span class="likeCount">`)
var topics_37 = []byte(`</span>
</span> </span>
</div> </div>
<div class="rowitem topic_right passive datarow `) <div class="rowitem topic_right passive datarow `)
var topics_36 = []byte(`topic_sticky`) var topics_38 = []byte(`topic_sticky`)
var topics_37 = []byte(`topic_closed`) var topics_39 = []byte(`topic_closed`)
var topics_38 = []byte(`"> var topics_40 = []byte(`">
`) `)
var topics_39 = []byte(`<img src="`) var topics_41 = []byte(`<img src="`)
var topics_40 = []byte(`" height="64" />`) var topics_42 = []byte(`" height="64" />`)
var topics_41 = []byte(` var topics_43 = []byte(`
<span> <span>
<a href="`) <a href="`)
var topics_42 = []byte(`" class="lastName" style="font-size: 14px;">`) var topics_44 = []byte(`" class="lastName" style="font-size: 14px;">`)
var topics_43 = []byte(`</a><br> var topics_45 = []byte(`</a><br>
<span class="rowsmall lastReplyAt">Last: `) <span class="rowsmall lastReplyAt">`)
var topics_44 = []byte(`</span> var topics_46 = []byte(`</span>
</span> </span>
</div> </div>
</div>`) </div>`)
var topics_45 = []byte(`<div class="rowitem passive">There aren't any topics yet.`) var topics_47 = []byte(`<div class="rowitem passive">There aren't any topics yet.`)
var topics_46 = []byte(` <a href="/topics/create/">Start one?</a>`) var topics_48 = []byte(` <a href="/topics/create/">Start one?</a>`)
var topics_47 = []byte(`</div>`) var topics_49 = []byte(`</div>`)
var topics_48 = []byte(` var topics_50 = []byte(`
</div> </div>
</main> </main>
@ -803,7 +810,7 @@ var forum_8 = []byte(`
<main> <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`) <div class="rowitem forum_title`)
var forum_9 = []byte(` has_opt`) var forum_9 = []byte(` has_opt`)
var forum_10 = []byte(`"><h1>`) var forum_10 = []byte(`"><h1>`)
@ -811,21 +818,26 @@ var forum_11 = []byte(`</h1>
</div> </div>
`) `)
var forum_12 = []byte(` 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_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_14 = []byte(`
var forum_15 = []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> <div style="clear: both;"></div>
`) `)
var forum_16 = []byte(` var forum_17 = []byte(`
</div> </div>
`) `)
var forum_17 = []byte(` var forum_18 = []byte(`
<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> <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="`) <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="formrow topic_name_row real_first_child">
<div class="formitem"> <div class="formitem">
<input form="topic_create_form_form" name="topic-name" placeholder="Topic title" required> <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"> <div class="formitem">
<button form="topic_create_form_form" name="topic-button" class="formbutton">Create Topic</button> <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;" /> <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> <label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>`) <div id="upload_file_dock"></div>`)
var forum_20 = []byte(` var forum_21 = []byte(`
<button class="formbutton close_form">Cancel</button> <button class="formbutton close_form">Cancel</button>
</div> </div>
</div> </div>
</div> </div>
`) `)
var forum_21 = []byte(` var forum_22 = []byte(`
<div id="forum_topic_list" class="rowblock topic_list"> <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 `) <div class="rowitem topic_left passive datarow `)
var forum_23 = []byte(`topic_sticky`) var forum_24 = []byte(`topic_sticky`)
var forum_24 = []byte(`topic_closed`) var forum_25 = []byte(`topic_closed`)
var forum_25 = []byte(`"> var forum_26 = []byte(`">
<span class="selector"></span>
`) `)
var forum_26 = []byte(`<img src="`) var forum_27 = []byte(`<img src="`)
var forum_27 = []byte(`" height="64" />`) var forum_28 = []byte(`" height="64" />`)
var forum_28 = []byte(` var forum_29 = []byte(`
<span class="topic_inner_left"> <span class="topic_inner_left">
<a class="rowtopic" href="`) <a class="rowtopic" href="`)
var forum_29 = []byte(`">`) var forum_30 = []byte(`"><span>`)
var forum_30 = []byte(`</a> var forum_31 = []byte(`</span></a>
<br /><a class="rowsmall starter" href="`) <br /><a class="rowsmall starter" href="`)
var forum_31 = []byte(`">`) var forum_32 = []byte(`">`)
var forum_32 = []byte(`</a> 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_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(`<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>`)
var forum_35 = []byte(` var forum_36 = []byte(`
</span> </span>
<span class="topic_inner_right rowsmall" style="float: right;"> <span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">`) <span class="replyCount">`)
var forum_36 = []byte(` replies</span><br /> var forum_37 = []byte(`</span><br />
<span class="topicCount">x topics</span> <span class="likeCount">`)
var forum_38 = []byte(`</span>
</span> </span>
</div> </div>
<div class="rowitem topic_right passive datarow `) <div class="rowitem topic_right passive datarow `)
var forum_37 = []byte(`topic_sticky`) var forum_39 = []byte(`topic_sticky`)
var forum_38 = []byte(`topic_closed`) var forum_40 = []byte(`topic_closed`)
var forum_39 = []byte(`"> var forum_41 = []byte(`">
`) `)
var forum_40 = []byte(`<img src="`) var forum_42 = []byte(`<img src="`)
var forum_41 = []byte(`" height="64" />`) var forum_43 = []byte(`" height="64" />`)
var forum_42 = []byte(` var forum_44 = []byte(`
<span> <span>
<a href="`) <a href="`)
var forum_43 = []byte(`" class="lastName" style="font-size: 14px;">`) var forum_45 = []byte(`" class="lastName" style="font-size: 14px;">`)
var forum_44 = []byte(`</a><br> var forum_46 = []byte(`</a><br>
<span class="rowsmall lastReplyAt">Last: `) <span class="rowsmall lastReplyAt">`)
var forum_45 = []byte(`</span> var forum_47 = []byte(`</span>
</span> </span>
</div> </div>
</div>`) </div>`)
var forum_46 = []byte(`<div class="rowitem passive">There aren't any topics in this forum yet.`) var forum_48 = []byte(`<div class="rowitem passive">There aren't any topics in this forum yet.`)
var forum_47 = []byte(` <a href="/topics/create/`) var forum_49 = []byte(` <a href="/topics/create/`)
var forum_48 = []byte(`">Start one?</a>`) var forum_50 = []byte(`">Start one?</a>`)
var forum_49 = []byte(`</div>`) var forum_51 = []byte(`</div>`)
var forum_50 = []byte(` var forum_52 = []byte(`
</div> </div>
</main> </main>

View File

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

View File

@ -6,18 +6,22 @@
<main> <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 class="rowitem forum_title{{if ne .CurrentUser.ID 0}} has_opt{{end}}"><h1>{{.Title}}</h1>
</div> </div>
{{if ne .CurrentUser.ID 0}} {{if ne .CurrentUser.ID 0}}
{{if .CurrentUser.Perms.CreateTopic}} {{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> <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>
{{else}}<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>{{end}} {{/** 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> <div style="clear: both;"></div>
{{end}} {{end}}
</div> </div>
{{if .CurrentUser.Perms.CreateTopic}} {{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> <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"> <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"> <div class="formrow topic_name_row real_first_child">
@ -45,24 +49,25 @@
<div id="forum_topic_list" class="rowblock topic_list"> <div id="forum_topic_list" class="rowblock topic_list">
{{range .ItemList}}<div class="topic_row"> {{range .ItemList}}<div class="topic_row">
<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}"> <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}} {{if .Creator.Avatar}}<img src="{{.Creator.Avatar}}" height="64" />{{end}}
<span class="topic_inner_left"> <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> <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 **/}} {{/** 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 .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}} {{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>{{end}}
</span> </span>
<span class="topic_inner_right rowsmall" style="float: right;"> <span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">{{.PostCount}} replies</span><br /> <span class="replyCount">{{.PostCount}}</span><br />
<span class="topicCount">x topics</span> <span class="likeCount">{{.LikeCount}}</span>
</span> </span>
</div> </div>
<div class="rowitem topic_right passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}"> <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}} {{if .LastUser.Avatar}}<img src="{{.LastUser.Avatar}}" height="64" />{{end}}
<span> <span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;">{{.LastUser.Name}}</a><br> <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> </span>
</div> </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}} </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" . }} {{template "header.html" . }}
<main> <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> <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 ne .CurrentUser.ID 0}}
{{if .ForumList}} {{if .ForumList}}
<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>
{{else}}<div class="opt locked_opt" title="You don't have the permissions needed to create a topic"><a></a></div>{{end}} {{/** 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> <div style="clear: both;"></div>
{{end}} {{end}}
</div> </div>
{{if ne .CurrentUser.ID 0}} {{if ne .CurrentUser.ID 0}}
{{if .ForumList}} {{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> <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="formrow topic_board_row real_first_child">
<div class="formitem"><select form="topic_create_form_form" id="topic_board_input" name="topic-board"> <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"> <div id="topic_list" class="rowblock topic_list" aria-label="A list containing topics from every forum">
{{range .TopicList}}<div class="topic_row"> {{range .TopicList}}<div class="topic_row">
<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}"> <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}} {{if .Creator.Avatar}}<img src="{{.Creator.Avatar}}" height="64" />{{end}}
<span class="topic_inner_left"> <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> <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 **/}} {{/** 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 .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}} {{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="Status: Pinned"> | &#x1F4CD;&#xFE0E</span>{{end}}
</span> </span>
<span class="topic_inner_right rowsmall" style="float: right;"> <span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">{{.PostCount}} replies</span><br /> <span class="replyCount">{{.PostCount}}</span><br />
<span class="topicCount">x topics</span> <span class="likeCount">{{.LikeCount}}</span>
</span> </span>
</div> </div>
<div class="rowitem topic_right passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}"> <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}} {{if .LastUser.Avatar}}<img src="{{.LastUser.Avatar}}" height="64" />{{end}}
<span> <span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;">{{.LastUser.Name}}</a><br> <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> </span>
</div> </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}} </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%); --header-border-color: hsl(0,0%,85%);
--element-border-color: hsl(0,0%,90%); --element-border-color: hsl(0,0%,90%);
--element-background-color: white; --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; -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 { body {
font-size: 16px; font-size: 16px;
font-family: arial; font-family: arial;
@ -18,7 +30,7 @@ body {
a { a {
text-decoration: none; text-decoration: none;
color: hsl(0,0%,40%); color: var(--primary-link-color);
} }
#back { #back {
@ -53,10 +65,11 @@ li {
.menu_overview { .menu_overview {
font-size: 22px; font-size: 22px;
margin-right: 12px; margin-right: 12px;
letter-spacing: 1px;
} }
.menu_overview a:after { .menu_overview a:after {
margin-left: 14px; margin-left: 13px;
margin-right: 4px; margin-right: 5px;
position: relative; position: relative;
top: -3px; top: -3px;
} }
@ -74,8 +87,37 @@ li {
top: -2px; top: -2px;
} }
.menu_alerts { .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; 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 { .menu_account a:before {
content: "\f2c3"; content: "\f2c3";
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
@ -117,8 +159,8 @@ ul {
border: 1px solid var(--header-border-color); border: 1px solid var(--header-border-color);
border-bottom: 2px solid var(--header-border-color); border-bottom: 2px solid var(--header-border-color);
background-color: var(--element-background-color); background-color: var(--element-background-color);
margin-left: 4px; margin-left: 12px;
margin-right: 4px; margin-right: 12px;
} }
.rowhead { .rowhead {
@ -135,6 +177,23 @@ ul {
display: inline-block; 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 { .topic_list {
border: none; border: none;
} }
@ -152,26 +211,37 @@ ul {
} }
.topic_list .rowtopic { .topic_list .rowtopic {
font-size: 18px; font-size: 17px;
color: hsl(0,0%,30%); color: hsl(0,0%,30%);
margin-right: 2px; margin-right: 1px;
max-width: 230px; white-space: nowrap;
display: inline-block;
}
.topic_list .rowtopic span {
max-width: 112px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; }
.topic_list .rowsmall {
font-size: 15px;
} }
.topic_list .rowsmall.starter:before { .topic_list .rowsmall.starter:before {
content: "\f007"; content: "\f007";
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
margin-right: 5px; margin-right: 5px;
font-size: 16px; font-size: 15px;
} }
.topic_list .rowsmall.starter:before { .topic_list .rowsmall.starter:before {
content: "\f007"; content: "\f007";
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
margin-right: 5px; margin-right: 5px;
font-size: 16px; font-size: 15px;
}
.topic_list .lastReplyAt {
font-size: 14px;
} }
.topic_list .topic_status_e { .topic_list .topic_status_e {
@ -179,19 +249,50 @@ ul {
} }
.topic_left { .topic_left {
flex: 1 1 calc(100% - 400px); flex: 1 1 calc(100% - 380px);
border-right: none; border-right: none;
} }
.topic_inner_right { .topic_inner_right {
margin-left: auto; margin-left: 15%;
margin-right: auto; margin-right: auto;
font-size: 17px; font-size: 17px;
}
.topic_inner_right.rowsmall {
font-size: 14px;
margin-top: 15px;
}
/* Experimenting here */
.topic_inner_right {
margin-top: 12px; 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 { .topic_right {
flex: 1 1 150px; flex: 1 1 0px; /*150px*/
border-left: none; border-left: none;
} }
@ -205,15 +306,16 @@ ul {
.topic_right img { .topic_right img {
border-radius: 30px; border-radius: 30px;
height: 48px; height: 42px;
width: 48px; width: 42px;
margin-top: 8px; margin-top: 10px;
} }
.topic_left .topic_inner_left { .topic_left .topic_inner_left {
margin-top: 12px; margin-top: 12px;
margin-left: 8px; margin-left: 8px;
margin-bottom: 14px; margin-bottom: 14px;
width: 240px;
} }
.topic_right > span { .topic_right > span {
@ -222,11 +324,22 @@ ul {
} }
.topic_sticky { .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) { @media(max-width: 670px) {
.topic_inner_right { .topic_inner_right {

View File

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

View File

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

View File

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

View File

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

79
user.go
View File

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

View File

@ -24,7 +24,7 @@ type UserStore interface {
//BulkGet(ids []int) ([]*User, error) //BulkGet(ids []int) ([]*User, error)
BulkGetMap(ids []int) (map[int]*User, error) BulkGetMap(ids []int) (map[int]*User, error)
BypassGet(id 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 GlobalCount() int
} }
@ -124,6 +124,7 @@ func (mus *MemoryUserStore) Get(id int) (*User, error) {
user = &User{ID: id, Loggedin: true} 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) 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 != "" {
if user.Avatar[0] == '.' { if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar 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 return nil, err
} }
// TODO: Add an init method to User rather than writing this same bit of code over and over
// Initialise the user // Initialise the user
if user.Avatar != "" { if user.Avatar != "" {
if user.Avatar[0] == '.' { if user.Avatar[0] == '.' {
@ -253,6 +255,7 @@ func (mus *MemoryUserStore) BypassGet(id int) (*User, error) {
user := &User{ID: id, Loggedin: true} 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) 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 != "" {
if user.Avatar[0] == '.' { if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
@ -274,6 +277,7 @@ func (mus *MemoryUserStore) Reload(id int) error {
return err 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 != "" {
if user.Avatar[0] == '.' { if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar 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? // 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..? // Is this username already taken..?
err := mus.usernameExists.QueryRow(username).Scan(&username) err := mus.usernameExists.QueryRow(username).Scan(&username)
if err != ErrNoRows { if err != ErrNoRows {
@ -405,8 +409,7 @@ func (mus *MemoryUserStore) GetCapacity() int {
} }
// GlobalCount returns the total number of users registered on the forums // GlobalCount returns the total number of users registered on the forums
func (mus *MemoryUserStore) GlobalCount() int { func (mus *MemoryUserStore) GlobalCount() (ucount int) {
var ucount int
err := mus.userCount.QueryRow().Scan(&ucount) err := mus.userCount.QueryRow().Scan(&ucount)
if err != nil { if err != nil {
LogError(err) LogError(err)
@ -548,7 +551,7 @@ func (mus *SQLUserStore) Exists(id int) bool {
return err != ErrNoRows 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..? // Is this username already taken..?
err := mus.usernameExists.QueryRow(username).Scan(&username) err := mus.usernameExists.QueryRow(username).Scan(&username)
if err != ErrNoRows { 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 // GlobalCount returns the total number of users registered on the forums
func (mus *SQLUserStore) GlobalCount() int { func (mus *SQLUserStore) GlobalCount() (ucount int) {
var ucount int
err := mus.userCount.QueryRow().Scan(&ucount) err := mus.userCount.QueryRow().Scan(&ucount)
if err != nil { if err != nil {
LogError(err) LogError(err)
} }
return ucount 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
}