package handlers import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "sort" "strings" "time" "warpbox.dev/backend/libs/helpers" "warpbox.dev/backend/libs/jobs" "warpbox.dev/backend/libs/services" "warpbox.dev/backend/libs/web" ) type downloadPageData struct { Box boxView Files []fileView ZipURL string Locked bool Obfuscated bool CanPreview bool DownloadCount int MaxDownloads int ExpiresLabel string EmojiTabs []emojiTabView } type boxView struct { ID string } type fileView struct { ID string Name string Size string SizeBytes int64 ContentType string PreviewKind string URL string DownloadURL string ThumbnailURL string SceneURL string HasThumbnail bool HasScene bool IconURL string IconRetroURL string ReactURL string Reactions []reactionView ReactionMore int Reacted bool Processing bool } type reactionView struct { EmojiID string `json:"emojiId"` URL string `json:"url"` Label string `json:"label"` Count int `json:"count"` Visible bool `json:"visible"` } type emojiTabView struct { ID string Label string Emojis []emojiOptionView } type emojiOptionView struct { ID string `json:"id"` URL string `json:"url"` Label string `json:"label"` } type previewPageData struct { Box boxView File fileView Locked bool DownloadURL string } func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) { box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { a.logger.Warn("download page missing box", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4040, "box_id", r.PathValue("boxID"))...) http.NotFound(w, r) return } if err := a.uploadService.CanDownload(box); err != nil { a.logger.Warn("download page unavailable", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "error", err.Error())...) a.renderPage(w, r, http.StatusForbidden, "download.html", web.PageData{ Title: "Download unavailable", Description: "This Warpbox link is no longer available.", Data: downloadPageData{ Box: boxView{ID: box.ID}, ExpiresLabel: err.Error(), }, }) return } locked := a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) if isSocialPreviewBot(r) && !locked && len(box.Files) == 1 { file := box.Files[0] if file.Processing { http.Error(w, "file is still processing", http.StatusAccepted) return } if shouldServeRawSocialMedia(file) { a.serveFileContent(w, r, box, file, false) a.logger.Info("single-file media served inline for social preview", withRequestLogAttrs(r, "source", "download", "severity", "user_activity", "code", 2008, "box_id", box.ID, "file_id", file.ID)...) return } } visitorID := a.reactionVisitorID(w, r) reactionsByFile, reactedByFile, err := a.reactionService.SummaryForBox(box.ID, visitorID) if err != nil { a.logger.Warn("failed to load file reactions", withRequestLogAttrs(r, "source", "reactions", "severity", "warn", "code", 4300, "box_id", box.ID, "error", err.Error())...) } files := make([]fileView, 0, len(box.Files)) if !(locked && box.Obfuscate) { for _, file := range box.Files { files = append(files, a.fileViewWithReactions(box, file, reactionsByFile[file.ID], reactedByFile[file.ID])) } } emojiTabs, err := a.emojiTabs() if err != nil { a.logger.Warn("failed to load emoji tabs", withRequestLogAttrs(r, "source", "reactions", "severity", "warn", "code", 4301, "box_id", box.ID, "error", err.Error())...) } expiresLabel := boxExpiryLabel(box.ExpiresAt, "Jan 2, 2006 15:04 MST") title := "Shared files on Warpbox" description := fmt.Sprintf("%d file%s shared via Warpbox · expires %s", len(box.Files), plural(len(box.Files)), expiresLabel) ogImage := absoluteURL(r, fmt.Sprintf("/d/%s/og-image.jpg", box.ID)) imageAlt := fmt.Sprintf("%d shared file%s on Warp Box", len(box.Files), plural(len(box.Files))) imageType := "image/jpeg" if !locked && len(box.Files) == 1 && !box.Files[0].Processing { file := box.Files[0] view := a.fileView(box, file) fileSize := helpers.FormatBytes(file.Size) title = file.Name description = fileShareDescription(fileSize, file.ContentType, box.ExpiresAt) ogImage = socialImageURL(r, box, file, view) imageAlt = fmt.Sprintf("Download card for %s", file.Name) imageType = socialImageType(file) } if locked && box.Obfuscate { title = "Protected Warpbox link" description = "This shared box is password protected." } pageURL := absoluteURL(r, fmt.Sprintf("/d/%s", box.ID)) // All user uploads are private/temporary — noindex by default. robots := web.RobotsNone a.renderPage(w, r, http.StatusOK, "download.html", web.PageData{ Title: title, Description: description, CanonicalURL: pageURL, Robots: robots, ImageURL: ogImage, ImageAlt: imageAlt, ImageType: imageType, Data: downloadPageData{ Box: boxView{ID: box.ID}, Files: files, ZipURL: fmt.Sprintf("/d/%s/zip", box.ID), Locked: locked, Obfuscated: box.Obfuscate, DownloadCount: box.DownloadCount, MaxDownloads: box.MaxDownloads, ExpiresLabel: expiresLabel, EmojiTabs: emojiTabs, }, }) a.logger.Info("download page viewed", withRequestLogAttrs(r, "source", "download", "severity", "user_activity", "code", 2003, "box_id", box.ID, "locked", locked)...) } func plural(n int) string { if n == 1 { return "" } return "s" } func shouldServeRawSocialMedia(file services.File) bool { return file.PreviewKind == "image" || file.PreviewKind == "video" } func fileShareDescription(size, contentType string, expiresAt time.Time) string { if strings.TrimSpace(contentType) == "" { contentType = "file" } return fmt.Sprintf("%s · %s · click to preview or download · expires %s", size, contentType, boxExpiryLabel(expiresAt, "Jan 2, 2006")) } func socialImageURL(r *http.Request, box services.Box, file services.File, view fileView) string { if file.PreviewKind == "image" { return absoluteURL(r, view.DownloadURL+"?inline=1") } if file.PreviewKind == "video" && view.HasThumbnail { return absoluteURL(r, view.ThumbnailURL) } return absoluteURL(r, fmt.Sprintf("/d/%s/f/%s/og-image.jpg", box.ID, file.ID)) } func socialImageType(file services.File) string { if file.PreviewKind == "image" { return file.ContentType } return "image/jpeg" } func socialOGType(file services.File) string { switch file.PreviewKind { case "video": return "video.other" default: return "website" } } func (a *App) DownloadFile(w http.ResponseWriter, r *http.Request) { box, file, ok := a.loadFileForRequest(w, r) if !ok { return } locked := a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) if isSocialPreviewBot(r) && !locked { if file.Processing { http.Error(w, "file is still processing", http.StatusAccepted) return } if shouldServeRawSocialMedia(file) { a.serveFileContent(w, r, box, file, false) a.logger.Info("media file served inline for social preview", withRequestLogAttrs(r, "source", "download", "severity", "user_activity", "code", 2009, "box_id", box.ID, "file_id", file.ID)...) return } } view := a.fileView(box, file) fileSize := helpers.FormatBytes(file.Size) title := file.Name description := fileShareDescription(fileSize, file.ContentType, box.ExpiresAt) imageURL := socialImageURL(r, box, file, view) imageAlt := fmt.Sprintf("Download card for %s", file.Name) ogType := socialOGType(file) mediaURL := "" if file.PreviewKind == "video" { mediaURL = absoluteURL(r, view.DownloadURL+"?inline=1") } if locked && box.Obfuscate { title = "Protected Warpbox file" description = "This shared file is password protected." imageURL = absoluteURL(r, "/static/img/file-placeholder.webp") imageAlt = "Password protected file on Warp Box" ogType = "website" mediaURL = "" } pageURL := absoluteURL(r, fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID)) a.renderPage(w, r, http.StatusOK, "preview.html", web.PageData{ Title: title, Description: description, CanonicalURL: pageURL, Robots: web.RobotsNone, OGType: ogType, ImageURL: imageURL, ImageAlt: imageAlt, ImageType: socialImageType(file), MediaURL: mediaURL, MediaType: file.ContentType, Data: previewPageData{ Box: boxView{ID: box.ID}, File: view, Locked: locked, DownloadURL: view.DownloadURL, }, }) a.logger.Info("file preview page viewed", withRequestLogAttrs(r, "source", "download", "severity", "user_activity", "code", 2004, "box_id", box.ID, "file_id", file.ID)...) } func (a *App) DownloadFileContent(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Robots-Tag", "noindex, nofollow, noarchive") box, file, ok := a.loadFileForRequest(w, r) if !ok { return } if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) { a.logger.Warn("protected file download blocked", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4013, "box_id", box.ID, "file_id", file.ID)...) http.Error(w, "password required", http.StatusUnauthorized) return } if file.Processing { http.Error(w, "file is still processing", http.StatusAccepted) return } a.serveFileContent(w, r, box, file, r.URL.Query().Get("inline") != "1") a.logger.Info("file content served", withRequestLogAttrs(r, "source", "download", "severity", "user_activity", "code", 2005, "box_id", box.ID, "file_id", file.ID, "attachment", r.URL.Query().Get("inline") != "1")...) } func (a *App) Thumbnail(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Robots-Tag", "noindex, nofollow, noarchive") box, file, ok := a.loadFileForRequest(w, r) if !ok { return } if a.uploadService.IsProtected(box) && box.Obfuscate && !a.isBoxUnlocked(r, box) { a.servePlaceholderThumbnail(w, r) return } object, err := a.uploadService.OpenThumbnailObject(r.Context(), box, file) if err != nil { if thumbnail := a.generateMissingThumbnailForRequest(r, box, file); thumbnail != "" { file.Thumbnail = thumbnail object, err = a.uploadService.OpenThumbnailObject(r.Context(), box, file) if err == nil { defer object.Body.Close() w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "public, max-age=604800, immutable") http.ServeContent(w, r, file.ID+"-thumbnail.jpg", object.ModTime, readSeekCloser(object.Body)) return } } // The thumbnail isn't generated yet (background job pending). Serve the // placeholder but mark it non-cacheable, otherwise the browser would // keep showing the placeholder until a hard refresh once the real // thumbnail lands. The real thumbnail below is content-stable, so it // gets a long immutable cache. a.servePlaceholderThumbnail(w, r) return } defer object.Body.Close() w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "public, max-age=604800, immutable") http.ServeContent(w, r, file.ID+"-thumbnail.jpg", object.ModTime, readSeekCloser(object.Body)) } func (a *App) VideoScenesPreview(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Robots-Tag", "noindex, nofollow, noarchive") box, file, ok := a.loadFileForRequest(w, r) if !ok { return } if !jobs.NeedsVideoScenes(file) { http.NotFound(w, r) return } if a.uploadService.IsProtected(box) && box.Obfuscate && !a.isBoxUnlocked(r, box) { a.servePlaceholderThumbnail(w, r) return } object, err := a.uploadService.OpenSceneThumbnailObject(r.Context(), box, file) if err != nil { if scene := a.generateMissingVideoScenesForRequest(r, box, file); scene != "" { file.SceneThumbnail = scene object, err = a.uploadService.OpenSceneThumbnailObject(r.Context(), box, file) if err == nil { defer object.Body.Close() w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "public, max-age=604800, immutable") http.ServeContent(w, r, file.ID+"-scenes.jpg", object.ModTime, readSeekCloser(object.Body)) return } } a.servePlaceholderThumbnail(w, r) return } defer object.Body.Close() w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "public, max-age=604800, immutable") http.ServeContent(w, r, file.ID+"-scenes.jpg", object.ModTime, readSeekCloser(object.Body)) } func (a *App) generateMissingThumbnailForRequest(r *http.Request, box services.Box, file services.File) string { if file.Thumbnail != "" || !jobs.NeedsThumbnail(file) { return "" } thumbnail, err := jobs.GenerateThumbnailForFile(a.uploadService, box, file) if err != nil || thumbnail == "" { if err != nil { a.logger.Warn("on-demand thumbnail generation failed", withRequestLogAttrs(r, "source", "thumbnail", "severity", "warn", "code", 4102, "box_id", box.ID, "file_id", file.ID, "error", err.Error())...) } return "" } for i := range box.Files { if box.Files[i].ID == file.ID { box.Files[i].Thumbnail = thumbnail break } } if err := a.uploadService.SaveBox(box); err != nil { a.logger.Warn("on-demand thumbnail metadata save failed", withRequestLogAttrs(r, "source", "thumbnail", "severity", "warn", "code", 4103, "box_id", box.ID, "file_id", file.ID, "error", err.Error())...) return "" } return thumbnail } func (a *App) generateMissingVideoScenesForRequest(r *http.Request, box services.Box, file services.File) string { if file.SceneThumbnail != "" || !jobs.NeedsVideoScenes(file) { return "" } scene, err := jobs.GenerateVideoScenesForFile(a.uploadService, box, file) if err != nil || scene == "" { if err != nil { a.logger.Warn("on-demand video scenes preview generation failed", withRequestLogAttrs(r, "source", "thumbnail", "severity", "warn", "code", 4105, "box_id", box.ID, "file_id", file.ID, "error", err.Error())...) } return "" } for i := range box.Files { if box.Files[i].ID == file.ID { box.Files[i].SceneThumbnail = scene break } } if err := a.uploadService.SaveBox(box); err != nil { a.logger.Warn("on-demand video scenes preview metadata save failed", withRequestLogAttrs(r, "source", "thumbnail", "severity", "warn", "code", 4106, "box_id", box.ID, "file_id", file.ID, "error", err.Error())...) return "" } return scene } // servePlaceholderThumbnail serves the fallback image with no-store so the // browser re-requests on the next load and picks up the real thumbnail as soon // as it has been generated. func (a *App) servePlaceholderThumbnail(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store, must-revalidate") http.ServeFile(w, r, filepath.Join(a.cfg.StaticDir, "img", "file-placeholder.webp")) } func (a *App) UnlockBox(w http.ResponseWriter, r *http.Request) { box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { http.NotFound(w, r) return } if err := r.ParseForm(); err != nil { http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther) return } if !a.uploadService.VerifyPassword(box, r.FormValue("password")) { a.logger.Warn("box unlock failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4011, "box_id", box.ID)...) http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther) return } http.SetCookie(w, &http.Cookie{ Name: unlockCookieName(box.ID), Value: a.uploadService.UnlockToken(box), Path: "/d/" + box.ID, HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, Expires: box.ExpiresAt, }) a.logger.Info("box unlocked", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2002, "box_id", box.ID)...) http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther) } func (a *App) loadFileForRequest(w http.ResponseWriter, r *http.Request) (services.Box, services.File, bool) { box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { a.logger.Warn("file request missing box", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4041, "box_id", r.PathValue("boxID"), "file_id", r.PathValue("fileID"))...) http.NotFound(w, r) return services.Box{}, services.File{}, false } if err := a.uploadService.CanDownload(box); err != nil { a.logger.Warn("file request unavailable", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "file_id", r.PathValue("fileID"), "error", err.Error())...) http.Error(w, err.Error(), statusForDownloadError(err)) return services.Box{}, services.File{}, false } file, err := a.uploadService.FindFile(box, r.PathValue("fileID")) if err != nil { a.logger.Warn("file request missing file", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4042, "box_id", box.ID, "file_id", r.PathValue("fileID"))...) http.NotFound(w, r) return services.Box{}, services.File{}, false } return box, file, true } func (a *App) serveFileContent(w http.ResponseWriter, r *http.Request, box services.Box, file services.File, attachment bool) { object, err := a.uploadService.OpenFileObject(r.Context(), box, file) if err != nil { a.logger.Warn("file object missing", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4043, "box_id", box.ID, "file_id", file.ID, "error", err.Error())...) http.NotFound(w, r) return } defer object.Body.Close() w.Header().Set("Content-Type", file.ContentType) if attachment { w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", file.Name)) } if seeker, ok := object.Body.(io.ReadSeeker); ok { http.ServeContent(w, r, file.Name, object.ModTime, seeker) } else { if object.Size > 0 { w.Header().Set("Content-Length", fmt.Sprintf("%d", object.Size)) } w.WriteHeader(http.StatusOK) _, _ = io.Copy(w, object.Body) } if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) { a.logger.Warn("failed to record file download", "source", "download", "severity", "warn", "code", 4002, "box_id", box.ID, "error", err.Error()) } } func readSeekCloser(source io.ReadCloser) io.ReadSeeker { data, err := io.ReadAll(source) if err != nil { return bytes.NewReader(nil) } return bytes.NewReader(data) } func (a *App) DownloadZip(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Robots-Tag", "noindex, nofollow, noarchive") box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { a.logger.Warn("zip request missing box", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4044, "box_id", r.PathValue("boxID"))...) http.NotFound(w, r) return } if err := a.uploadService.CanDownload(box); err != nil { a.logger.Warn("zip request unavailable", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "error", err.Error())...) http.Error(w, err.Error(), statusForDownloadError(err)) return } if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) { a.logger.Warn("protected zip download blocked", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4014, "box_id", box.ID)...) http.Error(w, "password required", http.StatusUnauthorized) return } w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", "warpbox-"+box.ID+".zip")) w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) if err := a.uploadService.WriteZip(w, box); err != nil { a.logger.Error("zip download failed", "source", "download", "severity", "error", "code", 5002, "box_id", box.ID, "error", err.Error()) return } if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) { a.logger.Warn("failed to record zip download", "source", "download", "severity", "warn", "code", 4003, "box_id", box.ID, "error", err.Error()) } a.logger.Info("zip downloaded", withRequestLogAttrs(r, "source", "download", "severity", "user_activity", "code", 2006, "box_id", box.ID, "files", len(box.Files))...) } func (a *App) fileView(box services.Box, file services.File) fileView { return a.fileViewWithReactions(box, file, nil, false) } func (a *App) fileViewWithReactions(box services.Box, file services.File, reactions []services.ReactionSummary, reacted bool) fileView { icon := a.fileIcons.lookup(file.Name, file.ContentType) reactionViews := a.reactionViews(reactions) return fileView{ ID: file.ID, Name: file.Name, Size: helpers.FormatBytes(file.Size), SizeBytes: file.Size, ContentType: file.ContentType, PreviewKind: file.PreviewKind, URL: fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID), DownloadURL: fmt.Sprintf("/d/%s/f/%s/download", box.ID, file.ID), ThumbnailURL: fmt.Sprintf("/d/%s/thumb/%s", box.ID, file.ID), SceneURL: fmt.Sprintf("/d/%s/scene/%s", box.ID, file.ID), HasThumbnail: file.Thumbnail != "" || jobs.NeedsThumbnail(file), HasScene: file.SceneThumbnail != "" || jobs.NeedsVideoScenes(file), IconURL: fileIconURL("standard", icon.Standard), IconRetroURL: fileIconURL("retro", icon.Retro), ReactURL: fmt.Sprintf("/d/%s/f/%s/react", box.ID, file.ID), Reactions: reactionViews, ReactionMore: reactionOverflowCount(reactionViews), Reacted: reacted, Processing: file.Processing, } } func (a *App) ReactToFile(w http.ResponseWriter, r *http.Request) { box, file, ok := a.loadFileForRequest(w, r) if !ok { return } if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) { http.Error(w, "password required", http.StatusUnauthorized) return } if err := r.ParseForm(); err != nil { http.Error(w, "invalid reaction", http.StatusBadRequest) return } emojiID := strings.TrimSpace(r.FormValue("emoji_id")) if !a.validEmojiID(emojiID) { http.Error(w, "unknown emoji", http.StatusBadRequest) return } visitorID := a.reactionVisitorID(w, r) reactions, err := a.reactionService.Add(box.ID, file.ID, visitorID, emojiID) if errors.Is(err, os.ErrExist) { writeJSON(w, http.StatusConflict, map[string]any{"error": "already reacted"}) return } if err != nil { a.logger.Warn("file reaction failed", withRequestLogAttrs(r, "source", "reactions", "severity", "warn", "code", 4302, "box_id", box.ID, "file_id", file.ID, "error", err.Error())...) http.Error(w, "could not save reaction", http.StatusInternalServerError) return } a.logger.Info("file reaction added", withRequestLogAttrs(r, "source", "reactions", "severity", "user_activity", "code", 2301, "box_id", box.ID, "file_id", file.ID, "emoji_id", emojiID)...) writeJSON(w, http.StatusCreated, map[string]any{ "reactions": a.reactionViews(reactions), "reacted": true, }) } func (a *App) reactionViews(reactions []services.ReactionSummary) []reactionView { views := make([]reactionView, 0, len(reactions)) for index, reaction := range reactions { views = append(views, reactionView{ EmojiID: reaction.EmojiID, URL: emojiURL(reaction.EmojiID), Label: emojiLabel(reaction.EmojiID), Count: reaction.Count, Visible: index < 2, }) } return views } func reactionOverflowCount(reactions []reactionView) int { if len(reactions) <= 2 { return 0 } return len(reactions) - 2 } func (a *App) emojiTabs() ([]emojiTabView, error) { root := a.emojiRoot() entries, err := os.ReadDir(root) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil, nil } return nil, err } tabs := make([]emojiTabView, 0, len(entries)) for _, entry := range entries { if !entry.IsDir() { continue } tabID := entry.Name() files, err := os.ReadDir(filepath.Join(root, tabID)) if err != nil { return nil, err } tab := emojiTabView{ID: tabID, Label: emojiTabLabel(tabID)} for _, file := range files { if file.IsDir() || !isEmojiFile(file.Name()) { continue } emojiID := tabID + "/" + file.Name() tab.Emojis = append(tab.Emojis, emojiOptionView{ ID: emojiID, URL: emojiURL(emojiID), Label: emojiLabel(emojiID), }) } sort.Slice(tab.Emojis, func(i, j int) bool { return tab.Emojis[i].ID < tab.Emojis[j].ID }) if len(tab.Emojis) > 0 { tabs = append(tabs, tab) } } sort.Slice(tabs, func(i, j int) bool { return tabs[i].ID < tabs[j].ID }) return tabs, nil } func (a *App) validEmojiID(id string) bool { id = strings.TrimSpace(id) if id == "" || strings.Contains(id, "\\") || strings.Contains(id, "..") || strings.HasPrefix(id, "/") { return false } parts := strings.Split(id, "/") if len(parts) != 2 || parts[0] == "" || parts[1] == "" || !isEmojiFile(parts[1]) { return false } info, err := os.Stat(filepath.Join(a.emojiRoot(), parts[0], parts[1])) return err == nil && !info.IsDir() } func (a *App) emojiRoot() string { return filepath.Join(a.cfg.DataDir, "emoji") } func (a *App) reactionVisitorID(w http.ResponseWriter, r *http.Request) string { const cookieName = "warpbox_reactor" if cookie, err := r.Cookie(cookieName); err == nil && strings.TrimSpace(cookie.Value) != "" { return cookie.Value } visitorID := services.RandomPublicToken(32) http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: visitorID, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, Expires: time.Now().AddDate(1, 0, 0), }) return visitorID } func isEmojiFile(name string) bool { ext := strings.ToLower(filepath.Ext(name)) return ext == ".svg" || ext == ".webp" || ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".gif" } func emojiTabLabel(id string) string { label := strings.NewReplacer("-", " ", "_", " ").Replace(id) if label == "" { return "Emoji" } return strings.ToUpper(label[:1]) + label[1:] } func emojiLabel(id string) string { base := strings.TrimSuffix(filepath.Base(id), filepath.Ext(id)) return strings.ReplaceAll(base, "-", " ") } func emojiURL(id string) string { parts := strings.Split(id, "/") if len(parts) != 2 { return "" } return "/emoji/" + url.PathEscape(parts[0]) + "/" + url.PathEscape(parts[1]) } func writeJSON(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) } func (a *App) isBoxUnlocked(r *http.Request, box services.Box) bool { if !a.uploadService.IsProtected(box) { return true } cookie, err := r.Cookie(unlockCookieName(box.ID)) if err != nil { return false } return cookie.Value == a.uploadService.UnlockToken(box) } func unlockCookieName(boxID string) string { return "warpbox_unlock_" + strings.NewReplacer("-", "_", ".", "_").Replace(boxID) } // neverExpires reports whether a box's expiry is far enough out to be treated as // "forever" (set via the unlimited / -1 expiry option). func neverExpires(t time.Time) bool { return time.Until(t) > 50*365*24*time.Hour } // boxExpiryLabel formats a box's expiry with the given layout, rendering // "forever" boxes as "Never" instead of a meaningless far-future date. func boxExpiryLabel(t time.Time, layout string) string { if neverExpires(t) { return "Never" } return t.Format(layout) } func absoluteURL(r *http.Request, path string) string { if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { return path } scheme := "http" if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { scheme = "https" } return fmt.Sprintf("%s://%s%s", scheme, r.Host, path) } func isSocialPreviewBot(r *http.Request) bool { agent := strings.ToLower(r.UserAgent()) if agent == "" { return false } bots := []string{ "discordbot", "twitterbot", "facebookexternalhit", "telegrambot", "whatsapp", "slackbot", "linkedinbot", "skypeuripreview", "embedly", "pinterest", "vkshare", "mattermost", "mastodon", } for _, bot := range bots { if strings.Contains(agent, bot) { return true } } return false }