-
Notifications
You must be signed in to change notification settings - Fork 34
/
post.go
487 lines (440 loc) · 13.3 KB
/
post.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
package model
import (
"database/sql"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"net/http"
"github.com/dingoblog/dingo/app/utils"
"github.com/russross/meddler"
)
const stmtGetPostById = `SELECT * FROM posts WHERE id = ?`
const stmtGetPostBySlug = `SELECT * FROM posts WHERE slug = ?`
const stmtGetPostsByTag = `SELECT * FROM posts WHERE %s id IN ( SELECT post_id FROM posts_tags WHERE tag_id = ? ) ORDER BY published_at DESC LIMIT ? OFFSET ?`
const stmtGetAllPostsByTag = `SELECT * FROM posts WHERE id IN ( SELECT post_id FROM posts_tags WHERE tag_id = ?) ORDER BY published_at DESC `
const stmtGetPostsCountByTag = "SELECT count(*) FROM posts, posts_tags WHERE posts_tags.post_id = posts.id AND posts.published AND posts_tags.tag_id = ?"
const stmtGetPostsOffsetLimit = `SELECT * FROM posts WHERE published = ? LIMIT ?, ?`
const stmtInsertPostTag = `INSERT INTO posts_tags (id, post_id, tag_id) VALUES (?, ?, ?)`
const stmtDeletePostTagsByPostId = `DELETE FROM posts_tags WHERE post_id = ?`
const stmtNumberOfPosts = "SELECT count(*) FROM posts WHERE %s"
const stmtGetAllPostList = `SELECT * FROM posts WHERE %s ORDER BY %s`
const stmtGetPostList = `SELECT * FROM posts WHERE %s ORDER BY %s LIMIT ? OFFSET ?`
const stmtDeletePostById = `DELETE FROM posts WHERE id = ?`
var safeOrderByStmt = map[string]string{
"created_at": "created_at",
"created_at DESC": "created_at DESC",
"updated_at": "updated_at",
"updated_at DESC": "updated_at DESC",
"published_at": "published_at",
"published_at DESC": "published_at DESC",
}
// A Post contains all the content required to populate a post or page on the
// blog. It also contains info to help sort and display the post.
type Post struct {
Id int64 `meddler:"id,pk",json:"id"`
Title string `meddler:"title",json:"title"`
Slug string `meddler:"slug",json:"slug"`
Markdown string `meddler:"markdown",json:"markdown"`
Html string `meddler:"html",json:"html"`
Image string `meddler:"image",json:"image"`
IsFeatured bool `meddler:"featured",json:"featured"`
IsPage bool `meddler:"page",json:"is_page"` // Using "is_page" instead of "page" since nouns are generally non-bools
AllowComment bool `meddler:"allow_comment",json:"allow_comment"`
CommentNum int64 `meddler:"comment_num",json:"comment_num"`
IsPublished bool `meddler:"published",json:"published"`
Language string `meddler:"language",json:"language"`
MetaTitle string `meddler:"meta_title",json:"meta_title"`
MetaDescription string `meddler:"meta_description",json:"meta_description"`
CreatedAt *time.Time `meddler:"created_at",json:"created_at"`
CreatedBy int64 `meddler:"created_by",json:"created_by"`
UpdatedAt *time.Time `meddler:"updated_at",json:"updated_at"`
UpdatedBy int64 `meddler:"updated_by",json:"updated_by"`
PublishedAt *time.Time `meddler:"published_at",json:"published_at"`
PublishedBy int64 `meddler:"published_by",json:"published_by"`
Hits int64 `meddler:"-"`
Category string `meddler:"-"`
}
// Posts is a slice of "Post"s
type Posts []*Post
// Len returns the amount of "Post"s.
func (p Posts) Len() int {
return len(p)
}
// Get returns the Post at the given index.
func (p Posts) Get(i int) *Post {
return p[i]
}
func (p Posts) AppendPosts(posts Posts) {
for i := range posts {
p = append(p, posts[i])
}
}
// NewPost creates a new Post, with CreatedAt set to the current time.
func NewPost() *Post {
return &Post{
CreatedAt: utils.Now(),
}
}
// TagString returns all the tags associated with a post as a single string.
func (p *Post) TagString() string {
tags := new(Tags)
_ = tags.GetTagsByPostId(p.Id)
var tagString string
for i := 0; i < tags.Len(); i++ {
if i != tags.Len()-1 {
tagString += tags.Get(i).Name + ", "
} else {
tagString += tags.Get(i).Name
}
}
return tagString
}
// Url returns the URL of the post.
func (p *Post) Url() string {
return "/" + p.Slug
}
// Tags returns a slice of every tag associated with the post.
func (p *Post) Tags() []*Tag {
tags := new(Tags)
err := tags.GetTagsByPostId(p.Id)
if err != nil {
return nil
}
return tags.GetAll()
}
// Author returns the User who authored the post.
func (p *Post) Author() *User {
user := &User{Id: p.CreatedBy}
err := user.GetUserById()
if err != nil {
return ghostUser
}
return user
}
// Comments returns all the comments associated with the post.
func (p *Post) Comments() []*Comment {
comments := new(Comments)
err := comments.GetCommentsByPostId(p.Id)
if err != nil {
return nil
}
return comments.GetAll()
}
// Summary returns the post summary.
func (p *Post) Summary() string {
text := strings.Split(p.Markdown, "<!--more-->")[0]
return utils.Markdown2Html(text)
}
// Excerpt returns the post execerpt, with a default length of 255 characters.
func (p *Post) Excerpt() string {
return utils.Html2Excerpt(p.Html, 255)
}
// Save saves a post to the DB, updating any given tags to include the Post ID.
func (p *Post) Save(tags ...*Tag) error {
p.Slug = strings.TrimLeft(p.Slug, "/")
p.Slug = strings.TrimRight(p.Slug, "/")
if p.Slug == "" {
return fmt.Errorf("Slug can not be empty or root")
}
if p.IsPublished {
p.PublishedAt = utils.Now()
p.PublishedBy = p.CreatedBy
}
p.UpdatedAt = utils.Now()
p.UpdatedBy = p.CreatedBy
if p.Id == 0 {
// Insert post
if err := p.Insert(); err != nil {
return err
}
} else {
if err := p.Update(); err != nil {
return err
}
}
tagIds := make([]int64, 0)
// Insert tags
for _, t := range tags {
t.CreatedAt = utils.Now()
t.CreatedBy = p.CreatedBy
t.Hidden = !p.IsPublished
t.Save()
tagIds = append(tagIds, t.Id)
}
// Delete old post-tag projections
err := DeletePostTagsByPostId(p.Id)
// Insert postTags
if err != nil {
return err
}
for _, tagId := range tagIds {
err := InsertPostTag(p.Id, tagId)
if err != nil {
return err
}
}
return DeleteOldTags()
}
// Insert saves a post to the DB.
func (p *Post) Insert() error {
if !PostChangeSlug(p.Slug) {
p.Slug = generateNewSlug(p.Slug, 1)
}
err := meddler.Insert(db, "posts", p)
return err
}
// InsertPostTag saves the Post ID to the given Tag ID in the DB.
func InsertPostTag(postID int64, tagID int64) error {
writeDB, err := db.Begin()
if err != nil {
writeDB.Rollback()
return err
}
_, err = writeDB.Exec(stmtInsertPostTag, nil, postID, tagID)
if err != nil {
writeDB.Rollback()
return err
}
return writeDB.Commit()
}
// Update updates an existing post in the DB.
func (p *Post) Update() error {
currentPost := &Post{Id: p.Id}
err := currentPost.GetPostById()
if err != nil {
return err
}
if p.Slug != currentPost.Slug && !PostChangeSlug(p.Slug) {
p.Slug = generateNewSlug(p.Slug, 1)
}
err = meddler.Update(db, "posts", p)
return err
}
// UpdateFromRequest updates an existing Post in the DB based on the data
// provided in the HTTP request.
func (p *Post) UpdateFromRequest(r *http.Request) {
p.Title = r.FormValue("title")
p.Image = r.FormValue("image")
p.Slug = r.FormValue("slug")
p.Markdown = r.FormValue("content")
p.Html = utils.Markdown2Html(p.Markdown)
p.AllowComment = r.FormValue("comment") == "on"
p.Category = r.FormValue("category")
p.IsPublished = r.FormValue("status") == "on"
}
func (p *Post) UpdateFromJSON(j []byte) error {
err := json.Unmarshal(j, p)
if err != nil {
return err
}
p.Html = utils.Markdown2Html(p.Markdown)
return nil
}
func (p *Post) Publish(by int64) error {
p.PublishedAt = utils.Now()
p.PublishedBy = by
p.IsPublished = true
err := meddler.Update(db, "posts", p)
return err
}
// DeletePostTagsByPostId deletes removes tags associated with the given post
// from the DB.
func DeletePostTagsByPostId(post_id int64) error {
writeDB, err := db.Begin()
if err != nil {
writeDB.Rollback()
return err
}
_, err = writeDB.Exec(stmtDeletePostTagsByPostId, post_id)
if err != nil {
writeDB.Rollback()
return err
}
return writeDB.Commit()
}
// DeletePostById deletes the given Post from the DB.
func DeletePostById(id int64) error {
writeDB, err := db.Begin()
if err != nil {
writeDB.Rollback()
return err
}
_, err = writeDB.Exec(stmtDeletePostById, id)
if err != nil {
writeDB.Rollback()
return err
}
err = writeDB.Commit()
if err != nil {
return err
}
err = DeletePostTagsByPostId(id)
if err != nil {
return err
}
return DeleteOldTags()
}
// GetPostById gets the post based on the Post ID.
func (post *Post) GetPostById(id ...int64) error {
var postId int64
if len(id) == 0 {
postId = post.Id
} else {
postId = id[0]
}
err := meddler.QueryRow(db, post, stmtGetPostById, postId)
return err
}
// GetPostBySlug gets the post based on the Post Slug.
func (p *Post) GetPostBySlug(slug string) error {
err := meddler.QueryRow(db, p, stmtGetPostBySlug, slug)
return err
}
// GetPostsByTag returns a new pager based all the Posts associated with a Tag.
func (p *Posts) GetPostsByTag(tagId, page, size int64, onlyPublished bool) (*utils.Pager, error) {
var (
pager *utils.Pager
count int64
)
row := db.QueryRow(stmtGetPostsCountByTag, tagId)
err := row.Scan(&count)
if err != nil {
utils.LogOnError(err, "Unable to get posts by tag.", true)
return nil, err
}
pager = utils.NewPager(page, size, count)
if !pager.IsValid {
return pager, fmt.Errorf("Page not found")
}
var where string
if onlyPublished {
where = "published AND"
}
err = meddler.QueryAll(db, p, fmt.Sprintf(stmtGetPostsByTag, where), tagId, size, pager.Begin)
return pager, err
}
// GetAllPostsByTag gets all the Posts with the associated Tag.
func (p *Posts) GetAllPostsByTag(tagId int64) error {
err := meddler.QueryAll(db, p, stmtGetAllPostsByTag, tagId)
return err
}
// GetNumberOfPosts gets the total number of posts in the DB.
func GetNumberOfPosts(isPage bool, published bool) (int64, error) {
var count int64
var where string
if isPage {
where = `page = 1`
} else {
where = `page = 0`
}
if published {
where = where + ` AND published`
}
var row *sql.Row
row = db.QueryRow(fmt.Sprintf(stmtNumberOfPosts, where))
err := row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
// GetPostList returns a new pager based on all the posts in the DB.
func (posts *Posts) GetPostList(page, size int64, isPage bool, onlyPublished bool, orderBy string) (*utils.Pager, error) {
var pager *utils.Pager
count, err := GetNumberOfPosts(isPage, onlyPublished)
pager = utils.NewPager(page, size, count)
if !pager.IsValid {
return pager, fmt.Errorf("Page not found")
}
var where string
if isPage {
where = `page = 1`
} else {
where = `page = 0`
}
if onlyPublished {
where = where + ` AND published`
}
safeOrderBy := getSafeOrderByStmt(orderBy)
err = meddler.QueryAll(db, posts, fmt.Sprintf(stmtGetPostList, where, safeOrderBy), size, pager.Begin)
return pager, err
}
// GetAllPostList gets all the posts, with the options to get only pages, or
// only published posts. It is also possible to order the posts, with the order
// by string being one of six options:
// "created_at"
// "created_at DESC"
// "updated_at"
// "updated_at DESC"
// "published_at"
// "published_at DESC"
func (p *Posts) GetAllPostList(isPage bool, onlyPublished bool, orderBy string) error {
var where string
if isPage {
where = `page = 1`
} else {
where = `page = 0`
}
if onlyPublished {
where = where + ` AND published`
}
safeOrderBy := getSafeOrderByStmt(orderBy)
err := meddler.QueryAll(db, p, fmt.Sprintf(stmtGetAllPostList, where, safeOrderBy))
return err
}
// PostChangeSlug checks to see if there is a post associated with the given
// slug, and returns true if there isn't.
func PostChangeSlug(slug string) bool {
post := new(Post)
err := post.GetPostBySlug(slug)
if err != nil {
return true
}
return false
}
func generateNewSlug(slug string, suffix int) string {
newSlug := slug + "-" + strconv.Itoa(suffix)
if !PostChangeSlug(newSlug) {
return generateNewSlug(slug, suffix+1)
}
return newSlug
}
// getSafeOrderByStmt returns a safe `ORDER BY` statement to be when used when
// building SQL queries, in order to prevent SQL injection.
//
// Since we can't use the placeholder `?` to specify the `ORDER BY` values in
// queries, we need to build them using `fmt.Sprintf`. Typically, doing so
// would open you up to SQL injection attacks, since any string can be passed
// into `fmt.Sprintf`, including strings that are valid SQL queries! By using
// this function to check a map of safe values, we guarantee that no unsafe
// values are ever passed to our query building function.
func getSafeOrderByStmt(orderBy string) string {
if stmt, ok := safeOrderByStmt[orderBy]; ok {
return stmt
}
return "published_at DESC"
}
func GetPublishedPosts(offset, limit int) (Posts, error) {
var posts Posts
err := meddler.QueryAll(db, &posts, stmtGetPostsOffsetLimit, 1, offset, limit)
return posts, err
}
func GetUnpublishedPosts(offset, limit int) (Posts, error) {
var posts Posts
err := meddler.QueryAll(db, &posts, stmtGetPostsOffsetLimit, 0, offset, limit)
return posts, err
}
func GetAllPosts(offset, limit int) ([]*Post, error) {
pubPosts, err := GetPublishedPosts(offset, limit)
if err != nil {
return nil, err
}
unpubPosts, err := GetUnpublishedPosts(offset, limit)
if err != nil {
return nil, err
}
posts := append(pubPosts, unpubPosts...)
return posts, nil
}