From c77a98542a78e042a472805b1eda51de0612dd5a Mon Sep 17 00:00:00 2001 From: Umputun Date: Sat, 2 Jun 2018 02:54:48 -0500 Subject: [PATCH] split too large rest file --- app/rest/api/rest.go | 484 ----------------------------------- app/rest/api/rest_private.go | 213 +++++++++++++++ app/rest/api/rest_public.go | 305 ++++++++++++++++++++++ 3 files changed, 518 insertions(+), 484 deletions(-) create mode 100644 app/rest/api/rest_private.go create mode 100644 app/rest/api/rest_public.go diff --git a/app/rest/api/rest.go b/app/rest/api/rest.go index 60c707a8..b09a8a06 100644 --- a/app/rest/api/rest.go +++ b/app/rest/api/rest.go @@ -2,15 +2,11 @@ package api import ( "bytes" - "compress/gzip" "context" - "crypto/sha1" - "encoding/base64" "encoding/json" "fmt" "log" "net/http" - "strconv" "strings" "sync" "time" @@ -177,486 +173,6 @@ func (s *Rest) routes() chi.Router { return router } -// POST /comment - adds comment, resets all immutable fields -func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) { - - comment := store.Comment{} - if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment") - return - } - - user, err := rest.GetUserInfo(r) - if err != nil { // this not suppose to happen (handled by Auth), just dbl-check - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - log.Printf("[DEBUG] create comment %+v", comment) - - comment.PrepareUntrusted() // clean all fields user not supposed to set - comment.User = user - comment.User.IP = strings.Split(r.RemoteAddr, ":")[0] - - comment.Orig = comment.Text // original comment text, prior to md render - if err = s.DataService.ValidateComment(&comment); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment") - return - } - comment.Text = string(blackfriday.Run([]byte(comment.Text), blackfriday.WithExtensions(mdExt))) - comment.Text = s.ImageProxy.Convert(comment.Text) - // check if user blocked - if s.adminService.checkBlocked(comment.Locator.SiteID, comment.User) { - rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "user blocked") - return - } - - if s.ReadOnlyAge > 0 { - if info, e := s.DataService.Info(comment.Locator, s.ReadOnlyAge); e == nil && info.ReadOnly { - rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "old post, read-only") - return - } - } - - id, err := s.DataService.Create(comment) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't save comment") - return - } - - // DataService modifies comment - finalComment, err := s.DataService.Get(comment.Locator, id) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't load created comment") - return - } - s.Cache.Flush(comment.Locator.URL, "last", comment.User.ID, comment.Locator.SiteID) - - render.Status(r, http.StatusCreated) - render.JSON(w, r, &finalComment) -} - -// POST /preview, body is a comment -func (s *Rest) previewCommentCtrl(w http.ResponseWriter, r *http.Request) { - comment := store.Comment{} - if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment") - return - } - - user, err := rest.GetUserInfo(r) - if err != nil { // this not suppose to happen (handled by Auth), just dbl-check - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - comment.User = user - comment.Orig = comment.Text - if err = s.DataService.ValidateComment(&comment); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment") - return - } - - //comment.Text = string(blackfriday.Run([]byte(comment.Text), - // blackfriday.WithRenderer(bfchroma.NewRenderer(bfchroma.WithoutAutodetect())))) - comment.Text = string(blackfriday.Run([]byte(comment.Text), blackfriday.WithExtensions(mdExt))) - comment.Text = s.ImageProxy.Convert(comment.Text) - comment.Sanitize() - render.HTML(w, r, comment.Text) -} - -// GET /info?site=siteID&url=post-url - get info about the post -func (s *Rest) infoCtrl(w http.ResponseWriter, r *http.Request) { - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - - data, err := s.Cache.Get(cache.Key(cache.URLKey(r), locator.SiteID, locator.URL), func() ([]byte, error) { - info, e := s.DataService.Info(locator, s.ReadOnlyAge) - if e != nil { - return nil, e - } - return encodeJSONWithHTML(info) - }) - - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get post info") - return - } - - renderJSONFromBytes(w, r, data) -} - -// PUT /comment/{id}?site=siteID&url=post-url - update comment -func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) { - - edit := struct { - Text string - Summary string - }{} - - if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &edit); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment") - return - } - - user, err := rest.GetUserInfo(r) - if err != nil { // this not suppose to happen (handled by Auth), just dbl-check - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - id := chi.URLParam(r, "id") - - log.Printf("[DEBUG] update comment %s", id) - - var currComment store.Comment - if currComment, err = s.DataService.Get(locator, id); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comment") - return - } - - if currComment.User.ID != user.ID { - rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "can not edit comments for other users") - return - } - - text := string(blackfriday.Run([]byte(edit.Text), blackfriday.WithExtensions(mdExt))) // render markdown - text = s.ImageProxy.Convert(text) - editReq := service.EditRequest{ - Text: text, - Orig: edit.Text, - Summary: edit.Summary, - } - - res, err := s.DataService.EditComment(locator, id, editReq) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't update comment") - return - } - - s.Cache.Flush(locator.URL, "last", user.ID) - render.JSON(w, r, res) -} - -// GET /find?site=siteID&url=post-url&format=[tree|plain]&sort=[+/-time|+/-score] -// find comments for given post. Returns in tree or plain formats, sorted -func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) { - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - sort := r.URL.Query().Get("sort") - if strings.HasPrefix(sort, " ") { // restore + replaced by " " - sort = "+" + sort[1:] - } - log.Printf("[DEBUG] get comments for %+v, sort %s, format %s", locator, sort, r.URL.Query().Get("format")) - - data, err := s.Cache.Get(cache.Key(cache.URLKey(r), locator.SiteID, locator.URL), func() ([]byte, error) { - comments, e := s.DataService.Find(locator, sort) - if e != nil { - return nil, e - } - maskedComments := s.adminService.alterComments(comments, r) - var b []byte - switch r.URL.Query().Get("format") { - case "tree": - tree := rest.MakeTree(maskedComments, sort, s.ReadOnlyAge) - if s.DataService.IsReadOnly(locator) { - tree.Info.ReadOnly = true - } - b, e = encodeJSONWithHTML(tree) - default: - b, e = encodeJSONWithHTML(maskedComments) - } - return b, e - }) - - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comments") - return - } - renderJSONFromBytes(w, r, data) -} - -// GET /last/{limit}?site=siteID - last comments for the siteID, across all posts, sorted by time -func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) { - siteID := r.URL.Query().Get("site") - log.Printf("[DEBUG] get last comments for %s", siteID) - - limit, err := strconv.Atoi(chi.URLParam(r, "limit")) - if err != nil { - limit = 0 - } - - data, err := s.Cache.Get(cache.Key(cache.URLKey(r), "last", siteID), func() ([]byte, error) { - comments, e := s.DataService.Last(siteID, limit) - if e != nil { - return nil, e - } - comments = s.adminService.alterComments(comments, r) - - // filter deleted from last comments view. Blocked marked as deleted and will sneak in without - filterDeleted := []store.Comment{} - for _, c := range comments { - if c.Deleted { - continue - } - filterDeleted = append(filterDeleted, c) - } - - return encodeJSONWithHTML(filterDeleted) - }) - - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get last comments") - return - } - renderJSONFromBytes(w, r, data) -} - -// GET /id/{id}?site=siteID&url=post-url - gets a comment by id -func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { - - id := chi.URLParam(r, "id") - siteID := r.URL.Query().Get("site") - url := r.URL.Query().Get("url") - - log.Printf("[DEBUG] get comments by id %s, %s %s", id, siteID, url) - - comment, err := s.DataService.Get(store.Locator{SiteID: siteID, URL: url}, id) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by id") - return - } - comment = s.adminService.alterComments([]store.Comment{comment}, r)[0] - render.Status(r, http.StatusOK) - renderJSONWithHTML(w, r, comment) -} - -// GET /comments?site=siteID&user=id - returns comments for given userID -func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) { - - userID := r.URL.Query().Get("user") - siteID := r.URL.Query().Get("site") - - limit, err := strconv.Atoi(r.URL.Query().Get("limit")) - if err != nil { - limit = 0 - } - - resp := struct { - Comments []store.Comment - Count int - }{} - - log.Printf("[DEBUG] get comments for userID %s, %s", userID, siteID) - - data, err := s.Cache.Get(cache.Key(cache.URLKey(r), userID, siteID), func() ([]byte, error) { - comments, e := s.DataService.User(siteID, userID, limit, 0) - if e != nil { - return nil, e - } - comments = s.adminService.alterComments(comments, r) - - count, e := s.DataService.UserCount(siteID, userID) - if e != nil { - return nil, e - } - resp.Comments, resp.Count = comments, count - return encodeJSONWithHTML(resp) - }) - - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by user id") - return - } - renderJSONFromBytes(w, r, data) -} - -// GET /config?site=siteID - returns configuration -func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) { - type config struct { - Version string `json:"version"` - EditDuration int `json:"edit_duration"` - MaxCommentSize int `json:"max_comment_size"` - Admins []string `json:"admins"` - Auth []string `json:"auth_providers"` - LowScore int `json:"low_score"` - CriticalScore int `json:"critical_score"` - ReadOnlyAge int `json:"readonly_age"` - } - - cnf := config{ - Version: s.Version, - EditDuration: int(s.DataService.EditDuration.Seconds()), - MaxCommentSize: s.DataService.MaxCommentSize, - Admins: s.Authenticator.Admins, - LowScore: s.ScoreThresholds.Low, - CriticalScore: s.ScoreThresholds.Critical, - ReadOnlyAge: s.ReadOnlyAge, - } - - cnf.Auth = []string{} - for _, ap := range s.Authenticator.Providers { - cnf.Auth = append(cnf.Auth, ap.Name) - } - - if cnf.Admins == nil { // prevent json serialization to nil - cnf.Admins = []string{} - } - render.Status(r, http.StatusOK) - render.JSON(w, r, cnf) -} - -// GET /user - returns user info -func (s *Rest) userInfoCtrl(w http.ResponseWriter, r *http.Request) { - user, err := rest.GetUserInfo(r) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - render.JSON(w, r, user) -} - -// GET /count?site=siteID&url=post-url - get number of comments for given post -func (s *Rest) countCtrl(w http.ResponseWriter, r *http.Request) { - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - count, err := s.DataService.Count(locator) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get count") - return - } - render.JSON(w, r, JSON{"count": count, "locator": locator}) -} - -// POST /count?site=siteID - get number of comments for posts from post body -func (s *Rest) countMultiCtrl(w http.ResponseWriter, r *http.Request) { - siteID := r.URL.Query().Get("site") - posts := []string{} - if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &posts); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get list of posts from request") - return - } - - // key could be long for multiple posts, make it sha1 - key := cache.URLKey(r) + strings.Join(posts, ",") - hasher := sha1.New() - if _, err := hasher.Write([]byte(key)); err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't make sha1 for list of urls") - return - } - sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) - - data, err := s.Cache.Get(cache.Key(sha, siteID), func() ([]byte, error) { - counts, e := s.DataService.Counts(siteID, posts) - if e != nil { - return nil, e - } - return encodeJSONWithHTML(counts) - }) - - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get counts for "+siteID) - return - } - renderJSONFromBytes(w, r, data) -} - -// GET /list?site=siteID&limit=50&skip=10 - list posts with comments -func (s *Rest) listCtrl(w http.ResponseWriter, r *http.Request) { - - siteID := r.URL.Query().Get("site") - limit, skip := 0, 0 - - if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil { - limit = v - } - if v, err := strconv.Atoi(r.URL.Query().Get("skip")); err == nil { - skip = v - } - - data, err := s.Cache.Get(cache.Key(cache.URLKey(r), siteID), func() ([]byte, error) { - posts, e := s.DataService.List(siteID, limit, skip) - if e != nil { - return nil, e - } - return encodeJSONWithHTML(posts) - }) - - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get list of comments for "+siteID) - return - } - renderJSONFromBytes(w, r, data) -} - -// PUT /vote/{id}?site=siteID&url=post-url&vote=1 - vote for/against comment -func (s *Rest) voteCtrl(w http.ResponseWriter, r *http.Request) { - - user, err := rest.GetUserInfo(r) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - id := chi.URLParam(r, "id") - log.Printf("[DEBUG] vote for comment %s", id) - - vote := r.URL.Query().Get("vote") == "1" - - comment, err := s.DataService.Vote(locator, id, user.ID, vote) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't vote for comment") - return - } - s.Cache.Flush(locator.URL) - render.JSON(w, r, JSON{"id": comment.ID, "score": comment.Score}) -} - -// GET /userdata?site=siteID - exports all data about the user as a json fragments -func (s *Rest) userAllDataCtrl(w http.ResponseWriter, r *http.Request) { - siteID := r.URL.Query().Get("site") - user, err := rest.GetUserInfo(r) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - userB, err := json.Marshal(&user) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't marshal user info") - return - } - - exportFile := fmt.Sprintf("%s-%s-%s.json.gz", siteID, user.ID, time.Now().Format("20060102")) - w.Header().Set("Content-Type", "application/gzip") - w.Header().Set("Content-Disposition", "attachment;filename="+exportFile) - gzWriter := gzip.NewWriter(w) - defer func() { - if e := gzWriter.Close(); e != nil { - log.Printf("[WARN] can't close gzip writer, %s", e) - } - }() - - if _, e := gzWriter.Write(userB); e != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, e, "can't write user info") - return - } - - for i := 0; i < 1000; i++ { - comments, err := s.DataService.User(siteID, user.ID, 1000, i*1000) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't write user comments") - return - } - b, err := json.Marshal(comments) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't marshal user comments") - return - } - if _, e := gzWriter.Write(b); e != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, e, "can't write user comment") - return - } - if len(comments) != 1000 { - break - } - } -} - // serves static files from /web func addFileServer(r chi.Router, path string, root http.FileSystem) { log.Printf("[INFO] run file server for %s, path %s", root, path) diff --git a/app/rest/api/rest_private.go b/app/rest/api/rest_private.go new file mode 100644 index 00000000..92a68b9a --- /dev/null +++ b/app/rest/api/rest_private.go @@ -0,0 +1,213 @@ +package api + +import ( + "compress/gzip" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/go-chi/chi" + "github.com/go-chi/render" + blackfriday "gopkg.in/russross/blackfriday.v2" + + "github.com/umputun/remark/app/rest" + "github.com/umputun/remark/app/store" + "github.com/umputun/remark/app/store/service" +) + +// POST /comment - adds comment, resets all immutable fields +func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) { + + comment := store.Comment{} + if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment") + return + } + + user, err := rest.GetUserInfo(r) + if err != nil { // this not suppose to happen (handled by Auth), just dbl-check + rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") + return + } + log.Printf("[DEBUG] create comment %+v", comment) + + comment.PrepareUntrusted() // clean all fields user not supposed to set + comment.User = user + comment.User.IP = strings.Split(r.RemoteAddr, ":")[0] + + comment.Orig = comment.Text // original comment text, prior to md render + if err = s.DataService.ValidateComment(&comment); err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment") + return + } + comment.Text = string(blackfriday.Run([]byte(comment.Text), blackfriday.WithExtensions(mdExt))) + comment.Text = s.ImageProxy.Convert(comment.Text) + // check if user blocked + if s.adminService.checkBlocked(comment.Locator.SiteID, comment.User) { + rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "user blocked") + return + } + + if s.ReadOnlyAge > 0 { + if info, e := s.DataService.Info(comment.Locator, s.ReadOnlyAge); e == nil && info.ReadOnly { + rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "old post, read-only") + return + } + } + + id, err := s.DataService.Create(comment) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't save comment") + return + } + + // DataService modifies comment + finalComment, err := s.DataService.Get(comment.Locator, id) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't load created comment") + return + } + s.Cache.Flush(comment.Locator.URL, "last", comment.User.ID, comment.Locator.SiteID) + + render.Status(r, http.StatusCreated) + render.JSON(w, r, &finalComment) +} + +// PUT /comment/{id}?site=siteID&url=post-url - update comment +func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) { + + edit := struct { + Text string + Summary string + }{} + + if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &edit); err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment") + return + } + + user, err := rest.GetUserInfo(r) + if err != nil { // this not suppose to happen (handled by Auth), just dbl-check + rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") + return + } + locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} + id := chi.URLParam(r, "id") + + log.Printf("[DEBUG] update comment %s", id) + + var currComment store.Comment + if currComment, err = s.DataService.Get(locator, id); err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comment") + return + } + + if currComment.User.ID != user.ID { + rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "can not edit comments for other users") + return + } + + text := string(blackfriday.Run([]byte(edit.Text), blackfriday.WithExtensions(mdExt))) // render markdown + text = s.ImageProxy.Convert(text) + editReq := service.EditRequest{ + Text: text, + Orig: edit.Text, + Summary: edit.Summary, + } + + res, err := s.DataService.EditComment(locator, id, editReq) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't update comment") + return + } + + s.Cache.Flush(locator.URL, "last", user.ID) + render.JSON(w, r, res) +} + +// GET /user - returns user info +func (s *Rest) userInfoCtrl(w http.ResponseWriter, r *http.Request) { + user, err := rest.GetUserInfo(r) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") + return + } + render.JSON(w, r, user) +} + +// PUT /vote/{id}?site=siteID&url=post-url&vote=1 - vote for/against comment +func (s *Rest) voteCtrl(w http.ResponseWriter, r *http.Request) { + + user, err := rest.GetUserInfo(r) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") + return + } + locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} + id := chi.URLParam(r, "id") + log.Printf("[DEBUG] vote for comment %s", id) + + vote := r.URL.Query().Get("vote") == "1" + + comment, err := s.DataService.Vote(locator, id, user.ID, vote) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't vote for comment") + return + } + s.Cache.Flush(locator.URL) + render.JSON(w, r, JSON{"id": comment.ID, "score": comment.Score}) +} + +// GET /userdata?site=siteID - exports all data about the user as a json fragments +func (s *Rest) userAllDataCtrl(w http.ResponseWriter, r *http.Request) { + siteID := r.URL.Query().Get("site") + user, err := rest.GetUserInfo(r) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") + return + } + userB, err := json.Marshal(&user) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't marshal user info") + return + } + + exportFile := fmt.Sprintf("%s-%s-%s.json.gz", siteID, user.ID, time.Now().Format("20060102")) + w.Header().Set("Content-Type", "application/gzip") + w.Header().Set("Content-Disposition", "attachment;filename="+exportFile) + gzWriter := gzip.NewWriter(w) + defer func() { + if e := gzWriter.Close(); e != nil { + log.Printf("[WARN] can't close gzip writer, %s", e) + } + }() + + if _, e := gzWriter.Write(userB); e != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, e, "can't write user info") + return + } + + for i := 0; i < 1000; i++ { + comments, err := s.DataService.User(siteID, user.ID, 1000, i*1000) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't write user comments") + return + } + b, err := json.Marshal(comments) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't marshal user comments") + return + } + if _, e := gzWriter.Write(b); e != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, e, "can't write user comment") + return + } + if len(comments) != 1000 { + break + } + } +} diff --git a/app/rest/api/rest_public.go b/app/rest/api/rest_public.go new file mode 100644 index 00000000..e4e9a8f6 --- /dev/null +++ b/app/rest/api/rest_public.go @@ -0,0 +1,305 @@ +package api + +import ( + "crypto/sha1" + "encoding/base64" + "log" + "net/http" + "strconv" + "strings" + + "github.com/go-chi/chi" + "github.com/go-chi/render" + blackfriday "gopkg.in/russross/blackfriday.v2" + + "github.com/umputun/remark/app/rest" + "github.com/umputun/remark/app/rest/cache" + "github.com/umputun/remark/app/store" +) + +// GET /find?site=siteID&url=post-url&format=[tree|plain]&sort=[+/-time|+/-score] +// find comments for given post. Returns in tree or plain formats, sorted +func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) { + locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} + sort := r.URL.Query().Get("sort") + if strings.HasPrefix(sort, " ") { // restore + replaced by " " + sort = "+" + sort[1:] + } + log.Printf("[DEBUG] get comments for %+v, sort %s, format %s", locator, sort, r.URL.Query().Get("format")) + + data, err := s.Cache.Get(cache.Key(cache.URLKey(r), locator.SiteID, locator.URL), func() ([]byte, error) { + comments, e := s.DataService.Find(locator, sort) + if e != nil { + return nil, e + } + maskedComments := s.adminService.alterComments(comments, r) + var b []byte + switch r.URL.Query().Get("format") { + case "tree": + tree := rest.MakeTree(maskedComments, sort, s.ReadOnlyAge) + if s.DataService.IsReadOnly(locator) { + tree.Info.ReadOnly = true + } + b, e = encodeJSONWithHTML(tree) + default: + b, e = encodeJSONWithHTML(maskedComments) + } + return b, e + }) + + if err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comments") + return + } + renderJSONFromBytes(w, r, data) +} + +// POST /preview, body is a comment, returns rendered html +func (s *Rest) previewCommentCtrl(w http.ResponseWriter, r *http.Request) { + comment := store.Comment{} + if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment") + return + } + + user, err := rest.GetUserInfo(r) + if err != nil { // this not suppose to happen (handled by Auth), just dbl-check + rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") + return + } + comment.User = user + comment.Orig = comment.Text + if err = s.DataService.ValidateComment(&comment); err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment") + return + } + + //comment.Text = string(blackfriday.Run([]byte(comment.Text), + // blackfriday.WithRenderer(bfchroma.NewRenderer(bfchroma.WithoutAutodetect())))) + comment.Text = string(blackfriday.Run([]byte(comment.Text), blackfriday.WithExtensions(mdExt))) + comment.Text = s.ImageProxy.Convert(comment.Text) + comment.Sanitize() + render.HTML(w, r, comment.Text) +} + +// GET /info?site=siteID&url=post-url - get info about the post +func (s *Rest) infoCtrl(w http.ResponseWriter, r *http.Request) { + locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} + + data, err := s.Cache.Get(cache.Key(cache.URLKey(r), locator.SiteID, locator.URL), func() ([]byte, error) { + info, e := s.DataService.Info(locator, s.ReadOnlyAge) + if e != nil { + return nil, e + } + return encodeJSONWithHTML(info) + }) + + if err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get post info") + return + } + + renderJSONFromBytes(w, r, data) +} + +// GET /last/{limit}?site=siteID - last comments for the siteID, across all posts, sorted by time +func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) { + siteID := r.URL.Query().Get("site") + log.Printf("[DEBUG] get last comments for %s", siteID) + + limit, err := strconv.Atoi(chi.URLParam(r, "limit")) + if err != nil { + limit = 0 + } + + data, err := s.Cache.Get(cache.Key(cache.URLKey(r), "last", siteID), func() ([]byte, error) { + comments, e := s.DataService.Last(siteID, limit) + if e != nil { + return nil, e + } + comments = s.adminService.alterComments(comments, r) + + // filter deleted from last comments view. Blocked marked as deleted and will sneak in without + filterDeleted := []store.Comment{} + for _, c := range comments { + if c.Deleted { + continue + } + filterDeleted = append(filterDeleted, c) + } + + return encodeJSONWithHTML(filterDeleted) + }) + + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get last comments") + return + } + renderJSONFromBytes(w, r, data) +} + +// GET /id/{id}?site=siteID&url=post-url - gets a comment by id +func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { + + id := chi.URLParam(r, "id") + siteID := r.URL.Query().Get("site") + url := r.URL.Query().Get("url") + + log.Printf("[DEBUG] get comments by id %s, %s %s", id, siteID, url) + + comment, err := s.DataService.Get(store.Locator{SiteID: siteID, URL: url}, id) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by id") + return + } + comment = s.adminService.alterComments([]store.Comment{comment}, r)[0] + render.Status(r, http.StatusOK) + renderJSONWithHTML(w, r, comment) +} + +// GET /comments?site=siteID&user=id - returns comments for given userID +func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) { + + userID := r.URL.Query().Get("user") + siteID := r.URL.Query().Get("site") + + limit, err := strconv.Atoi(r.URL.Query().Get("limit")) + if err != nil { + limit = 0 + } + + resp := struct { + Comments []store.Comment + Count int + }{} + + log.Printf("[DEBUG] get comments for userID %s, %s", userID, siteID) + + data, err := s.Cache.Get(cache.Key(cache.URLKey(r), userID, siteID), func() ([]byte, error) { + comments, e := s.DataService.User(siteID, userID, limit, 0) + if e != nil { + return nil, e + } + comments = s.adminService.alterComments(comments, r) + + count, e := s.DataService.UserCount(siteID, userID) + if e != nil { + return nil, e + } + resp.Comments, resp.Count = comments, count + return encodeJSONWithHTML(resp) + }) + + if err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by user id") + return + } + renderJSONFromBytes(w, r, data) +} + +// GET /config?site=siteID - returns configuration +func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) { + type config struct { + Version string `json:"version"` + EditDuration int `json:"edit_duration"` + MaxCommentSize int `json:"max_comment_size"` + Admins []string `json:"admins"` + Auth []string `json:"auth_providers"` + LowScore int `json:"low_score"` + CriticalScore int `json:"critical_score"` + ReadOnlyAge int `json:"readonly_age"` + } + + cnf := config{ + Version: s.Version, + EditDuration: int(s.DataService.EditDuration.Seconds()), + MaxCommentSize: s.DataService.MaxCommentSize, + Admins: s.Authenticator.Admins, + LowScore: s.ScoreThresholds.Low, + CriticalScore: s.ScoreThresholds.Critical, + ReadOnlyAge: s.ReadOnlyAge, + } + + cnf.Auth = []string{} + for _, ap := range s.Authenticator.Providers { + cnf.Auth = append(cnf.Auth, ap.Name) + } + + if cnf.Admins == nil { // prevent json serialization to nil + cnf.Admins = []string{} + } + render.Status(r, http.StatusOK) + render.JSON(w, r, cnf) +} + +// GET /count?site=siteID&url=post-url - get number of comments for given post +func (s *Rest) countCtrl(w http.ResponseWriter, r *http.Request) { + locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} + count, err := s.DataService.Count(locator) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get count") + return + } + render.JSON(w, r, JSON{"count": count, "locator": locator}) +} + +// POST /count?site=siteID - get number of comments for posts from post body +func (s *Rest) countMultiCtrl(w http.ResponseWriter, r *http.Request) { + siteID := r.URL.Query().Get("site") + posts := []string{} + if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &posts); err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get list of posts from request") + return + } + + // key could be long for multiple posts, make it sha1 + key := cache.URLKey(r) + strings.Join(posts, ",") + hasher := sha1.New() + if _, err := hasher.Write([]byte(key)); err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't make sha1 for list of urls") + return + } + sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + + data, err := s.Cache.Get(cache.Key(sha, siteID), func() ([]byte, error) { + counts, e := s.DataService.Counts(siteID, posts) + if e != nil { + return nil, e + } + return encodeJSONWithHTML(counts) + }) + + if err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get counts for "+siteID) + return + } + renderJSONFromBytes(w, r, data) +} + +// GET /list?site=siteID&limit=50&skip=10 - list posts with comments +func (s *Rest) listCtrl(w http.ResponseWriter, r *http.Request) { + + siteID := r.URL.Query().Get("site") + limit, skip := 0, 0 + + if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil { + limit = v + } + if v, err := strconv.Atoi(r.URL.Query().Get("skip")); err == nil { + skip = v + } + + data, err := s.Cache.Get(cache.Key(cache.URLKey(r), siteID), func() ([]byte, error) { + posts, e := s.DataService.List(siteID, limit, skip) + if e != nil { + return nil, e + } + return encodeJSONWithHTML(posts) + }) + + if err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get list of comments for "+siteID) + return + } + renderJSONFromBytes(w, r, data) +}