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.
pull/3227/head
WittF 1 week ago
parent bac8908691
commit 4ef97bf244
No known key found for this signature in database

@ -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')
}

Loading…
Cancel
Save