-
Notifications
You must be signed in to change notification settings - Fork 7
feat(content-gate): network product and access control #303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
fd0d561
feat: add Network ID metabox to WooCommerce products
miguelpeixe 2991132
feat: add product Network ID data event and sync
miguelpeixe 6b6f46f
feat: add network-aware access rule evaluation via product Network ID
miguelpeixe 22c80b6
feat: add product-level network purchase restrictions for content gates
miguelpeixe a7da93c
fix: align variable assignment in Access class
miguelpeixe 30e6e9f
fix: cast product ID to string for safe array key lookup
miguelpeixe b26a4f9
fix: break after first network subscription validation error
miguelpeixe c822cb8
fix: restrict network purchase checks to subscription products only
miguelpeixe e07c949
fix: disable autoload for network products option
miguelpeixe b84c44d
fix: allow multiple products to share the same Network ID
miguelpeixe cddca5e
test: add unit tests for Content Gate network access
miguelpeixe f6f9757
chore: standardize array formatting in unit tests for content gate ac…
miguelpeixe 0ed8466
fix: restrict network ID metabox to subscription product types
miguelpeixe 974e3bf
fix: process product updates for hub-originated events
miguelpeixe 11457a6
fix: add WooCommerce active check to product backfiller
miguelpeixe 0068b15
Update includes/hub/stores/event-log-items/class-product-updated.php
miguelpeixe c7e6228
fix: add WooCommerce active check to product_updated listener
miguelpeixe d73b938
fix: use save_post_product hook and guard against missing WooCommerce
miguelpeixe e1731b4
Merge branch 'feat/network-product-id' of https://github.com/Automatt…
miguelpeixe a34d529
fix: handle network ID for product variations
miguelpeixe 561a1b6
fix: lint and revert unneeded subscription change
miguelpeixe 85f663e
fix: remove unnecessary network ID check for variable subscriptions
miguelpeixe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| <?php | ||
| /** | ||
| * Data Backfiller for product_updated events. | ||
| * | ||
| * @package Newspack | ||
| */ | ||
|
|
||
| namespace Newspack_Network\Backfillers; | ||
|
|
||
| use Newspack_Network\Woocommerce\Product_Admin; | ||
| use Newspack_Network\Woocommerce\Events as Woocommerce_Events; | ||
| use WP_CLI; | ||
|
|
||
| /** | ||
| * Backfiller class. | ||
| */ | ||
| class Product_Updated extends Abstract_Backfiller { | ||
|
|
||
| /** | ||
| * Gets the output line about the processed item being processed in verbose mode. | ||
| * | ||
| * @param \Newspack_Network\Incoming_Events\Abstract_Incoming_Event $event The event. | ||
| * | ||
| * @return string | ||
| */ | ||
| protected function get_processed_item_output( $event ) { | ||
| return sprintf( 'Product #%d', $event->get_id() ); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the events to be processed. | ||
| * | ||
| * @return \Newspack_Network\Incoming_Events\Abstract_Incoming_Event[] $events An array of events. | ||
| */ | ||
| public function get_events() { | ||
| if ( ! function_exists( 'wc_get_product' ) ) { | ||
| return []; | ||
| } | ||
|
|
||
| $products = get_posts( | ||
| [ | ||
| 'post_type' => 'product', | ||
| 'post_status' => 'any', | ||
| 'numberposts' => -1, | ||
| 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query | ||
| [ | ||
| 'key' => Product_Admin::NETWORK_ID_META_KEY, | ||
| 'compare' => '!=', | ||
| 'value' => '', | ||
| ], | ||
| ], | ||
| 'date_query' => [ | ||
| 'column' => 'post_modified_gmt', | ||
| 'after' => $this->start, | ||
| 'before' => $this->end, | ||
| 'inclusive' => true, | ||
| ], | ||
| ] | ||
| ); | ||
|
|
||
| $this->maybe_initialize_progress_bar( 'Processing products', count( $products ) ); | ||
|
|
||
| $events = []; | ||
| WP_CLI::line( '' ); | ||
| WP_CLI::line( sprintf( 'Found %s product(s) with Network IDs eligible for sync.', count( $products ) ) ); | ||
| WP_CLI::line( '' ); | ||
|
|
||
| foreach ( $products as $product ) { | ||
| $product_data = Woocommerce_Events::product_updated( $product->ID ); | ||
| if ( ! $product_data ) { | ||
| continue; | ||
| } | ||
| $timestamp = strtotime( $product->post_modified_gmt ); | ||
| $events[] = new \Newspack_Network\Incoming_Events\Product_Updated( get_bloginfo( 'url' ), $product_data, $timestamp ); | ||
| } | ||
|
|
||
| return $events; | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| <?php | ||
| /** | ||
| * Newspack Network Content Gate Access integration. | ||
| * | ||
| * Hooks into newspack-plugin's access rules to grant access | ||
| * when a user has an active subscription on another network site | ||
| * for a product with a matching Network ID. | ||
| * | ||
| * @package Newspack | ||
| */ | ||
|
|
||
| namespace Newspack_Network\Content_Gate; | ||
|
|
||
| use Newspack_Network\Incoming_Events\Subscription_Changed; | ||
| use Newspack_Network\Incoming_Events\Product_Updated; | ||
| use Newspack_Network\Woocommerce\Product_Admin; | ||
|
|
||
| /** | ||
| * Class to handle network-aware content gate access. | ||
| */ | ||
| class Access { | ||
|
|
||
| /** | ||
| * Initializer. | ||
| */ | ||
| public static function init() { | ||
| add_filter( 'newspack_access_rules_has_active_subscription', [ __CLASS__, 'check_network_subscriptions' ], 10, 3 ); | ||
| } | ||
|
|
||
| /** | ||
| * Check if the user has an active subscription on another network site | ||
| * for a product with a matching Network ID. | ||
| * | ||
| * @param bool $has_subscription Whether the user already has an active subscription (from local checks). | ||
| * @param int $user_id User ID. | ||
| * @param array $product_ids Required product IDs (local). | ||
| * @return bool | ||
| */ | ||
| public static function check_network_subscriptions( $has_subscription, $user_id, $product_ids ) { | ||
| // If local check already passed, no need to check network. | ||
| if ( $has_subscription ) { | ||
| return true; | ||
| } | ||
|
|
||
| // If no products specified, we can't match by Network ID. | ||
| if ( empty( $product_ids ) ) { | ||
| return $has_subscription; | ||
| } | ||
|
|
||
| // Get Network IDs for the required local products. | ||
| $network_ids = self::get_network_ids_for_products( $product_ids ); | ||
| if ( empty( $network_ids ) ) { | ||
| return $has_subscription; | ||
| } | ||
|
|
||
| // Get user's network subscriptions. | ||
| $network_subscriptions = self::get_user_network_active_subscriptions( $user_id ); | ||
| if ( empty( $network_subscriptions ) ) { | ||
| return $has_subscription; | ||
| } | ||
|
|
||
| // Get synced product data from all network sites. | ||
| $network_products = get_option( Product_Updated::OPTION_NAME, [] ); | ||
|
|
||
| // Check if any network subscription has a product with a matching Network ID. | ||
| foreach ( $network_subscriptions as $site => $subscriptions ) { | ||
| $site_products = $network_products[ $site ] ?? []; | ||
| foreach ( $subscriptions as $subscription ) { | ||
| foreach ( $subscription['products'] as $product ) { | ||
| // Cast to string to handle int/string key mismatch from JSON round-tripping. | ||
| $product_id = (string) $product['id']; | ||
| // Look up this product's Network ID from synced data. | ||
| $remote_network_id = $site_products[ $product_id ]['network_id'] ?? ''; | ||
| if ( ! empty( $remote_network_id ) && in_array( $remote_network_id, $network_ids, true ) ) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return $has_subscription; | ||
| } | ||
|
|
||
| /** | ||
| * Get Network IDs for the given local product IDs. | ||
| * | ||
| * @param array $product_ids Local product IDs. | ||
| * @return array Array of Network IDs (non-empty values only). | ||
| */ | ||
| public static function get_network_ids_for_products( $product_ids ) { | ||
| $network_ids = []; | ||
| foreach ( $product_ids as $product_id ) { | ||
| $network_id = Product_Admin::get_network_id( $product_id ); | ||
| if ( ! empty( $network_id ) ) { | ||
| $network_ids[] = $network_id; | ||
| } | ||
| } | ||
| return array_unique( $network_ids ); | ||
| } | ||
|
|
||
| /** | ||
| * Check if a user has an active network subscription for a given Network ID. | ||
| * | ||
| * @param int $user_id The user ID. | ||
| * @param string $network_id The product Network ID to match. | ||
| * @return array|false Array with 'site' and 'subscription' keys if found, false otherwise. | ||
| */ | ||
| public static function user_has_active_network_subscription_for_network_id( $user_id, $network_id ) { | ||
| $network_subscriptions = self::get_user_network_active_subscriptions( $user_id ); | ||
| if ( empty( $network_subscriptions ) ) { | ||
| return false; | ||
| } | ||
|
|
||
| $network_products = get_option( Product_Updated::OPTION_NAME, [] ); | ||
|
|
||
| foreach ( $network_subscriptions as $site => $subscriptions ) { | ||
| $site_products = $network_products[ $site ] ?? []; | ||
| foreach ( $subscriptions as $subscription ) { | ||
| foreach ( $subscription['products'] as $product ) { | ||
| // Cast to string to handle int/string key mismatch from JSON round-tripping. | ||
| $product_id = (string) $product['id']; | ||
| $remote_network_id = $site_products[ $product_id ]['network_id'] ?? ''; | ||
| if ( ! empty( $remote_network_id ) && $remote_network_id === $network_id ) { | ||
| return [ | ||
| 'site' => $site, | ||
| 'subscription' => $subscription, | ||
| ]; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Gets all active subscriptions for a user across all network sites. | ||
| * | ||
| * @param int $user_id The user ID. | ||
| * @return array An array with the site as key and an array of subscriptions as value. | ||
| */ | ||
| public static function get_user_network_active_subscriptions( $user_id ) { | ||
| $meta = get_user_meta( $user_id, Subscription_Changed::USER_SUBSCRIPTIONS_META_KEY, true ); | ||
| if ( ! $meta ) { | ||
| return []; | ||
| } | ||
|
|
||
| $returned_subs = []; | ||
|
|
||
| foreach ( $meta as $site => $subscriptions ) { | ||
| $returned_subs[ $site ] = array_filter( | ||
| $subscriptions, | ||
| function ( $sub ) { | ||
| return in_array( $sub['status'], [ 'active', 'pending-cancel' ], true ); | ||
| } | ||
| ); | ||
| if ( empty( $returned_subs[ $site ] ) ) { | ||
| unset( $returned_subs[ $site ] ); | ||
| } | ||
| } | ||
|
|
||
| return $returned_subs; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.