Refactored a few bits and pieces.
Added Riot as a dependency, I'm still deciding over whether to use this or Bleve. Tweaked the topic view for Cosora. Fixed a crash bug in the group creator. Moved a few things from permissions.go into the ForumPermsStore, more to come here! Added more tests. Refactored the MySQL Query Generator. Refactored the BBCode Parser. Moved the presets into the Phrase System.
This commit is contained in:
parent
d0363f3eb1
commit
d0ffc4be78
|
@ -63,11 +63,11 @@ func initDatabase() (err error) {
|
|||
}
|
||||
|
||||
log.Print("Loading the forum permissions.")
|
||||
err = buildForumPermissions()
|
||||
fpstore = NewForumPermsStore()
|
||||
err = fpstore.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpstore = NewForumPermsStore()
|
||||
|
||||
log.Print("Loading the settings.")
|
||||
err = LoadSettings()
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
)
|
||||
|
||||
var fpstore *ForumPermsStore
|
||||
|
||||
type ForumPermsStore struct {
|
||||
|
@ -9,6 +14,156 @@ func NewForumPermsStore() *ForumPermsStore {
|
|||
return &ForumPermsStore{}
|
||||
}
|
||||
|
||||
func (fps *ForumPermsStore) Init() error {
|
||||
fids, err := fstore.GetAllIDs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dev.SuperDebug {
|
||||
log.Print("fids: ", fids)
|
||||
}
|
||||
|
||||
rows, err := getForumsPermissionsStmt.Query()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if dev.DebugMode {
|
||||
log.Print("Adding the forum permissions")
|
||||
if dev.SuperDebug {
|
||||
log.Print("forumPerms[gid][fid]")
|
||||
}
|
||||
}
|
||||
|
||||
// Temporarily store the forum perms in a map before transferring it to a much faster and thread-safe slice
|
||||
forumPerms = make(map[int]map[int]ForumPerms)
|
||||
for rows.Next() {
|
||||
var gid, fid int
|
||||
var perms []byte
|
||||
var pperms ForumPerms
|
||||
err = rows.Scan(&gid, &fid, &perms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dev.SuperDebug {
|
||||
log.Print("perms: ", string(perms))
|
||||
}
|
||||
err = json.Unmarshal(perms, &pperms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pperms.ExtData = make(map[string]bool)
|
||||
pperms.Overrides = true
|
||||
_, ok := forumPerms[gid]
|
||||
if !ok {
|
||||
forumPerms[gid] = make(map[int]ForumPerms)
|
||||
}
|
||||
|
||||
if dev.SuperDebug {
|
||||
log.Print("gid: ", gid)
|
||||
log.Print("fid: ", fid)
|
||||
log.Printf("perms: %+v\n", pperms)
|
||||
}
|
||||
forumPerms[gid][fid] = pperms
|
||||
}
|
||||
|
||||
return fps.cascadePermSetToGroups(forumPerms, fids)
|
||||
}
|
||||
|
||||
// TODO: Need a more thread-safe way of doing this. Possibly with sync.Map?
|
||||
func (fps *ForumPermsStore) Reload(fid int) error {
|
||||
if dev.DebugMode {
|
||||
log.Printf("Reloading the forum permissions for forum #%d", fid)
|
||||
}
|
||||
fids, err := fstore.GetAllIDs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := db.Query("select gid, permissions from forums_permissions where fid = ? order by gid asc", fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var gid int
|
||||
var perms []byte
|
||||
var pperms ForumPerms
|
||||
err := rows.Scan(&gid, &perms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(perms, &pperms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pperms.ExtData = make(map[string]bool)
|
||||
pperms.Overrides = true
|
||||
_, ok := forumPerms[gid]
|
||||
if !ok {
|
||||
forumPerms[gid] = make(map[int]ForumPerms)
|
||||
}
|
||||
forumPerms[gid][fid] = pperms
|
||||
}
|
||||
|
||||
return fps.cascadePermSetToGroups(forumPerms, fids)
|
||||
}
|
||||
|
||||
func (fps *ForumPermsStore) cascadePermSetToGroups(forumPerms map[int]map[int]ForumPerms, fids []int) error {
|
||||
groups, err := gstore.GetAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
if dev.DebugMode {
|
||||
log.Printf("Updating the forum permissions for Group #%d", group.ID)
|
||||
}
|
||||
group.Forums = []ForumPerms{BlankForumPerms}
|
||||
group.CanSee = []int{}
|
||||
fps.cascadePermSetToGroup(forumPerms, group, fids)
|
||||
|
||||
if dev.SuperDebug {
|
||||
log.Printf("group.CanSee (length %d): %+v \n", len(group.CanSee), group.CanSee)
|
||||
log.Printf("group.Forums (length %d): %+v\n", len(group.Forums), group.Forums)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fps *ForumPermsStore) cascadePermSetToGroup(forumPerms map[int]map[int]ForumPerms, group *Group, fids []int) {
|
||||
for _, fid := range fids {
|
||||
if dev.SuperDebug {
|
||||
log.Printf("Forum #%+v\n", fid)
|
||||
}
|
||||
forumPerm, ok := forumPerms[group.ID][fid]
|
||||
if ok {
|
||||
//log.Print("Overriding permissions for forum #%d",fid)
|
||||
group.Forums = append(group.Forums, forumPerm)
|
||||
} else {
|
||||
//log.Printf("Inheriting from group defaults for forum #%d",fid)
|
||||
forumPerm = BlankForumPerms
|
||||
group.Forums = append(group.Forums, forumPerm)
|
||||
}
|
||||
if forumPerm.Overrides {
|
||||
if forumPerm.ViewTopic {
|
||||
group.CanSee = append(group.CanSee, fid)
|
||||
}
|
||||
} else if group.Perms.ViewTopic {
|
||||
group.CanSee = append(group.CanSee, fid)
|
||||
}
|
||||
|
||||
if dev.SuperDebug {
|
||||
log.Print("group.ID: ", group.ID)
|
||||
log.Printf("forumPerm: %+v\n", forumPerm)
|
||||
log.Print("group.CanSee: ", group.CanSee)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fps *ForumPermsStore) Get(fid int, gid int) (fperms ForumPerms, err error) {
|
||||
// TODO: Add a hook here and have plugin_guilds use it
|
||||
group, err := gstore.Get(gid)
|
||||
|
|
|
@ -157,21 +157,24 @@ func (mgs *MemoryGroupStore) Reload(id int) error {
|
|||
func (mgs *MemoryGroupStore) initGroup(group *Group) error {
|
||||
err := json.Unmarshal(group.PermissionsText, &group.Perms)
|
||||
if err != nil {
|
||||
log.Printf("group: %+v\n", group)
|
||||
log.Print("bad group perms: ", group.PermissionsText)
|
||||
return err
|
||||
}
|
||||
if dev.DebugMode {
|
||||
log.Print(group.Name + ": ")
|
||||
log.Printf("%+v\n", group.Perms)
|
||||
log.Printf(group.Name+": %+v\n", group.Perms)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(group.PluginPermsText, &group.PluginPerms)
|
||||
if err != nil {
|
||||
log.Printf("group: %+v\n", group)
|
||||
log.Print("bad group plugin perms: ", group.PluginPermsText)
|
||||
return err
|
||||
}
|
||||
if dev.DebugMode {
|
||||
log.Print(group.Name + ": ")
|
||||
log.Printf("%+v\n", group.PluginPerms)
|
||||
log.Printf(group.Name+": %+v\n", group.PluginPerms)
|
||||
}
|
||||
|
||||
//group.Perms.ExtData = make(map[string]bool)
|
||||
// TODO: Can we optimise the bit where this cascades down to the user now?
|
||||
if group.IsAdmin || group.IsMod {
|
||||
|
@ -204,7 +207,7 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod
|
|||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
insertTx, err := qgen.Builder.SimpleInsertTx(tx, "users_groups", "name, tag, is_admin, is_mod, is_banned, permissions", "?,?,?,?,?,?")
|
||||
insertTx, err := qgen.Builder.SimpleInsertTx(tx, "users_groups", "name, tag, is_admin, is_mod, is_banned, permissions, plugin_perms", "?,?,?,?,?,?,'{}'")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -279,7 +282,7 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod
|
|||
mgs.Unlock()
|
||||
|
||||
for _, forum := range fdata {
|
||||
err = rebuildForumPermissions(forum.ID)
|
||||
err = fpstore.Reload(forum.ID)
|
||||
if err != nil {
|
||||
return gid, err
|
||||
}
|
||||
|
|
|
@ -22,6 +22,9 @@ go get -u gopkg.in/sourcemap.v1
|
|||
echo "Installing OttoJS"
|
||||
go get -u github.com/robertkrimen/otto
|
||||
|
||||
echo "Installing the Riot Search Engine"
|
||||
go get -u github.com/robertkrimen/otto
|
||||
|
||||
|
||||
echo "Building the installer"
|
||||
cd ./install
|
||||
|
|
|
@ -71,6 +71,13 @@ if %errorlevel% neq 0 (
|
|||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
echo Installing the Riot Search Engine
|
||||
go get -u github.com/go-ego/riot
|
||||
if %errorlevel% neq 0 (
|
||||
pause
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
|
||||
echo Building the installer
|
||||
go generate
|
||||
|
|
|
@ -42,6 +42,16 @@
|
|||
"SettingLabels": {
|
||||
"activation_type": "Activate All,Email Activation,Admin Approval"
|
||||
},
|
||||
"PermPresets": {
|
||||
"all":"Public",
|
||||
"announce":"Announcements",
|
||||
"members":"Member Only",
|
||||
"staff":"Staff Only",
|
||||
"admins":"Admin Only",
|
||||
"archive":"Archive",
|
||||
"custom":"Custom",
|
||||
"unknown":"Unknown"
|
||||
},
|
||||
"Accounts": {
|
||||
"VerifyEmailSubject": "Validate Your Email @ {{name}}",
|
||||
"VerifyEmailBody": "Dear {{username}}, following your registration on our forums, we ask you to validate your email, so that we can confirm that this email actually belongs to you.\n\nClick on the following link to do so. {{schema}}://{{url}}/user/edit/token/{{token}}\n\nIf you haven't created an account here, then please feel free to ignore this email.\nWe're sorry for the inconvenience this may have caused."
|
||||
|
|
86
misc_test.go
86
misc_test.go
|
@ -421,6 +421,8 @@ func userStoreTest(t *testing.T, newUserID int) {
|
|||
|
||||
_, err = users.Get(newUserID)
|
||||
recordMustNotExist(t, err, "UID #%d shouldn't exist", newUserID)
|
||||
|
||||
// TODO: Add tests for the Cache* methods
|
||||
}
|
||||
|
||||
// TODO: Add an error message to this?
|
||||
|
@ -445,6 +447,36 @@ func expect(t *testing.T, item bool, errmsg string) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPermsMiddleware(t *testing.T) {
|
||||
if !gloinited {
|
||||
err := gloinit()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if !pluginsInited {
|
||||
initPlugins()
|
||||
}
|
||||
|
||||
dummyResponseRecorder := httptest.NewRecorder()
|
||||
bytesBuffer := bytes.NewBuffer([]byte(""))
|
||||
dummyRequest := httptest.NewRequest("", "/forum/1", bytesBuffer)
|
||||
user := getDummyUser()
|
||||
|
||||
ferr := SuperModOnly(dummyResponseRecorder, dummyRequest, *user)
|
||||
expect(t, ferr != nil, "Blank users shouldn't be supermods")
|
||||
|
||||
user.IsSuperMod = false
|
||||
ferr = SuperModOnly(dummyResponseRecorder, dummyRequest, *user)
|
||||
expect(t, ferr != nil, "Non-supermods shouldn't be allowed through supermod gates")
|
||||
|
||||
user.IsSuperMod = true
|
||||
ferr = SuperModOnly(dummyResponseRecorder, dummyRequest, *user)
|
||||
expect(t, ferr == nil, "Supermods should be allowed through supermod gates")
|
||||
|
||||
// TODO: Loop over the Control Panel routes and make sure only supermods can get in
|
||||
}
|
||||
|
||||
func TestTopicStore(t *testing.T) {
|
||||
if !gloinited {
|
||||
err := gloinit()
|
||||
|
@ -525,31 +557,44 @@ func TestForumStore(t *testing.T) {
|
|||
if forum.ID != 1 {
|
||||
t.Error("forum.ID doesn't not match the requested FID. Got '" + strconv.Itoa(forum.ID) + "' instead.'")
|
||||
}
|
||||
if forum.Name != "Reports" {
|
||||
t.Error("FID #0 is named '" + forum.Name + "' and not 'Reports'")
|
||||
}
|
||||
// TODO: Check the preset and forum permissions
|
||||
expect(t, forum.Name == "Reports", fmt.Sprintf("FID #0 is named '%s' and not 'Reports'", forum.Name))
|
||||
expect(t, !forum.Active, fmt.Sprintf("The reports forum shouldn't be active"))
|
||||
var expectDesc = "All the reports go here"
|
||||
expect(t, forum.Desc == expectDesc, fmt.Sprintf("The forum description should be '%s' not '%s'", expectDesc, forum.Desc))
|
||||
|
||||
forum, err = fstore.Get(2)
|
||||
recordMustExist(t, err, "Couldn't find FID #1")
|
||||
|
||||
_ = forum
|
||||
expect(t, forum.ID == 2, fmt.Sprintf("The FID should be 2 not %d", forum.ID))
|
||||
expect(t, forum.Name == "General", fmt.Sprintf("The name of the forum should be 'General' not '%s'", forum.Name))
|
||||
expect(t, forum.Active, fmt.Sprintf("The general forum should be active"))
|
||||
expectDesc = "A place for general discussions which don't fit elsewhere"
|
||||
expect(t, forum.Desc == expectDesc, fmt.Sprintf("The forum description should be '%s' not '%s'", expectDesc, forum.Desc))
|
||||
|
||||
ok := fstore.Exists(-1)
|
||||
if ok {
|
||||
t.Error("FID #-1 shouldn't exist")
|
||||
}
|
||||
|
||||
expect(t, !ok, "FID #-1 shouldn't exist")
|
||||
ok = fstore.Exists(0)
|
||||
if ok {
|
||||
t.Error("FID #0 shouldn't exist")
|
||||
}
|
||||
|
||||
expect(t, !ok, "FID #0 shouldn't exist")
|
||||
ok = fstore.Exists(1)
|
||||
if !ok {
|
||||
t.Error("FID #1 should exist")
|
||||
expect(t, ok, "FID #1 should exist")
|
||||
|
||||
// TODO: Test forum creation
|
||||
// TODO: Test forum deletion
|
||||
// TODO: Test forum update
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
func TestForumPermsStore(t *testing.T) {
|
||||
if !gloinited {
|
||||
gloinit()
|
||||
}
|
||||
if !pluginsInited {
|
||||
initPlugins()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Test the group permissions
|
||||
func TestGroupStore(t *testing.T) {
|
||||
if !gloinited {
|
||||
gloinit()
|
||||
|
@ -646,7 +691,19 @@ func TestGroupStore(t *testing.T) {
|
|||
expect(t, group.IsMod, "This should be a mod group")
|
||||
expect(t, !group.IsBanned, "This shouldn't be a ban group")
|
||||
|
||||
// Make sure the data is static
|
||||
gstore.Reload(gid)
|
||||
|
||||
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
|
||||
// TODO: Test group reload
|
||||
// TODO: Test group cache set
|
||||
}
|
||||
|
||||
func TestReplyStore(t *testing.T) {
|
||||
|
@ -714,6 +771,7 @@ func TestSlugs(t *testing.T) {
|
|||
msgList = addMEPair(msgList, "--", "untitled")
|
||||
msgList = addMEPair(msgList, "é", "é")
|
||||
msgList = addMEPair(msgList, "-é-", "é")
|
||||
msgList = addMEPair(msgList, "-你好-", "untitled")
|
||||
|
||||
for _, item := range msgList {
|
||||
t.Log("Testing string '" + item.Msg + "'")
|
||||
|
|
|
@ -338,7 +338,6 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user User, sfi
|
|||
if err == ErrNoRows {
|
||||
return LocalError("The forum you're trying to edit doesn't exist.", w, r, user)
|
||||
} else if err != nil {
|
||||
|
||||
return InternalError(err, w, r)
|
||||
}
|
||||
|
||||
|
@ -465,6 +464,8 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use
|
|||
return InternalErrorJSQ(err, w, r, isJs)
|
||||
}
|
||||
|
||||
// ! IMPORTANT
|
||||
// TODO: Refactor this
|
||||
forumUpdateMutex.Lock()
|
||||
defer forumUpdateMutex.Unlock()
|
||||
if changed {
|
||||
|
@ -488,7 +489,6 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use
|
|||
}
|
||||
err = fstore.Reload(fid)
|
||||
if err != nil {
|
||||
// TODO: Log this? -- Another admin might have deleted it
|
||||
return LocalErrorJSQ("Unable to reload forum", w, r, user, isJs)
|
||||
}
|
||||
}
|
||||
|
|
182
permissions.go
182
permissions.go
|
@ -349,7 +349,7 @@ func permmapToQuery(permmap map[string]ForumPerms, fid int) error {
|
|||
|
||||
permUpdateMutex.Lock()
|
||||
defer permUpdateMutex.Unlock()
|
||||
return rebuildForumPermissions(fid)
|
||||
return fpstore.Reload(fid)
|
||||
}
|
||||
|
||||
func replaceForumPermsForGroup(gid int, presetSet map[int]string, permSets map[int]ForumPerms) error {
|
||||
|
@ -393,163 +393,7 @@ func replaceForumPermsForGroupTx(tx *sql.Tx, gid int, presetSets map[int]string,
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO: Need a more thread-safe way of doing this. Possibly with sync.Map?
|
||||
func rebuildForumPermissions(fid int) error {
|
||||
if dev.DebugMode {
|
||||
log.Print("Loading the forum permissions")
|
||||
}
|
||||
fids, err := fstore.GetAllIDs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := db.Query("select gid, permissions from forums_permissions where fid = ? order by gid asc", fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if dev.DebugMode {
|
||||
log.Print("Updating the forum permissions")
|
||||
}
|
||||
for rows.Next() {
|
||||
var gid int
|
||||
var perms []byte
|
||||
var pperms ForumPerms
|
||||
err := rows.Scan(&gid, &perms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(perms, &pperms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pperms.ExtData = make(map[string]bool)
|
||||
pperms.Overrides = true
|
||||
_, ok := forumPerms[gid]
|
||||
if !ok {
|
||||
forumPerms[gid] = make(map[int]ForumPerms)
|
||||
}
|
||||
forumPerms[gid][fid] = pperms
|
||||
}
|
||||
|
||||
return cascadePermSetToGroups(forumPerms, fids)
|
||||
}
|
||||
|
||||
func buildForumPermissions() error {
|
||||
fids, err := fstore.GetAllIDs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dev.SuperDebug {
|
||||
log.Print("fids: ", fids)
|
||||
}
|
||||
|
||||
rows, err := getForumsPermissionsStmt.Query()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if dev.DebugMode {
|
||||
log.Print("Adding the forum permissions")
|
||||
if dev.SuperDebug {
|
||||
log.Print("forumPerms[gid][fid]")
|
||||
}
|
||||
}
|
||||
|
||||
// Temporarily store the forum perms in a map before transferring it to a much faster and thread-safe slice
|
||||
forumPerms = make(map[int]map[int]ForumPerms)
|
||||
for rows.Next() {
|
||||
var gid, fid int
|
||||
var perms []byte
|
||||
var pperms ForumPerms
|
||||
err = rows.Scan(&gid, &fid, &perms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dev.SuperDebug {
|
||||
log.Print("perms: ", string(perms))
|
||||
}
|
||||
err = json.Unmarshal(perms, &pperms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pperms.ExtData = make(map[string]bool)
|
||||
pperms.Overrides = true
|
||||
_, ok := forumPerms[gid]
|
||||
if !ok {
|
||||
forumPerms[gid] = make(map[int]ForumPerms)
|
||||
}
|
||||
|
||||
if dev.SuperDebug {
|
||||
log.Print("gid: ", gid)
|
||||
log.Print("fid: ", fid)
|
||||
log.Printf("perms: %+v;", pperms)
|
||||
}
|
||||
forumPerms[gid][fid] = pperms
|
||||
}
|
||||
|
||||
return cascadePermSetToGroups(forumPerms, fids)
|
||||
}
|
||||
|
||||
func cascadePermSetToGroups(forumPerms map[int]map[int]ForumPerms, fids []int) error {
|
||||
groups, err := gstore.GetAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
if dev.DebugMode {
|
||||
log.Printf("Updating the forum permissions for Group #%d", group.ID)
|
||||
}
|
||||
group.Forums = []ForumPerms{BlankForumPerms}
|
||||
group.CanSee = []int{}
|
||||
cascadePermSetToGroup(forumPerms, group, fids)
|
||||
|
||||
if dev.SuperDebug {
|
||||
log.Printf("group.CanSee %+v\n", group.CanSee)
|
||||
log.Printf("group.Forums %+v\n", group.Forums)
|
||||
log.Print("len(group.CanSee): ", len(group.CanSee))
|
||||
log.Print("len(group.Forums): ", len(group.Forums)) // This counts blank aka 0
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cascadePermSetToGroup(forumPerms map[int]map[int]ForumPerms, group *Group, fids []int) {
|
||||
for _, fid := range fids {
|
||||
if dev.SuperDebug {
|
||||
log.Printf("Forum #%+v\n", fid)
|
||||
}
|
||||
forumPerm, ok := forumPerms[group.ID][fid]
|
||||
if ok {
|
||||
// Override group perms
|
||||
//log.Print("Overriding permissions for forum #" + strconv.Itoa(fid))
|
||||
group.Forums = append(group.Forums, forumPerm)
|
||||
} else {
|
||||
// Inherit from Group
|
||||
//log.Print("Inheriting from default for forum #" + strconv.Itoa(fid))
|
||||
forumPerm = BlankForumPerms
|
||||
group.Forums = append(group.Forums, forumPerm)
|
||||
}
|
||||
if forumPerm.Overrides {
|
||||
if forumPerm.ViewTopic {
|
||||
group.CanSee = append(group.CanSee, fid)
|
||||
}
|
||||
} else if group.Perms.ViewTopic {
|
||||
group.CanSee = append(group.CanSee, fid)
|
||||
}
|
||||
|
||||
if dev.SuperDebug {
|
||||
log.Print("group.ID: ", group.ID)
|
||||
log.Printf("forumPerm: %+v\n", forumPerm)
|
||||
log.Print("group.CanSee: ", group.CanSee)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Refactor this and write tests for it
|
||||
func forumPermsToGroupForumPreset(fperms ForumPerms) string {
|
||||
if !fperms.Overrides {
|
||||
return "default"
|
||||
|
@ -614,24 +458,12 @@ func stripInvalidPreset(preset string) string {
|
|||
|
||||
// TODO: Move this into the phrase system?
|
||||
func presetToLang(preset string) string {
|
||||
switch preset {
|
||||
case "all":
|
||||
return "Public"
|
||||
case "announce":
|
||||
return "Announcements"
|
||||
case "members":
|
||||
return "Member Only"
|
||||
case "staff":
|
||||
return "Staff Only"
|
||||
case "admins":
|
||||
return "Admin Only"
|
||||
case "archive":
|
||||
return "Archive"
|
||||
case "custom":
|
||||
return "Custom"
|
||||
default:
|
||||
return ""
|
||||
phrases := GetAllPermPresets()
|
||||
phrase, ok := phrases[preset]
|
||||
if !ok {
|
||||
phrase = phrases["unknown"]
|
||||
}
|
||||
return phrase
|
||||
}
|
||||
|
||||
// TODO: Is this racey?
|
||||
|
|
|
@ -39,6 +39,7 @@ type LanguagePack struct {
|
|||
GlobalPerms map[string]string
|
||||
LocalPerms map[string]string
|
||||
SettingLabels map[string]string
|
||||
PermPresets map[string]string
|
||||
Accounts map[string]string // TODO: Apply these phrases in the software proper
|
||||
}
|
||||
|
||||
|
@ -139,6 +140,10 @@ func GetAllSettingLabels() map[string]string {
|
|||
return currentLangPack.Load().(*LanguagePack).SettingLabels
|
||||
}
|
||||
|
||||
func GetAllPermPresets() map[string]string {
|
||||
return currentLangPack.Load().(*LanguagePack).PermPresets
|
||||
}
|
||||
|
||||
func GetAccountPhrase(name string) string {
|
||||
res, ok := currentLangPack.Load().(*LanguagePack).Accounts[name]
|
||||
if !ok {
|
||||
|
|
135
plugin_bbcode.go
135
plugin_bbcode.go
|
@ -261,8 +261,7 @@ func bbcodeFullParse(msg string) string {
|
|||
i += 6
|
||||
}
|
||||
//if msglen >= (i+5) {
|
||||
// log.Print("boo2")
|
||||
// log.Print(string(msgbytes[i:i+5]))
|
||||
// log.Print("boo2: ", string(msgbytes[i:i+5]))
|
||||
//}
|
||||
complexBbc = true
|
||||
}
|
||||
|
@ -317,73 +316,15 @@ func bbcodeFullParse(msg string) string {
|
|||
//log.Print("BBCode Pre:","`"+string(msgbytes)+"`")
|
||||
//log.Print("----")
|
||||
for ; i < len(msgbytes); i++ {
|
||||
MainLoop:
|
||||
if msgbytes[i] == '[' {
|
||||
OuterComplex:
|
||||
if msgbytes[i+1] == 'u' {
|
||||
if msgbytes[i+2] == 'r' && msgbytes[i+3] == 'l' && msgbytes[i+4] == ']' {
|
||||
start = i + 5
|
||||
outbytes = append(outbytes, msgbytes[lastTag:i]...)
|
||||
i = start
|
||||
i += partialURLBytesLen(msgbytes[start:])
|
||||
//log.Print("Partial Bytes:",string(msgbytes[start:]))
|
||||
//log.Print("-----")
|
||||
if !bytes.Equal(msgbytes[i:i+6], []byte("[/url]")) {
|
||||
//log.Print("Invalid Bytes:",string(msgbytes[i:i+6]))
|
||||
//log.Print("-----")
|
||||
outbytes = append(outbytes, invalidURL...)
|
||||
goto MainLoop
|
||||
}
|
||||
|
||||
outbytes = append(outbytes, urlOpen...)
|
||||
outbytes = append(outbytes, msgbytes[start:i]...)
|
||||
outbytes = append(outbytes, urlOpen2...)
|
||||
outbytes = append(outbytes, msgbytes[start:i]...)
|
||||
outbytes = append(outbytes, urlClose...)
|
||||
i += 6
|
||||
lastTag = i
|
||||
i, start, lastTag, outbytes = bbcodeParseURL(i, start, lastTag, msgbytes, outbytes)
|
||||
continue
|
||||
}
|
||||
} else if msgbytes[i+1] == 'r' {
|
||||
if bytes.Equal(msgbytes[i+2:i+6], []byte("and]")) {
|
||||
outbytes = append(outbytes, msgbytes[lastTag:i]...)
|
||||
start = i + 6
|
||||
i = start
|
||||
for ; ; i++ {
|
||||
if msgbytes[i] == '[' {
|
||||
if !bytes.Equal(msgbytes[i+1:i+7], []byte("/rand]")) {
|
||||
outbytes = append(outbytes, bbcodeMissingTag...)
|
||||
goto OuterComplex
|
||||
}
|
||||
break
|
||||
} else if (len(msgbytes) - 1) < (i + 10) {
|
||||
outbytes = append(outbytes, bbcodeMissingTag...)
|
||||
goto OuterComplex
|
||||
}
|
||||
}
|
||||
|
||||
number, err := strconv.ParseInt(string(msgbytes[start:i]), 10, 64)
|
||||
if err != nil {
|
||||
outbytes = append(outbytes, bbcodeInvalidNumber...)
|
||||
goto MainLoop
|
||||
}
|
||||
|
||||
// TODO: Add support for negative numbers?
|
||||
if number < 0 {
|
||||
outbytes = append(outbytes, bbcodeNoNegative...)
|
||||
goto MainLoop
|
||||
}
|
||||
|
||||
var dat []byte
|
||||
if number == 0 {
|
||||
dat = []byte("0")
|
||||
} else {
|
||||
dat = []byte(strconv.FormatInt((random.Int63n(number)), 10))
|
||||
}
|
||||
|
||||
outbytes = append(outbytes, dat...)
|
||||
//log.Print("Outputted the random number")
|
||||
i += 7
|
||||
lastTag = i
|
||||
i, start, lastTag, outbytes = bbcodeParseRand(i, start, lastTag, msgbytes, outbytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -411,3 +352,71 @@ func bbcodeFullParse(msg string) string {
|
|||
|
||||
return msg
|
||||
}
|
||||
|
||||
func bbcodeParseURL(i int, start int, lastTag int, msgbytes []byte, outbytes []byte) (int, int, int, []byte) {
|
||||
start = i + 5
|
||||
outbytes = append(outbytes, msgbytes[lastTag:i]...)
|
||||
i = start
|
||||
i += partialURLBytesLen(msgbytes[start:])
|
||||
//log.Print("Partial Bytes: ", string(msgbytes[start:]))
|
||||
//log.Print("-----")
|
||||
if !bytes.Equal(msgbytes[i:i+6], []byte("[/url]")) {
|
||||
//log.Print("Invalid Bytes: ", string(msgbytes[i:i+6]))
|
||||
//log.Print("-----")
|
||||
outbytes = append(outbytes, invalidURL...)
|
||||
return i, start, lastTag, outbytes
|
||||
}
|
||||
|
||||
outbytes = append(outbytes, urlOpen...)
|
||||
outbytes = append(outbytes, msgbytes[start:i]...)
|
||||
outbytes = append(outbytes, urlOpen2...)
|
||||
outbytes = append(outbytes, msgbytes[start:i]...)
|
||||
outbytes = append(outbytes, urlClose...)
|
||||
i += 6
|
||||
lastTag = i
|
||||
|
||||
return i, start, lastTag, outbytes
|
||||
}
|
||||
|
||||
func bbcodeParseRand(i int, start int, lastTag int, msgbytes []byte, outbytes []byte) (int, int, int, []byte) {
|
||||
outbytes = append(outbytes, msgbytes[lastTag:i]...)
|
||||
start = i + 6
|
||||
i = start
|
||||
for ; ; i++ {
|
||||
if msgbytes[i] == '[' {
|
||||
if !bytes.Equal(msgbytes[i+1:i+7], []byte("/rand]")) {
|
||||
outbytes = append(outbytes, bbcodeMissingTag...)
|
||||
return i, start, lastTag, outbytes
|
||||
}
|
||||
break
|
||||
} else if (len(msgbytes) - 1) < (i + 10) {
|
||||
outbytes = append(outbytes, bbcodeMissingTag...)
|
||||
return i, start, lastTag, outbytes
|
||||
}
|
||||
}
|
||||
|
||||
number, err := strconv.ParseInt(string(msgbytes[start:i]), 10, 64)
|
||||
if err != nil {
|
||||
outbytes = append(outbytes, bbcodeInvalidNumber...)
|
||||
return i, start, lastTag, outbytes
|
||||
}
|
||||
|
||||
// TODO: Add support for negative numbers?
|
||||
if number < 0 {
|
||||
outbytes = append(outbytes, bbcodeNoNegative...)
|
||||
return i, start, lastTag, outbytes
|
||||
}
|
||||
|
||||
var dat []byte
|
||||
if number == 0 {
|
||||
dat = []byte("0")
|
||||
} else {
|
||||
dat = []byte(strconv.FormatInt((random.Int63n(number)), 10))
|
||||
}
|
||||
|
||||
outbytes = append(outbytes, dat...)
|
||||
//log.Print("Outputted the random number")
|
||||
i += 7
|
||||
lastTag = i
|
||||
return i, start, lastTag, outbytes
|
||||
}
|
||||
|
|
|
@ -48,6 +48,8 @@ func TestBBCodeRender(t *testing.T) {
|
|||
msgList = addMEPair(msgList, "[quote][b]hi[/b][/quote]", "<span class='postQuote'><b>hi</b></span>")
|
||||
msgList = addMEPair(msgList, "[quote][b]h[/b][/quote]", "<span class='postQuote'><b>h</b></span>")
|
||||
msgList = addMEPair(msgList, "[quote][b][/b][/quote]", "<span class='postQuote'><b></b></span>")
|
||||
msgList = addMEPair(msgList, "-你好-", "-你好-")
|
||||
msgList = addMEPair(msgList, "[i]-你好-[/i]", "<i>-你好-</i>") // TODO: More of these Unicode tests? Emoji, Chinese, etc.?
|
||||
|
||||
t.Log("Testing bbcodeFullParse")
|
||||
for _, item := range msgList {
|
||||
|
@ -249,6 +251,8 @@ func TestMarkdownRender(t *testing.T) {
|
|||
msgList = addMEPair(msgList, "* *", "<i> </i>")
|
||||
msgList = addMEPair(msgList, "** **", "<b> </b>")
|
||||
msgList = addMEPair(msgList, "*** ***", "<b><i> </i></b>")
|
||||
msgList = addMEPair(msgList, "-你好-", "-你好-")
|
||||
msgList = addMEPair(msgList, "*-你好-*", "<i>-你好-</i>") // TODO: More of these Unicode tests? Emoji, Chinese, etc.?
|
||||
|
||||
for _, item := range msgList {
|
||||
res = markdownParse(item.Msg)
|
||||
|
|
|
@ -118,21 +118,7 @@ func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns str
|
|||
return "", errors.New("No input data found for SimpleInsert")
|
||||
}
|
||||
|
||||
var querystr = "INSERT INTO `" + table + "`("
|
||||
|
||||
// Escape the column names, just in case we've used a reserved keyword
|
||||
for _, column := range processColumns(columns) {
|
||||
if column.Type == "function" {
|
||||
querystr += column.Left + ","
|
||||
} else {
|
||||
querystr += "`" + column.Left + "`,"
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the trailing comma
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
querystr += ") VALUES ("
|
||||
var querystr = "INSERT INTO `" + table + "`(" + adapter.buildColumns(columns) + ") VALUES ("
|
||||
for _, field := range processFields(fields) {
|
||||
nameLen := len(field.Name)
|
||||
if field.Name[0] == '"' && field.Name[nameLen-1] == '"' && nameLen >= 3 {
|
||||
|
@ -149,6 +135,18 @@ func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns str
|
|||
return querystr + ")", nil
|
||||
}
|
||||
|
||||
func (adapter *MysqlAdapter) buildColumns(columns string) (querystr string) {
|
||||
// Escape the column names, just in case we've used a reserved keyword
|
||||
for _, column := range processColumns(columns) {
|
||||
if column.Type == "function" {
|
||||
querystr += column.Left + ","
|
||||
} else {
|
||||
querystr += "`" + column.Left + "`,"
|
||||
}
|
||||
}
|
||||
return querystr[0 : len(querystr)-1]
|
||||
}
|
||||
|
||||
// ! DEPRECATED
|
||||
func (adapter *MysqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) {
|
||||
if name == "" {
|
||||
|
@ -164,20 +162,7 @@ func (adapter *MysqlAdapter) SimpleReplace(name string, table string, columns st
|
|||
return "", errors.New("No input data found for SimpleInsert")
|
||||
}
|
||||
|
||||
var querystr = "REPLACE INTO `" + table + "`("
|
||||
|
||||
// Escape the column names, just in case we've used a reserved keyword
|
||||
for _, column := range processColumns(columns) {
|
||||
if column.Type == "function" {
|
||||
querystr += column.Left + ","
|
||||
} else {
|
||||
querystr += "`" + column.Left + "`,"
|
||||
}
|
||||
}
|
||||
// Remove the trailing comma
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
querystr += ") VALUES ("
|
||||
var querystr = "REPLACE INTO `" + table + "`(" + adapter.buildColumns(columns) + ") VALUES ("
|
||||
for _, field := range processFields(fields) {
|
||||
querystr += field.Name + ","
|
||||
}
|
||||
|
@ -260,29 +245,13 @@ func (adapter *MysqlAdapter) SimpleUpdate(name string, table string, set string,
|
|||
}
|
||||
querystr += ","
|
||||
}
|
||||
// Remove the trailing comma
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
// Add support for BETWEEN x.x
|
||||
if len(where) != 0 {
|
||||
querystr += " WHERE"
|
||||
for _, loc := range processWhere(where) {
|
||||
for _, token := range loc.Expr {
|
||||
switch token.Type {
|
||||
case "function", "operator", "number", "substitute":
|
||||
querystr += " " + token.Contents
|
||||
case "column":
|
||||
querystr += " `" + token.Contents + "`"
|
||||
case "string":
|
||||
querystr += " '" + token.Contents + "'"
|
||||
default:
|
||||
panic("This token doesn't exist o_o")
|
||||
}
|
||||
}
|
||||
querystr += " AND"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
whereStr, err := adapter.buildWhere(where)
|
||||
if err != nil {
|
||||
return querystr, err
|
||||
}
|
||||
querystr += whereStr
|
||||
|
||||
adapter.pushStatement(name, "update", querystr)
|
||||
return querystr, nil
|
||||
|
@ -335,32 +304,8 @@ func (adapter *MysqlAdapter) Purge(name string, table string) (string, error) {
|
|||
return "DELETE FROM `" + table + "`", nil
|
||||
}
|
||||
|
||||
func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if table == "" {
|
||||
return "", errors.New("You need a name for this table")
|
||||
}
|
||||
if len(columns) == 0 {
|
||||
return "", errors.New("No columns found for SimpleSelect")
|
||||
}
|
||||
|
||||
// Slice up the user friendly strings into something easier to process
|
||||
var colslice = strings.Split(strings.TrimSpace(columns), ",")
|
||||
|
||||
var querystr = "SELECT "
|
||||
|
||||
// Escape the column names, just in case we've used a reserved keyword
|
||||
for _, column := range colslice {
|
||||
querystr += "`" + strings.TrimSpace(column) + "`,"
|
||||
}
|
||||
// Remove the trailing comma
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
querystr += " FROM `" + table + "`"
|
||||
|
||||
// Add support for BETWEEN x.x
|
||||
// TODO: Add support for BETWEEN x.x
|
||||
func (adapter *MysqlAdapter) buildWhere(where string) (querystr string, err error) {
|
||||
if len(where) != 0 {
|
||||
querystr += " WHERE"
|
||||
for _, loc := range processWhere(where) {
|
||||
|
@ -373,14 +318,17 @@ func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns str
|
|||
case "string":
|
||||
querystr += " '" + token.Contents + "'"
|
||||
default:
|
||||
panic("This token doesn't exist o_o")
|
||||
return querystr, errors.New("This token doesn't exist o_o")
|
||||
}
|
||||
}
|
||||
querystr += " AND"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
}
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
func (adapter *MysqlAdapter) buildOrderby(orderby string) (querystr string) {
|
||||
if len(orderby) != 0 {
|
||||
querystr += " ORDER BY "
|
||||
for _, column := range processOrderby(orderby) {
|
||||
|
@ -389,10 +337,35 @@ func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns str
|
|||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
}
|
||||
return querystr
|
||||
}
|
||||
|
||||
if limit != "" {
|
||||
querystr += " LIMIT " + limit
|
||||
func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if table == "" {
|
||||
return "", errors.New("You need a name for this table")
|
||||
}
|
||||
if len(columns) == 0 {
|
||||
return "", errors.New("No columns found for SimpleSelect")
|
||||
}
|
||||
|
||||
var querystr = "SELECT "
|
||||
|
||||
// Slice up the user friendly strings into something easier to process
|
||||
var colslice = strings.Split(strings.TrimSpace(columns), ",")
|
||||
for _, column := range colslice {
|
||||
querystr += "`" + strings.TrimSpace(column) + "`,"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
whereStr, err := adapter.buildWhere(where)
|
||||
if err != nil {
|
||||
return querystr, err
|
||||
}
|
||||
|
||||
querystr += " FROM `" + table + "`" + whereStr + adapter.buildOrderby(orderby) + adapter.buildLimit(limit)
|
||||
|
||||
querystr = strings.TrimSpace(querystr)
|
||||
adapter.pushStatement(name, "select", querystr)
|
||||
|
@ -435,54 +408,15 @@ func (adapter *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 s
|
|||
}
|
||||
querystr += source + alias + ","
|
||||
}
|
||||
|
||||
// Remove the trailing comma
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
querystr += " FROM `" + table1 + "` LEFT JOIN `" + table2 + "` ON "
|
||||
for _, joiner := range processJoiner(joiners) {
|
||||
querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND "
|
||||
}
|
||||
// Remove the trailing AND
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
|
||||
// Add support for BETWEEN x.x
|
||||
if len(where) != 0 {
|
||||
querystr += " WHERE"
|
||||
for _, loc := range processWhere(where) {
|
||||
for _, token := range loc.Expr {
|
||||
switch token.Type {
|
||||
case "function", "operator", "number", "substitute":
|
||||
querystr += " " + token.Contents
|
||||
case "column":
|
||||
halves := strings.Split(token.Contents, ".")
|
||||
if len(halves) == 2 {
|
||||
querystr += " `" + halves[0] + "`.`" + halves[1] + "`"
|
||||
} else {
|
||||
querystr += " `" + token.Contents + "`"
|
||||
}
|
||||
case "string":
|
||||
querystr += " '" + token.Contents + "'"
|
||||
default:
|
||||
panic("This token doesn't exist o_o")
|
||||
}
|
||||
}
|
||||
querystr += " AND"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
whereStr, err := adapter.buildJoinWhere(where)
|
||||
if err != nil {
|
||||
return querystr, err
|
||||
}
|
||||
|
||||
if len(orderby) != 0 {
|
||||
querystr += " ORDER BY "
|
||||
for _, column := range processOrderby(orderby) {
|
||||
querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
}
|
||||
|
||||
if limit != "" {
|
||||
querystr += " LIMIT " + limit
|
||||
}
|
||||
querystr += " FROM `" + table1 + "` LEFT JOIN `" + table2 + "` ON " + adapter.buildJoiners(joiners) + whereStr + adapter.buildOrderby(orderby) + adapter.buildLimit(limit)
|
||||
|
||||
querystr = strings.TrimSpace(querystr)
|
||||
adapter.pushStatement(name, "select", querystr)
|
||||
|
@ -529,50 +463,12 @@ func (adapter *MysqlAdapter) SimpleInnerJoin(name string, table1 string, table2
|
|||
// Remove the trailing comma
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
querystr += " FROM `" + table1 + "` INNER JOIN `" + table2 + "` ON "
|
||||
for _, joiner := range processJoiner(joiners) {
|
||||
querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND "
|
||||
}
|
||||
// Remove the trailing AND
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
|
||||
// Add support for BETWEEN x.x
|
||||
if len(where) != 0 {
|
||||
querystr += " WHERE"
|
||||
for _, loc := range processWhere(where) {
|
||||
for _, token := range loc.Expr {
|
||||
switch token.Type {
|
||||
case "function", "operator", "number", "substitute":
|
||||
querystr += " " + token.Contents
|
||||
case "column":
|
||||
halves := strings.Split(token.Contents, ".")
|
||||
if len(halves) == 2 {
|
||||
querystr += " `" + halves[0] + "`.`" + halves[1] + "`"
|
||||
} else {
|
||||
querystr += " `" + token.Contents + "`"
|
||||
}
|
||||
case "string":
|
||||
querystr += " '" + token.Contents + "'"
|
||||
default:
|
||||
panic("This token doesn't exist o_o")
|
||||
}
|
||||
}
|
||||
querystr += " AND"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
whereStr, err := adapter.buildJoinWhere(where)
|
||||
if err != nil {
|
||||
return querystr, err
|
||||
}
|
||||
|
||||
if len(orderby) != 0 {
|
||||
querystr += " ORDER BY "
|
||||
for _, column := range processOrderby(orderby) {
|
||||
querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
}
|
||||
|
||||
if limit != "" {
|
||||
querystr += " LIMIT " + limit
|
||||
}
|
||||
querystr += " FROM `" + table1 + "` INNER JOIN `" + table2 + "` ON " + adapter.buildJoiners(joiners) + whereStr + adapter.buildOrderby(orderby) + adapter.buildLimit(limit)
|
||||
|
||||
querystr = strings.TrimSpace(querystr)
|
||||
adapter.pushStatement(name, "select", querystr)
|
||||
|
@ -581,7 +477,6 @@ func (adapter *MysqlAdapter) SimpleInnerJoin(name string, table1 string, table2
|
|||
|
||||
func (adapter *MysqlAdapter) SimpleInsertSelect(name string, ins DB_Insert, sel DB_Select) (string, error) {
|
||||
/* Insert Portion */
|
||||
|
||||
var querystr = "INSERT INTO `" + ins.Table + "`("
|
||||
|
||||
// Escape the column names, just in case we've used a reserved keyword
|
||||
|
@ -613,40 +508,12 @@ func (adapter *MysqlAdapter) SimpleInsertSelect(name string, ins DB_Insert, sel
|
|||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
querystr += " FROM `" + sel.Table + "`"
|
||||
|
||||
// Add support for BETWEEN x.x
|
||||
if len(sel.Where) != 0 {
|
||||
querystr += " WHERE"
|
||||
for _, loc := range processWhere(sel.Where) {
|
||||
for _, token := range loc.Expr {
|
||||
switch token.Type {
|
||||
case "function", "operator", "number", "substitute":
|
||||
querystr += " " + token.Contents
|
||||
case "column":
|
||||
querystr += " `" + token.Contents + "`"
|
||||
case "string":
|
||||
querystr += " '" + token.Contents + "'"
|
||||
default:
|
||||
panic("This token doesn't exist o_o")
|
||||
}
|
||||
}
|
||||
querystr += " AND"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
whereStr, err := adapter.buildWhere(sel.Where)
|
||||
if err != nil {
|
||||
return querystr, err
|
||||
}
|
||||
|
||||
if len(sel.Orderby) != 0 {
|
||||
querystr += " ORDER BY "
|
||||
for _, column := range processOrderby(sel.Orderby) {
|
||||
querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
}
|
||||
|
||||
if sel.Limit != "" {
|
||||
querystr += " LIMIT " + sel.Limit
|
||||
}
|
||||
querystr += " FROM `" + sel.Table + "`" + whereStr + adapter.buildOrderby(sel.Orderby) + adapter.buildLimit(sel.Limit)
|
||||
|
||||
querystr = strings.TrimSpace(querystr)
|
||||
adapter.pushStatement(name, "insert", querystr)
|
||||
|
@ -655,7 +522,6 @@ func (adapter *MysqlAdapter) SimpleInsertSelect(name string, ins DB_Insert, sel
|
|||
|
||||
func (adapter *MysqlAdapter) SimpleInsertLeftJoin(name string, ins DB_Insert, sel DB_Join) (string, error) {
|
||||
/* Insert Portion */
|
||||
|
||||
var querystr = "INSERT INTO `" + ins.Table + "`("
|
||||
|
||||
// Escape the column names, just in case we've used a reserved keyword
|
||||
|
@ -689,16 +555,32 @@ func (adapter *MysqlAdapter) SimpleInsertLeftJoin(name string, ins DB_Insert, se
|
|||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
querystr += " FROM `" + sel.Table1 + "` LEFT JOIN `" + sel.Table2 + "` ON "
|
||||
for _, joiner := range processJoiner(sel.Joiners) {
|
||||
whereStr, err := adapter.buildJoinWhere(sel.Where)
|
||||
if err != nil {
|
||||
return querystr, err
|
||||
}
|
||||
|
||||
querystr += " FROM `" + sel.Table1 + "` LEFT JOIN `" + sel.Table2 + "` ON " + adapter.buildJoiners(sel.Joiners) + whereStr + adapter.buildOrderby(sel.Orderby) + adapter.buildLimit(sel.Limit)
|
||||
|
||||
querystr = strings.TrimSpace(querystr)
|
||||
adapter.pushStatement(name, "insert", querystr)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
// TODO: Make this more consistent with the other build* methods?
|
||||
func (adapter *MysqlAdapter) buildJoiners(joiners string) (querystr string) {
|
||||
for _, joiner := range processJoiner(joiners) {
|
||||
querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND "
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
// Remove the trailing AND
|
||||
return querystr[0 : len(querystr)-4]
|
||||
}
|
||||
|
||||
// Add support for BETWEEN x.x
|
||||
if len(sel.Where) != 0 {
|
||||
// Add support for BETWEEN x.x
|
||||
func (adapter *MysqlAdapter) buildJoinWhere(where string) (querystr string, err error) {
|
||||
if len(where) != 0 {
|
||||
querystr += " WHERE"
|
||||
for _, loc := range processWhere(sel.Where) {
|
||||
for _, loc := range processWhere(where) {
|
||||
for _, token := range loc.Expr {
|
||||
switch token.Type {
|
||||
case "function", "operator", "number", "substitute":
|
||||
|
@ -713,34 +595,25 @@ func (adapter *MysqlAdapter) SimpleInsertLeftJoin(name string, ins DB_Insert, se
|
|||
case "string":
|
||||
querystr += " '" + token.Contents + "'"
|
||||
default:
|
||||
panic("This token doesn't exist o_o")
|
||||
return querystr, errors.New("This token doesn't exist o_o")
|
||||
}
|
||||
}
|
||||
querystr += " AND"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
}
|
||||
|
||||
if len(sel.Orderby) != 0 {
|
||||
querystr += " ORDER BY "
|
||||
for _, column := range processOrderby(sel.Orderby) {
|
||||
querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
}
|
||||
|
||||
if sel.Limit != "" {
|
||||
querystr += " LIMIT " + sel.Limit
|
||||
}
|
||||
|
||||
querystr = strings.TrimSpace(querystr)
|
||||
adapter.pushStatement(name, "insert", querystr)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
func (adapter *MysqlAdapter) buildLimit(limit string) (querystr string) {
|
||||
if limit != "" {
|
||||
querystr += " LIMIT " + limit
|
||||
}
|
||||
return querystr
|
||||
}
|
||||
|
||||
func (adapter *MysqlAdapter) SimpleInsertInnerJoin(name string, ins DB_Insert, sel DB_Join) (string, error) {
|
||||
/* Insert Portion */
|
||||
|
||||
var querystr = "INSERT INTO `" + ins.Table + "`("
|
||||
|
||||
// Escape the column names, just in case we've used a reserved keyword
|
||||
|
@ -774,56 +647,19 @@ func (adapter *MysqlAdapter) SimpleInsertInnerJoin(name string, ins DB_Insert, s
|
|||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
querystr += " FROM `" + sel.Table1 + "` INNER JOIN `" + sel.Table2 + "` ON "
|
||||
for _, joiner := range processJoiner(sel.Joiners) {
|
||||
querystr += "`" + joiner.LeftTable + "`.`" + joiner.LeftColumn + "` " + joiner.Operator + " `" + joiner.RightTable + "`.`" + joiner.RightColumn + "` AND "
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
|
||||
// Add support for BETWEEN x.x
|
||||
if len(sel.Where) != 0 {
|
||||
querystr += " WHERE"
|
||||
for _, loc := range processWhere(sel.Where) {
|
||||
for _, token := range loc.Expr {
|
||||
switch token.Type {
|
||||
case "function", "operator", "number", "substitute":
|
||||
querystr += " " + token.Contents
|
||||
case "column":
|
||||
halves := strings.Split(token.Contents, ".")
|
||||
if len(halves) == 2 {
|
||||
querystr += " `" + halves[0] + "`.`" + halves[1] + "`"
|
||||
} else {
|
||||
querystr += " `" + token.Contents + "`"
|
||||
}
|
||||
case "string":
|
||||
querystr += " '" + token.Contents + "'"
|
||||
default:
|
||||
panic("This token doesn't exist o_o")
|
||||
}
|
||||
}
|
||||
querystr += " AND"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
whereStr, err := adapter.buildJoinWhere(sel.Where)
|
||||
if err != nil {
|
||||
return querystr, err
|
||||
}
|
||||
|
||||
if len(sel.Orderby) != 0 {
|
||||
querystr += " ORDER BY "
|
||||
for _, column := range processOrderby(sel.Orderby) {
|
||||
querystr += column.Column + " " + strings.ToUpper(column.Order) + ","
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
}
|
||||
|
||||
if sel.Limit != "" {
|
||||
querystr += " LIMIT " + sel.Limit
|
||||
}
|
||||
querystr += " FROM `" + sel.Table1 + "` INNER JOIN `" + sel.Table2 + "` ON " + adapter.buildJoiners(sel.Joiners) + whereStr + adapter.buildOrderby(sel.Orderby) + adapter.buildLimit(sel.Limit)
|
||||
|
||||
querystr = strings.TrimSpace(querystr)
|
||||
adapter.pushStatement(name, "insert", querystr)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
func (adapter *MysqlAdapter) SimpleCount(name string, table string, where string, limit string) (string, error) {
|
||||
func (adapter *MysqlAdapter) SimpleCount(name string, table string, where string, limit string) (querystr string, err error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
|
@ -831,31 +667,12 @@ func (adapter *MysqlAdapter) SimpleCount(name string, table string, where string
|
|||
return "", errors.New("You need a name for this table")
|
||||
}
|
||||
|
||||
var querystr = "SELECT COUNT(*) AS `count` FROM `" + table + "`"
|
||||
|
||||
// TODO: Add support for BETWEEN x.x
|
||||
if len(where) != 0 {
|
||||
querystr += " WHERE"
|
||||
//log.Print("SimpleCount: ", name)
|
||||
//log.Print("where: ", where)
|
||||
//log.Print("processWhere: ", processWhere(where))
|
||||
for _, loc := range processWhere(where) {
|
||||
for _, token := range loc.Expr {
|
||||
switch token.Type {
|
||||
case "function", "operator", "number", "substitute":
|
||||
querystr += " " + token.Contents
|
||||
case "column":
|
||||
querystr += " `" + token.Contents + "`"
|
||||
case "string":
|
||||
querystr += " '" + token.Contents + "'"
|
||||
default:
|
||||
panic("This token doesn't exist o_o")
|
||||
}
|
||||
}
|
||||
querystr += " AND"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
querystr = "SELECT COUNT(*) AS `count` FROM `" + table + "`"
|
||||
whereStr, err := adapter.buildWhere(where)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
querystr += whereStr
|
||||
|
||||
if limit != "" {
|
||||
querystr += " LIMIT " + limit
|
||||
|
|
|
@ -351,7 +351,7 @@ var topic_alt_29 = []byte(`</textarea>
|
|||
<div class="button_container">
|
||||
`)
|
||||
var topic_alt_30 = []byte(`<a href="/topic/like/submit/`)
|
||||
var topic_alt_31 = []byte(`" class="action_button like_item">+1</a>`)
|
||||
var topic_alt_31 = []byte(`" class="action_button like_item add_like">+1</a>`)
|
||||
var topic_alt_32 = []byte(`<a href="/topic/edit/`)
|
||||
var topic_alt_33 = []byte(`" class="action_button open_edit">Edit</a>`)
|
||||
var topic_alt_34 = []byte(`<a href="/topic/delete/submit/`)
|
||||
|
@ -420,7 +420,7 @@ var topic_alt_72 = []byte(`</div>
|
|||
<div class="button_container">
|
||||
`)
|
||||
var topic_alt_73 = []byte(`<a href="/reply/like/submit/`)
|
||||
var topic_alt_74 = []byte(`" class="action_button like_item">+1</a>`)
|
||||
var topic_alt_74 = []byte(`" class="action_button like_item add_like">+1</a>`)
|
||||
var topic_alt_75 = []byte(`<a href="/reply/edit/submit/`)
|
||||
var topic_alt_76 = []byte(`" class="action_button edit_item">Edit</a>`)
|
||||
var topic_alt_77 = []byte(`<a href="/reply/delete/submit/`)
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
|
||||
<div class="button_container">
|
||||
{{if .CurrentUser.Loggedin}}
|
||||
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}" class="action_button like_item">+1</a>{{end}}
|
||||
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}" class="action_button like_item add_like">+1</a>{{end}}
|
||||
{{if .CurrentUser.Perms.EditTopic}}<a href="/topic/edit/{{.Topic.ID}}" class="action_button open_edit">Edit</a>{{end}}
|
||||
{{if .CurrentUser.Perms.DeleteTopic}}<a href="/topic/delete/submit/{{.Topic.ID}}" class="action_button delete_item">Delete</a>{{end}}
|
||||
{{if .CurrentUser.Perms.CloseTopic}}
|
||||
|
@ -68,7 +68,7 @@
|
|||
<div class="editable_block user_content" itemprop="text">{{.ContentHtml}}</div>
|
||||
<div class="button_container">
|
||||
{{if $.CurrentUser.Loggedin}}
|
||||
{{if $.CurrentUser.Perms.LikeItem}}<a href="/reply/like/submit/{{.ID}}" class="action_button like_item">+1</a>{{end}}
|
||||
{{if $.CurrentUser.Perms.LikeItem}}<a href="/reply/like/submit/{{.ID}}" class="action_button like_item add_like">+1</a>{{end}}
|
||||
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}" class="action_button edit_item">Edit</a>{{end}}
|
||||
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}" class="action_button delete_item">Delete</a>{{end}}
|
||||
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="action_button report_item">Report</a>
|
||||
|
|
|
@ -411,13 +411,20 @@ select, input, textarea {
|
|||
.topic_reply_form {
|
||||
margin: 0px;
|
||||
width: 100%;
|
||||
height: min-content;
|
||||
}
|
||||
.topic_reply_form .trumbowyg-button-pane:after {
|
||||
display: none;
|
||||
}
|
||||
.topic_reply_form .trumbowyg-box {
|
||||
min-height: auto;
|
||||
}
|
||||
.topic_reply_form .trumbowyg-editor {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
min-height: 103px;
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.topic_reply_form .quick_button_row {
|
||||
margin-bottom: 7px;
|
||||
|
@ -673,13 +680,21 @@ select, input, textarea {
|
|||
content: " likes";
|
||||
margin-right: 6px;
|
||||
}
|
||||
.created_at:before, .ip_item:before {
|
||||
|
||||
.post_item .add_like:after, .created_at:before, .ip_item:before {
|
||||
border-left: 1px solid var(--element-border-color);
|
||||
content: "";
|
||||
margin-right: 10px;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
.created_at:before, .ip_item:before {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.post_item .add_like:after {
|
||||
margin-left: 10px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.created_at {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
|
|
@ -9,8 +9,13 @@ $(document).ready(function(){
|
|||
$(".topic_create_form").addClass("selectedInput");
|
||||
});
|
||||
//$.trumbowyg.svgPath = false;
|
||||
$('#input_content').trumbowyg({
|
||||
$('.topic_create_form #input_content').trumbowyg({
|
||||
btns: [['viewHTML'],['undo','redo'],['formatting'],['strong','em','del'],['link'],['insertImage'],['unorderedList','orderedList'],['removeformat']],
|
||||
//hideButtonTexts: true
|
||||
});
|
||||
$('.topic_reply_form #input_content').trumbowyg({
|
||||
btns: [['viewHTML'],['undo','redo'],['formatting'],['strong','em','del'],['link'],['insertImage'],['unorderedList','orderedList'],['removeformat']],
|
||||
autogrow: true,
|
||||
//hideButtonTexts: true
|
||||
});
|
||||
});
|
|
@ -21,3 +21,6 @@ go get -u gopkg.in/sourcemap.v1
|
|||
|
||||
echo "Updating OttoJS"
|
||||
go get -u github.com/robertkrimen/otto
|
||||
|
||||
echo "Updating the Riot Search Engine"
|
||||
go get -u github.com/go-ego/riot
|
||||
|
|
|
@ -68,5 +68,12 @@ if %errorlevel% neq 0 (
|
|||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
echo Installing the Riot Search Engine
|
||||
go get -u github.com/go-ego/riot
|
||||
if %errorlevel% neq 0 (
|
||||
pause
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
echo The dependencies were successfully updated
|
||||
pause
|
||||
|
|
50
user.go
50
user.go
|
@ -228,11 +228,11 @@ func (user *User) ChangeGroup(group int) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
func (user *User) increasePostStats(wcount int, topic bool) error {
|
||||
func (user *User) increasePostStats(wcount int, topic bool) (err error) {
|
||||
var mod int
|
||||
baseScore := 1
|
||||
if topic {
|
||||
_, err := incrementUserTopicsStmt.Exec(1, user.ID)
|
||||
_, err = incrementUserTopicsStmt.Exec(1, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -241,24 +241,19 @@ func (user *User) increasePostStats(wcount int, topic bool) error {
|
|||
|
||||
settings := settingBox.Load().(SettingBox)
|
||||
if wcount >= settings["megapost_min_words"].(int) {
|
||||
_, err := incrementUserMegapostsStmt.Exec(1, 1, 1, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = incrementUserMegapostsStmt.Exec(1, 1, 1, user.ID)
|
||||
mod = 4
|
||||
} else if wcount >= settings["bigpost_min_words"].(int) {
|
||||
_, err := incrementUserBigpostsStmt.Exec(1, 1, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = incrementUserBigpostsStmt.Exec(1, 1, user.ID)
|
||||
mod = 1
|
||||
} else {
|
||||
_, err := incrementUserPostsStmt.Exec(1, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = incrementUserPostsStmt.Exec(1, user.ID)
|
||||
}
|
||||
_, err := incrementUserScoreStmt.Exec(baseScore+mod, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = incrementUserScoreStmt.Exec(baseScore+mod, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -269,11 +264,11 @@ func (user *User) increasePostStats(wcount int, topic bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (user *User) decreasePostStats(wcount int, topic bool) error {
|
||||
func (user *User) decreasePostStats(wcount int, topic bool) (err error) {
|
||||
var mod int
|
||||
baseScore := -1
|
||||
if topic {
|
||||
_, err := incrementUserTopicsStmt.Exec(-1, user.ID)
|
||||
_, err = incrementUserTopicsStmt.Exec(-1, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -282,24 +277,19 @@ func (user *User) decreasePostStats(wcount int, topic bool) error {
|
|||
|
||||
settings := settingBox.Load().(SettingBox)
|
||||
if wcount >= settings["megapost_min_words"].(int) {
|
||||
_, err := incrementUserMegapostsStmt.Exec(-1, -1, -1, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = incrementUserMegapostsStmt.Exec(-1, -1, -1, user.ID)
|
||||
mod = 4
|
||||
} else if wcount >= settings["bigpost_min_words"].(int) {
|
||||
_, err := incrementUserBigpostsStmt.Exec(-1, -1, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = incrementUserBigpostsStmt.Exec(-1, -1, user.ID)
|
||||
mod = 1
|
||||
} else {
|
||||
_, err := incrementUserPostsStmt.Exec(-1, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = incrementUserPostsStmt.Exec(-1, user.ID)
|
||||
}
|
||||
_, err := incrementUserScoreStmt.Exec(baseScore-mod, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = incrementUserScoreStmt.Exec(baseScore-mod, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue