1 package server
2
3 import (
4 "fmt"
5 "net/http"
6 "sort"
7 "strings"
8 "time"
9
10 "codeberg.org/tslocum/sriracha/internal/database"
11 . "codeberg.org/tslocum/sriracha/model"
12 . "codeberg.org/tslocum/sriracha/util"
13 "github.com/leonelquinteros/gotext"
14 )
15
16 func (s *Server) subscriptionConfirmKey(sub *Subscription) string {
17 return md5Sum(s.hashData(md5Sum(fmt.Sprintf("%s/%d", sub.Email, sub.Confirm))))
18 }
19
20 func (s *Server) serveSubscribe(db *database.DB, w http.ResponseWriter, r *http.Request) {
21 data := s.buildData(db, w, r)
22 data.Boards = db.AllBoards()
23 if !s.opt.Notifications {
24 data.BoardError(w, "Email notifications are disabled.")
25 return
26 }
27 data.Template = "subscribe"
28
29 key := r.URL.Query().Get("key")
30 if key != "" {
31 email := r.URL.Query().Get("email")
32 if email == "" {
33 data.BoardError(w, "Invalid email.")
34 return
35 }
36 expectedKey := md5Sum(s.hashData(md5Sum(email)))
37 if key != expectedKey {
38 data.BoardError(w, "Invalid access key.")
39 return
40 }
41 data.Extra = email
42 data.Extra2 = key
43
44 var confirmed bool
45 subs := db.SubscriptionsByEmail(email)
46 for _, sub := range subs {
47 if sub.Confirm == 0 {
48 confirmed = true
49 break
50 }
51 }
52 if !confirmed {
53 if len(subs) == 0 {
54 data.BoardError(w, "Your email address is unconfirmed. Subscribe to request a confirmation link.")
55 return
56 }
57 const errorMessage = "Click the confirmation link emailed to you."
58 confirmKey := r.URL.Query().Get("confirm")
59 if confirmKey == "" {
60 data.BoardError(w, "Your email address is unconfirmed. "+errorMessage)
61 return
62 }
63 expectedConfirmKey := s.subscriptionConfirmKey(subs[0])
64 if confirmKey != expectedConfirmKey {
65 data.BoardError(w, "Invalid confirmation key. "+errorMessage)
66 return
67 }
68 subs[0].Confirm = 0
69 subs[0].IP = ""
70 db.UpdateSubscription(subs[0])
71
72 data.Info = "Subscription confirmed."
73 }
74
75 if r.Method == http.MethodPost {
76 for _, sub := range subs {
77 v := FormNegInt(r, fmt.Sprintf("sub%d", sub.ID))
78
79
80 if sub.Board != 0 {
81 switch v {
82 case int(SubscriptionThreads), int(SubscriptionAll):
83 sub.Target = v
84 db.UpdateSubscription(sub)
85 case 1:
86 db.DeleteSubscription(sub)
87 }
88 continue
89 }
90
91
92 if v == 1 {
93 db.DeleteSubscription(sub)
94 }
95 }
96
97 subs = db.SubscriptionsByEmail(email)
98 }
99
100 boardLabels := make(map[int]string)
101 for _, board := range data.Boards {
102 boardLabels[board.ID] = board.Path()
103 }
104
105 sort.Slice(subs, func(i, j int) bool {
106 iBoard, jBoard := subs[i].Board != 0, subs[j].Board != 0
107 if iBoard != jBoard {
108 return iBoard
109 } else if iBoard && jBoard {
110 return boardLabels[subs[i].Board] < boardLabels[subs[j].Board]
111 }
112 return subs[i].Target < subs[j].Target
113 })
114 data.Subscriptions = subs
115
116 data.execute(w)
117 return
118 }
119
120 boardID := PathInt(r, "/sriracha/subscribe/board/")
121 if boardID > 0 {
122 board := db.BoardByID(boardID)
123 if board == nil {
124 data.BoardError(w, "Invalid board.")
125 return
126 }
127 data.Board = board
128 } else {
129 postID := PathInt(r, "/sriracha/subscribe/post/")
130 if postID > 0 {
131 post := db.PostByID(postID)
132 if post == nil {
133 data.BoardError(w, "Invalid post.")
134 return
135 }
136 data.Board = post.Board
137 data.Post = post
138 }
139 }
140
141 if data.Post == nil && data.Board == nil {
142 http.Redirect(w, r, "/sriracha/", http.StatusFound)
143 return
144 }
145
146 if r.Method == http.MethodPost {
147 email := FormString(r, "email")
148 if email == "" {
149 data.BoardError(w, "Enter your email address to subscribe.")
150 return
151 }
152
153 const confirmErrorMessage = "You already requested a confirmation link. You may request another confirmation link when 24 hours have passed."
154 ipHash := s.hashIP(r)
155 ipSub := db.SubscriptionByIP(ipHash)
156 if ipSub != nil {
157 data.BoardError(w, confirmErrorMessage)
158 return
159 }
160
161 var confirmed bool
162 subs := db.SubscriptionsByEmail(email)
163 for _, sub := range subs {
164 if sub.Confirm == 0 {
165 confirmed = true
166 break
167 }
168 }
169
170 var confirmTime int64
171 if !confirmed {
172 if len(subs) != 0 {
173 data.BoardError(w, confirmErrorMessage)
174 return
175 }
176
177 if s.notificationsPattern != nil {
178 var matched bool
179 address := ParseEmail(email)
180 if address != "" {
181 atSymbol := strings.IndexRune(address, '@')
182 if atSymbol != -1 {
183 domain := address[atSymbol+1:]
184 matched = s.notificationsPattern.MatchString(domain)
185 }
186 }
187 if !matched {
188 data.BoardError(w, "Sorry, only the following email address domains are allowed: "+s.config.MailDomains)
189 return
190 }
191 }
192
193 confirmTime = time.Now().Unix()
194 }
195
196 sub := &Subscription{
197 Confirm: confirmTime,
198 Email: email,
199 }
200 if !confirmed {
201 sub.IP = ipHash
202 }
203 if data.Post != nil {
204 sub.Target = data.Post.ID
205 } else {
206 sub.Board = data.Board.ID
207 sub.Target = int(FormRange(r, "notify", SubscriptionThreads, SubscriptionAll))
208 }
209 err := sub.Validate()
210 if err != nil {
211 data.BoardError(w, fmt.Sprintf("Failed to add subscription: %s", err))
212 return
213 }
214
215 var target string
216 if data.Post != nil {
217 target = fmt.Sprintf("No.%d", data.Post.ID)
218 } else {
219 target = data.Board.Path()
220 }
221 if !confirmed {
222 const errorMessage = "Failed to send confirmation link. Please try again later."
223 client, err := s.connectToMailServer()
224 if err != nil {
225 data.BoardError(w, errorMessage)
226 return
227 }
228 subject := gotext.Get("Subscribe to %s", target)
229 key := md5Sum(s.hashData(md5Sum(email)))
230 confirmKey := s.subscriptionConfirmKey(sub)
231 message := s.opt.SiteHome + "sriracha/subscribe/?email=" + email + "&key=" + key + "&confirm=" + confirmKey
232 err = s.sendMail(client, sub.Email, subject, message)
233 client.Close()
234 if err != nil {
235 data.BoardError(w, errorMessage)
236 return
237 }
238 }
239
240 var updated bool
241 if confirmed {
242 for _, existing := range subs {
243 if sub.Board == existing.Board && (sub.Board != 0 || sub.Target == existing.Target) {
244 existing.Board = sub.Board
245 existing.Target = sub.Target
246 db.UpdateSubscription(existing)
247 updated = true
248 break
249 }
250 }
251 }
252 if !updated {
253 db.AddSubscription(sub)
254 }
255
256 data.Template = "board_info"
257 if !confirmed {
258 data.Info = "Please confirm your subscription by clicking the link emailed to you."
259 } else {
260 data.Info = fmt.Sprintf("Subscribed to %s", target)
261 }
262 }
263
264 data.execute(w)
265 }
266
View as plain text