From 4ef97bf244b121f2a2818f32136e4a9f4a1137fe Mon Sep 17 00:00:00 2001 From: WittF Date: Fri, 23 Jan 2026 12:50:25 +0000 Subject: [PATCH] feat(share): add OG preview for home route share links Intercept /home route for social media bots viewing cloudreve://share paths and render OG preview directly. --- middleware/frontend.go | 109 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/middleware/frontend.go b/middleware/frontend.go index b1f10d69..efe5454d 100644 --- a/middleware/frontend.go +++ b/middleware/frontend.go @@ -1,12 +1,17 @@ package middleware import ( - "github.com/cloudreve/Cloudreve/v4/application/dependency" - "github.com/cloudreve/Cloudreve/v4/pkg/util" - "github.com/gin-gonic/gin" "io" "net/http" + "net/url" "strings" + + "github.com/cloudreve/Cloudreve/v4/application/constants" + "github.com/cloudreve/Cloudreve/v4/application/dependency" + filemanagerfs "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" + "github.com/cloudreve/Cloudreve/v4/pkg/sharepreview" + "github.com/cloudreve/Cloudreve/v4/pkg/util" + "github.com/gin-gonic/gin" ) // FrontendFileHandler 前端静态文件处理 @@ -50,8 +55,12 @@ func FrontendFileHandler(dep dependency.Dep) gin.HandlerFunc { return } + if renderSharePreviewForBot(c) { + return + } + // 不存在的路径和index.html均返回index.html - if (path == "/index.html") || (path == "/") || !fs.Exists("/", path) { + if path == "/index.html" || path == "/" || !fs.Exists("/", path) { // 读取、替换站点设置 settingClient := dep.SettingProvider() siteBasic := settingClient.SiteBasic(c) @@ -84,3 +93,95 @@ func FrontendFileHandler(dep dependency.Dep) gin.HandlerFunc { c.Abort() } } + +func renderSharePreviewForBot(c *gin.Context) bool { + path := c.Request.URL.Path + if path != "/home" && path != "/home/" { + return false + } + if !util.IsSocialMediaBot(c.GetHeader("User-Agent")) { + return false + } + shareURI := parseShareURI(c.Query("path")) + if shareURI == nil { + return false + } + shareID := shareURI.ID("") + if shareID == "" { + return false + } + + html, err := sharepreview.RenderOGPage(c, sharepreview.Options{ + ID: shareID, + Password: shareURI.Password(), + SharePath: shareURI.PathTrimmed(), + }) + if err != nil { + return false + } + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, html) + c.Abort() + return true +} + +func parseShareURI(raw string) *filemanagerfs.URI { + if raw == "" { + return nil + } + shareURI, err := filemanagerfs.NewUriFromString(raw) + if err != nil { + sanitized := sanitizeInvalidPercentEscapes(raw) + if sanitized != raw { + shareURI, err = filemanagerfs.NewUriFromString(sanitized) + } + } + if err != nil && strings.Contains(raw, "%") { + unescaped, err := url.QueryUnescape(raw) + if err == nil { + shareURI, err = filemanagerfs.NewUriFromString(unescaped) + if err != nil { + sanitized := sanitizeInvalidPercentEscapes(unescaped) + if sanitized != unescaped { + shareURI, err = filemanagerfs.NewUriFromString(sanitized) + } + } + } + } + if err != nil { + return nil + } + if shareURI.FileSystem() != constants.FileSystemShare { + return nil + } + return shareURI +} + +func sanitizeInvalidPercentEscapes(raw string) string { + if !strings.Contains(raw, "%") { + return raw + } + + var b strings.Builder + b.Grow(len(raw)) + for i := 0; i < len(raw); i++ { + if raw[i] != '%' { + b.WriteByte(raw[i]) + continue + } + if i+2 < len(raw) && isHexDigit(raw[i+1]) && isHexDigit(raw[i+2]) { + b.WriteByte('%') + b.WriteByte(raw[i+1]) + b.WriteByte(raw[i+2]) + i += 2 + continue + } + // Replace stray % to avoid url.Parse failures. + b.WriteString("%25") + } + return b.String() +} + +func isHexDigit(b byte) bool { + return ('0' <= b && b <= '9') || ('a' <= b && b <= 'f') || ('A' <= b && b <= 'F') +}