From e96196920797cbab8c36cb362f1640e4b9540490 Mon Sep 17 00:00:00 2001 From: truewhile <62226914+truewhile@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:25:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/reader_comment_test.go | 40 ++ internal/handler/reader_routes.go | 27 + internal/service/reader/browser_panel.go | 31 +- internal/service/reader/browser_panel_test.go | 43 ++ internal/service/reader/comment.go | 492 ++++++++++++++++++ internal/service/reader/comment_test.go | 230 ++++++++ internal/service/reader/reader.go | 8 + web/src/api/reader.ts | 21 + web/src/pages/reader/ReaderViewPage.tsx | 136 ++++- web/src/pages/reader/SourceLoginDialog.tsx | 12 +- 10 files changed, 1023 insertions(+), 17 deletions(-) create mode 100644 internal/handler/reader_comment_test.go create mode 100644 internal/service/reader/comment.go create mode 100644 internal/service/reader/comment_test.go diff --git a/internal/handler/reader_comment_test.go b/internal/handler/reader_comment_test.go new file mode 100644 index 0000000..f74fc70 --- /dev/null +++ b/internal/handler/reader_comment_test.go @@ -0,0 +1,40 @@ +package handler + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// 段评打开接口的 HTTP 层契约:路由存在、参数校验生效、内网地址被拒。 +// +// 正常路径(真的去抓评论页)依赖外部站点,放在服务层解析单测里覆盖; +// 这里只钉住契约与安全边界,避免把测试绑到网络。 +func TestReaderOpenCommentRejectsBadURL(t *testing.T) { + container := newReaderHandlerContainer(t) + router := registerReaderRoutesForTest(container) + + post := func(body string) int { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/reader/comments/open", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + return w.Code + } + + cases := []struct { + name string + body string + }{ + {"缺少 url", `{"book_id":"x"}`}, + {"非 http 协议", `{"book_id":"x","url":"javascript:alert(1)"}`}, + {"回环地址", `{"book_id":"x","url":"http://127.0.0.1/c"}`}, + {"内网地址", `{"book_id":"x","url":"http://192.168.1.1/c"}`}, + } + for _, c := range cases { + if code := post(c.body); code != http.StatusBadRequest { + t.Fatalf("%s:应 400,得到 %d", c.name, code) + } + } +} diff --git a/internal/handler/reader_routes.go b/internal/handler/reader_routes.go index 2cc38f3..6985a81 100644 --- a/internal/handler/reader_routes.go +++ b/internal/handler/reader_routes.go @@ -40,6 +40,9 @@ func registerReaderRoutes(authed *gin.RouterGroup, svc *service.Container) { // 页面内的 fetch/XHR 经此转发(iframe 是不透明源,请求带不上书源 Cookie) g.POST("/browser/xhr", readerBrowserXHRHandler(svc)) + // 段评:用宿主浏览器打开评论地址(带书源 Cookie/登录态,对应书源的 showCmt) + g.POST("/comments/open", readerOpenCommentHandler(svc)) + // 搜索(多源聚合) g.POST("/search", readerSearchHandler(svc)) @@ -249,6 +252,30 @@ func readerBrowserXHRHandler(svc *service.Container) gin.HandlerFunc { } } +// readerOpenCommentHandler 打开一条段评:复用书源登录态在宿主浏览器里承载评论页。 +// 书源的 showCmt 内部就是「java.ajax 取评论页 → java.showBrowser 展示」, +// 这里用承载登录页的同一套机制等价实现(见 reader.OpenContentComment)。 +func readerOpenCommentHandler(svc *service.Container) gin.HandlerFunc { + var body struct { + BookID string `json:"book_id" binding:"required"` + URL string `json:"url" binding:"required"` + Title string `json:"title"` + } + return func(c *gin.Context) { + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + userID := c.GetString(middleware.CtxUserID) + page, err := svc.Reader.OpenContentComment(c.Request.Context(), userID, body.BookID, body.URL, body.Title) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, page) + } +} + // readerBrowserPageHandler 承载待办页面本体。// // 鉴权走 HMAC 签名而非 JWT:这个地址要填进