Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
f5843ad
feat(content-gate): decouple content gate from WC Memberships
miguelpeixe Sep 19, 2025
fd8cc89
feat: add post restriction strategy and other fixes
miguelpeixe Sep 19, 2025
8975802
feat(content-gate): restriction control
miguelpeixe Sep 19, 2025
204120b
feat: init access rules poc
dkoo Sep 22, 2025
ddc3c11
feat: access rules UI
dkoo Sep 24, 2025
3ab7a50
fix: allow comma or line delimiters in whitelist field
dkoo Sep 24, 2025
744098f
Merge branch 'trunk' into feat/content-gate-restriction-control
miguelpeixe Oct 21, 2025
d528e02
Merge branch 'feat/content-gate-restriction-control' into feat/conten…
miguelpeixe Oct 21, 2025
251ce9f
fix: refactor access rule registration introduce reader data rule
miguelpeixe Oct 21, 2025
954f010
feat: add content gate settings array (#4249)
dkoo Oct 21, 2025
b56de16
chore: update translation files [skip ci]
matticbot Oct 21, 2025
93ff04b
feat: gates rest api
miguelpeixe Oct 21, 2025
2a0a421
feat: start content rules controls
dkoo Oct 21, 2025
0e2f7ad
Merge branch 'feat/content-gate-restriction-control--access-rules' in…
dkoo Oct 21, 2025
bdf51e8
feat: add gate access rules
miguelpeixe Oct 21, 2025
4d0aa85
feat: content rules, cont
dkoo Oct 22, 2025
c3c9b41
Merge branch 'feat/content-gate-restriction-control--access-rules' in…
dkoo Oct 22, 2025
0133864
fix: normalize content rules schema
dkoo Oct 22, 2025
834c986
feat: gate setttings and rule components
miguelpeixe Oct 22, 2025
12eeb73
feat: update gate settings api
miguelpeixe Oct 22, 2025
ee4c1af
Merge branch 'feat/content-gate-restriction-control--access-rules' in…
dkoo Oct 22, 2025
f691e9e
fix: read/write content_rules for gates
dkoo Oct 22, 2025
6be2266
Merge branch 'trunk' into feat/content-gate-restriction-control--cont…
dkoo Oct 22, 2025
affa157
feat(content-gate): implement restriction rules
miguelpeixe Oct 22, 2025
8fc70e2
fix: term check and validations
miguelpeixe Oct 22, 2025
daa8226
fix: check gate status and map gate post id
miguelpeixe Oct 22, 2025
b16aedf
fix: pass post id to gate id filter
miguelpeixe Oct 22, 2025
061940a
feat: content rule controls
dkoo Oct 22, 2025
78425d7
feat: content rule controls
dkoo Oct 22, 2025
4f6b7d3
fix: content rule control using FormTokenField
dkoo Oct 22, 2025
e23a49d
style: reduce spacing
dkoo Oct 22, 2025
2bdb6d4
feat: handle taxonomy
miguelpeixe Oct 23, 2025
4ee6856
fix: dynamic column width
dkoo Oct 23, 2025
32d4dad
fix: ts error
dkoo Oct 23, 2025
8386e04
fix: styles, add dummy exclusion checkbox
dkoo Oct 23, 2025
c9d2aba
styles: spacing inside content gate settings
dkoo Oct 23, 2025
99fffb6
merge trunk
miguelpeixe Oct 23, 2025
7a3788e
fix: card children border
miguelpeixe Oct 23, 2025
a160974
chore: typescript
miguelpeixe Oct 23, 2025
5a4e595
Merge branch 'feat/content-gate-restriction-control--content-rules' i…
miguelpeixe Oct 23, 2025
6f7b244
Merge branch 'trunk' into feat/content-gate-restriction-control--cont…
miguelpeixe Oct 28, 2025
ca4fbef
fix: remove title editor
miguelpeixe Oct 28, 2025
b893c4a
fix: improve components and rerenders
miguelpeixe Oct 29, 2025
c6e603b
Merge branch 'trunk' into feat/content-gate-restriction-control--cont…
miguelpeixe Oct 29, 2025
528afe0
fix: simplify rule toggling
miguelpeixe Oct 29, 2025
147f650
Merge branch 'feat/content-gate-restriction-control--content-rules' i…
miguelpeixe Oct 29, 2025
03c3f0f
chore: remove unused component
miguelpeixe Oct 29, 2025
632cb72
fix: type
miguelpeixe Oct 29, 2025
82176c8
Merge branch 'feat/content-gate-restriction-control--content-rules' i…
miguelpeixe Oct 29, 2025
e78c695
fix: remove unnecessary memoization
miguelpeixe Oct 30, 2025
7e2080f
Merge branch 'trunk' into feat/content-gate-restriction-control--cont…
miguelpeixe Oct 30, 2025
516499f
Merge branch 'trunk' into feat/content-gate-restriction-control--cont…
miguelpeixe Oct 30, 2025
1e1884c
fix: use wizardFetch for most API requests
dkoo Oct 30, 2025
30d6f9f
fix: add consts
dkoo Oct 30, 2025
02a4137
fix: update parent component state on save
miguelpeixe Oct 31, 2025
a7085a3
Merge branch 'feat/content-gate-restriction-control--content-rules' i…
miguelpeixe Oct 31, 2025
5934033
Merge branch 'trunk' into feat/content-gate-restriction-control--cont…
miguelpeixe Nov 3, 2025
cc8d7c8
Merge branch 'feat/content-gate-restriction-control--content-rules' i…
miguelpeixe Nov 3, 2025
429d4e6
Merge branch 'trunk' into feat/content-gate-restriction-rules
miguelpeixe Dec 2, 2025
07ae0fa
Merge branch 'trunk' into feat/content-gate-restriction-rules
dkoo Dec 2, 2025
68bb544
fix: default post ID
miguelpeixe Dec 3, 2025
980ac04
fix(content-gates): resolve fatals and mismatched data types (#4338)
dkoo Dec 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 12 additions & 19 deletions includes/content-gate/class-content-gate.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,12 @@ public static function restrict_post( $post, $query ) {
if ( ! $query->is_main_query() ) {
return;
}
if ( self::has_rendered() ) {
if ( ! is_singular() ) {
return;
}
if ( get_queried_object_id() !== $post->ID ) {
return;
}

// Don't apply our restriction strategy if Woo Memberships is active.
if ( Memberships::is_active() ) {
return;
Expand All @@ -105,9 +107,6 @@ public static function restrict_post( $post, $query ) {
if ( is_admin() ) {
return;
}
if ( ! self::has_gate() ) {
return;
}
if ( ! self::is_post_restricted( $post->ID ) ) {
return;
}
Expand All @@ -127,9 +126,7 @@ public static function restrict_post( $post, $query ) {

$content = self::get_restricted_post_excerpt( $post );

$content .= self::get_inline_gate_content();

$post->post_content = $content;
$post->post_content = $content . self::get_inline_gate_content();
$post->post_excerpt = $content;
$post->comment_status = 'closed';
$post->comment_count = 0;
Expand Down Expand Up @@ -390,11 +387,13 @@ public static function get_gate_post_id( $post_id = null ) {
$gate_post_id = false;
}

$post_id = $post_id ?? get_the_ID();

/**
* Filters the gate post ID.
*
* @param int $gate_post_id Gate post ID.
* @param int $post_id Post ID.
* @param int $post_id Post ID.
*/
return apply_filters( 'newspack_content_gate_post_id', $gate_post_id, $post_id );
}
Expand Down Expand Up @@ -474,16 +473,11 @@ public static function is_post_restricted( $post_id = null ) {

/**
* Filters whether the post is restricted for the current user.
* If the post is restricted by a content gate, return the gate post ID.
*
* @param int|bool $restricted_by If restricted, the gate post ID. False if not restricted.
* @param int $post_id Post ID.
* @param bool $restricted_by Whether the post is restricted.
* @param int $post_id Post ID.
*/
$restricted_by = apply_filters( 'newspack_is_post_restricted', false, $post_id );
if ( $restricted_by && is_int( $restricted_by ) ) {
self::$gate_post_id = $restricted_by;
}
return $restricted_by;
return apply_filters( 'newspack_is_post_restricted', false, $post_id );
}

/**
Expand Down Expand Up @@ -627,7 +621,7 @@ public static function get_restricted_post_excerpt( $post ) {
$content = apply_filters( 'newspack_gate_content', explode( '<!--more-->', $content )[0] );
} else {
$content = apply_filters( 'newspack_gate_content', $content );
$count = (int) get_post_meta( $gate_post_id, 'visible_paragraphs', true );
$count = max( 1, (int) get_post_meta( $gate_post_id, 'visible_paragraphs', true ) );
// Split into paragraphs.
$content = explode( '</p>', $content );
// Extract the first $x paragraphs only.
Expand Down Expand Up @@ -742,7 +736,6 @@ public static function get_gate( $id ) {
'id' => $post->ID,
'status' => $post->post_status,
'title' => $post->post_title,
'description' => $post->post_excerpt,
'metering' => Metering::get_metering_settings( $post->ID ),
'priority' => (int) get_post_meta( $post->ID, 'gate_priority', true ),
'access_rules' => Access_Rules::get_post_access_rules( $post->ID ),
Expand Down
95 changes: 70 additions & 25 deletions includes/content-gate/class-content-restriction-control.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@
* Main class.
*/
class Content_Restriction_Control {
/**
* Map of post IDs to gate IDs.
*
* @var array
*/
private static $post_gate_id_map = [];

/**
* Initialize hooks and filters.
*/
public static function init() {
add_filter( 'newspack_is_post_restricted', [ __CLASS__, 'is_post_restricted' ], 10, 2 );
add_filter( 'newspack_content_gate_post_id', [ __CLASS__, 'get_gate_post_id' ], 10, 2 );
}

/**
Expand Down Expand Up @@ -91,36 +98,52 @@ public static function get_available_taxonomies() {
*
* @param int $post_id Optional post ID.
*
* @return int[] Array of gate post IDs.
* @return array Array of post gates.
*/
public static function get_post_gates( $post_id = null ) {
$post_id = $post_id ?? \get_the_ID();
$post_type = \get_post_type( $post_id );
$categories = \wp_get_post_categories( $post_id );
$tags = \wp_get_post_tags( $post_id, [ 'fields' => 'ids' ] );
$post_id = $post_id ?? \get_the_ID();
if ( ! $post_id ) {
return [];
}

$gate_post_ids = [];
$gates = Content_Gate::get_gates();
$gates = Content_Gate::get_gates();
if ( empty( $gates ) ) {
return [];
}

$post_gates = [];
foreach ( $gates as $gate ) {
// TODO: Change this to read from the gate rules.
$gate_post_types = \get_post_meta( $gate['id'], 'post_types', true );
$gate_categories = \wp_get_post_categories( $gate['id'] );
$gate_tags = \wp_get_post_tags( $gate['id'], [ 'fields' => 'ids' ] );

if ( empty( $gate_post_types ) || ! in_array( $post_type, $gate_post_types, true ) ) {
if ( 'publish' !== $gate['status'] ) {
continue;
}
if ( ! empty( $gate_categories ) && empty( array_intersect( $gate_categories, $categories ) ) ) {
$content_rules = $gate['content_rules'];
if ( empty( $content_rules ) ) {
continue;
}
if ( ! empty( $gate_tags ) && empty( array_intersect( $gate_tags, $tags ) ) ) {
continue;

foreach ( $content_rules as $content_rule ) {
if ( $content_rule['slug'] === 'post_types' ) {
$post_type = get_post_type( $post_id );
if ( ! in_array( $post_type, $content_rule['value'], true ) ) {
continue 2;
}
} else {
$taxonomy = get_taxonomy( $content_rule['slug'] );
if ( ! $taxonomy ) {
continue 2;
}
$terms = wp_get_post_terms( $post_id, $content_rule['slug'], [ 'fields' => 'ids' ] );
if ( ! $terms || is_wp_error( $terms ) ) {
continue 2;
}
if ( empty( array_intersect( $terms, $content_rule['value'] ) ) ) {
continue 2;
}
}
}
$gate_post_ids[] = $gate['id'];
$post_gates[] = $gate;
}

return $gate_post_ids;
return $post_gates;
}

/**
Expand All @@ -142,23 +165,45 @@ public static function is_post_restricted( $is_post_restricted, $post_id = null
return $is_post_restricted;
}

$gate_ids = self::get_post_gates( $post_id );
if ( empty( $gate_ids ) ) {
$post_gates = self::get_post_gates( $post_id );
if ( empty( $post_gates ) ) {
return false;
}

foreach ( $gate_ids as $gate_id ) {
$access_rules = Access_Rules::get_post_access_rules( $gate_id );
// Return if the post gate has already been determined.
if ( ! empty( self::$post_gate_id_map[ $post_id ] ) ) {
return true;
}

foreach ( $post_gates as $gate ) {
$access_rules = $gate['access_rules'];
if ( empty( $access_rules ) ) {
continue;
}
foreach ( $access_rules as $rule ) {
if ( ! Access_Rules::evaluate_rule( $rule['slug'], $rule['value'] ?? null ) ) {
return false;
self::$post_gate_id_map[ $post_id ] = $gate['id'];
return true;
}
}
}
return true;
return false;
}

/**
* Get the current gate post ID.
*
* @param int $gate_post_id Gate post ID.
* @param int $post_id Post ID. If not given, uses the current post ID.
*
* @return int|false
*/
public static function get_gate_post_id( $gate_post_id, $post_id = null ) {
$post_id = $post_id ?? \get_the_ID();
if ( ! empty( self::$post_gate_id_map[ $post_id ] ) ) {
return self::$post_gate_id_map[ $post_id ];
}
return $gate_post_id;
}
}
Content_Restriction_Control::init();
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class Content_Gifting {
public static function init() {
add_action( 'init', [ __CLASS__, 'hook_gift_button' ] );
add_action( 'wp', [ __CLASS__, 'unrestrict_content' ], 5 );
add_filter( 'newspack_content_gate_restrict_post', [ __CLASS__, 'restrict_post' ] );
add_filter( 'newspack_content_gate_restrict_post', [ __CLASS__, 'restrict_post' ], 10, 2 );
add_filter( 'newspack_content_gate_metering_short_circuit', [ __CLASS__, 'short_circuit_metering' ] );
add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ] );
add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ] );
Expand Down
26 changes: 23 additions & 3 deletions includes/wizards/audience/class-audience-content-gates.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,7 @@ public function register_api_endpoints() {
*/
public function sanitize_gate( $gate ) {
return [
'title' => sanitize_text_field( $gate['title'] ),
'description' => sanitize_text_field( $gate['description'] ),
'title' => isset( $gate['title'] ) ? sanitize_text_field( $gate['title'] ) : __( 'Untitled Content Gate', 'newspack-plugin' ),
'metering' => $this->sanitize_metering( $gate['metering'] ),
'access_rules' => $this->sanitize_rules( $gate['access_rules'] ),
'content_rules' => $this->sanitize_rules( $gate['content_rules'], 'content' ),
Expand All @@ -249,6 +248,15 @@ public function sanitize_gate( $gate ) {
* @return array The sanitized metering.
*/
public function sanitize_metering( $metering ) {
$metering = wp_parse_args(
$metering,
[
'enabled' => false,
'anonymous_count' => 0,
'registered_count' => 0,
'period' => 'month',
]
);
return [
'enabled' => boolval( $metering['enabled'] ),
'anonymous_count' => intval( $metering['anonymous_count'] ),
Expand All @@ -267,6 +275,9 @@ public function sanitize_metering( $metering ) {
*/
public function sanitize_rules( $rules, $type = 'access' ) {
$sanitized_rules = [];
if ( ! is_array( $rules ) ) {
return $sanitized_rules;
}
foreach ( $rules as $rule ) {
$sanitized = $type === 'access' ? $this->sanitize_access_rule( $rule ) : $this->sanitize_content_rule( $rule );
if ( ! is_wp_error( $sanitized ) ) {
Expand Down Expand Up @@ -299,7 +310,16 @@ public function sanitize_access_rule( $access_rule ) {
if ( ! is_array( $access_rule['value'] ) ) {
return new \WP_Error( 'invalid_access_rule_value', __( 'Invalid access rule value.', 'newspack-plugin' ), [ 'status' => 400 ] );
}
$value = array_values( array_filter( array_map( 'sanitize_text_field', $access_rule['value'] ) ) );
$value = array_values(
array_filter(
array_map(
function( $value ) {
return is_numeric( $value ) ? intval( $value ) : sanitize_text_field( $value );
},
$access_rule['value']
)
)
);
} else {
$value = sanitize_text_field( $access_rule['value'] );
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
/**
* WordPress dependencies.
*/
import { CheckboxControl, SelectControl, TextControl } from '@wordpress/components';
import { CheckboxControl, TextControl } from '@wordpress/components';

/**
* Internal dependencies
*/
import { FormTokenField } from '../../../../../packages/components/src';

const noop = () => {};

Expand All @@ -16,12 +21,12 @@ export default function AccessRuleControl( { slug, value, onChange }: GateAccess
}
if ( rule.options && rule.options.length > 0 ) {
return (
<SelectControl
<FormTokenField
label={ rule.name }
value={ value as string }
onChange={ onChange }
options={ rule.options.map( option => ( { value: option.value, label: option.label } ) ) }
help={ rule.description }
value={ rule.options.filter( o => value.includes( o.value ) ).map( o => o.label ) }
onChange={ ( items: string[] ) => onChange( rule.options?.filter( o => items.includes( o.label ) ).map( o => o.value ) ?? [] ) }
suggestions={ rule.options.map( o => o.label ) }
__experimentalExpandOnFocus={ true }
/>
);
}
Expand Down