Files
nidus/internal/web/ics.go
T

131 lines
4.7 KiB
Go

package web
import (
"context"
"net/http"
"strings"
"git.arnef.de/arnef/nidus/internal/db"
"git.arnef.de/arnef/nidus/internal/web/templates"
)
// icsURLValid does a light sanity check on a subscription URL: it must be
// http(s):// (fetched directly) or webcal:// (rewritten to https:// by
// internal/icssub before fetching), and within a reasonable length.
func icsURLValid(u string) bool {
if len(u) == 0 || len(u) > 2048 {
return false
}
return strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "webcal://")
}
// handleICSResource handles POST (create) and DELETE (remove) for the
// current user's ICS/webcal calendar subscriptions, mounted at
// /resources/ics.
func (s *Server) handleICSResource(w http.ResponseWriter, r *http.Request) {
username := userFromContext(r.Context())
// htmx v2 sends DELETE request parameters as URL query parameters, not
// a request body (see internal/web/resources.go's handleResource).
if r.Method == http.MethodDelete {
r.PostForm = r.URL.Query()
} else if err := r.ParseForm(); err != nil {
http.Error(w, "invalid form", http.StatusBadRequest)
return
}
name := strings.TrimSpace(r.PostForm.Get("name"))
if !resourceNameRe.MatchString(name) {
http.Error(w, "name must be 1-64 letters, digits, '-' or '_'", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodPost:
url := strings.TrimSpace(r.PostForm.Get("url"))
if !icsURLValid(url) {
http.Error(w, "url must be a http://, https:// or webcal:// address", http.StatusBadRequest)
return
}
color := strings.TrimSpace(r.PostForm.Get("color"))
if color != "" && !hexColorRe.MatchString(color) {
http.Error(w, "color must be a hex value like #3b82f6", http.StatusBadRequest)
return
}
if err := s.dbase.CreateICSSubscription(username, name, url, color); err != nil {
if err == db.ErrResourceExists {
http.Error(w, "already exists", http.StatusConflict)
return
}
if err == db.ErrReservedName {
http.Error(w, "this name is reserved for a computed calendar", http.StatusConflict)
return
}
s.logger.Error("creating ics subscription", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
case http.MethodDelete:
if err := s.dbase.DeleteICSSubscription(username, name); err != nil && err != db.ErrResourceNotFound {
s.logger.Error("deleting ics subscription", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
default:
w.Header().Set("Allow", "POST, DELETE")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// The set of cards changed (one added/removed), so re-render the
// whole #resources list rather than a single card.
resources, err := s.resourceCards(username)
if err != nil {
s.logger.Error("listing resources", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = templates.ResourceList(resources).Render(context.Background(), w)
}
// handleICSColor updates the color of one of the current user's ICS
// subscriptions, mounted at /resources/ics/color. It re-renders just that
// card (not the whole list), since the set of cards doesn't change.
func (s *Server) handleICSColor(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", "POST")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
username := userFromContext(r.Context())
if err := r.ParseForm(); err != nil {
http.Error(w, "invalid form", http.StatusBadRequest)
return
}
name := strings.TrimSpace(r.PostForm.Get("name"))
color := strings.TrimSpace(r.PostForm.Get("color"))
if !resourceNameRe.MatchString(name) {
http.Error(w, "name must be 1-64 letters, digits, '-' or '_'", http.StatusBadRequest)
return
}
if color != "" && !hexColorRe.MatchString(color) {
http.Error(w, "color must be a hex value like #3b82f6", http.StatusBadRequest)
return
}
if err := s.dbase.SetICSSubscriptionColor(username, name, color); err != nil && err != db.ErrResourceNotFound {
s.logger.Error("setting ics subscription color", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
sub, err := s.dbase.GetICSSubscription(username, name)
if err != nil {
s.logger.Error("loading ics subscription", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
card := templates.ResourceCard{Kind: "ics", Name: sub.Name, Color: sub.Color, URL: sub.URL}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = templates.ResourceCardView(card).Render(r.Context(), w)
}