package handler import ( "bytes" "errors" "html/template" "net/http" "os" "strings" "time" "github.com/alecthomas/chroma/v2" chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" "ridgwaysystems.org/paste/internal/paste" ) // ── Public handlers ─────────────────────────────────────────────────────────── type listData struct { Pastes []*paste.Paste } // List renders the public paste index (non-expired, non-unlisted). func (h *Handler) List(w http.ResponseWriter, r *http.Request) { pastes, err := h.store.List() if err != nil { h.renderErr(w, http.StatusInternalServerError, "could not load pastes") return } h.render(w, r, "list", listData{Pastes: pastes}) } type pasteViewData struct { Paste *paste.Paste Highlighted template.HTML CSRFToken string IsAdmin bool } // ViewPaste renders a single paste with syntax highlighting. func (h *Handler) ViewPaste(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") p, err := h.store.Get(id) if err != nil { if errors.Is(err, os.ErrNotExist) { h.renderErr(w, http.StatusNotFound, "paste not found") } else { h.renderErr(w, http.StatusInternalServerError, "could not load paste") } return } if p.Expired() { h.renderErr(w, http.StatusGone, "this paste has expired") return } highlighted, err := highlightCode(p.Body, p.Language) if err != nil { // Fall back to plain pre-formatted text on highlight failure highlighted = template.HTML("
" + template.HTMLEscapeString(p.Body) + "
") } h.render(w, r, "paste", pasteViewData{ Paste: p, Highlighted: highlighted, CSRFToken: csrfToken(), IsAdmin: isAuthenticated(r), }) } // RawPaste returns the paste body as plain text. Route: GET /raw/{id} func (h *Handler) RawPaste(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") p, err := h.store.Get(id) if err != nil { if errors.Is(err, os.ErrNotExist) { http.Error(w, "paste not found", http.StatusNotFound) } else { http.Error(w, "could not load paste", http.StatusInternalServerError) } return } if p.Expired() { http.Error(w, "this paste has expired", http.StatusGone) return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Write([]byte(p.Body)) } // ── Admin / authenticated handlers ─────────────────────────────────────────── type newPasteData struct { Error string Languages []string CSRFToken string } // NewPasteForm renders the paste creation form (requires auth). func (h *Handler) NewPasteForm(w http.ResponseWriter, r *http.Request) { h.render(w, r, "new", newPasteData{ Languages: paste.Languages, CSRFToken: csrfToken(), }) } // NewPastePost handles paste creation (requires auth). func (h *Handler) NewPastePost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { h.renderErr(w, http.StatusBadRequest, "bad request") return } if !csrfValid(r.FormValue("csrf_token")) { h.renderErr(w, http.StatusForbidden, "invalid CSRF token") return } body := r.FormValue("body") if strings.TrimSpace(body) == "" { h.render(w, r, "new", newPasteData{ Error: "Paste body cannot be empty.", Languages: paste.Languages, CSRFToken: csrfToken(), }) return } title := strings.TrimSpace(r.FormValue("title")) if title == "" { title = "Untitled" } lang := r.FormValue("language") if lang == "" { lang = "text" } expiry := paste.ParseExpiry(r.FormValue("expiry")) unlisted := r.FormValue("unlisted") == "on" p := &paste.Paste{ Title: title, Language: lang, Body: body, CreatedAt: time.Now().UTC(), ExpiresAt: expiry, Unlisted: unlisted, } if err := h.store.Save(p); err != nil { h.render(w, r, "new", newPasteData{ Error: "Failed to save paste. Please try again.", Languages: paste.Languages, CSRFToken: csrfToken(), }) return } http.Redirect(w, r, "/"+p.ID, http.StatusSeeOther) } type adminData struct { Pastes []*paste.Paste CSRFToken string } // AdminDashboard renders the admin view of all pastes (requires auth). func (h *Handler) AdminDashboard(w http.ResponseWriter, r *http.Request) { pastes, err := h.store.ListAll() if err != nil { h.renderErr(w, http.StatusInternalServerError, "could not load pastes") return } h.render(w, r, "admin-dashboard", adminData{ Pastes: pastes, CSRFToken: csrfToken(), }) } // DeletePaste deletes a paste by ID (requires auth). func (h *Handler) DeletePaste(w http.ResponseWriter, r *http.Request) { if !csrfValid(r.FormValue("csrf_token")) { h.renderErr(w, http.StatusForbidden, "invalid CSRF token") return } id := r.PathValue("id") if err := h.store.Delete(id); err != nil && !errors.Is(err, os.ErrNotExist) { h.renderErr(w, http.StatusInternalServerError, "could not delete paste") return } http.Redirect(w, r, "/admin", http.StatusSeeOther) } // ── Auth handlers ───────────────────────────────────────────────────────────── type loginData struct { Error string CSRFToken string } // AdminLogin renders the login form. func (h *Handler) AdminLogin(w http.ResponseWriter, r *http.Request) { if isAuthenticated(r) { http.Redirect(w, r, "/admin", http.StatusSeeOther) return } h.render(w, r, "admin-login", loginData{CSRFToken: csrfToken()}) } // AdminLoginPost processes the login form. func (h *Handler) AdminLoginPost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { h.renderErr(w, http.StatusBadRequest, "bad request") return } if !csrfValid(r.FormValue("csrf_token")) { h.renderErr(w, http.StatusForbidden, "invalid CSRF token") return } if !checkPassword(r.FormValue("password")) { h.render(w, r, "admin-login", loginData{ Error: "Invalid password.", CSRFToken: csrfToken(), }) return } setSession(w) http.Redirect(w, r, "/admin", http.StatusSeeOther) } // AdminLogout clears the session and redirects to the login page. func (h *Handler) AdminLogout(w http.ResponseWriter, r *http.Request) { clearSession(w) http.Redirect(w, r, "/admin/login", http.StatusSeeOther) } // ── Syntax highlighting ─────────────────────────────────────────────────────── func highlightCode(body, language string) (template.HTML, error) { var lexer chroma.Lexer if language != "" && language != "text" { lexer = lexers.Get(language) } if lexer == nil { lexer = lexers.Fallback } lexer = chroma.Coalesce(lexer) formatter := chromahtml.New( chromahtml.WithClasses(true), chromahtml.WithLineNumbers(true), chromahtml.LineNumbersInTable(true), ) iterator, err := lexer.Tokenise(nil, body) if err != nil { return "", err } var buf bytes.Buffer if err := formatter.Format(&buf, styles.Get("github"), iterator); err != nil { return "", err } return template.HTML(buf.String()), nil }