github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/routes/topic.go (about) 1 package routes 2 3 import ( 4 "crypto/sha256" 5 "database/sql" 6 "encoding/hex" 7 "encoding/json" 8 "errors" 9 "io" 10 11 "image" 12 "image/gif" 13 "image/jpeg" 14 "image/png" 15 "log" 16 "net/http" 17 "os" 18 "regexp" 19 "strconv" 20 "strings" 21 22 "golang.org/x/image/tiff" 23 24 c "github.com/Azareal/Gosora/common" 25 co "github.com/Azareal/Gosora/common/counters" 26 p "github.com/Azareal/Gosora/common/phrases" 27 qgen "github.com/Azareal/Gosora/query_gen" 28 ) 29 30 type TopicStmts struct { 31 getLikedTopic *sql.Stmt 32 } 33 34 var topicStmts TopicStmts 35 36 // TODO: Move these DbInits into a TopicList abstraction 37 func init() { 38 c.DbInits.Add(func(acc *qgen.Accumulator) error { 39 topicStmts = TopicStmts{ 40 getLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy=? && targetItem=? && targetType='topics'").Prepare(), 41 } 42 return acc.FirstError() 43 }) 44 } 45 46 func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header, urlBit string) c.RouteError { 47 page, _ := strconv.Atoi(r.FormValue("page")) 48 _, tid, err := ParseSEOURL(urlBit) 49 if err != nil { 50 return c.SimpleError(p.GetErrorPhrase("url_id_must_be_integer"), w, r, h) 51 } 52 53 // Get the topic... 54 topic, err := c.GetTopicUser(user, tid) 55 if err == sql.ErrNoRows { 56 return c.NotFound(w, r, nil) // TODO: Can we add a simplified invocation of header here? This is likely to be an extremely common NotFound 57 } else if err != nil { 58 return c.InternalError(err, w, r) 59 } 60 61 ferr := c.ForumUserCheck(h, w, r, user, topic.ParentID) 62 if ferr != nil { 63 return ferr 64 } 65 if !user.Perms.ViewTopic { 66 return c.NoPermissions(w, r, user) 67 } 68 h.Title = topic.Title 69 h.Path = topic.Link 70 //h.Path = c.BuildTopicURL(c.NameToSlug(topic.Title), topic.ID) 71 72 postGroup, err := c.Groups.Get(topic.Group) 73 if err != nil { 74 return c.InternalError(err, w, r) 75 } 76 77 topic.ContentLines = strings.Count(topic.Content, "\n") 78 if !user.Loggedin && user.LastAgent != c.SimpleBots[0] && user.LastAgent != c.SimpleBots[1] { 79 if len(topic.Content) > 200 { 80 h.OGDesc = topic.Content[:197] + "..." 81 } else { 82 h.OGDesc = topic.Content 83 } 84 h.OGDesc = c.H_topic_ogdesc_assign_hook(h.Hooks, h.OGDesc) 85 } 86 87 var parseSettings *c.ParseSettings 88 if (c.Config.NoEmbed || !postGroup.Perms.AutoEmbed) && (user.ParseSettings == nil || !user.ParseSettings.NoEmbed) { 89 parseSettings = c.DefaultParseSettings.CopyPtr() 90 parseSettings.NoEmbed = true 91 } else { 92 parseSettings = user.ParseSettings 93 } 94 95 // TODO: Cache ContentHTML when possible? 96 topic.ContentHTML, h.ExternalMedia = c.ParseMessage2(topic.Content, topic.ParentID, "forums", parseSettings, user) 97 // TODO: Do this more efficiently by avoiding the allocations entirely in ParseMessage, if there's nothing to do. 98 if topic.ContentHTML == topic.Content { 99 topic.ContentHTML = topic.Content 100 } 101 102 topic.Tag = postGroup.Tag 103 if postGroup.IsMod { 104 topic.ClassName = c.Config.StaffCSS 105 } 106 topic.Deletable = user.Perms.DeleteTopic || topic.CreatedBy == user.ID 107 108 forum, err := c.Forums.Get(topic.ParentID) 109 if err != nil { 110 return c.InternalError(err, w, r) 111 } 112 113 var poll *c.Poll 114 if topic.Poll != 0 { 115 pPoll, err := c.Polls.Get(topic.Poll) 116 if err != nil { 117 log.Print("Couldn't find the attached poll for topic " + strconv.Itoa(topic.ID)) 118 return c.InternalError(err, w, r) 119 } 120 poll = new(c.Poll) 121 *poll = pPoll.Copy() 122 } 123 124 if topic.LikeCount > 0 && user.Liked > 0 { 125 var disp int // Discard this value 126 err = topicStmts.getLikedTopic.QueryRow(user.ID, topic.ID).Scan(&disp) 127 if err == nil { 128 topic.Liked = true 129 } else if err != nil && err != sql.ErrNoRows { 130 return c.InternalError(err, w, r) 131 } 132 } 133 134 if topic.AttachCount > 0 { 135 attachs, err := c.Attachments.MiniGetList("topics", topic.ID) 136 if err != nil && err != sql.ErrNoRows { 137 // TODO: We might want to be a little permissive here in-case of a desync? 138 return c.InternalError(err, w, r) 139 } 140 topic.Attachments = attachs 141 } 142 143 // Calculate the offset 144 offset, page, lastPage := c.PageOffset(topic.PostCount, page, c.Config.ItemsPerPage) 145 pageList := c.Paginate(page, lastPage, 5) 146 tpage := c.TopicPage{h, nil, topic, forum, poll, c.Paginator{pageList, page, lastPage}} 147 148 // Get the replies if we have any... 149 if topic.PostCount > 0 { 150 /*var pFrag int 151 if strings.HasPrefix(r.URL.Fragment, "post-") { 152 pFrag, _ = strconv.Atoi(strings.TrimPrefix(r.URL.Fragment, "post-")) 153 }*/ 154 rlist, externalHead, err := topic.Replies(offset /* pFrag,*/, user) 155 if err == sql.ErrNoRows { 156 return c.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user) 157 } else if err != nil { 158 return c.InternalError(err, w, r) 159 } 160 //fmt.Printf("rlist: %+v\n",rlist) 161 tpage.ItemList = rlist 162 if externalHead { 163 h.ExternalMedia = true 164 } 165 } 166 167 h.Zone = "view_topic" 168 h.ZoneID = topic.ID 169 h.ZoneData = topic 170 171 var rerr c.RouteError 172 tmpl := forum.Tmpl 173 if r.FormValue("i") == "1" { 174 if tpage.Poll != nil { 175 h.AddXRes("chartist/chartist.min.css", "chartist/chartist.min.js") 176 } 177 if tmpl == "" { 178 rerr = renderTemplate("topic_mini", w, r, h, tpage) 179 } else { 180 tmpl = "topic_mini" + tmpl 181 err = renderTemplate3(tmpl, tmpl, w, r, h, tpage) 182 if err != nil { 183 rerr = renderTemplate("topic_mini", w, r, h, tpage) 184 } 185 } 186 } else { 187 if tpage.Poll != nil { 188 h.AddSheet("chartist/chartist.min.css") 189 h.AddScript("chartist/chartist.min.js") 190 } 191 if tmpl == "" { 192 rerr = renderTemplate("topic", w, r, h, tpage) 193 } else { 194 tmpl = "topic_" + tmpl 195 err = renderTemplate3(tmpl, tmpl, w, r, h, tpage) 196 if err != nil { 197 rerr = renderTemplate("topic", w, r, h, tpage) 198 } 199 } 200 } 201 co.TopicViewCounter.Bump(topic.ID) // TODO: Move this into the router? 202 co.ForumViewCounter.Bump(topic.ParentID) 203 return rerr 204 } 205 206 func AttachTopicActCommon(w http.ResponseWriter, r *http.Request, u *c.User, stid string) (t *c.Topic, ferr c.RouteError) { 207 tid, e := strconv.Atoi(stid) 208 if e != nil { 209 return t, c.LocalErrorJS(p.GetErrorPhrase("id_must_be_integer"), w, r) 210 } 211 t, e = c.Topics.Get(tid) 212 if e != nil { 213 return t, c.NotFoundJS(w, r) 214 } 215 _, ferr = c.SimpleForumUserCheck(w, r, u, t.ParentID) 216 if ferr != nil { 217 return t, ferr 218 } 219 if t.IsClosed && !u.Perms.CloseTopic { 220 return t, c.NoPermissionsJS(w, r, u) 221 } 222 return t, nil 223 } 224 225 // TODO: Avoid uploading this again if the attachment already exists? They'll resolve to the same hash either way, but we could save on some IO / bandwidth here 226 // TODO: Enforce the max request limit on all of this topic's attachments 227 // TODO: Test this route 228 func AddAttachToTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid string) c.RouteError { 229 topic, ferr := AttachTopicActCommon(w, r, u, stid) 230 if ferr != nil { 231 return ferr 232 } 233 if !u.Perms.ViewTopic || !u.Perms.EditTopic || !u.Perms.UploadFiles { 234 return c.NoPermissionsJS(w, r, u) 235 } 236 237 // Handle the file attachments 238 pathMap, rerr := uploadAttachment(w, r, u, topic.ParentID, "forums", topic.ID, "topics", "") 239 if rerr != nil { 240 // TODO: This needs to be a JS error... 241 return rerr 242 } 243 if len(pathMap) == 0 { 244 return c.InternalErrorJS(errors.New("no paths for attachment add"), w, r) 245 } 246 247 var elemStr string 248 for path, aids := range pathMap { 249 elemStr += "\"" + path + "\":\"" + aids + "\"," 250 } 251 if len(elemStr) > 1 { 252 elemStr = elemStr[:len(elemStr)-1] 253 } 254 255 w.Write([]byte(`{"success":1,"elems":[{` + elemStr + `}]}`)) 256 return nil 257 } 258 259 func RemoveAttachFromTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid string) c.RouteError { 260 _, ferr := AttachTopicActCommon(w, r, u, stid) 261 if ferr != nil { 262 return ferr 263 } 264 if !u.Perms.ViewTopic || !u.Perms.EditTopic { 265 return c.NoPermissionsJS(w, r, u) 266 } 267 268 for _, said := range strings.Split(r.PostFormValue("aids"), ",") { 269 aid, err := strconv.Atoi(said) 270 if err != nil { 271 return c.LocalErrorJS(p.GetErrorPhrase("id_must_be_integer"), w, r) 272 } 273 rerr := deleteAttachment(w, r, u, aid, true) 274 if rerr != nil { 275 // TODO: This needs to be a JS error... 276 return rerr 277 } 278 } 279 280 w.Write(successJSONBytes) 281 return nil 282 } 283 284 // ? - Should we add a new permission or permission zone (like per-forum permissions) specifically for profile comment creation 285 // ? - Should we allow banned users to make reports? How should we handle report abuse? 286 // TODO: Add a permission to stop certain users from using custom avatars 287 // ? - Log username changes and put restrictions on this? 288 // TODO: Test this 289 // TODO: Revamp this route 290 func CreateTopic(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header, sfid string) c.RouteError { 291 var fid int 292 var err error 293 if sfid != "" { 294 fid, err = strconv.Atoi(sfid) 295 if err != nil { 296 return c.LocalError(p.GetErrorPhrase("url_id_must_be_integer"), w, r, u) 297 } 298 } 299 if fid == 0 { 300 fid = c.Config.DefaultForum 301 } 302 303 ferr := c.ForumUserCheck(h, w, r, u, fid) 304 if ferr != nil { 305 return ferr 306 } 307 if !u.Perms.ViewTopic || !u.Perms.CreateTopic { 308 return c.NoPermissions(w, r, u) 309 } 310 // TODO: Add a phrase for this 311 h.Title = p.GetTitlePhrase("create_topic") 312 h.Zone = "create_topic" 313 314 // Lock this to the forum being linked? 315 // Should we always put it in strictmode when it's linked from another forum? Well, the user might end up changing their mind on what forum they want to post in and it would be a hassle, if they had to switch pages, even if it is a single click for many (exc. mobile) 316 var strict bool 317 h.Hooks.VhookNoRet("topic_create_pre_loop", w, r, fid, h, u, &strict) 318 319 // TODO: Re-add support for plugin_guilds 320 var forumList []c.Forum 321 var canSee []int 322 if u.IsSuperAdmin { 323 canSee, err = c.Forums.GetAllVisibleIDs() 324 if err != nil { 325 return c.InternalError(err, w, r) 326 } 327 } else { 328 group, err := c.Groups.Get(u.Group) 329 if err != nil { 330 // TODO: Refactor this 331 c.LocalError("Something weird happened behind the scenes", w, r, u) 332 log.Printf("Group #%d doesn't exist, but it's set on c.User #%d", u.Group, u.ID) 333 return nil 334 } 335 canSee = group.CanSee 336 } 337 338 // TODO: plugin_superadmin needs to be able to override this loop. Skip flag on topic_create_pre_loop? 339 for _, ffid := range canSee { 340 // TODO: Surely, there's a better way of doing this. I've added it in for now to support plugin_guilds, but we really need to clean this up 341 if strict && ffid != fid { 342 continue 343 } 344 345 // Do a bulk forum fetch, just in case it's the SqlForumStore? 346 f := c.Forums.DirtyGet(ffid) 347 if f.Name != "" && f.Active { 348 fcopy := f.Copy() 349 // TODO: Abstract this 350 //if h.Hooks.HookSkip("topic_create_frow_assign", &fcopy) { 351 if c.H_topic_create_frow_assign_hook(h.Hooks, &fcopy) { 352 continue 353 } 354 forumList = append(forumList, fcopy) 355 } 356 } 357 358 return renderTemplate("create_topic", w, r, h, c.CreateTopicPage{h, forumList, fid}) 359 } 360 361 func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 362 fid, err := strconv.Atoi(r.PostFormValue("board")) 363 if err != nil { 364 return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, u) 365 } 366 // TODO: Add hooks to make use of headerLite 367 lite, ferr := c.SimpleForumUserCheck(w, r, u, fid) 368 if ferr != nil { 369 return ferr 370 } 371 if !u.Perms.ViewTopic || !u.Perms.CreateTopic { 372 return c.NoPermissions(w, r, u) 373 } 374 375 name := c.SanitiseSingleLine(r.PostFormValue("name")) 376 content := c.PreparseMessage(r.PostFormValue("content")) 377 // TODO: Fully parse the post and store it in the parsed column 378 tid, err := c.Topics.Create(fid, name, content, u.ID, u.GetIP()) 379 if err != nil { 380 switch err { 381 case c.ErrNoRows: 382 return c.LocalError("Something went wrong, perhaps the forum got deleted?", w, r, u) 383 case c.ErrNoTitle: 384 return c.LocalError("This topic doesn't have a title", w, r, u) 385 case c.ErrLongTitle: 386 return c.LocalError("The length of the title is too long, max: "+strconv.Itoa(c.Config.MaxTopicTitleLength), w, r, u) 387 case c.ErrNoBody: 388 return c.LocalError("This topic doesn't have a body", w, r, u) 389 } 390 return c.InternalError(err, w, r) 391 } 392 393 topic, err := c.Topics.Get(tid) 394 if err != nil { 395 return c.LocalError("Unable to load the topic", w, r, u) 396 } 397 if r.PostFormValue("has_poll") == "1" { 398 maxPollOptions := 10 399 pollInputItems := make(map[int]string) 400 for key, values := range r.Form { 401 if !strings.HasPrefix(key, "pollinputitem[") { 402 continue 403 } 404 halves := strings.Split(key, "[") 405 if len(halves) != 2 { 406 return c.LocalError("Malformed pollinputitem", w, r, u) 407 } 408 halves[1] = strings.TrimSuffix(halves[1], "]") 409 410 index, err := strconv.Atoi(halves[1]) 411 if err != nil { 412 return c.LocalError("Malformed pollinputitem", w, r, u) 413 } 414 for _, value := range values { 415 // If there are duplicates, then something has gone horribly wrong, so let's ignore them, this'll likely happen during an attack 416 _, exists := pollInputItems[index] 417 // TODO: Should we use SanitiseBody instead to keep the newlines? 418 if !exists && len(c.SanitiseSingleLine(value)) != 0 { 419 pollInputItems[index] = c.SanitiseSingleLine(value) 420 if len(pollInputItems) >= maxPollOptions { 421 break 422 } 423 } 424 } 425 } 426 427 if len(pollInputItems) > 0 { 428 // Make sure the indices are sequential to avoid out of bounds issues 429 seqPollInputItems := make(map[int]string) 430 for i := 0; i < len(pollInputItems); i++ { 431 seqPollInputItems[i] = pollInputItems[i] 432 } 433 434 pollType := 0 // Basic single choice 435 _, err := c.Polls.Create(topic, pollType, seqPollInputItems) 436 if err != nil { 437 return c.LocalError("Failed to add poll to topic", w, r, u) // TODO: Might need to be an internal error as it could leave phantom polls? 438 } 439 } 440 } 441 442 err = c.Subscriptions.Add(u.ID, tid, "topic") 443 if err != nil { 444 return c.InternalError(err, w, r) 445 } 446 err = u.IncreasePostStats(c.WordCount(content), true) 447 if err != nil { 448 return c.InternalError(err, w, r) 449 } 450 451 // Handle the file attachments 452 if u.Perms.UploadFiles { 453 _, rerr := uploadAttachment(w, r, u, fid, "forums", tid, "topics", "") 454 if rerr != nil { 455 return rerr 456 } 457 } 458 459 co.PostCounter.Bump() 460 co.TopicCounter.Bump() 461 // TODO: Pass more data to this hook? 462 skip, rerr := lite.Hooks.VhookSkippable("action_end_create_topic", tid, u) 463 if skip || rerr != nil { 464 return rerr 465 } 466 http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) 467 return nil 468 } 469 470 // TODO: Move this function 471 func uploadFilesWithHash(w http.ResponseWriter, r *http.Request, u *c.User, dir string) (filenames []string, rerr c.RouteError) { 472 files, ok := r.MultipartForm.File["upload_files"] 473 if !ok { 474 return nil, nil 475 } 476 if len(files) > 5 { 477 return nil, c.LocalError("You can't attach more than five files", w, r, u) 478 } 479 disableEncode := r.PostFormValue("ko") == "1" 480 481 for _, file := range files { 482 if file.Filename == "" { 483 continue 484 } 485 //c.DebugLog("file.Filename ", file.Filename) 486 487 extarr := strings.Split(file.Filename, ".") 488 if len(extarr) < 2 { 489 return nil, c.LocalError("Bad file", w, r, u) 490 } 491 ext := extarr[len(extarr)-1] 492 493 // TODO: Can we do this without a regex? 494 reg, err := regexp.Compile("[^A-Za-z0-9]+") 495 if err != nil { 496 return nil, c.LocalError("Bad file extension", w, r, u) 497 } 498 ext = strings.ToLower(reg.ReplaceAllString(ext, "")) 499 if !c.AllowedFileExts.Contains(ext) { 500 return nil, c.LocalError("You're not allowed to upload files with this extension", w, r, u) 501 } 502 503 inFile, err := file.Open() 504 if err != nil { 505 return nil, c.LocalError("Upload failed", w, r, u) 506 } 507 defer inFile.Close() 508 509 hasher := sha256.New() 510 _, err = io.Copy(hasher, inFile) 511 if err != nil { 512 return nil, c.LocalError("Upload failed [Hashing Failed]", w, r, u) 513 } 514 inFile.Close() 515 516 checksum := hex.EncodeToString(hasher.Sum(nil)) 517 filename := checksum + "." + ext 518 519 inFile, err = file.Open() 520 if err != nil { 521 return nil, c.LocalError("Upload failed", w, r, u) 522 } 523 defer inFile.Close() 524 525 outFile, err := os.Create(dir + filename) 526 if err != nil { 527 return nil, c.LocalError("Upload failed [File Creation Failed]", w, r, u) 528 } 529 defer outFile.Close() 530 531 if disableEncode || (ext != "jpg" && ext != "jpeg" && ext != "png" && ext != "gif" && ext != "tiff" && ext != "tif") { 532 _, err = io.Copy(outFile, inFile) 533 if err != nil { 534 return nil, c.LocalError("Upload failed [Copy Failed]", w, r, u) 535 } 536 } else { 537 img, _, err := image.Decode(inFile) 538 if err != nil { 539 return nil, c.LocalError("Upload failed [Image Decoding Failed]", w, r, u) 540 } 541 542 switch ext { 543 case "gif": 544 err = gif.Encode(outFile, img, nil) 545 case "png": 546 err = png.Encode(outFile, img) 547 case "tiff", "tif": 548 err = tiff.Encode(outFile, img, nil) 549 default: 550 err = jpeg.Encode(outFile, img, nil) 551 } 552 if err != nil { 553 return nil, c.LocalError("Upload failed [Image Encoding Failed]", w, r, u) 554 } 555 } 556 557 filenames = append(filenames, filename) 558 } 559 560 return filenames, nil 561 } 562 563 // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes 564 // TODO: Disable stat updates in posts handled by plugin_guilds 565 func EditTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid string) c.RouteError { 566 js := (r.PostFormValue("js") == "1") 567 tid, err := strconv.Atoi(stid) 568 if err != nil { 569 return c.PreErrorJSQ(p.GetErrorPhrase("id_must_be_integer"), w, r, js) 570 } 571 topic, err := c.Topics.Get(tid) 572 if err == sql.ErrNoRows { 573 return c.PreErrorJSQ("The topic you tried to edit doesn't exist.", w, r, js) 574 } else if err != nil { 575 return c.InternalErrorJSQ(err, w, r, js) 576 } 577 578 // TODO: Add hooks to make use of headerLite 579 lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID) 580 if ferr != nil { 581 return ferr 582 } 583 if !u.Perms.ViewTopic || !u.Perms.EditTopic { 584 return c.NoPermissionsJSQ(w, r, u, js) 585 } 586 if topic.IsClosed && !u.Perms.CloseTopic { 587 return c.NoPermissionsJSQ(w, r, u, js) 588 } 589 590 err = topic.Update(r.PostFormValue("name"), r.PostFormValue("content")) 591 // TODO: Avoid duplicating this across this route and the topic creation route 592 if err != nil { 593 switch err { 594 case c.ErrNoTitle: 595 return c.LocalErrorJSQ("This topic doesn't have a title", w, r, u, js) 596 case c.ErrLongTitle: 597 return c.LocalErrorJSQ("The length of the title is too long, max: "+strconv.Itoa(c.Config.MaxTopicTitleLength), w, r, u, js) 598 case c.ErrNoBody: 599 return c.LocalErrorJSQ("This topic doesn't have a body", w, r, u, js) 600 } 601 return c.InternalErrorJSQ(err, w, r, js) 602 } 603 604 err = c.Forums.UpdateLastTopic(topic.ID, u.ID, topic.ParentID) 605 if err != nil && err != sql.ErrNoRows { 606 return c.InternalErrorJSQ(err, w, r, js) 607 } 608 609 // TODO: Avoid the load to get this faster? 610 topic, err = c.Topics.Get(topic.ID) 611 if err == sql.ErrNoRows { 612 return c.PreErrorJSQ("The updated topic doesn't exist.", w, r, js) 613 } else if err != nil { 614 return c.InternalErrorJSQ(err, w, r, js) 615 } 616 617 skip, rerr := lite.Hooks.VhookSkippable("action_end_edit_topic", topic.ID, u) 618 if skip || rerr != nil { 619 return rerr 620 } 621 622 if !js { 623 http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) 624 } else { 625 outBytes, err := json.Marshal(JsonReply{c.ParseMessage(topic.Content, topic.ParentID, "forums", u.ParseSettings, u)}) 626 if err != nil { 627 return c.InternalErrorJSQ(err, w, r, js) 628 } 629 w.Write(outBytes) 630 } 631 return nil 632 } 633 634 // TODO: Add support for soft-deletion and add a permission for hard delete in addition to the usual 635 // TODO: Disable stat updates in posts handled by plugin_guilds 636 func DeleteTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { 637 // TODO: Move this to some sort of middleware 638 var tids []int 639 js := c.ReqIsJson(r) 640 if js { 641 if r.Body == nil { 642 return c.PreErrorJS("No request body", w, r) 643 } 644 err := json.NewDecoder(r.Body).Decode(&tids) 645 if err != nil { 646 return c.PreErrorJS("We weren't able to parse your data", w, r) 647 } 648 } else { 649 tid, err := strconv.Atoi(r.URL.Path[len("/topic/delete/submit/"):]) 650 if err != nil { 651 return c.PreError("The provided TopicID is not a valid number.", w, r) 652 } 653 tids = []int{tid} 654 } 655 if len(tids) == 0 { 656 return c.LocalErrorJSQ("You haven't provided any IDs", w, r, user, js) 657 } 658 659 for _, tid := range tids { 660 topic, err := c.Topics.Get(tid) 661 if err == sql.ErrNoRows { 662 return c.PreErrorJSQ("The topic you tried to delete doesn't exist.", w, r, js) 663 } else if err != nil { 664 return c.InternalErrorJSQ(err, w, r, js) 665 } 666 667 // TODO: Add hooks to make use of headerLite 668 lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID) 669 if ferr != nil { 670 return ferr 671 } 672 if topic.CreatedBy != user.ID { 673 if !user.Perms.ViewTopic || !user.Perms.DeleteTopic { 674 return c.NoPermissionsJSQ(w, r, user, js) 675 } 676 } 677 678 // We might be able to handle this err better 679 err = topic.Delete() 680 if err != nil { 681 return c.InternalErrorJSQ(err, w, r, js) 682 } 683 err = c.ModLogs.Create("delete", tid, "topic", user.GetIP(), user.ID) 684 if err != nil { 685 return c.InternalErrorJSQ(err, w, r, js) 686 } 687 688 // ? - We might need to add soft-delete before we can do an action reply for this 689 /*_, err = stmts.createActionReply.Exec(tid,"delete",ip,user.ID) 690 if err != nil { 691 return c.InternalErrorJSQ(err,w,r,js) 692 }*/ 693 694 // TODO: Do a bulk delete action hook? 695 skip, rerr := lite.Hooks.VhookSkippable("action_end_delete_topic", topic.ID, user) 696 if skip || rerr != nil { 697 return rerr 698 } 699 700 log.Printf("Topic #%d was deleted by UserID #%d", tid, user.ID) 701 } 702 http.Redirect(w, r, "/", http.StatusSeeOther) 703 return nil 704 } 705 706 func StickTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid string) c.RouteError { 707 topic, lite, rerr := topicActionPre(stid, "pin", w, r, u) 708 if rerr != nil { 709 return rerr 710 } 711 if !u.Perms.ViewTopic || !u.Perms.PinTopic { 712 return c.NoPermissions(w, r, u) 713 } 714 return topicActionPost(topic.Stick(), "stick", w, r, lite, topic, u) 715 } 716 717 // 718 // 719 // mark 720 // 721 // 722 func topicActionPre(stid, action string, w http.ResponseWriter, r *http.Request, u *c.User) (*c.Topic, *c.HeaderLite, c.RouteError) { 723 tid, err := strconv.Atoi(stid) 724 if err != nil { 725 return nil, nil, c.PreError(p.GetErrorPhrase("id_must_be_integer"), w, r) 726 } 727 t, err := c.Topics.Get(tid) 728 if err == sql.ErrNoRows { 729 return nil, nil, c.PreError("The topic you tried to "+action+" doesn't exist.", w, r) 730 } else if err != nil { 731 return nil, nil, c.InternalError(err, w, r) 732 } 733 734 // TODO: Add hooks to make use of headerLite 735 lite, ferr := c.SimpleForumUserCheck(w, r, u, t.ParentID) 736 if ferr != nil { 737 return nil, nil, ferr 738 } 739 return t, lite, nil 740 } 741 742 func topicActionPost(err error, action string, w http.ResponseWriter, r *http.Request, lite *c.HeaderLite, topic *c.Topic, u *c.User) c.RouteError { 743 if err != nil { 744 return c.InternalError(err, w, r) 745 } 746 err = addTopicAction(action, topic, u) 747 if err != nil { 748 return c.InternalError(err, w, r) 749 } 750 skip, rerr := lite.Hooks.VhookSkippable("action_end_"+action+"_topic", topic.ID, u) 751 if skip || rerr != nil { 752 return rerr 753 } 754 http.Redirect(w, r, "/topic/"+strconv.Itoa(topic.ID), http.StatusSeeOther) 755 return nil 756 } 757 758 func UnstickTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid string) c.RouteError { 759 t, lite, rerr := topicActionPre(stid, "unpin", w, r, u) 760 if rerr != nil { 761 return rerr 762 } 763 if !u.Perms.ViewTopic || !u.Perms.PinTopic { 764 return c.NoPermissions(w, r, u) 765 } 766 return topicActionPost(t.Unstick(), "unstick", w, r, lite, t, u) 767 } 768 769 func LockTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 770 // TODO: Move this to some sort of middleware 771 var tids []int 772 js := c.ReqIsJson(r) 773 if js { 774 if r.Body == nil { 775 return c.PreErrorJS("No request body", w, r) 776 } 777 err := json.NewDecoder(r.Body).Decode(&tids) 778 if err != nil { 779 return c.PreErrorJS("We weren't able to parse your data", w, r) 780 } 781 } else { 782 tid, err := strconv.Atoi(r.URL.Path[len("/topic/lock/submit/"):]) 783 if err != nil { 784 return c.PreError("The provided TopicID is not a valid number.", w, r) 785 } 786 tids = append(tids, tid) 787 } 788 if len(tids) == 0 { 789 return c.LocalErrorJSQ("You haven't provided any IDs", w, r, u, js) 790 } 791 792 for _, tid := range tids { 793 topic, err := c.Topics.Get(tid) 794 if err == sql.ErrNoRows { 795 return c.PreErrorJSQ("The topic you tried to lock doesn't exist.", w, r, js) 796 } else if err != nil { 797 return c.InternalErrorJSQ(err, w, r, js) 798 } 799 800 // TODO: Add hooks to make use of headerLite 801 lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID) 802 if ferr != nil { 803 return ferr 804 } 805 if !u.Perms.ViewTopic || !u.Perms.CloseTopic { 806 return c.NoPermissionsJSQ(w, r, u, js) 807 } 808 809 err = topic.Lock() 810 if err != nil { 811 return c.InternalErrorJSQ(err, w, r, js) 812 } 813 814 err = addTopicAction("lock", topic, u) 815 if err != nil { 816 return c.InternalErrorJSQ(err, w, r, js) 817 } 818 819 // TODO: Do a bulk lock action hook? 820 skip, rerr := lite.Hooks.VhookSkippable("action_end_lock_topic", topic.ID, u) 821 if skip || rerr != nil { 822 return rerr 823 } 824 } 825 826 if len(tids) == 1 { 827 http.Redirect(w, r, "/topic/"+strconv.Itoa(tids[0]), http.StatusSeeOther) 828 } 829 return nil 830 } 831 832 func UnlockTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid string) c.RouteError { 833 t, lite, rerr := topicActionPre(stid, "unlock", w, r, u) 834 if rerr != nil { 835 return rerr 836 } 837 if !u.Perms.ViewTopic || !u.Perms.CloseTopic { 838 return c.NoPermissions(w, r, u) 839 } 840 return topicActionPost(t.Unlock(), "unlock", w, r, lite, t, u) 841 } 842 843 // ! JS only route 844 // TODO: Figure a way to get this route to work without JS 845 func MoveTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, sfid string) c.RouteError { 846 fid, err := strconv.Atoi(sfid) 847 if err != nil { 848 return c.PreErrorJS(p.GetErrorPhrase("id_must_be_integer"), w, r) 849 } 850 // TODO: Move this to some sort of middleware 851 var tids []int 852 if r.Body == nil { 853 return c.PreErrorJS("No request body", w, r) 854 } 855 err = json.NewDecoder(r.Body).Decode(&tids) 856 if err != nil { 857 return c.PreErrorJS("We weren't able to parse your data", w, r) 858 } 859 if len(tids) == 0 { 860 return c.LocalErrorJS("You haven't provided any IDs", w, r) 861 } 862 863 for _, tid := range tids { 864 topic, err := c.Topics.Get(tid) 865 if err == sql.ErrNoRows { 866 return c.PreErrorJS("The topic you tried to move doesn't exist.", w, r) 867 } else if err != nil { 868 return c.InternalErrorJS(err, w, r) 869 } 870 871 // TODO: Add hooks to make use of headerLite 872 _, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID) 873 if ferr != nil { 874 return ferr 875 } 876 if !u.Perms.ViewTopic || !u.Perms.MoveTopic { 877 return c.NoPermissionsJS(w, r, u) 878 } 879 lite, ferr := c.SimpleForumUserCheck(w, r, u, fid) 880 if ferr != nil { 881 return ferr 882 } 883 if !u.Perms.ViewTopic || !u.Perms.MoveTopic { 884 return c.NoPermissionsJS(w, r, u) 885 } 886 887 err = topic.MoveTo(fid) 888 if err != nil { 889 return c.InternalErrorJS(err, w, r) 890 } 891 // ? - Is there a better way of doing this? 892 err = addTopicAction("move-"+strconv.Itoa(fid), topic, u) 893 if err != nil { 894 return c.InternalErrorJS(err, w, r) 895 } 896 897 // TODO: Do a bulk move action hook? 898 skip, rerr := lite.Hooks.VhookSkippable("action_end_move_topic", topic.ID, u) 899 if skip || rerr != nil { 900 return rerr 901 } 902 } 903 904 if len(tids) == 1 { 905 http.Redirect(w, r, "/topic/"+strconv.Itoa(tids[0]), http.StatusSeeOther) 906 } 907 return nil 908 } 909 910 func addTopicAction(action string, t *c.Topic, u *c.User) error { 911 err := c.ModLogs.Create(action, t.ID, "topic", u.GetIP(), u.ID) 912 if err != nil { 913 return err 914 } 915 return t.CreateActionReply(action, u.GetIP(), u.ID) 916 } 917 918 // TODO: Refactor this 919 func LikeTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid string) c.RouteError { 920 js := r.PostFormValue("js") == "1" 921 tid, err := strconv.Atoi(stid) 922 if err != nil { 923 return c.PreErrorJSQ(p.GetErrorPhrase("id_must_be_integer"), w, r, js) 924 } 925 topic, err := c.Topics.Get(tid) 926 if err == sql.ErrNoRows { 927 return c.PreErrorJSQ("The requested topic doesn't exist.", w, r, js) 928 } else if err != nil { 929 return c.InternalErrorJSQ(err, w, r, js) 930 } 931 932 // TODO: Add hooks to make use of headerLite 933 lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID) 934 if ferr != nil { 935 return ferr 936 } 937 if !u.Perms.ViewTopic || !u.Perms.LikeItem { 938 return c.NoPermissionsJSQ(w, r, u, js) 939 } 940 if topic.CreatedBy == u.ID { 941 return c.LocalErrorJSQ("You can't like your own topics", w, r, u, js) 942 } 943 944 _, err = c.Users.Get(topic.CreatedBy) 945 if err != nil && err == sql.ErrNoRows { 946 return c.LocalErrorJSQ("The target user doesn't exist", w, r, u, js) 947 } else if err != nil { 948 return c.InternalErrorJSQ(err, w, r, js) 949 } 950 951 score := 1 952 err = topic.Like(score, u.ID) 953 if err == c.ErrAlreadyLiked { 954 return c.LocalErrorJSQ("You already liked this", w, r, u, js) 955 } else if err != nil { 956 return c.InternalErrorJSQ(err, w, r, js) 957 } 958 959 // ! Be careful about leaking per-route permission state with user ptr 960 alert := c.Alert{ActorID: u.ID, TargetUserID: topic.CreatedBy, Event: "like", ElementType: "topic", ElementID: tid, Actor: u} 961 err = c.AddActivityAndNotifyTarget(alert) 962 if err != nil { 963 return c.InternalErrorJSQ(err, w, r, js) 964 } 965 966 skip, rerr := lite.Hooks.VhookSkippable("action_end_like_topic", topic.ID, u) 967 if skip || rerr != nil { 968 return rerr 969 } 970 return actionSuccess(w, r, "/topic/"+strconv.Itoa(tid), js) 971 } 972 func UnlikeTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid string) c.RouteError { 973 js := r.PostFormValue("js") == "1" 974 tid, err := strconv.Atoi(stid) 975 if err != nil { 976 return c.PreErrorJSQ(p.GetErrorPhrase("id_must_be_integer"), w, r, js) 977 } 978 topic, err := c.Topics.Get(tid) 979 if err == sql.ErrNoRows { 980 return c.PreErrorJSQ("The requested topic doesn't exist.", w, r, js) 981 } else if err != nil { 982 return c.InternalErrorJSQ(err, w, r, js) 983 } 984 985 // TODO: Add hooks to make use of headerLite 986 lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID) 987 if ferr != nil { 988 return ferr 989 } 990 if !u.Perms.ViewTopic || !u.Perms.LikeItem { 991 return c.NoPermissionsJSQ(w, r, u, js) 992 } 993 994 _, err = c.Users.Get(topic.CreatedBy) 995 if err != nil && err == sql.ErrNoRows { 996 return c.LocalErrorJSQ("The target user doesn't exist", w, r, u, js) 997 } else if err != nil { 998 return c.InternalErrorJSQ(err, w, r, js) 999 } 1000 err = topic.Unlike(u.ID) 1001 if err != nil { 1002 return c.InternalErrorJSQ(err, w, r, js) 1003 } 1004 1005 // TODO: Better coupling between the two params queries 1006 aids, err := c.Activity.AidsByParams("like", topic.ID, "topic") 1007 if err != nil { 1008 return c.InternalErrorJSQ(err, w, r, js) 1009 } 1010 for _, aid := range aids { 1011 c.DismissAlert(topic.CreatedBy, aid) 1012 } 1013 err = c.Activity.DeleteByParams("like", topic.ID, "topic") 1014 if err != nil { 1015 return c.InternalErrorJSQ(err, w, r, js) 1016 } 1017 1018 skip, rerr := lite.Hooks.VhookSkippable("action_end_unlike_topic", topic.ID, u) 1019 if skip || rerr != nil { 1020 return rerr 1021 } 1022 return actionSuccess(w, r, "/topic/"+strconv.Itoa(tid), js) 1023 }