package wasmguest import ( "context" "encoding/json" "net/http" "net/url" abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" "git.dev.alexdunmow.com/block/pluginsdk/blocks" "github.com/google/uuid" ) // contextFromRenderContext rebuilds the context.Context that block and // template code expects, applying each RenderContext field through the same // context keys core/blocks/context.go defines. Existing render code reading // via blocks.Get* works unchanged inside the guest. // // Function-valued context entries (SlotRenderer, MediaResolver, // EmbedResolver, Queries) do not cross the wire; their v1 mappings are // documented in core/docs/wasm-abi.md. RenderContext.theme_json and .locale // have no core/blocks context key yet and are ignored here until a consumer // key exists. func contextFromRenderContext(ctx context.Context, rc *abiv1.RenderContext) context.Context { if rc == nil { return ctx } if req := requestFromInfo(rc.GetRequest()); req != nil { ctx = blocks.WithRequest(ctx, req) } if bc := blockContextFromProto(rc.GetBlockContext()); bc != nil { ctx = blocks.WithBlockContext(ctx, bc) } if rc.GetTemplateKey() != "" { ctx = blocks.WithTemplateKey(ctx, rc.GetTemplateKey()) } if p := rc.GetPage(); p != nil { ctx = blocks.WithCurrentPage(ctx, &blocks.PageContext{ ID: parseUUID(p.GetId()), Slug: p.GetSlug(), Title: p.GetTitle(), PostType: p.GetPostType(), Status: p.GetStatus(), }) } if p := rc.GetPost(); p != nil { post := &blocks.PostContext{ ID: parseUUID(p.GetId()), Slug: p.GetSlug(), Title: p.GetTitle(), Excerpt: p.GetExcerpt(), FeaturedImageURL: p.GetFeaturedImageUrl(), AuthorID: parseUUID(p.GetAuthorId()), ReadingTime: int(p.GetReadingTime()), IsFeatured: p.GetIsFeatured(), } if ts := p.GetPublishedAt(); ts != nil { post.PublishedAt = ts.AsTime() } ctx = blocks.WithCurrentBlogPost(ctx, post) } if a := rc.GetAuthor(); a != nil { ctx = blocks.WithCurrentAuthor(ctx, &blocks.AuthorContext{ ID: parseUUID(a.GetId()), Name: a.GetName(), Slug: a.GetSlug(), Bio: a.GetBio(), AvatarURL: a.GetAvatarUrl(), }) } if c := rc.GetCategory(); c != nil { ctx = blocks.WithCurrentCategory(ctx, &blocks.CategoryContext{ ID: parseUUID(c.GetId()), Name: c.GetName(), Slug: c.GetSlug(), }) } if mp := rc.GetMasterPage(); mp != nil { ctx = blocks.WithMasterPage(ctx, &blocks.MasterPageContext{ ID: parseUUID(mp.GetId()), Slug: mp.GetSlug(), Title: mp.GetTitle(), }) } if rc.GetRequestedPath() != "" { ctx = blocks.WithRequestedPath(ctx, rc.GetRequestedPath()) } if slots := rc.GetInjectedSlots(); len(slots) > 0 { ctx = blocks.WithInjectedSlots(ctx, slots) } if rc.GetIsEditor() { ctx = blocks.WithIsEditor(ctx, true) } if slots := rc.GetExpectedSlots(); len(slots) > 0 { ctx = blocks.WithExpectedSlots(ctx, slots) } if id := parseUUID(rc.GetBlockId()); id != uuid.Nil { ctx = blocks.WithBlockID(ctx, id) } if id := parseUUID(rc.GetCurrentPageId()); id != uuid.Nil { ctx = blocks.WithCurrentPageID(ctx, id) } if hp := rc.GetHumanProofBanner(); hp != nil { ctx = blocks.WithHumanProofBanner(ctx, &blocks.HumanProofBannerData{ ActiveTimeMinutes: int(hp.GetActiveTimeMinutes()), KeystrokeCount: int(hp.GetKeystrokeCount()), SessionCount: int(hp.GetSessionCount()), PostSlug: hp.GetPostSlug(), }) } if dr := rc.GetDetailRow(); dr != nil { ctx = blocks.WithDetailRow(ctx, dr.GetTableId(), dr.GetRowId(), unmarshalMap(dr.GetDataJson())) } return ctx } // requestFromInfo rebuilds a *http.Request carrying the subset of fields // render code reads via blocks.GetRequest. func requestFromInfo(ri *abiv1.RequestInfo) *http.Request { if ri == nil { return nil } u, err := url.Parse(ri.GetUrl()) if err != nil || ri.GetUrl() == "" { u = &url.URL{Path: ri.GetPath(), RawQuery: ri.GetRawQuery()} } req := &http.Request{ Method: ri.GetMethod(), URL: u, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: make(http.Header, len(ri.GetHeaders())+2), Host: ri.GetHost(), RemoteAddr: ri.GetRemoteIp(), RequestURI: u.RequestURI(), } for k, v := range ri.GetHeaders() { req.Header.Set(k, v) } if ri.GetReferrer() != "" && req.Header.Get("Referer") == "" { req.Header.Set("Referer", ri.GetReferrer()) } if ri.GetUserAgent() != "" && req.Header.Get("User-Agent") == "" { req.Header.Set("User-Agent", ri.GetUserAgent()) } for name, value := range ri.GetCookies() { req.AddCookie(&http.Cookie{Name: name, Value: value}) } return req } func blockContextFromProto(pb *abiv1.BlockContext) *blocks.BlockContext { if pb == nil { return nil } bc := &blocks.BlockContext{ URL: pb.GetUrl(), Path: pb.GetPath(), Slug: pb.GetSlug(), PageID: pb.GetPageId(), PageTitle: pb.GetPageTitle(), TemplateKey: pb.GetTemplateKey(), IsEditor: pb.GetIsEditor(), Timestamp: pb.GetTimestamp(), IsLoggedIn: pb.GetIsLoggedIn(), UserID: pb.GetUserId(), UserEmail: pb.GetUserEmail(), UserRole: pb.GetUserRole(), Method: pb.GetMethod(), Host: pb.GetHost(), Query: pb.GetQuery(), Referrer: pb.GetReferrer(), UserAgent: pb.GetUserAgent(), IP: pb.GetIp(), Cookies: pb.GetCookies(), Headers: pb.GetHeaders(), Country: pb.GetCountry(), City: pb.GetCity(), Timezone: pb.GetTimezone(), CurrentAuthor: unmarshalMap(pb.GetCurrentAuthorJson()), CurrentPost: unmarshalMap(pb.GetCurrentPostJson()), CurrentCategory: unmarshalMap(pb.GetCurrentCategoryJson()), Site: unmarshalMap(pb.GetSiteJson()), IsPublicLoggedIn: pb.GetIsPublicLoggedIn(), PublicUserID: pb.GetPublicUserId(), PublicUsername: pb.GetPublicUsername(), PublicDisplayName: pb.GetPublicDisplayName(), PublicEmailVerified: pb.GetPublicEmailVerified(), DetailRowID: pb.GetDetailRowId(), DetailTableID: pb.GetDetailTableId(), DetailRowData: unmarshalMap(pb.GetDetailRowDataJson()), BlogIndexURL: pb.GetBlogIndexUrl(), CategoryPageURL: pb.GetCategoryPageUrl(), } if ts := pb.GetNow(); ts != nil { bc.Now = ts.AsTime() } return bc } // unmarshalMap decodes a JSON object into map[string]any, returning nil for // empty or undecodable input (render code treats absent maps as nil). func unmarshalMap(raw []byte) map[string]any { if len(raw) == 0 { return nil } var m map[string]any if err := json.Unmarshal(raw, &m); err != nil { return nil } return m } func parseUUID(s string) uuid.UUID { if s == "" { return uuid.Nil } id, err := uuid.Parse(s) if err != nil { return uuid.Nil } return id }