links.go (21900B)
1 package handlers 2 3 import ( 4 "bytes" 5 "dkforest/pkg/captcha" 6 "dkforest/pkg/database" 7 "dkforest/pkg/utils" 8 hutils "dkforest/pkg/web/handlers/utils" 9 "encoding/base64" 10 "encoding/csv" 11 "encoding/pem" 12 "fmt" 13 "github.com/asaskevich/govalidator" 14 "github.com/labstack/echo" 15 "github.com/sirupsen/logrus" 16 "gorm.io/gorm" 17 "net/http" 18 "net/url" 19 "regexp" 20 "strings" 21 ) 22 23 func LinksHandler(c echo.Context) error { 24 authUser := c.Get("authUser").(*database.User) 25 db := c.Get("database").(*database.DkfDB) 26 var data linksData 27 28 data.Categories, _ = db.GetCategories() 29 30 data.Search = c.QueryParam("search") 31 filterCategory := c.QueryParam("category") 32 33 if filterCategory != "" { 34 if filterCategory == "uncategorized" { 35 db.DB().Raw(`SELECT l.* 36 FROM links l 37 LEFT JOIN links_categories_links cl ON cl.link_id = l.id 38 WHERE cl.link_id IS NULL AND l.deleted_at IS NULL 39 ORDER BY l.title COLLATE NOCASE ASC`).Scan(&data.Links) 40 data.LinksCount = int64(len(data.Links)) 41 } else { 42 db.DB().Raw(`SELECT l.* 43 FROM links_categories_links cl 44 INNER JOIN links l ON l.id = cl.link_id 45 WHERE cl.category_id = (SELECT id FROM links_categories WHERE name = ?) AND l.deleted_at IS NULL 46 ORDER BY l.title COLLATE NOCASE ASC`, filterCategory).Scan(&data.Links) 47 data.LinksCount = int64(len(data.Links)) 48 } 49 } else if data.Search != "" { 50 if govalidator.IsURL(data.Search) { 51 if searchedURL, err := url.Parse(data.Search); err == nil { 52 h := searchedURL.Scheme + "://" + searchedURL.Hostname() 53 var l database.Link 54 query := db.DB() 55 if authUser.IsModerator() { 56 query = query.Unscoped() 57 } 58 if err := query.First(&l, "url = ?", h).Error; err == nil { 59 data.Links = append(data.Links, l) 60 } 61 data.LinksCount = int64(len(data.Links)) 62 } 63 } else { 64 if err := db.DB().Raw(`select l.id, l.uuid, l.url, l.title, l.description 65 from fts5_links l 66 where fts5_links match ? 67 ORDER BY rank, l.title COLLATE NOCASE ASC 68 LIMIT 100`, data.Search).Scan(&data.Links).Error; err != nil { 69 logrus.Error(err) 70 } 71 data.LinksCount = int64(len(data.Links)) 72 } 73 } else { 74 if err := db.DB().Table("links"). 75 Scopes(func(query *gorm.DB) *gorm.DB { 76 data.CurrentPage, data.MaxPage, data.LinksCount, query = NewPaginator().Paginate(c, query) 77 return query 78 }).Order("title COLLATE NOCASE ASC").Find(&data.Links).Error; err != nil { 79 logrus.Error(err) 80 } 81 } 82 83 // Get all links IDs 84 linksIDs := make([]int64, 0) 85 for _, l := range data.Links { 86 linksIDs = append(linksIDs, l.ID) 87 } 88 // Keep pointers to links for fast access 89 linksCache := make(map[int64]*database.Link) 90 for i, l := range data.Links { 91 linksCache[l.ID] = &data.Links[i] 92 } 93 // Get all mirrors for all links that we have 94 var mirrors []database.LinksMirror 95 db.DB().Raw(`select * from links_mirrors where link_id in (?)`, linksIDs).Scan(&mirrors) 96 // Put mirrors in links 97 for _, m := range mirrors { 98 if l, ok := linksCache[m.LinkID]; ok { 99 l.Mirrors = append(l.Mirrors, m) 100 } 101 } 102 103 return c.Render(http.StatusOK, "links", data) 104 } 105 106 func LinksDownloadHandler(c echo.Context) error { 107 authUser := c.Get("authUser").(*database.User) 108 db := c.Get("database").(*database.DkfDB) 109 fileName := "dkf_links.csv" 110 111 // Captcha for bigger files 112 var data captchaRequiredData 113 data.CaptchaDescription = "Captcha required" 114 data.CaptchaID, data.CaptchaImg = captcha.New() 115 const captchaRequiredTmpl = "captcha-required" 116 if c.Request().Method == http.MethodGet { 117 return c.Render(http.StatusOK, captchaRequiredTmpl, data) 118 } 119 captchaID := c.Request().PostFormValue("captcha_id") 120 captchaInput := c.Request().PostFormValue("captcha") 121 if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil { 122 data.ErrCaptcha = err.Error() 123 return c.Render(http.StatusOK, captchaRequiredTmpl, data) 124 } 125 126 // Keep track of user downloads 127 if _, err := db.CreateDownload(authUser.ID, fileName); err != nil { 128 logrus.Error(err) 129 } 130 131 // Get all categories and make a hashmap for fast access 132 categories, _ := db.GetLinksCategories() 133 categoriesMap := make(map[int64]string) 134 for _, category := range categories { 135 categoriesMap[category.ID] = category.Name 136 } 137 // Get all "categories links" associations between links and their categories 138 categoriesLinks, _ := db.GetCategoriesLinks() 139 // Build a map of all categories IDs for a given link ID 140 categoriesLinksMap := make(map[int64][]int64) 141 for _, cl := range categoriesLinks { 142 categoriesLinksMap[cl.LinkID] = append(categoriesLinksMap[cl.LinkID], cl.CategoryID) 143 } 144 145 links, _ := db.GetLinks() 146 by := make([]byte, 0) 147 buf := bytes.NewBuffer(by) 148 w := csv.NewWriter(buf) 149 _ = w.Write([]string{"UUID", "URL", "Title", "Description", "Categories"}) 150 for _, link := range links { 151 // Get all categories for the link 152 categoryNames := make([]string, 0) 153 categoryIDs := categoriesLinksMap[link.ID] 154 for _, tagID := range categoryIDs { 155 categoryNames = append(categoryNames, categoriesMap[tagID]) 156 } 157 _ = w.Write([]string{link.UUID, link.URL, link.Title, link.Description, strings.Join(categoryNames, ",")}) 158 } 159 w.Flush() 160 c.Response().Header().Set("Content-Disposition", `attachment; filename="`+fileName+`"`) 161 return c.Stream(http.StatusOK, "application/octet-stream", buf) 162 } 163 164 func LinkHandler(c echo.Context) error { 165 db := c.Get("database").(*database.DkfDB) 166 shorthand := c.Param("shorthand") 167 linkUUID := c.Param("linkUUID") 168 var data linkData 169 var err error 170 if shorthand != "" { 171 data.Link, err = db.GetLinkByShorthand(shorthand) 172 } else { 173 data.Link, err = db.GetLinkByUUID(linkUUID) 174 } 175 if err != nil { 176 return c.Redirect(http.StatusFound, "/links") 177 } 178 data.PgpKeys, _ = db.GetLinkPgps(data.Link.ID) 179 data.Mirrors, _ = db.GetLinkMirrors(data.Link.ID) 180 return c.Render(http.StatusOK, "link", data) 181 } 182 183 func RestoreLinkHandler(c echo.Context) error { 184 authUser := c.Get("authUser").(*database.User) 185 db := c.Get("database").(*database.DkfDB) 186 if !authUser.IsModerator() { 187 return c.Redirect(http.StatusFound, "/") 188 } 189 linkUUID := c.Param("linkUUID") 190 var link database.Link 191 if err := db.DB().Unscoped().First(&link, "uuid = ?", linkUUID).Error; err != nil { 192 return hutils.RedirectReferer(c) 193 } 194 db.NewAudit(*authUser, fmt.Sprintf("restore link %s", link.URL)) 195 db.DB().Unscoped().Model(&database.Link{}).Where("id = ?", link.ID).Update("deleted_at", nil) 196 return hutils.RedirectReferer(c) 197 } 198 199 func ClaimLinkHandler(c echo.Context) error { 200 authUser := c.Get("authUser").(*database.User) 201 db := c.Get("database").(*database.DkfDB) 202 linkUUID := c.Param("linkUUID") 203 link, err := db.GetLinkByUUID(linkUUID) 204 if err != nil { 205 return c.Redirect(http.StatusFound, "/") 206 } 207 var data claimLinkData 208 data.Link = link 209 data.Certificate = link.GenOwnershipCert(authUser.Username) 210 211 if c.Request().Method == http.MethodGet { 212 return c.Render(http.StatusOK, "link-claim", data) 213 } 214 215 data.Signature = c.Request().PostFormValue("signature") 216 217 b64Sig, err := base64.StdEncoding.DecodeString(data.Signature) 218 if err != nil { 219 data.Error = "invalid signature" 220 return c.Render(http.StatusOK, "link-claim", data) 221 } 222 pemSign := string(pem.EncodeToMemory(&pem.Block{Type: "SIGNATURE", Bytes: b64Sig})) 223 224 isValid := utils.VerifyTorSign(link.GetOnionAddr(), data.Certificate, pemSign) 225 if !isValid { 226 data.Error = "invalid signature" 227 return c.Render(http.StatusOK, "link-claim", data) 228 } 229 230 signedCert := "-----BEGIN SIGNED MESSAGE-----\n" + 231 data.Certificate + "\n" + 232 pemSign 233 234 link.SignedCertificate = signedCert 235 link.OwnerUserID = &authUser.ID 236 link.DoSave(db) 237 238 return c.Redirect(http.StatusFound, "/links/"+link.UUID) 239 } 240 241 func ClaimDownloadCertificateLinkHandler(c echo.Context) error { 242 authUser := c.Get("authUser").(*database.User) 243 db := c.Get("database").(*database.DkfDB) 244 245 linkUUID := c.Param("linkUUID") 246 link, err := db.GetLinkByUUID(linkUUID) 247 if err != nil { 248 return c.Redirect(http.StatusFound, "/") 249 } 250 251 fileName := "certificate.txt" 252 253 // Keep track of user downloads 254 if _, err := db.CreateDownload(authUser.ID, fileName); err != nil { 255 logrus.Error(err) 256 } 257 258 c.Response().Header().Set("Content-Disposition", `attachment; filename="`+fileName+`"`) 259 return c.Stream(http.StatusOK, "application/octet-stream", strings.NewReader(link.GenOwnershipCert(authUser.Username))) 260 } 261 262 func ClaimCertificateLinkHandler(c echo.Context) error { 263 linkUUID := c.Param("linkUUID") 264 db := c.Get("database").(*database.DkfDB) 265 link, err := db.GetLinkByUUID(linkUUID) 266 if err != nil { 267 return c.Redirect(http.StatusFound, "/") 268 } 269 return c.String(http.StatusOK, link.SignedCertificate) 270 } 271 272 func EditLinkHandler(c echo.Context) error { 273 authUser := c.Get("authUser").(*database.User) 274 db := c.Get("database").(*database.DkfDB) 275 if !authUser.IsModerator() { 276 return c.Redirect(http.StatusFound, "/") 277 } 278 linkUUID := c.Param("linkUUID") 279 link, err := db.GetLinkByUUID(linkUUID) 280 if err != nil { 281 return c.Redirect(http.StatusFound, "/") 282 } 283 out, _ := db.GetLinkCategories(link.ID) 284 categories := make([]string, 0) 285 for _, el := range out { 286 categories = append(categories, el.Name) 287 } 288 out1, err := db.GetLinkTags(link.ID) 289 tags := make([]string, 0) 290 for _, el := range out1 { 291 tags = append(tags, el.Name) 292 } 293 var data editLinkData 294 data.IsEdit = true 295 data.Link = link.URL 296 data.Title = link.Title 297 data.Description = link.Description 298 if link.Shorthand != nil { 299 data.Shorthand = *link.Shorthand 300 } 301 data.Categories = strings.Join(categories, ",") 302 data.Tags = strings.Join(tags, ",") 303 data.Mirrors, _ = db.GetLinkMirrors(link.ID) 304 data.LinkPgps, _ = db.GetLinkPgps(link.ID) 305 //data.Categories = link 306 307 if c.Request().Method == http.MethodPost { 308 formName := c.Request().PostFormValue("formName") 309 if formName == "createLink" { 310 _ = db.DeleteLinkCategories(link.ID) 311 _ = db.DeleteLinkTags(link.ID) 312 313 // If link is signed, we can no longer edit the link URL 314 if link.SignedCertificate == "" { 315 data.Link = c.Request().PostFormValue("link") 316 } 317 data.Title = c.Request().PostFormValue("title") 318 data.Description = c.Request().PostFormValue("description") 319 data.Shorthand = c.Request().PostFormValue("shorthand") 320 data.Categories = c.Request().PostFormValue("categories") 321 data.Tags = c.Request().PostFormValue("tags") 322 if !govalidator.Matches(data.Link, `^https?://[a-z2-7]{56}\.onion$`) { 323 data.ErrorLink = "invalid link" 324 return c.Render(http.StatusOK, "new-link", data) 325 } 326 if !govalidator.RuneLength(data.Title, "0", "255") { 327 data.ErrorTitle = "title must have 255 characters max" 328 return c.Render(http.StatusOK, "new-link", data) 329 } 330 if !govalidator.RuneLength(data.Description, "0", "1000") { 331 data.ErrorCategories = "description must have 1000 characters max" 332 return c.Render(http.StatusOK, "new-link", data) 333 } 334 if data.Shorthand != "" { 335 if !govalidator.Matches(data.Shorthand, `^[\w-_]{3,50}$`) { 336 data.ErrorLink = "invalid shorthand" 337 return c.Render(http.StatusOK, "new-link", data) 338 } 339 } 340 categoryRgx := regexp.MustCompile(`^\w{3,20}$`) 341 var tagsStr, categoriesStr []string 342 if data.Categories != "" { 343 categoriesStr = strings.Split(strings.ToLower(data.Categories), ",") 344 for _, category := range categoriesStr { 345 category = strings.TrimSpace(category) 346 if !categoryRgx.MatchString(category) { 347 data.ErrorCategories = `invalid category "` + category + `"` 348 return c.Render(http.StatusOK, "new-link", data) 349 } 350 } 351 } 352 if data.Tags != "" { 353 tagsStr = strings.Split(strings.ToLower(data.Tags), ",") 354 for _, tag := range tagsStr { 355 tag = strings.TrimSpace(tag) 356 if !categoryRgx.MatchString(tag) { 357 data.ErrorTags = `invalid tag "` + tag + `"` 358 return c.Render(http.StatusOK, "new-link", data) 359 } 360 } 361 } 362 //------------ 363 var categories []database.LinksCategory 364 var tags []database.LinksTag 365 for _, categoryStr := range categoriesStr { 366 category, _ := db.CreateLinksCategory(categoryStr) 367 categories = append(categories, category) 368 } 369 for _, tagStr := range tagsStr { 370 tag, _ := db.CreateLinksTag(tagStr) 371 tags = append(tags, tag) 372 } 373 link.URL = data.Link 374 link.Title = data.Title 375 link.Description = data.Description 376 if data.Shorthand != "" { 377 link.Shorthand = &data.Shorthand 378 } 379 if err := db.DB().Save(&link).Error; err != nil { 380 if strings.Contains(err.Error(), "UNIQUE constraint failed: links.shorthand") { 381 data.ErrorShorthand = "shorthand already used" 382 } else { 383 data.ErrorLink = "failed to update link" 384 } 385 return c.Render(http.StatusOK, "new-link", data) 386 } 387 for _, category := range categories { 388 _ = db.AddLinkCategory(link.ID, category.ID) 389 } 390 for _, tag := range tags { 391 _ = db.AddLinkTag(link.ID, tag.ID) 392 } 393 db.NewAudit(*authUser, fmt.Sprintf("updated link %s", link.URL)) 394 return c.Redirect(http.StatusFound, "/links") 395 396 } else if formName == "createPgp" { 397 data.PGPTitle = c.Request().PostFormValue("pgp_title") 398 if !govalidator.RuneLength(data.PGPTitle, "3", "255") { 399 data.ErrorPGPTitle = "title must have 3-255 characters" 400 return c.Render(http.StatusOK, "new-link", data) 401 } 402 data.PGPDescription = c.Request().PostFormValue("pgp_description") 403 data.PGPPublicKey = c.Request().PostFormValue("pgp_public_key") 404 if _, err = db.CreateLinkPgp(link.ID, data.PGPTitle, data.PGPDescription, data.PGPPublicKey); err != nil { 405 logrus.Error(err) 406 } 407 db.NewAudit(*authUser, fmt.Sprintf("create gpg for link %s", link.URL)) 408 return hutils.RedirectReferer(c) 409 410 } else if formName == "createMirror" { 411 data.MirrorLink = c.Request().PostFormValue("mirror_link") 412 if !govalidator.Matches(data.MirrorLink, `^https?://[a-z2-7]{56}\.onion$`) { 413 data.ErrorMirrorLink = "invalid link" 414 return c.Render(http.StatusOK, "new-link", data) 415 } 416 if _, err = db.CreateLinkMirror(link.ID, data.MirrorLink); err != nil { 417 logrus.Error(err) 418 } 419 db.NewAudit(*authUser, fmt.Sprintf("create mirror for link %s", link.URL)) 420 return hutils.RedirectReferer(c) 421 } 422 return c.Redirect(http.StatusFound, "/links") 423 } 424 425 return c.Render(http.StatusOK, "new-link", data) 426 } 427 428 func LinkDeleteHandler(c echo.Context) error { 429 authUser := c.Get("authUser").(*database.User) 430 db := c.Get("database").(*database.DkfDB) 431 linkUUID := c.Param("linkUUID") 432 link, err := db.GetLinkByUUID(linkUUID) 433 if err != nil { 434 return hutils.RedirectReferer(c) 435 } 436 437 if !authUser.IsModerator() { 438 return hutils.RedirectReferer(c) 439 } 440 441 var data deleteLinkData 442 data.Link = link 443 444 if c.Request().Method == http.MethodPost { 445 db.NewAudit(*authUser, fmt.Sprintf("deleted link %s", link.URL)) 446 if err := db.DeleteLinkByID(link.ID); err != nil { 447 logrus.Error(err) 448 } 449 return c.Redirect(http.StatusFound, "/links") 450 } 451 452 return c.Render(http.StatusOK, "link-delete", data) 453 } 454 455 func LinkPgpDownloadHandler(c echo.Context) error { 456 authUser := c.Get("authUser").(*database.User) 457 db := c.Get("database").(*database.DkfDB) 458 459 pgpID := utils.DoParseInt64(c.Param("linkPgpID")) 460 linkPgp, err := db.GetLinkPgpByID(pgpID) 461 if err != nil { 462 return c.NoContent(http.StatusNotFound) 463 } 464 465 fileName := linkPgp.Title + ".asc" 466 467 // Keep track of user downloads 468 if _, err := db.CreateDownload(authUser.ID, fileName); err != nil { 469 logrus.Error(err) 470 } 471 472 c.Response().Header().Set("Content-Disposition", `attachment; filename="`+fileName+`"`) 473 return c.Stream(http.StatusOK, "application/octet-stream", strings.NewReader(linkPgp.PgpPublicKey)) 474 } 475 476 func LinkPgpDeleteHandler(c echo.Context) error { 477 authUser := c.Get("authUser").(*database.User) 478 db := c.Get("database").(*database.DkfDB) 479 linkPgpID := utils.DoParseInt64(c.Param("linkPgpID")) 480 linkPgp, err := db.GetLinkPgpByID(linkPgpID) 481 if err != nil { 482 return hutils.RedirectReferer(c) 483 } 484 link, err := db.GetLinkByID(linkPgp.LinkID) 485 if err != nil { 486 return hutils.RedirectReferer(c) 487 } 488 489 if !authUser.IsModerator() { 490 return hutils.RedirectReferer(c) 491 } 492 493 var data deleteLinkPgpData 494 data.Link = link 495 data.LinkPgp = linkPgp 496 497 if c.Request().Method == http.MethodPost { 498 if err := db.DeleteLinkPgpByID(linkPgp.ID); err != nil { 499 logrus.Error(err) 500 } 501 return c.Redirect(http.StatusFound, "/links/"+link.UUID+"/edit") 502 } 503 504 return c.Render(http.StatusOK, "link-pgp-delete", data) 505 } 506 507 func LinkMirrorDeleteHandler(c echo.Context) error { 508 authUser := c.Get("authUser").(*database.User) 509 db := c.Get("database").(*database.DkfDB) 510 linkMirrorID := utils.DoParseInt64(c.Param("linkMirrorID")) 511 linkMirror, err := db.GetLinkMirrorByID(linkMirrorID) 512 if err != nil { 513 return c.Redirect(http.StatusFound, "/links") 514 } 515 link, err := db.GetLinkByID(linkMirror.LinkID) 516 if err != nil { 517 return c.Redirect(http.StatusFound, "/links") 518 } 519 520 if !authUser.IsModerator() { 521 return c.Redirect(http.StatusFound, "/links") 522 } 523 524 var data deleteLinkMirrorData 525 data.Link = link 526 data.LinkMirror = linkMirror 527 528 if c.Request().Method == http.MethodPost { 529 if err := db.DeleteLinkMirrorByID(linkMirror.ID); err != nil { 530 logrus.Error(err) 531 } 532 return c.Redirect(http.StatusFound, "/links/"+link.UUID+"/edit") 533 } 534 535 return c.Render(http.StatusOK, "link-mirror-delete", data) 536 } 537 538 type CsvLink struct { 539 URL string 540 Title string 541 } 542 543 func LinksUploadHandler(c echo.Context) error { 544 db := c.Get("database").(*database.DkfDB) 545 var data linksUploadData 546 if c.Request().Method == http.MethodPost { 547 data.CsvStr = c.Request().PostFormValue("csv") 548 getValidLinks := func() (out []CsvLink, err error) { 549 r := csv.NewReader(strings.NewReader(data.CsvStr)) 550 records, err := r.ReadAll() 551 if err != nil { 552 return out, err 553 } 554 for idx, record := range records { 555 link := strings.TrimSpace(strings.TrimRight(record[0], "/")) 556 title := record[1] 557 if !govalidator.Matches(link, `^https?://[a-z2-7]{56}\.onion$`) { 558 return out, fmt.Errorf("invalid link %s", link) 559 } 560 if !govalidator.RuneLength(title, "0", "255") { 561 return out, fmt.Errorf("title must have 255 characters max : record #%d", idx) 562 } 563 csvLink := CsvLink{ 564 URL: link, 565 Title: title, 566 } 567 out = append(out, csvLink) 568 } 569 return out, nil 570 } 571 csvLinks, err := getValidLinks() 572 if err != nil { 573 data.Error = err.Error() 574 return c.Render(http.StatusOK, "links-upload", data) 575 } 576 for _, csvLink := range csvLinks { 577 _, err := db.CreateLink(csvLink.URL, csvLink.Title, "", "") 578 if err != nil { 579 logrus.Error(err) 580 } 581 } 582 return hutils.RedirectReferer(c) 583 } 584 return c.Render(http.StatusOK, "links-upload", data) 585 } 586 587 func NewLinkHandler(c echo.Context) error { 588 authUser := c.Get("authUser").(*database.User) 589 db := c.Get("database").(*database.DkfDB) 590 if !authUser.IsModerator() { 591 return c.Redirect(http.StatusFound, "/") 592 } 593 var data newLinkData 594 if c.Request().Method == http.MethodPost { 595 data.Link = c.Request().PostFormValue("link") 596 data.Title = c.Request().PostFormValue("title") 597 data.Description = c.Request().PostFormValue("description") 598 data.Shorthand = c.Request().PostFormValue("shorthand") 599 data.Categories = c.Request().PostFormValue("categories") 600 data.Tags = c.Request().PostFormValue("tags") 601 if !govalidator.Matches(data.Link, `^https?://[a-z2-7]{56}\.onion$`) { 602 data.ErrorLink = "invalid link" 603 return c.Render(http.StatusOK, "new-link", data) 604 } 605 if !govalidator.RuneLength(data.Title, "0", "255") { 606 data.ErrorTitle = "title must have 255 characters max" 607 return c.Render(http.StatusOK, "new-link", data) 608 } 609 if !govalidator.RuneLength(data.Description, "0", "1000") { 610 data.ErrorCategories = "description must have 1000 characters max" 611 return c.Render(http.StatusOK, "new-link", data) 612 } 613 if data.Shorthand != "" { 614 if !govalidator.Matches(data.Shorthand, `^[\w-_]{3,50}$`) { 615 data.ErrorLink = "invalid shorthand" 616 return c.Render(http.StatusOK, "new-link", data) 617 } 618 } 619 categoryRgx := regexp.MustCompile(`^\w{3,20}$`) 620 var tagsStr, categoriesStr []string 621 if data.Categories != "" { 622 categoriesStr = strings.Split(strings.ToLower(data.Categories), ",") 623 for _, category := range categoriesStr { 624 category = strings.TrimSpace(category) 625 if !categoryRgx.MatchString(category) { 626 data.ErrorCategories = `invalid category "` + category + `"` 627 return c.Render(http.StatusOK, "new-link", data) 628 } 629 } 630 } 631 if data.Tags != "" { 632 tagsStr = strings.Split(strings.ToLower(data.Tags), ",") 633 for _, tag := range tagsStr { 634 tag = strings.TrimSpace(tag) 635 if !categoryRgx.MatchString(tag) { 636 data.ErrorTags = `invalid tag "` + tag + `"` 637 return c.Render(http.StatusOK, "new-link", data) 638 } 639 } 640 } 641 //------------ 642 var categories []database.LinksCategory 643 var tags []database.LinksTag 644 for _, categoryStr := range categoriesStr { 645 category, _ := db.CreateLinksCategory(categoryStr) 646 categories = append(categories, category) 647 } 648 for _, tagStr := range tagsStr { 649 tag, _ := db.CreateLinksTag(tagStr) 650 tags = append(tags, tag) 651 } 652 link, err := db.CreateLink(data.Link, data.Title, data.Description, data.Shorthand) 653 if err != nil { 654 logrus.Error(err) 655 data.ErrorLink = "failed to create link" 656 return c.Render(http.StatusOK, "new-link", data) 657 } 658 for _, category := range categories { 659 _ = db.AddLinkCategory(link.ID, category.ID) 660 } 661 for _, tag := range tags { 662 _ = db.AddLinkTag(link.ID, tag.ID) 663 } 664 db.NewAudit(*authUser, fmt.Sprintf("create link %s", link.URL)) 665 return c.Redirect(http.StatusFound, "/links") 666 } 667 return c.Render(http.StatusOK, "new-link", data) 668 } 669 670 func LinksClaimInstructionsHandler(c echo.Context) error { 671 return c.Render(http.StatusOK, "links-claim-instructions", nil) 672 } 673 674 func LinksReindexHandler(c echo.Context) error { 675 db := c.Get("database").(*database.DkfDB) 676 if err := db.DB().Exec(`INSERT INTO fts5_links(fts5_links) VALUES('rebuild')`).Error; err != nil { 677 logrus.Error(err) 678 } 679 db.DB().Exec(`delete from fts5_links where rowid in (select id from links where deleted_at is not null)`) 680 return hutils.RedirectReferer(c) 681 }