feat(content-gate): add per-block access control for Group, Stack, and Row blocks#4646
feat(content-gate): add per-block access control for Group, Stack, and Row blocks#4646
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ix setRegistration param
There was a problem hiding this comment.
Pull request overview
Adds per-block visibility controls for core/group, core/stack, and core/row blocks using Newspack Content Gate access rules, enabling editors to show/hide individual blocks to specific reader segments without creating a full gate.
Changes:
- Adds a new editor Inspector “Access Control” panel and registers block attributes for visibility mode + rule configuration.
- Adds a PHP
render_blockfilter to enforce per-block visibility on the frontend, with per-request caching. - Adds PHPUnit + Jest coverage and wires the new editor bundle into Webpack.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
webpack.config.js |
Adds a new build entry for the block-visibility editor script. |
includes/content-gate/class-content-gate.php |
Includes the new Block_Visibility feature file. |
includes/content-gate/class-block-visibility.php |
Implements server-side attribute registration, editor asset enqueue, and frontend render filtering/caching. |
src/content-gate/editor/block-visibility.tsx |
Registers attributes and injects the Inspector panel UI for target blocks. |
src/content-gate/editor/editor.scss |
Styles the Access Control panel UI elements. |
src/content-gate/editor/index.d.ts |
Adds shared TS types used by the content-gate editor code. |
tests/unit-tests/content-gate/class-block-visibility.php |
Adds PHPUnit tests for rule evaluation, rendering behavior, and caching. |
src/content-gate/editor/block-visibility.test.ts |
Adds Jest tests for the attribute registration filter. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Use [] not stdClass as default for newspackAccessControlRules block attribute - Add comment on get_post_type() === false in enqueue_block_editor_assets - Fix setRegistration to not mutate its parameter; use immutable spread - Fix counting_rule test to use unique ID per run to avoid closure issues - Fix "Visiblity" typo in VisibilityControl help text - Guard test_rule registration against duplicate registration across tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bypass access control during REST requests so blocks are never hidden in the editor's block renderer or preview contexts - Defensively cast newspackAccessControlRules to array in case the block parser yields a stdClass for the object-typed attribute - Fix FormTokenField empty label; use config.name + hideLabelFromVision Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover the case where a user with edit_post capability on the current post sees all blocks regardless of access rules, and verify that a non-editor is still subject to normal rule evaluation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Blocks can now be linked to one or more existing content gates instead
of defining access rules inline. Gate rules are resolved server-side at
render time so any change to a gate is immediately reflected in every
block that references it.
- Add newspackAccessControlMode attribute ('gate' default, 'custom')
- Add newspackAccessControlGateIds array attribute for gate links
- Gate mode uses OR logic: reader must satisfy any one selected gate
- Deleted/unpublished gates are silently skipped; a block with only
inactive gates passes through with no restriction
- Localize available published gates to newspackBlockVisibility JS data
- Add GateControls FormTokenField component to the inspector panel
- Add mode toggle (Gate / Custom) with Gate as the default
- Add 7 new PHPUnit tests covering gate mode evaluation paths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Pushed a small design tweak in 5292632: replaced the two hardcoded px values in |
|
Reverted the |
- Always render Require verification toggle (disabled when parent off) to avoid layout jump - Split registration section into sibling PanelRows - Swap verification CheckboxControl for ToggleControl - Move intro copy onto the Access mode ToggleGroupControl help prop - Opt all inputs in the panel into __nextHasNoMarginBottom - Restore FormTokenField help margin-top stripped by Gutenberg's inspector p-reset
|
Pushed a few more design refinements in 04640ad:
Marking as Design Approved — the panel feels much closer to WP core's native inspector patterns now. |
There was a problem hiding this comment.
Tested the editor UI and frontend rendering in an isolated env with NEWSPACK_CONTENT_GATES enabled. Both Gate and Custom modes work as described -- the Access Control panel renders correctly for Group blocks, visibility toggles behave as expected (disabled until rules are configured, reset when rules are cleared), and the render_block filter correctly hides restricted content from logged-out users while showing it to editors who can edit the post.
Two issues to be addressed before approving (see inline comments for details):
- TARGET_BLOCKS sync risk: The PHP filter
newspack_content_gate_block_visibility_blockslets developers add blocks server-side, but the JS side hardcodesTARGET_BLOCKS. If the filter is used, the UI and server get out of sync. - Deleted gates + hidden visibility: In gate mode with "hidden" visibility, if all selected gates are deleted/unpublished, the block silently disappears for everyone instead of passing through.
Also noted a few minor items inline (FormTokenField duplicate-title fragility, default value type mismatch).
| /** | ||
| * Target block types that receive access control attributes. | ||
| */ | ||
| const TARGET_BLOCKS = [ 'core/group', 'core/stack', 'core/row' ]; |
There was a problem hiding this comment.
This list is hardcoded, but the PHP side exposes a newspack_content_gate_block_visibility_blocks filter (class-block-visibility.php:40) that allows developers to add or remove blocks. If someone adds e.g. core/columns via the filter, the server would evaluate access rules for it but the editor UI would never show the Access Control panel for that block.
Consider passing the target block list from PHP to JS via the existing wp_localize_script call in enqueue_block_editor_assets() so they stay in sync:
const TARGET_BLOCKS = window.newspackBlockVisibility?.target_blocks ?? ['core/group', 'core/stack', 'core/row'];There was a problem hiding this comment.
3d3d276 localizes the result of get_target_blocks() and uses that in the JS, so the list should always match.
| return $user_matches ? $block_content : ''; | ||
| } | ||
| // 'hidden' | ||
| return $user_matches ? '' : $block_content; |
There was a problem hiding this comment.
Edge case: when all selected gates are deleted or unpublished, compute_gate_rules_match() returns true (line 306 -- "no active restriction, pass-through"). But here, $user_matches = true combined with visibility = 'hidden' returns '' -- the block is hidden from everyone.
The intent of "all gates gone = no restriction" should mean the block always renders, regardless of visibility mode. Consider returning $block_content early (before the visibility check) when the gate-mode evaluation determines there are no active gates, similar to how no-gates-selected is handled on line 69:
if ( 'gate' === $mode && ! self::has_active_gates( $gate_ids ) ) {
return $block_content;
}There was a problem hiding this comment.
3d3d276 addresses this by bailing early if the block attribute value contains no active gates.
| ], | ||
| 'newspackAccessControlRules' => [ | ||
| 'type' => 'object', | ||
| 'default' => [], |
There was a problem hiding this comment.
Minor: 'default' => [] for a 'type' => 'object' attribute is technically an array, not an object. While WP's block attribute handling is lenient today, the JS side defaults to {}. For consistency and to future-proof against stricter JSON schema validation, consider 'default' => (object) [].
- Sync TARGET_BLOCKS with PHP: pass get_target_blocks() result via
wp_localize_script so the JS honours the
newspack_content_gate_block_visibility_blocks filter
- Fix hidden-mode bug when all gates are inactive: add has_active_gates()
guard in filter_render_block() so a block whose gates are all deleted or
unpublished passes through regardless of the visibility setting
- Use (object) [] as default for newspackAccessControlRules to match the
object type declaration and the JS default of {}
- Add regression test: deleted gate + hidden mode must show the block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…D-based selection
Switch both FormTokenField instances (GateControls and AccessRuleValueControl)
from plain title/label strings to { value, title } token objects. The value
field carries the item ID so token removal is keyed by ID rather than display
string, which means two items with identical labels can coexist as tokens and
are removed independently.
New tokens added from suggestions still arrive as plain strings and are
resolved to IDs by label lookup — an inherent FormTokenField limitation, but
removal reliability is the more common concern in practice.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ld for ID-based selection" This reverts commit fdb124a.
|
Thanks for the feedback, @adekbadek—this is ready for another look. |
All Submissions:
Changes proposed in this Pull Request:
Adds per-block visibility controls to
core/group,core/stack, andcore/rowblocks, allowing editors to show or hide individual blocks based on one or more content gates, or custom rules using the same Registered and Paid access rules used by content gates."Gate" mode:

"Custom" mode:

"Custom" mode exists because publishers often want to control per-block visibility at a different and/or more granular level than content gates. To use a real-world example, a subscriber might have access to restricted content across the site if they have an active subscription to either a Digital-Only or Print+Digital subscription product, but PDF issue embeds on magazine-focused pages should be visible only to Print+Digital subscribers.
Editor UI (
edit_others_postscapability required, on supported post types only):FormTokenField; free-text rules render aTextControl.Frontend evaluation (via
render_blockfilter):visiblemode renders the block only to matching readers;hiddenmode renders it only to non-matching readers.Access_Rules::evaluate_rules(), using the same AND/OR grouped logic as content gates.user_id + md5(rules)) so repeated blocks with identical rules evaluate only once.New files:
includes/content-gate/class-block-visibility.php— PHP class withrender_blockfilter, server-side attribute registration, and asset enqueue.src/content-gate/editor/block-visibility.tsx— React/TS entry: attribute registration filter, Inspector HOC, and all panel components.src/content-gate/editor/index.d.ts— Shared TypeScript types for the content-gate editor directory.tests/unit-tests/content-gate/class-block-visibility.php— 24 PHPUnit tests.src/content-gate/editor/block-visibility.test.ts— 7 Jest tests for the attribute registration filter.Closes NPPD-1430.
How to test the changes in this Pull Request:
Prerequisites:
NEWSPACK_CONTENT_GATESistrue(n wp config set NEWSPACK_CONTENT_GATES true --raw).n build newspack-plugin. Confirmdist/content-gate-block-visibility.jsexists.Inspector panel:
Frontend — "Custom" mode with "visible" visibility:
Frontend — "Custom" mode with "hidden" visibility:
Verification sub-option:
Front-end — "Gate" mode
Post type guard:
Content_Restriction_Control::get_available_post_types(). Confirm the Access Control panel does not appear and the JS asset is not enqueued.Editors are exempt:
Graceful degradation:
Other information: