dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

commit 1fef8048e5e8741359d4f9fc19074e420e519ec2
parent 3e05c9421d40975196801552db9b3cf904725285
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Mon, 12 Jun 2023 19:53:06 -0700

move code

Diffstat:
Mpkg/web/handlers/forum.go | 46++++++++++++++++++++++++++++++++++++++++++++++
Mpkg/web/handlers/handlers.go | 752-------------------------------------------------------------------------------
Apkg/web/handlers/links.go | 681+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpkg/web/handlers/settings.go | 45+++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 772 insertions(+), 752 deletions(-)

diff --git a/pkg/web/handlers/forum.go b/pkg/web/handlers/forum.go @@ -7,6 +7,7 @@ import ( hutils "dkforest/pkg/web/handlers/utils" "fmt" "github.com/asaskevich/govalidator" + "github.com/jinzhu/gorm" "github.com/labstack/echo" "github.com/sirupsen/logrus" html2 "html" @@ -368,3 +369,48 @@ func NewThreadHandler(c echo.Context) error { return c.Render(http.StatusOK, "new-thread", data) } + +func ThreadHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + threadUUID := database.ForumThreadUUID(c.Param("threadUUID")) + thread, err := db.GetForumThreadByUUID(threadUUID) + if err != nil { + return c.Redirect(http.StatusFound, "/") + } + var data threadData + data.Thread = thread + + if err := db.DB(). + Table("forum_messages"). + Where("thread_id = ?", thread.ID). + Scopes(func(query *gorm.DB) *gorm.DB { + data.CurrentPage, data.MaxPage, data.MessagesCount, query = NewPaginator().Paginate(c, query) + return query + }). + Order("id ASC"). + Where("thread_id = ?", thread.ID). + Preload("User"). + Find(&data.Messages).Error; err != nil { + logrus.Error(err) + } + + if authUser != nil { + data.IsSubscribed = db.IsUserSubscribedToForumThread(authUser.ID, thread.ID) + // Update read record + db.UpdateForumReadRecord(authUser.ID, thread.ID) + } + + return c.Render(http.StatusOK, "thread", data) +} + +func ForumReindexHandler(c echo.Context) error { + db := c.Get("database").(*database.DkfDB) + if err := db.DB().Exec(`INSERT INTO fts5_forum_threads(fts5_forum_threads) VALUES('rebuild')`).Error; err != nil { + logrus.Error(err) + } + if err := db.DB().Exec(`INSERT INTO fts5_forum_messages(fts5_forum_messages) VALUES('rebuild')`).Error; err != nil { + logrus.Error(err) + } + return c.Redirect(http.StatusFound, c.Request().Referer()) +} diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go @@ -13,10 +13,8 @@ import ( v1 "dkforest/pkg/web/handlers/api/v1" "dkforest/pkg/web/handlers/streamModals" "encoding/base64" - "encoding/csv" "encoding/hex" "encoding/json" - "encoding/pem" "errors" "fmt" "github.com/PuerkitoBio/goquery" @@ -1214,622 +1212,6 @@ func NewsHandler(c echo.Context) error { return c.Render(http.StatusOK, "news", data) } -func LinksHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - var data linksData - - data.Categories, _ = db.GetCategories() - - data.Search = c.QueryParam("search") - filterCategory := c.QueryParam("category") - - if filterCategory != "" { - if filterCategory == "uncategorized" { - db.DB().Raw(`SELECT l.* -FROM links l -LEFT JOIN links_categories_links cl ON cl.link_id = l.id -WHERE cl.link_id IS NULL AND l.deleted_at IS NULL -ORDER BY l.title COLLATE NOCASE ASC`).Scan(&data.Links) - data.LinksCount = int64(len(data.Links)) - } else { - db.DB().Raw(`SELECT l.* -FROM links_categories_links cl -INNER JOIN links l ON l.id = cl.link_id -WHERE cl.category_id = (SELECT id FROM links_categories WHERE name = ?) AND l.deleted_at IS NULL -ORDER BY l.title COLLATE NOCASE ASC`, filterCategory).Scan(&data.Links) - data.LinksCount = int64(len(data.Links)) - } - } else if data.Search != "" { - if govalidator.IsURL(data.Search) { - if searchedURL, err := url.Parse(data.Search); err == nil { - h := searchedURL.Scheme + "://" + searchedURL.Hostname() - var l database.Link - query := db.DB() - if authUser.IsModerator() { - query = query.Unscoped() - } - if err := query.First(&l, "url = ?", h).Error; err == nil { - data.Links = append(data.Links, l) - } - data.LinksCount = int64(len(data.Links)) - } - } else { - if err := db.DB().Raw(`select l.id, l.uuid, l.url, l.title, l.description -from fts5_links l -where fts5_links match ? -ORDER BY rank, l.title COLLATE NOCASE ASC -LIMIT 100`, data.Search).Scan(&data.Links).Error; err != nil { - logrus.Error(err) - } - data.LinksCount = int64(len(data.Links)) - } - } else { - if err := db.DB().Table("links"). - Scopes(func(query *gorm.DB) *gorm.DB { - data.CurrentPage, data.MaxPage, data.LinksCount, query = NewPaginator().Paginate(c, query) - return query - }).Order("title COLLATE NOCASE ASC").Find(&data.Links).Error; err != nil { - logrus.Error(err) - } - } - - // Get all links IDs - linksIDs := make([]int64, 0) - for _, l := range data.Links { - linksIDs = append(linksIDs, l.ID) - } - // Keep pointers to links for fast access - linksCache := make(map[int64]*database.Link) - for i, l := range data.Links { - linksCache[l.ID] = &data.Links[i] - } - // Get all mirrors for all links that we have - var mirrors []database.LinksMirror - db.DB().Raw(`select * from links_mirrors where link_id in (?)`, linksIDs).Scan(&mirrors) - // Put mirrors in links - for _, m := range mirrors { - if l, ok := linksCache[m.LinkID]; ok { - l.Mirrors = append(l.Mirrors, m) - } - } - - return c.Render(http.StatusOK, "links", data) -} - -func LinksDownloadHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - fileName := "dkf_links.csv" - - // Captcha for bigger files - var data captchaRequiredData - data.CaptchaDescription = "Captcha required" - data.CaptchaID, data.CaptchaImg = captcha.New() - const captchaRequiredTmpl = "captcha-required" - if c.Request().Method == http.MethodGet { - return c.Render(http.StatusOK, captchaRequiredTmpl, data) - } - captchaID := c.Request().PostFormValue("captcha_id") - captchaInput := c.Request().PostFormValue("captcha") - if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil { - data.ErrCaptcha = err.Error() - return c.Render(http.StatusOK, captchaRequiredTmpl, data) - } - - // Keep track of user downloads - if _, err := db.CreateDownload(authUser.ID, fileName); err != nil { - logrus.Error(err) - } - - // Get all categories and make a hashmap for fast access - categories, _ := db.GetLinksCategories() - categoriesMap := make(map[int64]string) - for _, category := range categories { - categoriesMap[category.ID] = category.Name - } - // Get all "categories links" associations between links and their categories - categoriesLinks, _ := db.GetCategoriesLinks() - // Build a map of all categories IDs for a given link ID - categoriesLinksMap := make(map[int64][]int64) - for _, cl := range categoriesLinks { - categoriesLinksMap[cl.LinkID] = append(categoriesLinksMap[cl.LinkID], cl.CategoryID) - } - - links, _ := db.GetLinks() - by := make([]byte, 0) - buf := bytes.NewBuffer(by) - w := csv.NewWriter(buf) - _ = w.Write([]string{"UUID", "URL", "Title", "Description", "Categories"}) - for _, link := range links { - // Get all categories for the link - categoryNames := make([]string, 0) - categoryIDs := categoriesLinksMap[link.ID] - for _, tagID := range categoryIDs { - categoryNames = append(categoryNames, categoriesMap[tagID]) - } - _ = w.Write([]string{link.UUID, link.URL, link.Title, link.Description, strings.Join(categoryNames, ",")}) - } - w.Flush() - c.Response().Header().Set("Content-Disposition", `attachment; filename="`+fileName+`"`) - return c.Stream(http.StatusOK, "application/octet-stream", buf) -} - -func LinkPgpDownloadHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - - pgpID := utils.DoParseInt64(c.Param("linkPgpID")) - linkPgp, err := db.GetLinkPgpByID(pgpID) - if err != nil { - return c.NoContent(http.StatusNotFound) - } - - fileName := linkPgp.Title + ".asc" - - // Keep track of user downloads - if _, err := db.CreateDownload(authUser.ID, fileName); err != nil { - logrus.Error(err) - } - - c.Response().Header().Set("Content-Disposition", `attachment; filename="`+fileName+`"`) - return c.Stream(http.StatusOK, "application/octet-stream", strings.NewReader(linkPgp.PgpPublicKey)) -} - -func LinksClaimInstructionsHandler(c echo.Context) error { - return c.Render(http.StatusOK, "links-claim-instructions", nil) -} - -func LinkHandler(c echo.Context) error { - db := c.Get("database").(*database.DkfDB) - shorthand := c.Param("shorthand") - linkUUID := c.Param("linkUUID") - var data linkData - var err error - if shorthand != "" { - data.Link, err = db.GetLinkByShorthand(shorthand) - } else { - data.Link, err = db.GetLinkByUUID(linkUUID) - } - if err != nil { - return c.Redirect(http.StatusFound, "/links") - } - data.PgpKeys, _ = db.GetLinkPgps(data.Link.ID) - data.Mirrors, _ = db.GetLinkMirrors(data.Link.ID) - return c.Render(http.StatusOK, "link", data) -} - -type CsvLink struct { - URL string - Title string -} - -func LinksUploadHandler(c echo.Context) error { - db := c.Get("database").(*database.DkfDB) - var data linksUploadData - if c.Request().Method == http.MethodPost { - data.CsvStr = c.Request().PostFormValue("csv") - getValidLinks := func() (out []CsvLink, err error) { - r := csv.NewReader(strings.NewReader(data.CsvStr)) - records, err := r.ReadAll() - if err != nil { - return out, err - } - for idx, record := range records { - link := strings.TrimSpace(strings.TrimRight(record[0], "/")) - title := record[1] - if !govalidator.Matches(link, `^https?://[a-z2-7]{56}\.onion$`) { - return out, fmt.Errorf("invalid link %s", link) - } - if !govalidator.RuneLength(title, "0", "255") { - return out, fmt.Errorf("title must have 255 characters max : record #%d", idx) - } - csvLink := CsvLink{ - URL: link, - Title: title, - } - out = append(out, csvLink) - } - return out, nil - } - csvLinks, err := getValidLinks() - if err != nil { - data.Error = err.Error() - return c.Render(http.StatusOK, "links-upload", data) - } - for _, csvLink := range csvLinks { - _, err := db.CreateLink(csvLink.URL, csvLink.Title, "", "") - if err != nil { - logrus.Error(err) - } - } - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - return c.Render(http.StatusOK, "links-upload", data) -} - -func LinksReindexHandler(c echo.Context) error { - db := c.Get("database").(*database.DkfDB) - if err := db.DB().Exec(`INSERT INTO fts5_links(fts5_links) VALUES('rebuild')`).Error; err != nil { - logrus.Error(err) - } - db.DB().Exec(`delete from fts5_links where rowid in (select id from links where deleted_at is not null)`) - return c.Redirect(http.StatusFound, c.Request().Referer()) -} - -func ForumReindexHandler(c echo.Context) error { - db := c.Get("database").(*database.DkfDB) - if err := db.DB().Exec(`INSERT INTO fts5_forum_threads(fts5_forum_threads) VALUES('rebuild')`).Error; err != nil { - logrus.Error(err) - } - if err := db.DB().Exec(`INSERT INTO fts5_forum_messages(fts5_forum_messages) VALUES('rebuild')`).Error; err != nil { - logrus.Error(err) - } - return c.Redirect(http.StatusFound, c.Request().Referer()) -} - -func NewLinkHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - if !authUser.IsModerator() { - return c.Redirect(http.StatusFound, "/") - } - var data newLinkData - if c.Request().Method == http.MethodPost { - data.Link = c.Request().PostFormValue("link") - data.Title = c.Request().PostFormValue("title") - data.Description = c.Request().PostFormValue("description") - data.Shorthand = c.Request().PostFormValue("shorthand") - data.Categories = c.Request().PostFormValue("categories") - data.Tags = c.Request().PostFormValue("tags") - if !govalidator.Matches(data.Link, `^https?://[a-z2-7]{56}\.onion$`) { - data.ErrorLink = "invalid link" - return c.Render(http.StatusOK, "new-link", data) - } - if !govalidator.RuneLength(data.Title, "0", "255") { - data.ErrorTitle = "title must have 255 characters max" - return c.Render(http.StatusOK, "new-link", data) - } - if !govalidator.RuneLength(data.Description, "0", "1000") { - data.ErrorCategories = "description must have 1000 characters max" - return c.Render(http.StatusOK, "new-link", data) - } - if data.Shorthand != "" { - if !govalidator.Matches(data.Shorthand, `^[\w-_]{3,50}$`) { - data.ErrorLink = "invalid shorthand" - return c.Render(http.StatusOK, "new-link", data) - } - } - categoryRgx := regexp.MustCompile(`^\w{3,20}$`) - var tagsStr, categoriesStr []string - if data.Categories != "" { - categoriesStr = strings.Split(strings.ToLower(data.Categories), ",") - for _, category := range categoriesStr { - category = strings.TrimSpace(category) - if !categoryRgx.MatchString(category) { - data.ErrorCategories = `invalid category "` + category + `"` - return c.Render(http.StatusOK, "new-link", data) - } - } - } - if data.Tags != "" { - tagsStr = strings.Split(strings.ToLower(data.Tags), ",") - for _, tag := range tagsStr { - tag = strings.TrimSpace(tag) - if !categoryRgx.MatchString(tag) { - data.ErrorTags = `invalid tag "` + tag + `"` - return c.Render(http.StatusOK, "new-link", data) - } - } - } - //------------ - var categories []database.LinksCategory - var tags []database.LinksTag - for _, categoryStr := range categoriesStr { - category, _ := db.CreateLinksCategory(categoryStr) - categories = append(categories, category) - } - for _, tagStr := range tagsStr { - tag, _ := db.CreateLinksTag(tagStr) - tags = append(tags, tag) - } - link, err := db.CreateLink(data.Link, data.Title, data.Description, data.Shorthand) - if err != nil { - logrus.Error(err) - data.ErrorLink = "failed to create link" - return c.Render(http.StatusOK, "new-link", data) - } - for _, category := range categories { - _ = db.AddLinkCategory(link.ID, category.ID) - } - for _, tag := range tags { - _ = db.AddLinkTag(link.ID, tag.ID) - } - db.NewAudit(*authUser, fmt.Sprintf("create link %s", link.URL)) - return c.Redirect(http.StatusFound, "/links") - } - return c.Render(http.StatusOK, "new-link", data) -} - -func RestoreLinkHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - if !authUser.IsModerator() { - return c.Redirect(http.StatusFound, "/") - } - linkUUID := c.Param("linkUUID") - var link database.Link - if err := db.DB().Unscoped().First(&link, "uuid = ?", linkUUID).Error; err != nil { - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - db.NewAudit(*authUser, fmt.Sprintf("restore link %s", link.URL)) - db.DB().Unscoped().Model(&database.Link{}).Where("id = ?", link.ID).Update("deleted_at", nil) - return c.Redirect(http.StatusFound, c.Request().Referer()) -} - -func EditLinkHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - if !authUser.IsModerator() { - return c.Redirect(http.StatusFound, "/") - } - linkUUID := c.Param("linkUUID") - link, err := db.GetLinkByUUID(linkUUID) - if err != nil { - return c.Redirect(http.StatusFound, "/") - } - out, _ := db.GetLinkCategories(link.ID) - categories := make([]string, 0) - for _, el := range out { - categories = append(categories, el.Name) - } - out1, err := db.GetLinkTags(link.ID) - tags := make([]string, 0) - for _, el := range out1 { - tags = append(tags, el.Name) - } - var data editLinkData - data.IsEdit = true - data.Link = link.URL - data.Title = link.Title - data.Description = link.Description - if link.Shorthand != nil { - data.Shorthand = *link.Shorthand - } - data.Categories = strings.Join(categories, ",") - data.Tags = strings.Join(tags, ",") - data.Mirrors, _ = db.GetLinkMirrors(link.ID) - data.LinkPgps, _ = db.GetLinkPgps(link.ID) - //data.Categories = link - - if c.Request().Method == http.MethodPost { - formName := c.Request().PostFormValue("formName") - if formName == "createLink" { - _ = db.DeleteLinkCategories(link.ID) - _ = db.DeleteLinkTags(link.ID) - - // If link is signed, we can no longer edit the link URL - if link.SignedCertificate == "" { - data.Link = c.Request().PostFormValue("link") - } - data.Title = c.Request().PostFormValue("title") - data.Description = c.Request().PostFormValue("description") - data.Shorthand = c.Request().PostFormValue("shorthand") - data.Categories = c.Request().PostFormValue("categories") - data.Tags = c.Request().PostFormValue("tags") - if !govalidator.Matches(data.Link, `^https?://[a-z2-7]{56}\.onion$`) { - data.ErrorLink = "invalid link" - return c.Render(http.StatusOK, "new-link", data) - } - if !govalidator.RuneLength(data.Title, "0", "255") { - data.ErrorTitle = "title must have 255 characters max" - return c.Render(http.StatusOK, "new-link", data) - } - if !govalidator.RuneLength(data.Description, "0", "1000") { - data.ErrorCategories = "description must have 1000 characters max" - return c.Render(http.StatusOK, "new-link", data) - } - if data.Shorthand != "" { - if !govalidator.Matches(data.Shorthand, `^[\w-_]{3,50}$`) { - data.ErrorLink = "invalid shorthand" - return c.Render(http.StatusOK, "new-link", data) - } - } - categoryRgx := regexp.MustCompile(`^\w{3,20}$`) - var tagsStr, categoriesStr []string - if data.Categories != "" { - categoriesStr = strings.Split(strings.ToLower(data.Categories), ",") - for _, category := range categoriesStr { - category = strings.TrimSpace(category) - if !categoryRgx.MatchString(category) { - data.ErrorCategories = `invalid category "` + category + `"` - return c.Render(http.StatusOK, "new-link", data) - } - } - } - if data.Tags != "" { - tagsStr = strings.Split(strings.ToLower(data.Tags), ",") - for _, tag := range tagsStr { - tag = strings.TrimSpace(tag) - if !categoryRgx.MatchString(tag) { - data.ErrorTags = `invalid tag "` + tag + `"` - return c.Render(http.StatusOK, "new-link", data) - } - } - } - //------------ - var categories []database.LinksCategory - var tags []database.LinksTag - for _, categoryStr := range categoriesStr { - category, _ := db.CreateLinksCategory(categoryStr) - categories = append(categories, category) - } - for _, tagStr := range tagsStr { - tag, _ := db.CreateLinksTag(tagStr) - tags = append(tags, tag) - } - link.URL = data.Link - link.Title = data.Title - link.Description = data.Description - if data.Shorthand != "" { - link.Shorthand = &data.Shorthand - } - if err := db.DB().Save(&link).Error; err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed: links.shorthand") { - data.ErrorShorthand = "shorthand already used" - } else { - data.ErrorLink = "failed to update link" - } - return c.Render(http.StatusOK, "new-link", data) - } - for _, category := range categories { - _ = db.AddLinkCategory(link.ID, category.ID) - } - for _, tag := range tags { - _ = db.AddLinkTag(link.ID, tag.ID) - } - db.NewAudit(*authUser, fmt.Sprintf("updated link %s", link.URL)) - return c.Redirect(http.StatusFound, "/links") - - } else if formName == "createPgp" { - data.PGPTitle = c.Request().PostFormValue("pgp_title") - if !govalidator.RuneLength(data.PGPTitle, "3", "255") { - data.ErrorPGPTitle = "title must have 3-255 characters" - return c.Render(http.StatusOK, "new-link", data) - } - data.PGPDescription = c.Request().PostFormValue("pgp_description") - data.PGPPublicKey = c.Request().PostFormValue("pgp_public_key") - if _, err = db.CreateLinkPgp(link.ID, data.PGPTitle, data.PGPDescription, data.PGPPublicKey); err != nil { - logrus.Error(err) - } - db.NewAudit(*authUser, fmt.Sprintf("create gpg for link %s", link.URL)) - return c.Redirect(http.StatusFound, c.Request().Referer()) - - } else if formName == "createMirror" { - data.MirrorLink = c.Request().PostFormValue("mirror_link") - if !govalidator.Matches(data.MirrorLink, `^https?://[a-z2-7]{56}\.onion$`) { - data.ErrorMirrorLink = "invalid link" - return c.Render(http.StatusOK, "new-link", data) - } - if _, err = db.CreateLinkMirror(link.ID, data.MirrorLink); err != nil { - logrus.Error(err) - } - db.NewAudit(*authUser, fmt.Sprintf("create mirror for link %s", link.URL)) - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - return c.Redirect(http.StatusFound, "/links") - } - - return c.Render(http.StatusOK, "new-link", data) -} - -func ClaimLinkHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - linkUUID := c.Param("linkUUID") - link, err := db.GetLinkByUUID(linkUUID) - if err != nil { - return c.Redirect(http.StatusFound, "/") - } - var data claimLinkData - data.Link = link - data.Certificate = link.GenOwnershipCert(authUser.Username) - - if c.Request().Method == http.MethodGet { - return c.Render(http.StatusOK, "link-claim", data) - } - - data.Signature = c.Request().PostFormValue("signature") - - b64Sig, err := base64.StdEncoding.DecodeString(data.Signature) - if err != nil { - data.Error = "invalid signature" - return c.Render(http.StatusOK, "link-claim", data) - } - pemSign := string(pem.EncodeToMemory(&pem.Block{Type: "SIGNATURE", Bytes: b64Sig})) - - isValid := utils.VerifyTorSign(link.GetOnionAddr(), data.Certificate, pemSign) - if !isValid { - data.Error = "invalid signature" - return c.Render(http.StatusOK, "link-claim", data) - } - - signedCert := "-----BEGIN SIGNED MESSAGE-----\n" + - data.Certificate + "\n" + - pemSign - - link.SignedCertificate = signedCert - link.OwnerUserID = &authUser.ID - link.DoSave(db) - - return c.Redirect(http.StatusFound, "/links/"+link.UUID) -} - -func ClaimDownloadCertificateLinkHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - - linkUUID := c.Param("linkUUID") - link, err := db.GetLinkByUUID(linkUUID) - if err != nil { - return c.Redirect(http.StatusFound, "/") - } - - fileName := "certificate.txt" - - // Keep track of user downloads - if _, err := db.CreateDownload(authUser.ID, fileName); err != nil { - logrus.Error(err) - } - - c.Response().Header().Set("Content-Disposition", `attachment; filename="`+fileName+`"`) - return c.Stream(http.StatusOK, "application/octet-stream", strings.NewReader(link.GenOwnershipCert(authUser.Username))) -} - -func ClaimCertificateLinkHandler(c echo.Context) error { - linkUUID := c.Param("linkUUID") - db := c.Get("database").(*database.DkfDB) - link, err := db.GetLinkByUUID(linkUUID) - if err != nil { - return c.Redirect(http.StatusFound, "/") - } - return c.String(http.StatusOK, link.SignedCertificate) -} - -func ThreadHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - threadUUID := database.ForumThreadUUID(c.Param("threadUUID")) - thread, err := db.GetForumThreadByUUID(threadUUID) - if err != nil { - return c.Redirect(http.StatusFound, "/") - } - var data threadData - data.Thread = thread - - if err := db.DB(). - Table("forum_messages"). - Where("thread_id = ?", thread.ID). - Scopes(func(query *gorm.DB) *gorm.DB { - data.CurrentPage, data.MaxPage, data.MessagesCount, query = NewPaginator().Paginate(c, query) - return query - }). - Order("id ASC"). - Where("thread_id = ?", thread.ID). - Preload("User"). - Find(&data.Messages).Error; err != nil { - logrus.Error(err) - } - - if authUser != nil { - data.IsSubscribed = db.IsUserSubscribedToForumThread(authUser.ID, thread.ID) - // Update read record - db.UpdateForumReadRecord(authUser.ID, thread.ID) - } - - return c.Render(http.StatusOK, "thread", data) -} - func GistHandler(c echo.Context) error { authUser := c.Get("authUser").(*database.User) db := c.Get("database").(*database.DkfDB) @@ -1910,95 +1292,6 @@ func WerewolfHandler(c echo.Context) error { return c.Render(http.StatusOK, "werewolf", nil) } -func LinkDeleteHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - linkUUID := c.Param("linkUUID") - link, err := db.GetLinkByUUID(linkUUID) - if err != nil { - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - - if !authUser.IsModerator() { - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - - var data deleteLinkData - data.Link = link - - if c.Request().Method == http.MethodPost { - db.NewAudit(*authUser, fmt.Sprintf("deleted link %s", link.URL)) - if err := db.DeleteLinkByID(link.ID); err != nil { - logrus.Error(err) - } - return c.Redirect(http.StatusFound, "/links") - } - - return c.Render(http.StatusOK, "link-delete", data) -} - -func LinkPgpDeleteHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - linkPgpID := utils.DoParseInt64(c.Param("linkPgpID")) - linkPgp, err := db.GetLinkPgpByID(linkPgpID) - if err != nil { - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - link, err := db.GetLinkByID(linkPgp.LinkID) - if err != nil { - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - - if !authUser.IsModerator() { - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - - var data deleteLinkPgpData - data.Link = link - data.LinkPgp = linkPgp - - if c.Request().Method == http.MethodPost { - if err := db.DeleteLinkPgpByID(linkPgp.ID); err != nil { - logrus.Error(err) - } - return c.Redirect(http.StatusFound, "/links/"+link.UUID+"/edit") - } - - return c.Render(http.StatusOK, "link-pgp-delete", data) -} - -func LinkMirrorDeleteHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - linkMirrorID := utils.DoParseInt64(c.Param("linkMirrorID")) - linkMirror, err := db.GetLinkMirrorByID(linkMirrorID) - if err != nil { - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - link, err := db.GetLinkByID(linkMirror.LinkID) - if err != nil { - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - - if !authUser.IsModerator() { - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - - var data deleteLinkMirrorData - data.Link = link - data.LinkMirror = linkMirror - - if c.Request().Method == http.MethodPost { - if err := db.DeleteLinkMirrorByID(linkMirror.ID); err != nil { - logrus.Error(err) - } - return c.Redirect(http.StatusFound, "/links/"+link.UUID+"/edit") - } - - return c.Render(http.StatusOK, "link-mirror-delete", data) -} - func VipHandler(c echo.Context) error { db := c.Get("database").(*database.DkfDB) var data vipData @@ -2213,51 +1506,6 @@ func SettingsSecretPhraseHandler(c echo.Context) error { return c.Render(http.StatusFound, "flash", FlashResponse{Message: "Secret phrase changed successfully", Redirect: c.Request().Referer()}) } -// SettingsInvitationsHandler ... -func SettingsInvitationsHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - var data settingsInvitationsData - data.ActiveTab = "invitations" - data.DkfOnion = config.DkfOnion - - if c.Request().Method == http.MethodPost { - if _, err := db.CreateInvitation(authUser.ID); err != nil { - logrus.Error(err) - } - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - - data.Invitations, _ = db.GetUserUnusedInvitations(authUser.ID) - return c.Render(http.StatusOK, "settings.invitations", data) -} - -// SettingsWebsiteHandler ... -func SettingsWebsiteHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - var data settingsWebsiteData - data.ActiveTab = "website" - settings := db.GetSettings() - data.SignupEnabled = settings.SignupEnabled - data.ForumEnabled = settings.ForumEnabled - data.SilentSelfKick = settings.SilentSelfKick - if c.Request().Method == http.MethodPost { - settings.SignupEnabled = utils.DoParseBool(c.Request().PostFormValue("signupEnabled")) - settings.ForumEnabled = utils.DoParseBool(c.Request().PostFormValue("forumEnabled")) - settings.SilentSelfKick = utils.DoParseBool(c.Request().PostFormValue("silentSelfKick")) - settings.DoSave(db) - config.SignupEnabled.Store(settings.SignupEnabled) - config.ForumEnabled.Store(settings.ForumEnabled) - config.SilentSelfKick.Store(settings.SilentSelfKick) - db.NewAudit(*authUser, fmt.Sprintf("website settings, signup: %t, forum: %t, sk: %t", - settings.SignupEnabled, settings.ForumEnabled, settings.SilentSelfKick)) - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - - return c.Render(http.StatusOK, "settings.website", data) -} - func ChatDeleteHandler(c echo.Context) error { authUser := c.Get("authUser").(*database.User) db := c.Get("database").(*database.DkfDB) diff --git a/pkg/web/handlers/links.go b/pkg/web/handlers/links.go @@ -0,0 +1,681 @@ +package handlers + +import ( + "bytes" + "dkforest/pkg/captcha" + "dkforest/pkg/database" + "dkforest/pkg/utils" + hutils "dkforest/pkg/web/handlers/utils" + "encoding/base64" + "encoding/csv" + "encoding/pem" + "fmt" + "github.com/asaskevich/govalidator" + "github.com/jinzhu/gorm" + "github.com/labstack/echo" + "github.com/sirupsen/logrus" + "net/http" + "net/url" + "regexp" + "strings" +) + +func LinksHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + var data linksData + + data.Categories, _ = db.GetCategories() + + data.Search = c.QueryParam("search") + filterCategory := c.QueryParam("category") + + if filterCategory != "" { + if filterCategory == "uncategorized" { + db.DB().Raw(`SELECT l.* +FROM links l +LEFT JOIN links_categories_links cl ON cl.link_id = l.id +WHERE cl.link_id IS NULL AND l.deleted_at IS NULL +ORDER BY l.title COLLATE NOCASE ASC`).Scan(&data.Links) + data.LinksCount = int64(len(data.Links)) + } else { + db.DB().Raw(`SELECT l.* +FROM links_categories_links cl +INNER JOIN links l ON l.id = cl.link_id +WHERE cl.category_id = (SELECT id FROM links_categories WHERE name = ?) AND l.deleted_at IS NULL +ORDER BY l.title COLLATE NOCASE ASC`, filterCategory).Scan(&data.Links) + data.LinksCount = int64(len(data.Links)) + } + } else if data.Search != "" { + if govalidator.IsURL(data.Search) { + if searchedURL, err := url.Parse(data.Search); err == nil { + h := searchedURL.Scheme + "://" + searchedURL.Hostname() + var l database.Link + query := db.DB() + if authUser.IsModerator() { + query = query.Unscoped() + } + if err := query.First(&l, "url = ?", h).Error; err == nil { + data.Links = append(data.Links, l) + } + data.LinksCount = int64(len(data.Links)) + } + } else { + if err := db.DB().Raw(`select l.id, l.uuid, l.url, l.title, l.description +from fts5_links l +where fts5_links match ? +ORDER BY rank, l.title COLLATE NOCASE ASC +LIMIT 100`, data.Search).Scan(&data.Links).Error; err != nil { + logrus.Error(err) + } + data.LinksCount = int64(len(data.Links)) + } + } else { + if err := db.DB().Table("links"). + Scopes(func(query *gorm.DB) *gorm.DB { + data.CurrentPage, data.MaxPage, data.LinksCount, query = NewPaginator().Paginate(c, query) + return query + }).Order("title COLLATE NOCASE ASC").Find(&data.Links).Error; err != nil { + logrus.Error(err) + } + } + + // Get all links IDs + linksIDs := make([]int64, 0) + for _, l := range data.Links { + linksIDs = append(linksIDs, l.ID) + } + // Keep pointers to links for fast access + linksCache := make(map[int64]*database.Link) + for i, l := range data.Links { + linksCache[l.ID] = &data.Links[i] + } + // Get all mirrors for all links that we have + var mirrors []database.LinksMirror + db.DB().Raw(`select * from links_mirrors where link_id in (?)`, linksIDs).Scan(&mirrors) + // Put mirrors in links + for _, m := range mirrors { + if l, ok := linksCache[m.LinkID]; ok { + l.Mirrors = append(l.Mirrors, m) + } + } + + return c.Render(http.StatusOK, "links", data) +} + +func LinksDownloadHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + fileName := "dkf_links.csv" + + // Captcha for bigger files + var data captchaRequiredData + data.CaptchaDescription = "Captcha required" + data.CaptchaID, data.CaptchaImg = captcha.New() + const captchaRequiredTmpl = "captcha-required" + if c.Request().Method == http.MethodGet { + return c.Render(http.StatusOK, captchaRequiredTmpl, data) + } + captchaID := c.Request().PostFormValue("captcha_id") + captchaInput := c.Request().PostFormValue("captcha") + if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil { + data.ErrCaptcha = err.Error() + return c.Render(http.StatusOK, captchaRequiredTmpl, data) + } + + // Keep track of user downloads + if _, err := db.CreateDownload(authUser.ID, fileName); err != nil { + logrus.Error(err) + } + + // Get all categories and make a hashmap for fast access + categories, _ := db.GetLinksCategories() + categoriesMap := make(map[int64]string) + for _, category := range categories { + categoriesMap[category.ID] = category.Name + } + // Get all "categories links" associations between links and their categories + categoriesLinks, _ := db.GetCategoriesLinks() + // Build a map of all categories IDs for a given link ID + categoriesLinksMap := make(map[int64][]int64) + for _, cl := range categoriesLinks { + categoriesLinksMap[cl.LinkID] = append(categoriesLinksMap[cl.LinkID], cl.CategoryID) + } + + links, _ := db.GetLinks() + by := make([]byte, 0) + buf := bytes.NewBuffer(by) + w := csv.NewWriter(buf) + _ = w.Write([]string{"UUID", "URL", "Title", "Description", "Categories"}) + for _, link := range links { + // Get all categories for the link + categoryNames := make([]string, 0) + categoryIDs := categoriesLinksMap[link.ID] + for _, tagID := range categoryIDs { + categoryNames = append(categoryNames, categoriesMap[tagID]) + } + _ = w.Write([]string{link.UUID, link.URL, link.Title, link.Description, strings.Join(categoryNames, ",")}) + } + w.Flush() + c.Response().Header().Set("Content-Disposition", `attachment; filename="`+fileName+`"`) + return c.Stream(http.StatusOK, "application/octet-stream", buf) +} + +func LinkHandler(c echo.Context) error { + db := c.Get("database").(*database.DkfDB) + shorthand := c.Param("shorthand") + linkUUID := c.Param("linkUUID") + var data linkData + var err error + if shorthand != "" { + data.Link, err = db.GetLinkByShorthand(shorthand) + } else { + data.Link, err = db.GetLinkByUUID(linkUUID) + } + if err != nil { + return c.Redirect(http.StatusFound, "/links") + } + data.PgpKeys, _ = db.GetLinkPgps(data.Link.ID) + data.Mirrors, _ = db.GetLinkMirrors(data.Link.ID) + return c.Render(http.StatusOK, "link", data) +} + +func RestoreLinkHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + if !authUser.IsModerator() { + return c.Redirect(http.StatusFound, "/") + } + linkUUID := c.Param("linkUUID") + var link database.Link + if err := db.DB().Unscoped().First(&link, "uuid = ?", linkUUID).Error; err != nil { + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + db.NewAudit(*authUser, fmt.Sprintf("restore link %s", link.URL)) + db.DB().Unscoped().Model(&database.Link{}).Where("id = ?", link.ID).Update("deleted_at", nil) + return c.Redirect(http.StatusFound, c.Request().Referer()) +} + +func ClaimLinkHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + linkUUID := c.Param("linkUUID") + link, err := db.GetLinkByUUID(linkUUID) + if err != nil { + return c.Redirect(http.StatusFound, "/") + } + var data claimLinkData + data.Link = link + data.Certificate = link.GenOwnershipCert(authUser.Username) + + if c.Request().Method == http.MethodGet { + return c.Render(http.StatusOK, "link-claim", data) + } + + data.Signature = c.Request().PostFormValue("signature") + + b64Sig, err := base64.StdEncoding.DecodeString(data.Signature) + if err != nil { + data.Error = "invalid signature" + return c.Render(http.StatusOK, "link-claim", data) + } + pemSign := string(pem.EncodeToMemory(&pem.Block{Type: "SIGNATURE", Bytes: b64Sig})) + + isValid := utils.VerifyTorSign(link.GetOnionAddr(), data.Certificate, pemSign) + if !isValid { + data.Error = "invalid signature" + return c.Render(http.StatusOK, "link-claim", data) + } + + signedCert := "-----BEGIN SIGNED MESSAGE-----\n" + + data.Certificate + "\n" + + pemSign + + link.SignedCertificate = signedCert + link.OwnerUserID = &authUser.ID + link.DoSave(db) + + return c.Redirect(http.StatusFound, "/links/"+link.UUID) +} + +func ClaimDownloadCertificateLinkHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + + linkUUID := c.Param("linkUUID") + link, err := db.GetLinkByUUID(linkUUID) + if err != nil { + return c.Redirect(http.StatusFound, "/") + } + + fileName := "certificate.txt" + + // Keep track of user downloads + if _, err := db.CreateDownload(authUser.ID, fileName); err != nil { + logrus.Error(err) + } + + c.Response().Header().Set("Content-Disposition", `attachment; filename="`+fileName+`"`) + return c.Stream(http.StatusOK, "application/octet-stream", strings.NewReader(link.GenOwnershipCert(authUser.Username))) +} + +func ClaimCertificateLinkHandler(c echo.Context) error { + linkUUID := c.Param("linkUUID") + db := c.Get("database").(*database.DkfDB) + link, err := db.GetLinkByUUID(linkUUID) + if err != nil { + return c.Redirect(http.StatusFound, "/") + } + return c.String(http.StatusOK, link.SignedCertificate) +} + +func EditLinkHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + if !authUser.IsModerator() { + return c.Redirect(http.StatusFound, "/") + } + linkUUID := c.Param("linkUUID") + link, err := db.GetLinkByUUID(linkUUID) + if err != nil { + return c.Redirect(http.StatusFound, "/") + } + out, _ := db.GetLinkCategories(link.ID) + categories := make([]string, 0) + for _, el := range out { + categories = append(categories, el.Name) + } + out1, err := db.GetLinkTags(link.ID) + tags := make([]string, 0) + for _, el := range out1 { + tags = append(tags, el.Name) + } + var data editLinkData + data.IsEdit = true + data.Link = link.URL + data.Title = link.Title + data.Description = link.Description + if link.Shorthand != nil { + data.Shorthand = *link.Shorthand + } + data.Categories = strings.Join(categories, ",") + data.Tags = strings.Join(tags, ",") + data.Mirrors, _ = db.GetLinkMirrors(link.ID) + data.LinkPgps, _ = db.GetLinkPgps(link.ID) + //data.Categories = link + + if c.Request().Method == http.MethodPost { + formName := c.Request().PostFormValue("formName") + if formName == "createLink" { + _ = db.DeleteLinkCategories(link.ID) + _ = db.DeleteLinkTags(link.ID) + + // If link is signed, we can no longer edit the link URL + if link.SignedCertificate == "" { + data.Link = c.Request().PostFormValue("link") + } + data.Title = c.Request().PostFormValue("title") + data.Description = c.Request().PostFormValue("description") + data.Shorthand = c.Request().PostFormValue("shorthand") + data.Categories = c.Request().PostFormValue("categories") + data.Tags = c.Request().PostFormValue("tags") + if !govalidator.Matches(data.Link, `^https?://[a-z2-7]{56}\.onion$`) { + data.ErrorLink = "invalid link" + return c.Render(http.StatusOK, "new-link", data) + } + if !govalidator.RuneLength(data.Title, "0", "255") { + data.ErrorTitle = "title must have 255 characters max" + return c.Render(http.StatusOK, "new-link", data) + } + if !govalidator.RuneLength(data.Description, "0", "1000") { + data.ErrorCategories = "description must have 1000 characters max" + return c.Render(http.StatusOK, "new-link", data) + } + if data.Shorthand != "" { + if !govalidator.Matches(data.Shorthand, `^[\w-_]{3,50}$`) { + data.ErrorLink = "invalid shorthand" + return c.Render(http.StatusOK, "new-link", data) + } + } + categoryRgx := regexp.MustCompile(`^\w{3,20}$`) + var tagsStr, categoriesStr []string + if data.Categories != "" { + categoriesStr = strings.Split(strings.ToLower(data.Categories), ",") + for _, category := range categoriesStr { + category = strings.TrimSpace(category) + if !categoryRgx.MatchString(category) { + data.ErrorCategories = `invalid category "` + category + `"` + return c.Render(http.StatusOK, "new-link", data) + } + } + } + if data.Tags != "" { + tagsStr = strings.Split(strings.ToLower(data.Tags), ",") + for _, tag := range tagsStr { + tag = strings.TrimSpace(tag) + if !categoryRgx.MatchString(tag) { + data.ErrorTags = `invalid tag "` + tag + `"` + return c.Render(http.StatusOK, "new-link", data) + } + } + } + //------------ + var categories []database.LinksCategory + var tags []database.LinksTag + for _, categoryStr := range categoriesStr { + category, _ := db.CreateLinksCategory(categoryStr) + categories = append(categories, category) + } + for _, tagStr := range tagsStr { + tag, _ := db.CreateLinksTag(tagStr) + tags = append(tags, tag) + } + link.URL = data.Link + link.Title = data.Title + link.Description = data.Description + if data.Shorthand != "" { + link.Shorthand = &data.Shorthand + } + if err := db.DB().Save(&link).Error; err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed: links.shorthand") { + data.ErrorShorthand = "shorthand already used" + } else { + data.ErrorLink = "failed to update link" + } + return c.Render(http.StatusOK, "new-link", data) + } + for _, category := range categories { + _ = db.AddLinkCategory(link.ID, category.ID) + } + for _, tag := range tags { + _ = db.AddLinkTag(link.ID, tag.ID) + } + db.NewAudit(*authUser, fmt.Sprintf("updated link %s", link.URL)) + return c.Redirect(http.StatusFound, "/links") + + } else if formName == "createPgp" { + data.PGPTitle = c.Request().PostFormValue("pgp_title") + if !govalidator.RuneLength(data.PGPTitle, "3", "255") { + data.ErrorPGPTitle = "title must have 3-255 characters" + return c.Render(http.StatusOK, "new-link", data) + } + data.PGPDescription = c.Request().PostFormValue("pgp_description") + data.PGPPublicKey = c.Request().PostFormValue("pgp_public_key") + if _, err = db.CreateLinkPgp(link.ID, data.PGPTitle, data.PGPDescription, data.PGPPublicKey); err != nil { + logrus.Error(err) + } + db.NewAudit(*authUser, fmt.Sprintf("create gpg for link %s", link.URL)) + return c.Redirect(http.StatusFound, c.Request().Referer()) + + } else if formName == "createMirror" { + data.MirrorLink = c.Request().PostFormValue("mirror_link") + if !govalidator.Matches(data.MirrorLink, `^https?://[a-z2-7]{56}\.onion$`) { + data.ErrorMirrorLink = "invalid link" + return c.Render(http.StatusOK, "new-link", data) + } + if _, err = db.CreateLinkMirror(link.ID, data.MirrorLink); err != nil { + logrus.Error(err) + } + db.NewAudit(*authUser, fmt.Sprintf("create mirror for link %s", link.URL)) + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + return c.Redirect(http.StatusFound, "/links") + } + + return c.Render(http.StatusOK, "new-link", data) +} + +func LinkDeleteHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + linkUUID := c.Param("linkUUID") + link, err := db.GetLinkByUUID(linkUUID) + if err != nil { + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + + if !authUser.IsModerator() { + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + + var data deleteLinkData + data.Link = link + + if c.Request().Method == http.MethodPost { + db.NewAudit(*authUser, fmt.Sprintf("deleted link %s", link.URL)) + if err := db.DeleteLinkByID(link.ID); err != nil { + logrus.Error(err) + } + return c.Redirect(http.StatusFound, "/links") + } + + return c.Render(http.StatusOK, "link-delete", data) +} + +func LinkPgpDownloadHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + + pgpID := utils.DoParseInt64(c.Param("linkPgpID")) + linkPgp, err := db.GetLinkPgpByID(pgpID) + if err != nil { + return c.NoContent(http.StatusNotFound) + } + + fileName := linkPgp.Title + ".asc" + + // Keep track of user downloads + if _, err := db.CreateDownload(authUser.ID, fileName); err != nil { + logrus.Error(err) + } + + c.Response().Header().Set("Content-Disposition", `attachment; filename="`+fileName+`"`) + return c.Stream(http.StatusOK, "application/octet-stream", strings.NewReader(linkPgp.PgpPublicKey)) +} + +func LinkPgpDeleteHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + linkPgpID := utils.DoParseInt64(c.Param("linkPgpID")) + linkPgp, err := db.GetLinkPgpByID(linkPgpID) + if err != nil { + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + link, err := db.GetLinkByID(linkPgp.LinkID) + if err != nil { + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + + if !authUser.IsModerator() { + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + + var data deleteLinkPgpData + data.Link = link + data.LinkPgp = linkPgp + + if c.Request().Method == http.MethodPost { + if err := db.DeleteLinkPgpByID(linkPgp.ID); err != nil { + logrus.Error(err) + } + return c.Redirect(http.StatusFound, "/links/"+link.UUID+"/edit") + } + + return c.Render(http.StatusOK, "link-pgp-delete", data) +} + +func LinkMirrorDeleteHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + linkMirrorID := utils.DoParseInt64(c.Param("linkMirrorID")) + linkMirror, err := db.GetLinkMirrorByID(linkMirrorID) + if err != nil { + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + link, err := db.GetLinkByID(linkMirror.LinkID) + if err != nil { + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + + if !authUser.IsModerator() { + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + + var data deleteLinkMirrorData + data.Link = link + data.LinkMirror = linkMirror + + if c.Request().Method == http.MethodPost { + if err := db.DeleteLinkMirrorByID(linkMirror.ID); err != nil { + logrus.Error(err) + } + return c.Redirect(http.StatusFound, "/links/"+link.UUID+"/edit") + } + + return c.Render(http.StatusOK, "link-mirror-delete", data) +} + +type CsvLink struct { + URL string + Title string +} + +func LinksUploadHandler(c echo.Context) error { + db := c.Get("database").(*database.DkfDB) + var data linksUploadData + if c.Request().Method == http.MethodPost { + data.CsvStr = c.Request().PostFormValue("csv") + getValidLinks := func() (out []CsvLink, err error) { + r := csv.NewReader(strings.NewReader(data.CsvStr)) + records, err := r.ReadAll() + if err != nil { + return out, err + } + for idx, record := range records { + link := strings.TrimSpace(strings.TrimRight(record[0], "/")) + title := record[1] + if !govalidator.Matches(link, `^https?://[a-z2-7]{56}\.onion$`) { + return out, fmt.Errorf("invalid link %s", link) + } + if !govalidator.RuneLength(title, "0", "255") { + return out, fmt.Errorf("title must have 255 characters max : record #%d", idx) + } + csvLink := CsvLink{ + URL: link, + Title: title, + } + out = append(out, csvLink) + } + return out, nil + } + csvLinks, err := getValidLinks() + if err != nil { + data.Error = err.Error() + return c.Render(http.StatusOK, "links-upload", data) + } + for _, csvLink := range csvLinks { + _, err := db.CreateLink(csvLink.URL, csvLink.Title, "", "") + if err != nil { + logrus.Error(err) + } + } + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + return c.Render(http.StatusOK, "links-upload", data) +} + +func NewLinkHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + if !authUser.IsModerator() { + return c.Redirect(http.StatusFound, "/") + } + var data newLinkData + if c.Request().Method == http.MethodPost { + data.Link = c.Request().PostFormValue("link") + data.Title = c.Request().PostFormValue("title") + data.Description = c.Request().PostFormValue("description") + data.Shorthand = c.Request().PostFormValue("shorthand") + data.Categories = c.Request().PostFormValue("categories") + data.Tags = c.Request().PostFormValue("tags") + if !govalidator.Matches(data.Link, `^https?://[a-z2-7]{56}\.onion$`) { + data.ErrorLink = "invalid link" + return c.Render(http.StatusOK, "new-link", data) + } + if !govalidator.RuneLength(data.Title, "0", "255") { + data.ErrorTitle = "title must have 255 characters max" + return c.Render(http.StatusOK, "new-link", data) + } + if !govalidator.RuneLength(data.Description, "0", "1000") { + data.ErrorCategories = "description must have 1000 characters max" + return c.Render(http.StatusOK, "new-link", data) + } + if data.Shorthand != "" { + if !govalidator.Matches(data.Shorthand, `^[\w-_]{3,50}$`) { + data.ErrorLink = "invalid shorthand" + return c.Render(http.StatusOK, "new-link", data) + } + } + categoryRgx := regexp.MustCompile(`^\w{3,20}$`) + var tagsStr, categoriesStr []string + if data.Categories != "" { + categoriesStr = strings.Split(strings.ToLower(data.Categories), ",") + for _, category := range categoriesStr { + category = strings.TrimSpace(category) + if !categoryRgx.MatchString(category) { + data.ErrorCategories = `invalid category "` + category + `"` + return c.Render(http.StatusOK, "new-link", data) + } + } + } + if data.Tags != "" { + tagsStr = strings.Split(strings.ToLower(data.Tags), ",") + for _, tag := range tagsStr { + tag = strings.TrimSpace(tag) + if !categoryRgx.MatchString(tag) { + data.ErrorTags = `invalid tag "` + tag + `"` + return c.Render(http.StatusOK, "new-link", data) + } + } + } + //------------ + var categories []database.LinksCategory + var tags []database.LinksTag + for _, categoryStr := range categoriesStr { + category, _ := db.CreateLinksCategory(categoryStr) + categories = append(categories, category) + } + for _, tagStr := range tagsStr { + tag, _ := db.CreateLinksTag(tagStr) + tags = append(tags, tag) + } + link, err := db.CreateLink(data.Link, data.Title, data.Description, data.Shorthand) + if err != nil { + logrus.Error(err) + data.ErrorLink = "failed to create link" + return c.Render(http.StatusOK, "new-link", data) + } + for _, category := range categories { + _ = db.AddLinkCategory(link.ID, category.ID) + } + for _, tag := range tags { + _ = db.AddLinkTag(link.ID, tag.ID) + } + db.NewAudit(*authUser, fmt.Sprintf("create link %s", link.URL)) + return c.Redirect(http.StatusFound, "/links") + } + return c.Render(http.StatusOK, "new-link", data) +} + +func LinksClaimInstructionsHandler(c echo.Context) error { + return c.Render(http.StatusOK, "links-claim-instructions", nil) +} + +func LinksReindexHandler(c echo.Context) error { + db := c.Get("database").(*database.DkfDB) + if err := db.DB().Exec(`INSERT INTO fts5_links(fts5_links) VALUES('rebuild')`).Error; err != nil { + logrus.Error(err) + } + db.DB().Exec(`delete from fts5_links where rowid in (select id from links where deleted_at is not null)`) + return c.Redirect(http.StatusFound, c.Request().Referer()) +} diff --git a/pkg/web/handlers/settings.go b/pkg/web/handlers/settings.go @@ -893,3 +893,48 @@ func AddAgeHandler(c echo.Context) error { } return c.Render(http.StatusOK, "age", data) } + +// SettingsWebsiteHandler ... +func SettingsWebsiteHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + var data settingsWebsiteData + data.ActiveTab = "website" + settings := db.GetSettings() + data.SignupEnabled = settings.SignupEnabled + data.ForumEnabled = settings.ForumEnabled + data.SilentSelfKick = settings.SilentSelfKick + if c.Request().Method == http.MethodPost { + settings.SignupEnabled = utils.DoParseBool(c.Request().PostFormValue("signupEnabled")) + settings.ForumEnabled = utils.DoParseBool(c.Request().PostFormValue("forumEnabled")) + settings.SilentSelfKick = utils.DoParseBool(c.Request().PostFormValue("silentSelfKick")) + settings.DoSave(db) + config.SignupEnabled.Store(settings.SignupEnabled) + config.ForumEnabled.Store(settings.ForumEnabled) + config.SilentSelfKick.Store(settings.SilentSelfKick) + db.NewAudit(*authUser, fmt.Sprintf("website settings, signup: %t, forum: %t, sk: %t", + settings.SignupEnabled, settings.ForumEnabled, settings.SilentSelfKick)) + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + + return c.Render(http.StatusOK, "settings.website", data) +} + +// SettingsInvitationsHandler ... +func SettingsInvitationsHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + var data settingsInvitationsData + data.ActiveTab = "invitations" + data.DkfOnion = config.DkfOnion + + if c.Request().Method == http.MethodPost { + if _, err := db.CreateInvitation(authUser.ID); err != nil { + logrus.Error(err) + } + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + + data.Invitations, _ = db.GetUserUnusedInvitations(authUser.ID) + return c.Render(http.StatusOK, "settings.invitations", data) +}