| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package repo |
| 4 | |
| 5 | import ( |
| 6 | "net/http" |
| 7 | "net/url" |
| 8 | "strings" |
| 9 | |
| 10 | "github.com/tenseleyFlow/shithub/internal/social" |
| 11 | "github.com/tenseleyFlow/shithub/internal/web/middleware" |
| 12 | ) |
| 13 | |
| 14 | type repoActionView struct { |
| 15 | IsLoggedIn bool |
| 16 | LoginURL string |
| 17 | ReturnTo string |
| 18 | Starred bool |
| 19 | WatchLevel string |
| 20 | WatchOptions []repoWatchOptionView |
| 21 | } |
| 22 | |
| 23 | type repoWatchOptionView struct { |
| 24 | Level string |
| 25 | Label string |
| 26 | Description string |
| 27 | Checked bool |
| 28 | } |
| 29 | |
| 30 | func (h *Handlers) repoActions(r *http.Request, repoID int64) repoActionView { |
| 31 | viewer := middleware.CurrentUserFromContext(r.Context()) |
| 32 | returnTo := r.URL.RequestURI() |
| 33 | if returnTo == "" { |
| 34 | returnTo = r.URL.Path |
| 35 | } |
| 36 | if returnTo == "" { |
| 37 | returnTo = "/" |
| 38 | } |
| 39 | out := repoActionView{ |
| 40 | IsLoggedIn: !viewer.IsAnonymous(), |
| 41 | LoginURL: "/login?next=" + url.QueryEscape(returnTo), |
| 42 | ReturnTo: returnTo, |
| 43 | WatchLevel: string(social.WatchParticipating), |
| 44 | } |
| 45 | if viewer.IsAnonymous() { |
| 46 | out.WatchOptions = repoWatchOptions(social.WatchParticipating) |
| 47 | return out |
| 48 | } |
| 49 | deps := h.socialDeps() |
| 50 | starred, err := social.HasStar(r.Context(), deps, viewer.ID, repoID) |
| 51 | if err != nil { |
| 52 | h.d.Logger.WarnContext(r.Context(), "repo actions: star lookup", "error", err, "repo_id", repoID, "user_id", viewer.ID) |
| 53 | } else { |
| 54 | out.Starred = starred |
| 55 | } |
| 56 | level, err := social.CurrentLevel(r.Context(), deps, viewer.ID, repoID) |
| 57 | if err != nil { |
| 58 | h.d.Logger.WarnContext(r.Context(), "repo actions: watch lookup", "error", err, "repo_id", repoID, "user_id", viewer.ID) |
| 59 | level = social.WatchParticipating |
| 60 | } |
| 61 | out.WatchLevel = string(level) |
| 62 | out.WatchOptions = repoWatchOptions(level) |
| 63 | return out |
| 64 | } |
| 65 | |
| 66 | func repoWatchOptions(current social.WatchLevel) []repoWatchOptionView { |
| 67 | return []repoWatchOptionView{ |
| 68 | { |
| 69 | Level: string(social.WatchParticipating), |
| 70 | Label: "Participating and @mentions", |
| 71 | Description: "Only receive notifications from this repository when participating or mentioned.", |
| 72 | Checked: current == social.WatchParticipating, |
| 73 | }, |
| 74 | { |
| 75 | Level: string(social.WatchAll), |
| 76 | Label: "All Activity", |
| 77 | Description: "Notified of all notifications on this repository.", |
| 78 | Checked: current == social.WatchAll, |
| 79 | }, |
| 80 | { |
| 81 | Level: string(social.WatchIgnore), |
| 82 | Label: "Ignore", |
| 83 | Description: "Never notified.", |
| 84 | Checked: current == social.WatchIgnore, |
| 85 | }, |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | func redirectAfterRepoAction(w http.ResponseWriter, r *http.Request, fallback string) { |
| 90 | dest := fallback |
| 91 | if err := r.ParseForm(); err == nil { |
| 92 | if returnTo := strings.TrimSpace(r.PostFormValue("return_to")); safeLocalPath(returnTo) { |
| 93 | dest = returnTo |
| 94 | } |
| 95 | } |
| 96 | http.Redirect(w, r, dest, http.StatusSeeOther) |
| 97 | } |
| 98 | |
| 99 | func safeLocalPath(path string) bool { |
| 100 | if path == "" || !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") { |
| 101 | return false |
| 102 | } |
| 103 | u, err := url.Parse(path) |
| 104 | if err != nil { |
| 105 | return false |
| 106 | } |
| 107 | return !u.IsAbs() && u.Host == "" && strings.HasPrefix(u.Path, "/") |
| 108 | } |
| 109 |