From 83fdde57596ce07635a8b1559dc810e902d1fcfe Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 15:49:04 -0300 Subject: [PATCH 01/21] feat: add ActionScheduler group helper methods --- .../reader-activation/class-integrations.php | 61 +++++++++++++++++++ .../integrations/class-test-integrations.php | 31 ++++++++++ 2 files changed, 92 insertions(+) diff --git a/includes/reader-activation/class-integrations.php b/includes/reader-activation/class-integrations.php index 76558804ec..e2c18ccbea 100644 --- a/includes/reader-activation/class-integrations.php +++ b/includes/reader-activation/class-integrations.php @@ -62,6 +62,67 @@ class Integrations { */ const OPTION_NAME = 'newspack_reader_activation_enabled_integrations'; + /** + * ActionScheduler group prefix for integration-specific actions. + */ + const ACTION_GROUP_PREFIX = 'newspack-'; + + /** + * Default ActionScheduler group for non-integration actions. + */ + const DEFAULT_ACTION_GROUP = 'newspack'; + + /** + * Get the ActionScheduler group name for a specific integration. + * + * @param string $integration_id The integration ID. + * + * @return string The group name (e.g., 'newspack-esp'). + */ + public static function get_action_group( $integration_id ) { + return self::ACTION_GROUP_PREFIX . $integration_id; + } + + /** + * Resolve the ActionScheduler group for a data event handler. + * + * Looks up the handler in the internal handler map and returns the + * per-integration group. Falls back to the default 'newspack' group + * if the handler is not registered through an integration. + * + * @param string $class The handler class name. + * @param string $action_name The data event action name. + * + * @return string The group name. + */ + public static function get_action_group_for_handler( $class, $action_name ) { + $key = $class . '::' . $action_name; + if ( isset( self::$handler_map[ $key ] ) ) { + return self::get_action_group( self::$handler_map[ $key ]['integration_id'] ); + } + return self::DEFAULT_ACTION_GROUP; + } + + /** + * Get all ActionScheduler group slugs for Newspack integrations. + * + * Queries the actionscheduler_groups table for slugs matching + * the integration prefix. Used by UI queries to fetch actions + * across all integrations. + * + * @return string[] Array of group slug strings. + */ + public static function get_all_action_groups() { + global $wpdb; + $table = $wpdb->prefix . 'actionscheduler_groups'; + return $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "SELECT slug FROM {$table} WHERE slug LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->esc_like( self::ACTION_GROUP_PREFIX ) . '%' + ) + ); + } + /** * Initialize integrations system. */ diff --git a/tests/unit-tests/integrations/class-test-integrations.php b/tests/unit-tests/integrations/class-test-integrations.php index d639514053..753918af9a 100644 --- a/tests/unit-tests/integrations/class-test-integrations.php +++ b/tests/unit-tests/integrations/class-test-integrations.php @@ -849,4 +849,35 @@ public function pull_contact_data( $user_id ) { $stored = get_user_meta( $user_id, 'newspack_reader_data_item_ajax_field', true ); $this->assertSame( wp_json_encode( 'ajax_value' ), $stored ); } + + /** + * Test get_action_group returns prefixed integration ID. + */ + public function test_get_action_group() { + $this->assertSame( 'newspack-esp', Integrations::get_action_group( 'esp' ) ); + $this->assertSame( 'newspack-my-crm', Integrations::get_action_group( 'my-crm' ) ); + } + + /** + * Test get_action_group_for_handler returns group for registered handler. + */ + public function test_get_action_group_for_handler_returns_group() { + $action_name = 'test_group_event'; + Data_Events::register_action( $action_name ); + + $integration = new Sample_Integration( 'test-id', 'Test' ); + Integrations::register( $integration ); + $integration->test_register_handler( $action_name, 'handle_test_event' ); + + $group = Integrations::get_action_group_for_handler( Sample_Integration::class, $action_name ); + $this->assertSame( 'newspack-test-id', $group ); + } + + /** + * Test get_action_group_for_handler falls back to 'newspack' for unknown handler. + */ + public function test_get_action_group_for_handler_fallback() { + $group = Integrations::get_action_group_for_handler( 'NonExistent', 'unknown_action' ); + $this->assertSame( 'newspack', $group ); + } } From b75e46c46bc04d3eed57c1f7779b396acc663332 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 15:53:17 -0300 Subject: [PATCH 02/21] feat: use per-integration AS group for handler retries --- includes/data-events/class-data-events.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/includes/data-events/class-data-events.php b/includes/data-events/class-data-events.php index 2c7ce08129..e734c3da0a 100644 --- a/includes/data-events/class-data-events.php +++ b/includes/data-events/class-data-events.php @@ -810,11 +810,16 @@ private static function schedule_handler_retry( $handler, $action_name, $timesta 'reason' => $error->getMessage(), ]; + $group = \Newspack\Reader_Activation\Integrations::get_action_group_for_handler( + is_array( $handler ) ? $handler[0] : '', + $action_name + ); + $action_id = \as_schedule_single_action( time() + $backoff_seconds, self::HANDLER_RETRY_HOOK, [ $retry_data ], - 'newspack' + $group ); if ( $action_id ) { From 9be6dec7018ee38c46c6f9550dbb1c82685c47aa Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 15:53:53 -0300 Subject: [PATCH 03/21] feat: use per-integration AS group for sync retries --- includes/reader-activation/sync/class-contact-sync.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/reader-activation/sync/class-contact-sync.php b/includes/reader-activation/sync/class-contact-sync.php index 43b84e0229..a04a8f3dee 100644 --- a/includes/reader-activation/sync/class-contact-sync.php +++ b/includes/reader-activation/sync/class-contact-sync.php @@ -256,7 +256,7 @@ private static function schedule_integration_retry( $integration_id, $contact, $ time() + $backoff_seconds, self::RETRY_HOOK, [ $retry_data ], - 'newspack' + Integrations::get_action_group( $integration_id ) ); static::log( From a5bbe44bf67422129c2f25c3068aead2fcaf5e9e Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 15:53:55 -0300 Subject: [PATCH 04/21] feat: use per-integration AS group for async pulls --- .../reader-activation/integrations/class-contact-pull.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/includes/reader-activation/integrations/class-contact-pull.php b/includes/reader-activation/integrations/class-contact-pull.php index 66a2f6e96a..0c13bae40a 100644 --- a/includes/reader-activation/integrations/class-contact-pull.php +++ b/includes/reader-activation/integrations/class-contact-pull.php @@ -287,14 +287,16 @@ private static function schedule_async_pulls( $user_id, $integrations ) { ], ]; - if ( function_exists( 'as_has_scheduled_action' ) && \as_has_scheduled_action( self::ASYNC_PULL_HOOK, $args, 'newspack' ) ) { + $group = Integrations::get_action_group( $integration->get_id() ); + + if ( function_exists( 'as_has_scheduled_action' ) && \as_has_scheduled_action( self::ASYNC_PULL_HOOK, $args, $group ) ) { continue; } \as_enqueue_async_action( self::ASYNC_PULL_HOOK, $args, - 'newspack' + $group ); } } From 27ba501e8264bcaec0ac0429535cca47db03238c Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 15:53:56 -0300 Subject: [PATCH 05/21] feat: use dedicated AS group for webhook actions --- includes/data-events/class-webhooks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/data-events/class-webhooks.php b/includes/data-events/class-webhooks.php index fee742d302..e4be5524c1 100644 --- a/includes/data-events/class-webhooks.php +++ b/includes/data-events/class-webhooks.php @@ -732,7 +732,7 @@ private static function schedule_request( $request_id, $delay = 1 ) { $time, 'newspack_webhooks_as_process_request', [ $request_id ], - 'newspack', + 'newspack-webhooks', false, self::get_request_priority( $request_id ) ); From c579f104dd594679a59bdbb7972ac33e76713439 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 15:53:58 -0300 Subject: [PATCH 06/21] fix: explicitly set AS group for bulk sync actions --- includes/reader-activation/sync/class-contact-sync-admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/reader-activation/sync/class-contact-sync-admin.php b/includes/reader-activation/sync/class-contact-sync-admin.php index eea91ea48c..d84c049ae7 100644 --- a/includes/reader-activation/sync/class-contact-sync-admin.php +++ b/includes/reader-activation/sync/class-contact-sync-admin.php @@ -136,7 +136,7 @@ public static function handle_bulk_actions( $sendback, $doaction, $items ) { \wp_die( \esc_html__( 'You do not have permission to do that.', 'newspack-plugin' ) ); } foreach ( $items as $user_id ) { - as_schedule_single_action( time(), 'newspack_sync_admin_batch', [ 'user_id' => $user_id ] ); + as_schedule_single_action( time(), 'newspack_sync_admin_batch', [ 'user_id' => $user_id ], 'newspack-sync' ); } $sendback = \add_query_arg( [ From dc11ece9657dcc620495f72cfa86446d66710276 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 15:55:35 -0300 Subject: [PATCH 07/21] feat: add get_scheduled_actions query helper for UI --- .../reader-activation/class-integrations.php | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/includes/reader-activation/class-integrations.php b/includes/reader-activation/class-integrations.php index e2c18ccbea..cba79ea784 100644 --- a/includes/reader-activation/class-integrations.php +++ b/includes/reader-activation/class-integrations.php @@ -123,6 +123,88 @@ public static function get_all_action_groups() { ); } + /** + * Get ActionScheduler actions for Newspack integrations. + * + * Two-step query: resolves group slugs to group IDs, then queries + * the actions table with IN() on the indexed group_id column. + * + * @param array $args { + * Optional. Query arguments. + * + * @type string $integration_id Filter by a single integration ID. + * @type string $status ActionScheduler status (pending, complete, failed, canceled). + * @type int $per_page Number of actions to return. Default 20. + * @type int $offset Offset for pagination. Default 0. + * @type string $orderby Column to order by. Default 'scheduled_date_gmt'. + * @type string $order ASC or DESC. Default 'DESC'. + * } + * + * @return array Array of action row objects. + */ + public static function get_scheduled_actions( $args = [] ) { + global $wpdb; + + $defaults = [ + 'integration_id' => '', + 'status' => '', + 'per_page' => 20, + 'offset' => 0, + 'orderby' => 'scheduled_date_gmt', + 'order' => 'DESC', + ]; + $args = wp_parse_args( $args, $defaults ); + + // Resolve group slugs. + if ( ! empty( $args['integration_id'] ) ) { + $slugs = [ self::get_action_group( $args['integration_id'] ) ]; + } else { + $slugs = self::get_all_action_groups(); + } + + if ( empty( $slugs ) ) { + return []; + } + + // Get group IDs from slugs. + $groups_table = $wpdb->prefix . 'actionscheduler_groups'; + $slug_placeholders = implode( ',', array_fill( 0, count( $slugs ), '%s' ) ); + $group_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "SELECT group_id FROM {$groups_table} WHERE slug IN ({$slug_placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + ...$slugs + ) + ); + + if ( empty( $group_ids ) ) { + return []; + } + + // Query actions. + $actions_table = $wpdb->prefix . 'actionscheduler_actions'; + $id_placeholders = implode( ',', array_fill( 0, count( $group_ids ), '%d' ) ); + + $where = $wpdb->prepare( + "WHERE group_id IN ({$id_placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + ...array_map( 'absint', $group_ids ) + ); + + if ( ! empty( $args['status'] ) ) { + $where .= $wpdb->prepare( ' AND status = %s', $args['status'] ); + } + + $allowed_orderby = [ 'scheduled_date_gmt', 'action_id', 'hook', 'status' ]; + $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'scheduled_date_gmt'; + $order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC'; + + $limit = absint( $args['per_page'] ); + $offset = absint( $args['offset'] ); + + return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + "SELECT * FROM {$actions_table} {$where} ORDER BY {$orderby} {$order} LIMIT {$limit} OFFSET {$offset}" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + ); + } + /** * Initialize integrations system. */ From e3bd4560bec3afbb3a2bfdefb69ee78c829e97c1 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 15:58:04 -0300 Subject: [PATCH 08/21] test: update sync tests to use per-integration AS groups --- tests/unit-tests/reader-activation-sync.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit-tests/reader-activation-sync.php b/tests/unit-tests/reader-activation-sync.php index aa473c39c6..cd8b5ca758 100644 --- a/tests/unit-tests/reader-activation-sync.php +++ b/tests/unit-tests/reader-activation-sync.php @@ -327,7 +327,7 @@ public function test_integration_retry_scheduling() { $pending = as_get_scheduled_actions( [ 'hook' => Contact_Sync::RETRY_HOOK, - 'group' => 'newspack', + 'group' => Integrations::get_action_group( 'failing_mock' ), 'status' => \ActionScheduler_Store::STATUS_PENDING, ], 'ARRAY_A' @@ -377,7 +377,7 @@ public function test_integration_retry_success() { $pending = as_get_scheduled_actions( [ 'hook' => Contact_Sync::RETRY_HOOK, - 'group' => 'newspack', + 'group' => Integrations::get_action_group( 'success_mock' ), 'status' => \ActionScheduler_Store::STATUS_PENDING, ], 'ARRAY_A' @@ -420,7 +420,7 @@ public function test_integration_max_retries() { $pending = as_get_scheduled_actions( [ 'hook' => Contact_Sync::RETRY_HOOK, - 'group' => 'newspack', + 'group' => Integrations::get_action_group( 'max_mock' ), 'status' => \ActionScheduler_Store::STATUS_PENDING, ], 'ARRAY_A' @@ -614,7 +614,7 @@ public function test_integration_retry_invalid_data() { $pending = as_get_scheduled_actions( [ 'hook' => Contact_Sync::RETRY_HOOK, - 'group' => 'newspack', + 'group' => Integrations::get_action_group( 'failing_mock' ), 'status' => \ActionScheduler_Store::STATUS_PENDING, ], 'ARRAY_A' From 95cff003cf0c8896e5a6175417078555e15df452 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 16:02:05 -0300 Subject: [PATCH 09/21] feat: add action group shortcuts to Integration base class --- .../integrations/class-integration.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/includes/reader-activation/integrations/class-integration.php b/includes/reader-activation/integrations/class-integration.php index 54830c5438..cdd39b5248 100644 --- a/includes/reader-activation/integrations/class-integration.php +++ b/includes/reader-activation/integrations/class-integration.php @@ -249,6 +249,27 @@ final public function health_check() { return true; } + /** + * Get the ActionScheduler group name for this integration. + * + * @return string The group name (e.g., 'newspack-esp'). + */ + public function get_action_group() { + return Integrations::get_action_group( $this->id ); + } + + /** + * Get ActionScheduler actions for this integration. + * + * @param array $args Optional. Query arguments (status, per_page, offset, orderby, order). + * + * @return array Array of action row objects. + */ + public function get_scheduled_actions( $args = [] ) { + $args['integration_id'] = $this->id; + return Integrations::get_scheduled_actions( $args ); + } + /** * Get the enabled outgoing metadata fields for this integration. * From 81e22b299c91d93eb6ef73a75f9cd56c714ea308 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 16:04:44 -0300 Subject: [PATCH 10/21] fix: make action methods final --- includes/reader-activation/integrations/class-integration.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/reader-activation/integrations/class-integration.php b/includes/reader-activation/integrations/class-integration.php index cdd39b5248..170f3e0dfb 100644 --- a/includes/reader-activation/integrations/class-integration.php +++ b/includes/reader-activation/integrations/class-integration.php @@ -254,7 +254,7 @@ final public function health_check() { * * @return string The group name (e.g., 'newspack-esp'). */ - public function get_action_group() { + final public function get_action_group() { return Integrations::get_action_group( $this->id ); } @@ -265,7 +265,7 @@ public function get_action_group() { * * @return array Array of action row objects. */ - public function get_scheduled_actions( $args = [] ) { + final public function get_scheduled_actions( $args = [] ) { $args['integration_id'] = $this->id; return Integrations::get_scheduled_actions( $args ); } From 30b73662937f6b261ea0ba414c6ad01d57dbb6f9 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 16:30:34 -0300 Subject: [PATCH 11/21] feat: decouple integrations from data events handler action group name --- includes/data-events/class-data-events.php | 24 ++++++++++++++++++- .../reader-activation/class-integrations.php | 17 +++++++++++++ .../integrations/class-test-integrations.php | 23 ++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/includes/data-events/class-data-events.php b/includes/data-events/class-data-events.php index e734c3da0a..9bd41481f8 100644 --- a/includes/data-events/class-data-events.php +++ b/includes/data-events/class-data-events.php @@ -624,6 +624,28 @@ public static function execute_queued_dispatches() { self::$queued_dispatches = []; } + /** + * Get the ActionScheduler group for a handler. + * + * Returns a filterable default of 'newspack'. Integrations or other + * systems can filter this to assign handlers to specific groups. + * + * @param string $class The handler class name. + * @param string $action_name The data event action name. + * + * @return string The ActionScheduler group name. + */ + public static function get_handler_action_group( $class, $action_name ) { + /** + * Filters the ActionScheduler group for a data event handler. + * + * @param string $group The group name. Default 'newspack'. + * @param string $class The handler class name. + * @param string $action_name The data event action name. + */ + return \apply_filters( 'newspack_data_events_handler_action_group', 'newspack', $class, $action_name ); + } + /** * Dispatch queued events via Action Scheduler. * @@ -810,7 +832,7 @@ private static function schedule_handler_retry( $handler, $action_name, $timesta 'reason' => $error->getMessage(), ]; - $group = \Newspack\Reader_Activation\Integrations::get_action_group_for_handler( + $group = self::get_handler_action_group( is_array( $handler ) ? $handler[0] : '', $action_name ); diff --git a/includes/reader-activation/class-integrations.php b/includes/reader-activation/class-integrations.php index cba79ea784..4e2c78e3fe 100644 --- a/includes/reader-activation/class-integrations.php +++ b/includes/reader-activation/class-integrations.php @@ -103,6 +103,22 @@ public static function get_action_group_for_handler( $class, $action_name ) { return self::DEFAULT_ACTION_GROUP; } + /** + * Filter the ActionScheduler group for a data event handler. + * + * Hooked to 'newspack_data_events_handler_action_group' to assign + * integration-specific groups to handlers registered through integrations. + * + * @param string $group The default group name. + * @param string $class The handler class name. + * @param string $action_name The data event action name. + * + * @return string The filtered group name. + */ + public static function filter_handler_action_group( $group, $class, $action_name ) { + return self::get_action_group_for_handler( $class, $action_name ); + } + /** * Get all ActionScheduler group slugs for Newspack integrations. * @@ -216,6 +232,7 @@ public static function init() { add_action( 'init', [ __CLASS__, 'register_integrations' ], 5 ); add_action( 'init', [ __CLASS__, 'schedule_health_check' ] ); add_action( self::HEALTH_CHECK_CRON_HOOK, [ __CLASS__, 'run_health_checks' ] ); + add_filter( 'newspack_data_events_handler_action_group', [ __CLASS__, 'filter_handler_action_group' ], 10, 3 ); Integrations\Contact_Pull::init(); } diff --git a/tests/unit-tests/integrations/class-test-integrations.php b/tests/unit-tests/integrations/class-test-integrations.php index 753918af9a..d10623307f 100644 --- a/tests/unit-tests/integrations/class-test-integrations.php +++ b/tests/unit-tests/integrations/class-test-integrations.php @@ -880,4 +880,27 @@ public function test_get_action_group_for_handler_fallback() { $group = Integrations::get_action_group_for_handler( 'NonExistent', 'unknown_action' ); $this->assertSame( 'newspack', $group ); } + + /** + * Test Data_Events::get_handler_action_group returns 'newspack' by default. + */ + public function test_data_events_get_handler_action_group_default() { + $group = Data_Events::get_handler_action_group( 'SomeClass', 'some_action' ); + $this->assertSame( 'newspack', $group ); + } + + /** + * Test Data_Events::get_handler_action_group is filtered by Integrations. + */ + public function test_data_events_get_handler_action_group_filtered() { + $action_name = 'test_filtered_group_event'; + Data_Events::register_action( $action_name ); + + $integration = new Sample_Integration( 'filtered-id', 'Filtered' ); + Integrations::register( $integration ); + $integration->test_register_handler( $action_name, 'handle_test_event' ); + + $group = Data_Events::get_handler_action_group( Sample_Integration::class, $action_name ); + $this->assertSame( 'newspack-filtered-id', $group ); + } } From be1aef18fc24c2f11d92c3160aedbbef8b3bc9a5 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 16:36:45 -0300 Subject: [PATCH 12/21] chore: remove default action group constant and update fallback behavior --- includes/reader-activation/class-integrations.php | 9 ++------- .../unit-tests/integrations/class-test-integrations.php | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/includes/reader-activation/class-integrations.php b/includes/reader-activation/class-integrations.php index 4e2c78e3fe..0a1f27a59f 100644 --- a/includes/reader-activation/class-integrations.php +++ b/includes/reader-activation/class-integrations.php @@ -67,11 +67,6 @@ class Integrations { */ const ACTION_GROUP_PREFIX = 'newspack-'; - /** - * Default ActionScheduler group for non-integration actions. - */ - const DEFAULT_ACTION_GROUP = 'newspack'; - /** * Get the ActionScheduler group name for a specific integration. * @@ -100,7 +95,7 @@ public static function get_action_group_for_handler( $class, $action_name ) { if ( isset( self::$handler_map[ $key ] ) ) { return self::get_action_group( self::$handler_map[ $key ]['integration_id'] ); } - return self::DEFAULT_ACTION_GROUP; + return null; } /** @@ -116,7 +111,7 @@ public static function get_action_group_for_handler( $class, $action_name ) { * @return string The filtered group name. */ public static function filter_handler_action_group( $group, $class, $action_name ) { - return self::get_action_group_for_handler( $class, $action_name ); + return self::get_action_group_for_handler( $class, $action_name ) ?? $group; } /** diff --git a/tests/unit-tests/integrations/class-test-integrations.php b/tests/unit-tests/integrations/class-test-integrations.php index d10623307f..97592eb90c 100644 --- a/tests/unit-tests/integrations/class-test-integrations.php +++ b/tests/unit-tests/integrations/class-test-integrations.php @@ -878,7 +878,7 @@ public function test_get_action_group_for_handler_returns_group() { */ public function test_get_action_group_for_handler_fallback() { $group = Integrations::get_action_group_for_handler( 'NonExistent', 'unknown_action' ); - $this->assertSame( 'newspack', $group ); + $this->assertNull( $group ); } /** From bdff70e985abf25bb2722c9f4daa41127838ce09 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 16:38:02 -0300 Subject: [PATCH 13/21] docs: update return type --- includes/reader-activation/class-integrations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/reader-activation/class-integrations.php b/includes/reader-activation/class-integrations.php index 0a1f27a59f..02fc808dc3 100644 --- a/includes/reader-activation/class-integrations.php +++ b/includes/reader-activation/class-integrations.php @@ -88,7 +88,7 @@ public static function get_action_group( $integration_id ) { * @param string $class The handler class name. * @param string $action_name The data event action name. * - * @return string The group name. + * @return string|null The group name or null if the handler is not registered through an integration. */ public static function get_action_group_for_handler( $class, $action_name ) { $key = $class . '::' . $action_name; From 2be5e66b489505c6b18063a6434b1f1269296f75 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 17:49:04 -0300 Subject: [PATCH 14/21] feat: introduce ActionScheduler class and integrate with existing components --- includes/class-action-scheduler.php | 122 ++++++++++++++++++ includes/class-newspack.php | 1 + includes/data-events/class-data-events.php | 4 +- .../reader-activation/class-integrations.php | 81 ++---------- .../integrations/class-integration.php | 2 +- .../integrations/class-test-integrations.php | 8 +- 6 files changed, 138 insertions(+), 80 deletions(-) create mode 100644 includes/class-action-scheduler.php diff --git a/includes/class-action-scheduler.php b/includes/class-action-scheduler.php new file mode 100644 index 0000000000..c6896f4932 --- /dev/null +++ b/includes/class-action-scheduler.php @@ -0,0 +1,122 @@ +prefix . 'actionscheduler_groups'; + return $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "SELECT slug FROM {$table} WHERE slug LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->esc_like( $prefix ) . '%' + ) + ); + } + + /** + * Query ActionScheduler actions by group slugs. + * + * @param array $args { + * Query arguments. + * + * @type string[] $groups Array of group slugs to query. + * @type string $status ActionScheduler status (pending, complete, failed, canceled). + * @type int $per_page Number of actions to return. Default 20. + * @type int $offset Offset for pagination. Default 0. + * @type string $orderby Column to order by. Default 'scheduled_date_gmt'. + * @type string $order ASC or DESC. Default 'DESC'. + * } + * + * @return array Array of action row objects. + */ + public static function get_scheduled_actions( $args = [] ) { + global $wpdb; + + $defaults = [ + 'groups' => [], + 'status' => '', + 'per_page' => 20, + 'offset' => 0, + 'orderby' => 'scheduled_date_gmt', + 'order' => 'DESC', + ]; + $args = wp_parse_args( $args, $defaults ); + + $slugs = $args['groups']; + if ( empty( $slugs ) ) { + $slugs = array_merge( + [ self::DEFAULT_GROUP ], + self::get_groups_by_prefix( self::GROUP_PREFIX ) + ); + } + if ( empty( $slugs ) ) { + return []; + } + + // Get group IDs from slugs. + $groups_table = $wpdb->prefix . 'actionscheduler_groups'; + $slug_placeholders = implode( ',', array_fill( 0, count( $slugs ), '%s' ) ); + $group_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "SELECT group_id FROM {$groups_table} WHERE slug IN ({$slug_placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + ...$slugs + ) + ); + + if ( empty( $group_ids ) ) { + return []; + } + + // Query actions. + $actions_table = $wpdb->prefix . 'actionscheduler_actions'; + $id_placeholders = implode( ',', array_fill( 0, count( $group_ids ), '%d' ) ); + + $where = $wpdb->prepare( + "WHERE group_id IN ({$id_placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + ...array_map( 'absint', $group_ids ) + ); + + if ( ! empty( $args['status'] ) ) { + $where .= $wpdb->prepare( ' AND status = %s', $args['status'] ); + } + + $allowed_orderby = [ 'scheduled_date_gmt', 'action_id', 'hook', 'status' ]; + $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'scheduled_date_gmt'; + $order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC'; + + $limit = absint( $args['per_page'] ); + $offset = absint( $args['offset'] ); + + return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + "SELECT * FROM {$actions_table} {$where} ORDER BY {$orderby} {$order} LIMIT {$limit} OFFSET {$offset}" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + ); + } +} diff --git a/includes/class-newspack.php b/includes/class-newspack.php index bbe294412e..892a32e80a 100644 --- a/includes/class-newspack.php +++ b/includes/class-newspack.php @@ -105,6 +105,7 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/reader-activation/sync/class-woocommerce.php'; include_once NEWSPACK_ABSPATH . 'includes/reader-activation/sync/class-contact-sync.php'; include_once NEWSPACK_ABSPATH . 'includes/reader-activation/sync/class-contact-sync-admin.php'; + include_once NEWSPACK_ABSPATH . 'includes/class-action-scheduler.php'; include_once NEWSPACK_ABSPATH . 'includes/reader-activation/class-integrations.php'; \Newspack\Reader_Activation\Integrations::init(); include_once NEWSPACK_ABSPATH . 'includes/data-events/class-utils.php'; diff --git a/includes/data-events/class-data-events.php b/includes/data-events/class-data-events.php index 9bd41481f8..bc35db66dd 100644 --- a/includes/data-events/class-data-events.php +++ b/includes/data-events/class-data-events.php @@ -643,7 +643,7 @@ public static function get_handler_action_group( $class, $action_name ) { * @param string $class The handler class name. * @param string $action_name The data event action name. */ - return \apply_filters( 'newspack_data_events_handler_action_group', 'newspack', $class, $action_name ); + return \apply_filters( 'newspack_data_events_handler_action_group', Action_Scheduler::DEFAULT_GROUP, $class, $action_name ); } /** @@ -656,7 +656,7 @@ private static function dispatch_via_action_scheduler() { \as_enqueue_async_action( self::DISPATCH_AS_HOOK, [ self::$queued_dispatches ], - 'newspack' + Action_Scheduler::DEFAULT_GROUP ); self::log( sprintf( 'Scheduled %d dispatch(es) via Action Scheduler.', count( self::$queued_dispatches ) ) ); diff --git a/includes/reader-activation/class-integrations.php b/includes/reader-activation/class-integrations.php index 02fc808dc3..fc87999e36 100644 --- a/includes/reader-activation/class-integrations.php +++ b/includes/reader-activation/class-integrations.php @@ -62,20 +62,15 @@ class Integrations { */ const OPTION_NAME = 'newspack_reader_activation_enabled_integrations'; - /** - * ActionScheduler group prefix for integration-specific actions. - */ - const ACTION_GROUP_PREFIX = 'newspack-'; - /** * Get the ActionScheduler group name for a specific integration. * * @param string $integration_id The integration ID. * - * @return string The group name (e.g., 'newspack-esp'). + * @return string The group name (e.g., 'newspack-integration-esp'). */ public static function get_action_group( $integration_id ) { - return self::ACTION_GROUP_PREFIX . $integration_id; + return \Newspack\Action_Scheduler::GROUP_PREFIX . 'integration-' . $integration_id; } /** @@ -117,29 +112,15 @@ public static function filter_handler_action_group( $group, $class, $action_name /** * Get all ActionScheduler group slugs for Newspack integrations. * - * Queries the actionscheduler_groups table for slugs matching - * the integration prefix. Used by UI queries to fetch actions - * across all integrations. - * * @return string[] Array of group slug strings. */ public static function get_all_action_groups() { - global $wpdb; - $table = $wpdb->prefix . 'actionscheduler_groups'; - return $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->prepare( - "SELECT slug FROM {$table} WHERE slug LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->esc_like( self::ACTION_GROUP_PREFIX ) . '%' - ) - ); + return \Newspack\Action_Scheduler::get_groups_by_prefix( \Newspack\Action_Scheduler::GROUP_PREFIX . 'integration-' ); } /** * Get ActionScheduler actions for Newspack integrations. * - * Two-step query: resolves group slugs to group IDs, then queries - * the actions table with IN() on the indexed group_id column. - * * @param array $args { * Optional. Query arguments. * @@ -154,66 +135,20 @@ public static function get_all_action_groups() { * @return array Array of action row objects. */ public static function get_scheduled_actions( $args = [] ) { - global $wpdb; - $defaults = [ 'integration_id' => '', - 'status' => '', - 'per_page' => 20, - 'offset' => 0, - 'orderby' => 'scheduled_date_gmt', - 'order' => 'DESC', ]; $args = wp_parse_args( $args, $defaults ); - // Resolve group slugs. + // Resolve integration_id to group slugs. if ( ! empty( $args['integration_id'] ) ) { - $slugs = [ self::get_action_group( $args['integration_id'] ) ]; + $args['groups'] = [ self::get_action_group( $args['integration_id'] ) ]; } else { - $slugs = self::get_all_action_groups(); - } - - if ( empty( $slugs ) ) { - return []; + $args['groups'] = self::get_all_action_groups(); } + unset( $args['integration_id'] ); - // Get group IDs from slugs. - $groups_table = $wpdb->prefix . 'actionscheduler_groups'; - $slug_placeholders = implode( ',', array_fill( 0, count( $slugs ), '%s' ) ); - $group_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->prepare( - "SELECT group_id FROM {$groups_table} WHERE slug IN ({$slug_placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare - ...$slugs - ) - ); - - if ( empty( $group_ids ) ) { - return []; - } - - // Query actions. - $actions_table = $wpdb->prefix . 'actionscheduler_actions'; - $id_placeholders = implode( ',', array_fill( 0, count( $group_ids ), '%d' ) ); - - $where = $wpdb->prepare( - "WHERE group_id IN ({$id_placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare - ...array_map( 'absint', $group_ids ) - ); - - if ( ! empty( $args['status'] ) ) { - $where .= $wpdb->prepare( ' AND status = %s', $args['status'] ); - } - - $allowed_orderby = [ 'scheduled_date_gmt', 'action_id', 'hook', 'status' ]; - $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'scheduled_date_gmt'; - $order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC'; - - $limit = absint( $args['per_page'] ); - $offset = absint( $args['offset'] ); - - return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - "SELECT * FROM {$actions_table} {$where} ORDER BY {$orderby} {$order} LIMIT {$limit} OFFSET {$offset}" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - ); + return \Newspack\Action_Scheduler::get_scheduled_actions( $args ); } /** diff --git a/includes/reader-activation/integrations/class-integration.php b/includes/reader-activation/integrations/class-integration.php index 170f3e0dfb..60774ffe39 100644 --- a/includes/reader-activation/integrations/class-integration.php +++ b/includes/reader-activation/integrations/class-integration.php @@ -252,7 +252,7 @@ final public function health_check() { /** * Get the ActionScheduler group name for this integration. * - * @return string The group name (e.g., 'newspack-esp'). + * @return string The group name (e.g., 'newspack-integration-esp'). */ final public function get_action_group() { return Integrations::get_action_group( $this->id ); diff --git a/tests/unit-tests/integrations/class-test-integrations.php b/tests/unit-tests/integrations/class-test-integrations.php index 97592eb90c..e391abcf70 100644 --- a/tests/unit-tests/integrations/class-test-integrations.php +++ b/tests/unit-tests/integrations/class-test-integrations.php @@ -854,8 +854,8 @@ public function pull_contact_data( $user_id ) { * Test get_action_group returns prefixed integration ID. */ public function test_get_action_group() { - $this->assertSame( 'newspack-esp', Integrations::get_action_group( 'esp' ) ); - $this->assertSame( 'newspack-my-crm', Integrations::get_action_group( 'my-crm' ) ); + $this->assertSame( 'newspack-integration-esp', Integrations::get_action_group( 'esp' ) ); + $this->assertSame( 'newspack-integration-my-crm', Integrations::get_action_group( 'my-crm' ) ); } /** @@ -870,7 +870,7 @@ public function test_get_action_group_for_handler_returns_group() { $integration->test_register_handler( $action_name, 'handle_test_event' ); $group = Integrations::get_action_group_for_handler( Sample_Integration::class, $action_name ); - $this->assertSame( 'newspack-test-id', $group ); + $this->assertSame( 'newspack-integration-test-id', $group ); } /** @@ -901,6 +901,6 @@ public function test_data_events_get_handler_action_group_filtered() { $integration->test_register_handler( $action_name, 'handle_test_event' ); $group = Data_Events::get_handler_action_group( Sample_Integration::class, $action_name ); - $this->assertSame( 'newspack-filtered-id', $group ); + $this->assertSame( 'newspack-integration-filtered-id', $group ); } } From f7a7c1fc79469140ef6034293c5467c4a99d250c Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 18:22:25 -0300 Subject: [PATCH 15/21] chore: move init up --- .../reader-activation/class-integrations.php | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/includes/reader-activation/class-integrations.php b/includes/reader-activation/class-integrations.php index fc87999e36..ace7abc079 100644 --- a/includes/reader-activation/class-integrations.php +++ b/includes/reader-activation/class-integrations.php @@ -62,6 +62,22 @@ class Integrations { */ const OPTION_NAME = 'newspack_reader_activation_enabled_integrations'; + /** + * Initialize integrations system. + */ + public static function init() { + // Include required files. + require_once __DIR__ . '/integrations/class-integration.php'; + require_once __DIR__ . '/integrations/class-contact-pull.php'; + + add_action( 'init', [ __CLASS__, 'register_integrations' ], 5 ); + add_action( 'init', [ __CLASS__, 'schedule_health_check' ] ); + add_action( self::HEALTH_CHECK_CRON_HOOK, [ __CLASS__, 'run_health_checks' ] ); + add_filter( 'newspack_data_events_handler_action_group', [ __CLASS__, 'filter_handler_action_group' ], 10, 3 ); + + Integrations\Contact_Pull::init(); + } + /** * Get the ActionScheduler group name for a specific integration. * @@ -151,22 +167,6 @@ public static function get_scheduled_actions( $args = [] ) { return \Newspack\Action_Scheduler::get_scheduled_actions( $args ); } - /** - * Initialize integrations system. - */ - public static function init() { - // Include required files. - require_once __DIR__ . '/integrations/class-integration.php'; - require_once __DIR__ . '/integrations/class-contact-pull.php'; - - add_action( 'init', [ __CLASS__, 'register_integrations' ], 5 ); - add_action( 'init', [ __CLASS__, 'schedule_health_check' ] ); - add_action( self::HEALTH_CHECK_CRON_HOOK, [ __CLASS__, 'run_health_checks' ] ); - add_filter( 'newspack_data_events_handler_action_group', [ __CLASS__, 'filter_handler_action_group' ], 10, 3 ); - - Integrations\Contact_Pull::init(); - } - /** * Register integrations. */ From b4e1ec2a72b200a81c8088d892ec1fd187b61cf6 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 18:25:37 -0300 Subject: [PATCH 16/21] feat: return empty array for empty groups in get_scheduled_actions --- includes/reader-activation/class-integrations.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/includes/reader-activation/class-integrations.php b/includes/reader-activation/class-integrations.php index ace7abc079..3322587bd7 100644 --- a/includes/reader-activation/class-integrations.php +++ b/includes/reader-activation/class-integrations.php @@ -164,6 +164,11 @@ public static function get_scheduled_actions( $args = [] ) { } unset( $args['integration_id'] ); + // No groups to query, return empty array. + if ( empty( $args['groups'] ) ) { + return []; + } + return \Newspack\Action_Scheduler::get_scheduled_actions( $args ); } From 1ef00a77379ec383861bdcbda9e689d36399a5ab Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 18:37:23 -0300 Subject: [PATCH 17/21] refactor: optimize get_scheduled_actions query --- includes/class-action-scheduler.php | 55 +++++++++++++---------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/includes/class-action-scheduler.php b/includes/class-action-scheduler.php index c6896f4932..e942691ffc 100644 --- a/includes/class-action-scheduler.php +++ b/includes/class-action-scheduler.php @@ -81,42 +81,37 @@ public static function get_scheduled_actions( $args = [] ) { return []; } - // Get group IDs from slugs. + $allowed_orderby = [ 'scheduled_date_gmt', 'action_id', 'hook', 'status' ]; + $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'scheduled_date_gmt'; + $order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC'; + + $actions_table = $wpdb->prefix . 'actionscheduler_actions'; $groups_table = $wpdb->prefix . 'actionscheduler_groups'; $slug_placeholders = implode( ',', array_fill( 0, count( $slugs ), '%s' ) ); - $group_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->prepare( - "SELECT group_id FROM {$groups_table} WHERE slug IN ({$slug_placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare - ...$slugs - ) - ); - - if ( empty( $group_ids ) ) { - return []; - } - - // Query actions. - $actions_table = $wpdb->prefix . 'actionscheduler_actions'; - $id_placeholders = implode( ',', array_fill( 0, count( $group_ids ), '%d' ) ); - - $where = $wpdb->prepare( - "WHERE group_id IN ({$id_placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare - ...array_map( 'absint', $group_ids ) - ); + $prepare_args = $slugs; + // Build optional status filter. + $status_clause = ''; if ( ! empty( $args['status'] ) ) { - $where .= $wpdb->prepare( ' AND status = %s', $args['status'] ); + $status_clause = 'AND a.status = %s '; + $prepare_args[] = $args['status']; } - $allowed_orderby = [ 'scheduled_date_gmt', 'action_id', 'hook', 'status' ]; - $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'scheduled_date_gmt'; - $order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC'; - - $limit = absint( $args['per_page'] ); - $offset = absint( $args['offset'] ); - - return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - "SELECT * FROM {$actions_table} {$where} ORDER BY {$orderby} {$order} LIMIT {$limit} OFFSET {$offset}" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $prepare_args[] = absint( $args['per_page'] ); + $prepare_args[] = absint( $args['offset'] ); + + // Table names are built from $wpdb->prefix + hardcoded strings, safe for interpolation. + // $orderby and $order are validated via allowlist/ternary above. + $query = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber + "SELECT a.* FROM {$actions_table} a " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "INNER JOIN {$groups_table} g ON a.group_id = g.group_id " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "WHERE g.slug IN ({$slug_placeholders}) " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + "{$status_clause}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "ORDER BY a.{$orderby} {$order} " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + 'LIMIT %d OFFSET %d', + ...$prepare_args ); + + return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared } } From cda43b6440a2bb7e465210b77f98cd22ce32cf70 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 19:03:30 -0300 Subject: [PATCH 18/21] chore: fix docblock --- includes/reader-activation/class-integrations.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/reader-activation/class-integrations.php b/includes/reader-activation/class-integrations.php index 3322587bd7..9f7399e475 100644 --- a/includes/reader-activation/class-integrations.php +++ b/includes/reader-activation/class-integrations.php @@ -93,8 +93,8 @@ public static function get_action_group( $integration_id ) { * Resolve the ActionScheduler group for a data event handler. * * Looks up the handler in the internal handler map and returns the - * per-integration group. Falls back to the default 'newspack' group - * if the handler is not registered through an integration. + * per-integration group, or null if the handler is not registered + * through an integration. * * @param string $class The handler class name. * @param string $action_name The data event action name. From 43cdbc21cfbd2832e461ae41b918318ca78f90ce Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 19:04:27 -0300 Subject: [PATCH 19/21] chore: fix docblock --- tests/unit-tests/integrations/class-test-integrations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit-tests/integrations/class-test-integrations.php b/tests/unit-tests/integrations/class-test-integrations.php index e391abcf70..28066a2853 100644 --- a/tests/unit-tests/integrations/class-test-integrations.php +++ b/tests/unit-tests/integrations/class-test-integrations.php @@ -874,7 +874,7 @@ public function test_get_action_group_for_handler_returns_group() { } /** - * Test get_action_group_for_handler falls back to 'newspack' for unknown handler. + * Test get_action_group_for_handler returns null for unknown handler. */ public function test_get_action_group_for_handler_fallback() { $group = Integrations::get_action_group_for_handler( 'NonExistent', 'unknown_action' ); From a95cf5afc4190831aaa8425393c89beadd2f0e9d Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 19:09:12 -0300 Subject: [PATCH 20/21] feat: add is_available method and handle availability checks --- includes/class-action-scheduler.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/includes/class-action-scheduler.php b/includes/class-action-scheduler.php index e942691ffc..e5d7532ba8 100644 --- a/includes/class-action-scheduler.php +++ b/includes/class-action-scheduler.php @@ -23,6 +23,15 @@ class Action_Scheduler { */ const GROUP_PREFIX = 'newspack-'; + /** + * Whether ActionScheduler is available. + * + * @return bool + */ + public static function is_available() { + return class_exists( 'ActionScheduler' ); + } + /** * Get ActionScheduler group slugs matching a prefix. * @@ -31,6 +40,9 @@ class Action_Scheduler { * @return string[] Array of group slug strings. */ public static function get_groups_by_prefix( $prefix ) { + if ( ! self::is_available() ) { + return []; + } global $wpdb; $table = $wpdb->prefix . 'actionscheduler_groups'; return $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching @@ -58,6 +70,9 @@ public static function get_groups_by_prefix( $prefix ) { * @return array Array of action row objects. */ public static function get_scheduled_actions( $args = [] ) { + if ( ! self::is_available() ) { + return []; + } global $wpdb; $defaults = [ From caa12e5a8b5a595fda3bdb1c7b60f54dfab59c4f Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 11 Mar 2026 19:11:04 -0300 Subject: [PATCH 21/21] refactor: streamline SQL query preparation --- includes/class-action-scheduler.php | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/includes/class-action-scheduler.php b/includes/class-action-scheduler.php index e5d7532ba8..2a934405a4 100644 --- a/includes/class-action-scheduler.php +++ b/includes/class-action-scheduler.php @@ -115,18 +115,14 @@ public static function get_scheduled_actions( $args = [] ) { $prepare_args[] = absint( $args['per_page'] ); $prepare_args[] = absint( $args['offset'] ); - // Table names are built from $wpdb->prefix + hardcoded strings, safe for interpolation. - // $orderby and $order are validated via allowlist/ternary above. - $query = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber - "SELECT a.* FROM {$actions_table} a " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - "INNER JOIN {$groups_table} g ON a.group_id = g.group_id " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - "WHERE g.slug IN ({$slug_placeholders}) " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare - "{$status_clause}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - "ORDER BY a.{$orderby} {$order} " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - 'LIMIT %d OFFSET %d', - ...$prepare_args - ); + // Table names: $wpdb->prefix + hardcoded strings. $orderby/$order: allowlist/ternary validated. + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $sql = "SELECT a.* FROM {$actions_table} a INNER JOIN {$groups_table} g ON a.group_id = g.group_id WHERE g.slug IN ({$slug_placeholders}) {$status_clause}ORDER BY a.{$orderby} {$order} LIMIT %d OFFSET %d"; + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber + $query = $wpdb->prepare( $sql, ...$prepare_args ); - return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + return $wpdb->get_results( $query ); } }