package server import ( "io" "net/http" "regexp" "strings" "time" "git.hubris.network/dtoro/artifacto/internal/auth" "git.hubris.network/dtoro/artifacto/internal/store" ) var customSlugRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,31}$`) var titleRe = regexp.MustCompile(`(?is)(.*?)`) func (s *Server) postPublish(w http.ResponseWriter, r *http.Request) { maxBytes := s.cfg.MaxUploadMB * 1024 * 1024 r.Body = http.MaxBytesReader(w, r.Body, maxBytes) contentType := r.Header.Get("Content-Type") var body []byte var slug, customTitle, password, expiresIn string if strings.HasPrefix(contentType, "multipart/form-data") { if err := r.ParseMultipartForm(maxBytes); err != nil { http.Error(w, "upload too large or malformed", http.StatusBadRequest) return } slug = r.FormValue("slug") customTitle = r.FormValue("title") password = r.FormValue("password") expiresIn = r.FormValue("expires_in") body = []byte(r.FormValue("html")) if f, fh, err := r.FormFile("file"); err == nil { defer f.Close() if fh.Size > maxBytes { http.Error(w, "file too large", http.StatusRequestEntityTooLarge) return } b, err := io.ReadAll(f) if err != nil { http.Error(w, "read file", http.StatusBadRequest) return } body = b } } else { if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } slug = r.FormValue("slug") customTitle = r.FormValue("title") password = r.FormValue("password") expiresIn = r.FormValue("expires_in") body = []byte(r.FormValue("html")) } body = []byte(strings.TrimSpace(string(body))) if len(body) == 0 { http.Error(w, "empty HTML body", http.StatusBadRequest) return } if !looksLikeHTML(body) { http.Error(w, "body does not look like HTML", http.StatusBadRequest) return } slug = strings.ToLower(strings.TrimSpace(slug)) if slug != "" && !customSlugRe.MatchString(slug) { http.Error(w, "slug must match [a-z0-9][a-z0-9-]{2,31}", http.StatusBadRequest) return } title := strings.TrimSpace(customTitle) if title == "" { title = extractTitle(body) } var pwHash string if password != "" { h, err := auth.HashPassword(password) if err != nil { http.Error(w, "hash password: "+err.Error(), http.StatusInternalServerError) return } pwHash = h } var expiresAt *time.Time if expiresIn != "" && expiresIn != "never" { if d, ok := parseDuration(expiresIn); ok { t := time.Now().Add(d) expiresAt = &t } } art, err := s.store.CreateArtifact(store.CreateOptions{ Slug: slug, Title: title, Body: body, PasswordHash: pwHash, ExpiresAt: expiresAt, }) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Redirect browsers back to dashboard; return JSON-ish link to API clients. if wantsJSON(r) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"slug":"` + art.Slug + `","url":"` + s.cfg.BaseURL + "/p/" + art.Slug + `"}`)) return } http.Redirect(w, r, "/?new="+art.Slug, http.StatusSeeOther) } func looksLikeHTML(b []byte) bool { lower := strings.ToLower(strings.TrimSpace(string(b))) if strings.HasPrefix(lower, "") } func extractTitle(b []byte) string { m := titleRe.FindSubmatch(b) if len(m) < 2 { return "" } t := strings.TrimSpace(string(m[1])) if len(t) > 120 { t = t[:120] } return t } func parseDuration(s string) (time.Duration, bool) { switch s { case "1h": return time.Hour, true case "24h": return 24 * time.Hour, true case "7d": return 7 * 24 * time.Hour, true case "30d": return 30 * 24 * time.Hour, true } if d, err := time.ParseDuration(s); err == nil { return d, true } return 0, false } func wantsJSON(r *http.Request) bool { return strings.Contains(r.Header.Get("Accept"), "application/json") }