feat: multisite-aware block-content abilities via optional blog_id#2669
Merged
Conversation
The block-content abilities (get_post_blocks, edit_post_blocks, replace_post_blocks, insert_content) resolved post_id against whatever blog the request landed on. On a multisite network where the chat surface and the post live on different blogs, that silently targeted the wrong post (or none). Each ability now accepts an optional blog_id and runs its post read/write inside switch_to_blog() when the target differs from the current blog. The dimension is scoped to the content abilities and their pending-action handlers; the generic PendingActionStore, PendingActionHelper, and ResolvePendingActionAbility are untouched. blog_id rides inside the ability input and, for staged edits, inside apply_input (which already round-trips through the per-blog store), so the same switch_to_blog() that runs the preview also runs the apply on accept. The can_resolve edit_post capability check switches to the target blog first, since the meta-cap is mapped against the post on the blog it lives on. A shared BlogContext helper centralizes target resolution, network-site validation (rejecting archived/deleted/spam sites), and switch/restore. No-op on single-site or when blog_id is omitted.
Contributor
Homeboy Results —
|
This was referenced Jun 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes Data Machine's block-content editing stack multisite-aware. The four content abilities —
get_post_blocks,edit_post_blocks,replace_post_blocks,insert_content— now accept an optionalblog_idand run their post read/write (and the staged preview→apply cycle) in that blog's context viaswitch_to_blog().The gap this closes
The content abilities resolved
post_idagainst whatever blog the request landed on. On a single-site install that's always correct. On a multisite network where the chat surface and the post live on different blogs, it silently targeted the wrong post (or no post at all):get_post()/wp_update_post()/current_user_can('edit_post', …)are all current-blog-scoped.PendingActionStoreis$wpdb->prefix-scoped (per-blog), so a staged diff lives in the calling blog's table.This surfaced building a chat writing-assistant that edits a draft authored on the main site from a chat drawer running on another subsite (extrachill-roadie#48). The diff/accept-reject substrate (agents-api pending actions + chat DiffCards) is already generic; the missing piece was a target-blog dimension on the content layer.
Design — minimal, content-layer-scoped
blog_idis threaded only through the content abilities and their pending-action handlers. The generic primitives are untouched:PendingActionStore,PendingActionHelper, orResolvePendingActionAbility.blog_idrides inside the ability input and, for staged edits, insideapply_input— which already round-trips through the per-blog store. The store is staged and resolved on the same (calling) blog, so no cross-blog store lookup is needed; the ability's ownswitch_to_blog()re-targets the post on both preview and apply.ContentActionHandlers::can_resolve_post_edit()switches to the target blog before theedit_postmeta-cap check, since that capability is mapped against the post (and its author) on the blog it lives on.A new shared
BlogContexthelper centralizes:target_from_input) — returns 0 (no switch) on single-site, whenblog_idis omitted, or when it equals the current blog;is_valid) — rejects unknown, archived, deleted, and spam sites;enter/leave) with a context token soleave()is a no-op when no switch happened (and ignoresWP_Errortokens — no strayrestore_current_blog());apply_input/context stamping (with_blog_id) that omits a zero/absent blog_id so single-site payloads stay byte-identical.Backward compatible: omit
blog_id(or run on single-site) and behavior is unchanged.What changed
inc/Abilities/Content/BlogContext.php(new) — the shared targeting/validation/switch helper.GetPostBlocksAbility,EditPostBlocksAbility,ReplacePostBlocksAbility,InsertContentAbility— addedblog_idto ability input schema + chat-tool params; wrapped the post read/write inBlogContext::enter()/leave(); stampedblog_idinto stagedapply_input+ preview context. Edit/Replace/Insert split theirexecute()into a thin context-entering wrapper +execute_in_context()so the switch/restore is guaranteed byfinallywith minimal churn.ContentActionHandlers—can_resolve_post_edit()now switches to the stagedblog_idbefore theedit_postcapability check.Tests
tests/blog-context-content-smoke.php(new, 23 assertions, all pass) — covers target resolution (single-site no-op, same-blog no-op, cross-blog target), network-site validation (valid / unknown / archived / zero), enter/leave switch + restore, no-op tokens,WP_Errortokens,with_blog_idstamping/omission, and the stage→resolve replay round-trip (stamp on blog 1 while the resolve turn lands on blog 12, confirm it re-enters blog 1 and restores blog 12).php -lclean on all six touched/added files;phpcs(project ruleset) 0 findings on all of them incl. the new test.CI note
The pre-existing
pending-action-helper-approval-envelope-smoke.phpfails identically on cleanmain(it depends on the agents-api package absent from a barecomposer install) — unrelated to this change, confirmed by reproducing on a fresh clone. Matches the known standalone-smoke fixture debt.Manual acceptance
On a multisite network, from a chat surface on blog B editing a draft
#Nthat lives on blog A:get_post_blockswith{post_id: N, blog_id: A}returns blog A's blocks (not blog B's post N).edit_post_blockswith{post_id: N, blog_id: A, edits: […], preview: true}stages a diff; on accept (resolve_pending_action) the edit applies to blog A's post N, and theedit_postgate is evaluated on blog A.blog_id(or on single-site) behaves exactly as before.Follow-up
Unblocks extrachill-roadie#48 (Roadie writing-assistant editing a Studio draft on main) and any multisite DM install that wants cross-site inline content edits. The Roadie integration lands as a separate PR on top of this.