f85bf51103
Made progress with an internal error logging component for the Control Panel. Made LogWarning less susceptible to races and slightly improved log contention. Revamped the registration page on Cosora. Added the SanitiseSingleLine and SanitiseBody functions to better centralise sanitisation and to ensure more consistent sanitisation. Zero length spaces are no longer permitted in usernames to help prevent impersonation. More to come in this area. Plugins / internal components can now schedule hourly tasks. Reduced the chances of newlines breaking the visual layout in areas which expect none. Added the register_account_anti_spam phrase.
179 lines
5.9 KiB
Go
179 lines
5.9 KiB
Go
/*
|
|
*
|
|
* Gosora Topic Store
|
|
* Copyright Azareal 2017 - 2018
|
|
*
|
|
*/
|
|
package common
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"strings"
|
|
|
|
"../query_gen/lib"
|
|
)
|
|
|
|
// TODO: Add the watchdog goroutine
|
|
// TODO: Add BulkGetMap
|
|
// TODO: Add some sort of update method
|
|
// ? - Should we add stick, lock, unstick, and unlock methods? These might be better on the Topics not the TopicStore
|
|
var Topics TopicStore
|
|
var ErrNoTitle = errors.New("This message is missing a title")
|
|
var ErrLongTitle = errors.New("The title is too long")
|
|
var ErrNoBody = errors.New("This message is missing a body")
|
|
|
|
type TopicStore interface {
|
|
DirtyGet(id int) *Topic
|
|
Get(id int) (*Topic, error)
|
|
BypassGet(id int) (*Topic, error)
|
|
Exists(id int) bool
|
|
Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error)
|
|
AddLastTopic(item *Topic, fid int) error // unimplemented
|
|
Reload(id int) error // Too much SQL logic to move into TopicCache
|
|
// TODO: Implement these two methods
|
|
//Replies(tid int) ([]*Reply, error)
|
|
//RepliesRange(tid int, lower int, higher int) ([]*Reply, error)
|
|
GlobalCount() int
|
|
|
|
SetCache(cache TopicCache)
|
|
GetCache() TopicCache
|
|
}
|
|
|
|
type DefaultTopicStore struct {
|
|
cache TopicCache
|
|
|
|
get *sql.Stmt
|
|
exists *sql.Stmt
|
|
topicCount *sql.Stmt
|
|
create *sql.Stmt
|
|
}
|
|
|
|
// NewDefaultTopicStore gives you a new instance of DefaultTopicStore
|
|
func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) {
|
|
acc := qgen.Builder.Accumulator()
|
|
if cache == nil {
|
|
cache = NewNullTopicCache()
|
|
}
|
|
return &DefaultTopicStore{
|
|
cache: cache,
|
|
get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, poll, data").Where("tid = ?").Prepare(),
|
|
exists: acc.Select("topics").Columns("tid").Where("tid = ?").Prepare(),
|
|
topicCount: acc.Count("topics").Prepare(),
|
|
create: acc.Insert("topics").Columns("parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ipaddress, words, createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(),
|
|
}, acc.FirstError()
|
|
}
|
|
|
|
func (mts *DefaultTopicStore) DirtyGet(id int) *Topic {
|
|
topic, err := mts.cache.Get(id)
|
|
if err == nil {
|
|
return topic
|
|
}
|
|
|
|
topic = &Topic{ID: id}
|
|
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
|
|
if err == nil {
|
|
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
|
|
_ = mts.cache.Add(topic)
|
|
return topic
|
|
}
|
|
return BlankTopic()
|
|
}
|
|
|
|
// TODO: Log weird cache errors?
|
|
func (mts *DefaultTopicStore) Get(id int) (topic *Topic, err error) {
|
|
topic, err = mts.cache.Get(id)
|
|
if err == nil {
|
|
return topic, nil
|
|
}
|
|
|
|
topic = &Topic{ID: id}
|
|
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
|
|
if err == nil {
|
|
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
|
|
_ = mts.cache.Add(topic)
|
|
}
|
|
return topic, err
|
|
}
|
|
|
|
// BypassGet will always bypass the cache and pull the topic directly from the database
|
|
func (mts *DefaultTopicStore) BypassGet(id int) (*Topic, error) {
|
|
topic := &Topic{ID: id}
|
|
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
|
|
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
|
|
return topic, err
|
|
}
|
|
|
|
func (mts *DefaultTopicStore) Reload(id int) error {
|
|
topic := &Topic{ID: id}
|
|
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
|
|
if err == nil {
|
|
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
|
|
_ = mts.cache.Set(topic)
|
|
} else {
|
|
_ = mts.cache.Remove(id)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (mts *DefaultTopicStore) Exists(id int) bool {
|
|
return mts.exists.QueryRow(id).Scan(&id) == nil
|
|
}
|
|
|
|
func (mts *DefaultTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) {
|
|
if topicName == "" {
|
|
return 0, ErrNoTitle
|
|
}
|
|
// ? This number might be a little screwy with Unicode, but it's the only consistent thing we have, as Unicode characters can be any number of bytes in theory?
|
|
if len(topicName) > Config.MaxTopicTitleLength {
|
|
return 0, ErrLongTitle
|
|
}
|
|
|
|
parsedContent := strings.TrimSpace(ParseMessage(content, fid, "forums"))
|
|
if parsedContent == "" {
|
|
return 0, ErrNoBody
|
|
}
|
|
|
|
wcount := WordCount(content)
|
|
// TODO: Move this statement into the topic store
|
|
res, err := mts.create.Exec(fid, topicName, content, parsedContent, uid, ipaddress, wcount, uid)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
lastID, err := res.LastInsertId()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return int(lastID), Forums.AddTopic(int(lastID), uid, fid)
|
|
}
|
|
|
|
// ? - What is this? Do we need it? Should it be in the main store interface?
|
|
func (mts *DefaultTopicStore) AddLastTopic(item *Topic, fid int) error {
|
|
// Coming Soon...
|
|
return nil
|
|
}
|
|
|
|
// GlobalCount returns the total number of topics on these forums
|
|
func (mts *DefaultTopicStore) GlobalCount() (tcount int) {
|
|
err := mts.topicCount.QueryRow().Scan(&tcount)
|
|
if err != nil {
|
|
LogError(err)
|
|
}
|
|
return tcount
|
|
}
|
|
|
|
func (mts *DefaultTopicStore) SetCache(cache TopicCache) {
|
|
mts.cache = cache
|
|
}
|
|
|
|
// TODO: We're temporarily doing this so that you can do tcache != nil in getTopicUser. Refactor it.
|
|
func (mts *DefaultTopicStore) GetCache() TopicCache {
|
|
_, ok := mts.cache.(*NullTopicCache)
|
|
if ok {
|
|
return nil
|
|
}
|
|
return mts.cache
|
|
}
|