File: /home/ukqcurpj/www/wp-content/plugins/paid-memberships-pro/classes/class-pmpro-action-scheduler.php
<?php
/**
*
* Paid Memberships Pro — Action Scheduler (AS).
*
* This class provides methods to schedule, manage, and execute tasks asynchronously, and is both a replacement
* for older wp-cron based tasks and a more efficient and performant way to handle background and asynchronous tasks in WordPress.
*
* Keep in mind that: tasks are always handled asynchronously;
* Queued tasks are not guaranteed to run immediately;
* Action Scheduler always processes tasks in batches with potentially different sizes.
*
* @package pmpro_plugin
* @subpackage classes
* @since 3.5
*/
class PMPro_Action_Scheduler {
/**
* The single instance of the class.
*
* @var PMPro_Action_Scheduler
* @access protected
* @since 3.5
*/
protected static $instance = null;
/**
* The default queue threshold for async tasks.
* This is the maximum number of async tasks that can be queued before a delay is added to incoming tasks.
* This prevent overly taxing a server with task running over longer periods of time.
* The default is 500 tasks.
*/
public static $pmpro_as_queue_limit = 500;
/**
* The minimum version of Action Scheduler required for PMPro.
*/
private static $required_as_version = '3.9';
/**
* Get the queue limit for async tasks.
*
* @return int The maximum number of tasks that can be queued before a delay is added.
* @access public
* @since 3.5
*/
public static function get_pmpro_as_queue_limit() {
/**
* Filter the queue limit for async tasks.
*
* @param int $queue_limit The default queue limit.
*/
return apply_filters( 'pmpro_action_scheduler_queue_limit', self::$pmpro_as_queue_limit );
}
/**
* Constructor
*/
public function __construct() {
// Check if Action Scheduler is installed and activated.
if ( ! class_exists( \ActionScheduler::class ) ) {
return;
}
// Track library conflicts if detected.
self::track_library_conflicts();
// Show admin notice if Action Scheduler is out of date.
add_action( 'admin_notices', array( $this, 'show_outdated_notice' ) );
// Add custom hooks for quarter-hourly, hourly, daily and weekly tasks.
add_action( 'action_scheduler_init', array( $this, 'add_recurring_hooks' ) );
// Remove recurring hooks if PMPro is deactivated.
add_action( 'pmpro_deactivation', array( $this, 'remove_recurring_hooks' ) );
// Handle the monthly
add_action( 'pmpro_trigger_monthly', array( $this, 'handle_monthly_tasks' ) );
// Add dummy callbacks for scheduled tasks that may not have a handler.
add_action( 'action_scheduler_init', array( $this, 'add_dummy_callbacks' ) );
// Add late filters to modify the AS batch size and time limit. We effectively control the batch size and time limit,
// which is intentional since some of our tasks can be heavy and we want to ensure they run smoothly and don't slow down a site.
add_filter( 'action_scheduler_queue_runner_batch_size', array( $this, 'modify_batch_size' ), 999 );
add_filter( 'action_scheduler_queue_runner_time_limit', array( $this, 'modify_batch_time_limit' ), 999 );
// If PMPro is paused or the halt() method was called, don't allow async requests.
add_filter( 'action_scheduler_allow_async_request_runner', array( $this, 'action_scheduler_allow_async_request_runner' ), 999 );
add_action( 'admin_notices', array( $this, 'show_async_requests_paused_notice' ) );
}
/**
* Load the Action Scheduler library.
*
* @since 3.5
*/
private static function track_library_conflicts() {
// Get the version of Action Scheduler that is currently loaded and the plugin file it was loaded from.
$action_scheduler_version = ActionScheduler_Versions::instance()->latest_version(); // This is only available after plugins_loaded priority 0 which is why we do this here.
$previously_loaded_class = self::get_active_source_path();
// If we loaded Action Scheduler, this will do nothing.
pmpro_track_library_conflict( 'action-scheduler', $previously_loaded_class, $action_scheduler_version );
}
/**
* Show an admin notice if Action Scheduler is out of date.
*
* @since 3.5
*/
public static function show_outdated_notice() {
// Only show on PMPro admin pages.
if ( empty( $_REQUEST['page'] ) || strpos( $_REQUEST['page'], 'pmpro' ) === false ) {
return;
}
// Get the loaded version of Action Scheduler.
$action_scheduler_version = ActionScheduler_Versions::instance()->latest_version();
// If the version is current, we don't need to show the notice.
if ( version_compare( $action_scheduler_version, self::$required_as_version, '>=' ) ) {
return;
}
// If the version is outdated, show the notice.
?>
<div class="notice notice-error">
<p><strong><?php esc_html_e( 'Important Notice: An Outdated Version of Action Scheduler is Loaded', 'paid-memberships-pro' ); ?></strong></p>
<p>
<?php
echo wp_kses_post(
sprintf(
__( 'An outdated version of Action Scheduler (version %1$s) is being loaded by %2$s which may affect Paid Memberships Pro functionalilty on this website.', 'paid-memberships-pro' ),
$action_scheduler_version,
'<code>' . self::get_active_source_path() . '</code>'
)
)
?>
</p>
<p>
<?php
echo wp_kses_post(
sprintf(
__( 'For more information, please <a href="%s" target="_blank">review our documentation</a> on how to resolve library conflicts.', 'paid-memberships-pro' ),
'https://www.paidmembershipspro.com/fix-library-conflict/?utm_source=pmpro&utm_medium=plugin&utm_campaign=blog&utm_content=action-scheduler-plugin-conflict'
)
);
?>
</p>
</div>
<?php
}
/**
* Get the single instance of the class.
*
* @access public
* @since 3.5
* @return PMPro_Action_Scheduler
*/
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Prevent the instance from being cloned.
*
* @access public
* @since 3.5
* @return void
* @throws Exception If the instance is cloned.
*/
public function __clone() {
throw new Exception( esc_html__( 'Action Scheduler instance cannot be cloned', 'paid-memberships-pro' ) );
}
/**
* Prevent the instance from being unserialized.
*
* @access public
* @since 3.5
* @return void
* @throws Exception If the instance is unserialized.
*/
public function __wakeup() {
throw new Exception( esc_html__( 'Action Scheduler instance cannot be unserialized', 'paid-memberships-pro' ) );
}
/**
* Check if AS has an existing task in the pending or completed queue
*
* @access public
* @since 3.5
* @param string $hook The hook name for the task.
* @param array $args Arguments passed to the task hook.
* @param string $group The group the task belongs to.
* @param boolean $pending True to check pending tasks, false to check completed tasks.
* @param string|boolean $timestamp Optional timestamp to compare with task date.
* @return bool True if an existing task is found, false otherwise.
*/
public function has_existing_task( $hook, $args = array(), $group = '', $pending = true, $timestamp = false ) {
$status = $pending ? ActionScheduler_Store::STATUS_PENDING : ActionScheduler_Store::STATUS_COMPLETE;
$query_args = array(
'hook' => $hook,
'args' => $args,
'group' => $group,
'status' => $status,
);
// If we are looking for a task that's not in the future, we must provide a timestamp.
if ( ! $pending && $timestamp ) {
$query_args['date'] = $timestamp;
$query_args['date_compare'] = '>=';
} elseif ( ! $pending ) {
$query_args['date'] = current_time( 'mysql' );
$query_args['date_compare'] = '<=';
}
$results = as_get_scheduled_actions( $query_args );
return ! empty( $results );
}
/**
* Get the count of AS items currently queued for a group
*
* @access public
* @since 3.5
* @param string $group The task group name.
* @return int The number of tasks in the queue.
*/
public static function count_existing_tasks_for_group( $group ) {
$search_args = array(
'group' => $group,
'status' => ActionScheduler_Store::STATUS_PENDING,
);
return count( as_get_scheduled_actions( $search_args ) );
}
/**
* Check if a task exists in the queue of tasks before adding it; and maybe add a queue delay.
*
* @access public
* @since 3.5
* @param string $hook The hook for the task.
* @param mixed $args The data being passed to the task hook.
* @param string $group The group this task should be assigned to. Default is 'pmpro_async_tasks'.
* @param int|string|null $timestamp An as_strtotime datetime or human-readable string.
* @param boolean $run_asap Whether to bypass the count delay and run async asap.
* @return void
*/
public function maybe_add_task( $hook, $args, $group = 'pmpro_async_tasks', $timestamp = null, $run_asap = false ) {
// Convert human-readable string to timestamp if needed.
if ( ! is_null( $timestamp ) && ! is_int( $timestamp ) ) {
$converted = self::as_strtotime( $timestamp );
if ( $converted !== false ) {
$timestamp = $converted;
} else {
$timestamp = null;
}
}
// Check for a task in the queue matching this task exactly (hook, args, group).
if ( $this->has_existing_task( $hook, $args, $group ) ) {
return;
}
// We're going to add a timestamp
if ( null === $timestamp && false === $run_asap ) {
$task_count = self::count_existing_tasks_for_group( $group );
// If we have more than self::get_pmpro_as_queue() tasks in the queue, add a delay to the task.
// This will space out tasks and prevent overwhelming the server if the tasking is heavy.
if ( $task_count > self::get_pmpro_as_queue_limit() ) {
$delay = $task_count - self::get_pmpro_as_queue_limit();
$timestamp = self::as_strtotime( "+{$delay} seconds" );
} else {
// Less than self::get_pmpro_as_queue() tasks in the queue, queue this task immediately.
$timestamp = self::as_strtotime( 'now' );
}
}
// If we are running this task immediately, we need to use the async queue.
if ( $run_asap ) {
return as_enqueue_async_action( $hook, $args, $group );
}
// If we have a timestamp, we will schedule the task for the time provided.
if ( null !== $timestamp ) {
return as_schedule_single_action( $timestamp, $hook, $args, $group );
}
}
/**
* Maybe add a recurring task (if not exists)
*
* @access public
* @since 3.5
* @param string $hook The hook for the task.
* @param int|null $interval_in_seconds The interval in seconds this recurring task should run.
* @param int|null $first_run_datetime An as_strtotime datetime in the future this task should first run.
* @param string $group The group this task should be assigned to.
* @return void
*/
public function maybe_add_recurring_task( $hook, $interval_in_seconds = null, $first_run_datetime = null, $group = 'pmpro_recurring_tasks' ) {
if ( ! as_next_scheduled_action( $hook, array(), $group ) ) {
// Make sure first run datetime has been set.
$first_run_datetime = $first_run_datetime ?: self::as_strtotime( 'now +5 minutes' );
// Schedule this task in the future, and make it recurring.
if ( ! empty( $interval_in_seconds ) ) {
return as_schedule_recurring_action( $first_run_datetime, $interval_in_seconds, $hook, array(), $group, true );
} else {
throw new WP_Error( 'pmpro_action_scheduler_warning', esc_html__( 'An interval is required to queue an Action Scheduler recurring task.', 'paid-memberships-pro' ) );
}
}
}
/**
* Add the ability to store custom messages with Action Scheduler's logs when running a task.
*
* @access public
* @since 3.5
* @param string $hook The hook name.
* @param string $status The status of the action.
* @param string $message The log message to add.
* @return void
*/
public static function add_task_log( $hook, $status, $message = '' ) {
if ( empty( $hook ) || empty( $message ) ) {
// If we don't have a message or hook, we can't log anything.
return;
}
// We have to use ActionScheduler::store()->find_action() to get the action ID.
// This is because the action ID is not available in the context of the task.
$action_id = ActionScheduler::store()->find_action( $hook, array( 'status' => ActionScheduler_Store::STATUS_RUNNING ) );
if ( empty( $action_id ) ) {
// If we don't have an action ID, we can't log anything.
return;
}
// Log the message with the action ID when running a task.
ActionScheduler::logger()->log( $action_id, $message );
}
/**
* List all tasks in the queue for a given group or hook.
*
* At least one of $group or $hook must be provided.
*
* @access public
* @since 3.5.x
* @param string|null $group The task group name.
* @param string|null $hook The task hook name.
* @return array The list of tasks in the queue.
* @throws WP_Error If neither $group nor $hook is provided.
*/
public static function list_tasks( $group = null, $hook = null ) {
$args = array();
if ( ! empty( $group ) ) {
$args['group'] = $group;
}
if ( ! empty( $hook ) ) {
$args['hook'] = $hook;
}
if ( empty( $args ) ) {
// If neither group nor hook is provided, we cannot list tasks.
throw new WP_Error( 'pmpro_action_scheduler_warning', esc_html__( 'You must provide either a group or a hook to list tasks.', 'paid-memberships-pro' ) );
}
return as_get_scheduled_actions( $args );
}
/**
* Clear all tasks in the queue matching the given hook, args, and/or group.
*
* @access public
* @since 3.5
* @param string|null $hook The hook name for the task.
* @param array $args The arguments for the task.
* @param string|null $group The group the task belongs to.
* @param string $status The status of the task to delete (default: 'completed').
* @return int Number of tasks deleted.
* @throws WP_Error If no parameters are provided.
*/
public static function remove_actions( $hook = null, $args = array(), $group = null, $status = 'completed' ) {
// For cases where we're filtering by args or group only
$search_args = array(
'status' => self::get_as_status( $status ),
'per_page' => 100, // Process in batches
);
if ( ! empty( $hook ) ) {
$search_args['hook'] = $hook;
}
if ( ! empty( $args ) ) {
$search_args['args'] = $args;
}
if ( ! empty( $group ) ) {
$search_args['group'] = $group;
}
$count = 0;
$deleted_something = true;
// Continue deleting in batches until no more actions are found
while ( $deleted_something ) {
$deleted_something = false;
$actions = as_get_scheduled_actions( $search_args, 'ids' );
if ( ! empty( $actions ) && is_array( $actions ) ) {
foreach ( $actions as $action_id ) {
ActionScheduler::store()->delete_action( $action_id );
++$count;
$deleted_something = true;
}
} else {
break;
}
}
return $count;
}
/**
* Registers PMPro recurring scheduled hooks and ensures they are scheduled if not already.
*
* The following hooks are scheduled via Action Scheduler, using the 'pmpro_recurring_tasks' group:
* - pmpro_schedule_quarter_hourly: Runs every 15 minutes.
* - pmpro_schedule_hourly: Runs every hour.
* - pmpro_schedule_daily: Runs daily at 10:30am.
* - pmpro_schedule_weekly: Runs every Sunday at 8:00am.
* - pmpro_trigger_monthly: Runs on the first day of each month at 8:00am.
*
* Filters:
* - `pmpro_action_scheduler_recurring_schedules`: Modify or extend the recurring schedule definitions.
*
* Notes:
* - Each hook gets a dummy callback (see `add_dummy_callbacks()`) to prevent logging failures for unhandled actions.
* - The `pmpro_trigger_monthly` task is handled by `handle_monthly_tasks()` and automatically reschedules itself.
*
* @access public
* @since 3.5
*/
public function add_recurring_hooks() {
$schedules = apply_filters(
'pmpro_action_scheduler_recurring_schedules',
array(
array(
'hook' => 'pmpro_schedule_quarter_hourly',
'interval' => 15 * MINUTE_IN_SECONDS,
'start' => self::as_strtotime( 'now +15 minutes' ),
),
array(
'hook' => 'pmpro_schedule_hourly',
'interval' => HOUR_IN_SECONDS,
'start' => self::as_strtotime( 'now +1 hour' ),
),
array(
'hook' => 'pmpro_schedule_daily',
'interval' => DAY_IN_SECONDS,
'start' => self::as_strtotime( 'tomorrow 10:30am' ),
),
array(
'hook' => 'pmpro_schedule_weekly',
'interval' => WEEK_IN_SECONDS,
'start' => self::as_strtotime( 'next sunday 8:00am' ),
),
)
);
foreach ( $schedules as $schedule ) {
if ( ! empty( $schedule['hook'] ) && ! empty( $schedule['interval'] ) ) {
$this->maybe_add_recurring_task(
$schedule['hook'],
$schedule['interval'],
$schedule['start'],
'pmpro_recurring_tasks'
);
}
}
// Schedule the first instance of our monthly action if none exists.
if ( ! $this->has_existing_task( 'pmpro_trigger_monthly', array(), 'pmpro_recurring_tasks' ) ) {
$first = self::as_strtotime( 'first day of next month 8:00am' );
as_schedule_single_action( $first, 'pmpro_trigger_monthly', array(), 'pmpro_recurring_tasks' );
}
}
/**
* Remove all recurring hooks from Action Scheduler.
*
* This method unschedules all recurring tasks that were registered by PMPro.
*
* @access public
* @since 3.5.3
*/
public function remove_recurring_hooks() {
// Find all hooks that belong to the group 'pmpro_recurring_tasks'.
$hooks = as_get_scheduled_actions(
array(
'group' => 'pmpro_recurring_tasks',
'status' => ActionScheduler_Store::STATUS_PENDING,
),
ARRAY_A
);
// If no hooks are found, we can exit early.
if ( empty( $hooks ) ) {
return;
}
foreach ( $hooks as $hook ) {
as_unschedule_all_actions( $hook['hook'], array(), 'pmpro_recurring_tasks' );
}
}
/**
* Add dummy callbacks for our scheduled tasks to prevent AS from logging failed actions.
*
* This ensures that scheduled hooks without real handlers do not trigger "no callback" errors in the Action Scheduler log.
*
* NOTE: If you are using a custom schedule, you will need to add a dummy callback for it here.
*
* Filters:
* - `pmpro_action_scheduler_recurring_schedules`: Allows you to modify or extend the list of recurring hooks needing dummy callbacks.
*
* @access public
* @since 3.5
* @return void
*/
public function add_dummy_callbacks() {
$schedules = apply_filters(
'pmpro_action_scheduler_recurring_schedules',
array(
array( 'hook' => 'pmpro_schedule_quarter_hourly' ),
array( 'hook' => 'pmpro_schedule_hourly' ),
array( 'hook' => 'pmpro_schedule_daily' ),
array( 'hook' => 'pmpro_schedule_weekly' ),
)
);
// Add dummy callbacks for the scheduled tasks.
// This is to prevent PHP notices when the tasks are run.
foreach ( $schedules as $schedule ) {
if ( ! empty( $schedule['hook'] ) ) {
add_action(
$schedule['hook'],
function () use ( $schedule ) {
return;
}
);
}
}
// Ensure our custom monthly task also has a dummy callback.
add_action(
'pmpro_trigger_monthly',
function () {
return;
}
);
}
/**
* Handle the monthly schedule and reschedule the next instance.
*
* @access public
* @since 3.5
* @return void
*/
public function handle_monthly_tasks() {
// Run any logic needed for monthly jobs here.
do_action( 'pmpro_schedule_monthly' );
// Schedule the next run for the first day of next month.
$next_month = self::as_strtotime( 'first day of next month 8:00am' );
as_schedule_single_action( $next_month, 'pmpro_trigger_monthly', array(), 'pmpro_recurring_tasks' );
}
/**
* Action scheduler claims a batch of actions to process in each request. It keeps the batch
* fairly small (by default, 25) in order to prevent errors, like memory exhaustion.
*
* This method increases or decreases it so that more/less actions are processed in each queue, which speeds up the
* overall queue processing time due to latency in requests and the minimum 1 minute between each
* queue being processed.
*
* You can also set this to a different value using the pmpro_action_scheduler_batch_size filter.
*
* For more details on Action Scheduler batch sizes, see: https://actionscheduler.org/perf/#increasing-batch-size
*
* @access public
* @since 3.5
* @param int $batch_size The current batch size.
* @return int Modified batch size.
*/
public function modify_batch_size( $batch_size ) {
/**
* Public filter for adjusting the batch size in Action Scheduler.
*
* @param int $batch_size The batch size.
*/
$batch_size = apply_filters( 'pmpro_action_scheduler_batch_size', $batch_size );
return $batch_size;
}
/**
* Modify the default time limit for processing a batch of actions.
*
* Action Scheduler provides a default of 30 seconds in which to process actions.
*
* You can also set this to a different value using the pmpro_action_scheduler_time_limit_seconds filter.
*
* For more details on the Action Scheduler time limit, see: https://actionscheduler.org/perf/#increasing-time-limit
*
* @access public
* @since 3.5
* @param int $time_limit The current time limit in seconds.
* @return int Modified time limit in seconds.
*/
public function modify_batch_time_limit( $time_limit ) {
/**
* Public filter for adjusting the time limit for Action Scheduler batches.
*
* @param int $time_limit The time limit in seconds.
*/
$time_limit = apply_filters( 'pmpro_action_scheduler_time_limit_seconds', $time_limit );
return $time_limit;
}
/**
* Halt Action Scheduler if PMPro is paused or if our halt() method was called.
*
* @access public
* @since 3.5.5
*
* @param bool $allow Whether to allow Action Scheduler to run asynchronously.
* @return bool
*/
public function action_scheduler_allow_async_request_runner( $allow ) {
// If PMPro is paused, don't allow async requests.
if ( pmpro_is_paused() ) {
return false;
}
// If the halt() method was called, don't allow async requests.
if ( get_option( 'pmpro_as_halted', false ) ) {
return false;
}
return $allow;
}
/**
* Show a notice on the Action Scheduler page if PMPro is preventing async requests.
*
* @access public
* @since 3.5.5
*/
public function show_async_requests_paused_notice() {
// If this is not the action-scheduler page in the admin area, bail.
if ( ! is_admin() || empty( $_REQUEST['page'] ) || 'action-scheduler' !== $_REQUEST['page'] ) {
return;
}
if ( pmpro_is_paused() ) {
$message = __( 'Paid Memberships Pro services are currently paused. Scheduled actions will not be run automatically until services are resumed.', 'paid-memberships-pro' );
} elseif ( get_option( 'pmpro_as_halted', false ) ) {
$message = __( 'Paid Memberships Pro has temporarily halted scheduled actions while additional tasks are added. Actions will resume automatically once this process is complete.', 'paid-memberships-pro' );
}
if ( ! empty( $message ) ) {
echo '<div class="notice notice-warning"><p>' . esc_html( $message ) . '</p></div>';
}
}
/**
* Map text $status to the Action Scheduler status.
*
* Accepts fuzzy matches for the following statuses: queue, queued, waiting, pending | error, failed | in-progress, running, processing | completed, complete, done.
*
* @access private
* @since 3.5
* @param string $status The text status to map to AS status.
* @return string The mapped status.
*/
public static function get_as_status( $status ) {
// Map a text $status to the Action Scheduler status.
switch ( $status ) {
case 'queue':
case 'queued':
case 'waiting':
case 'pending':
$status = ActionScheduler_Store::STATUS_PENDING;
break;
case 'error':
case 'failed':
$status = ActionScheduler_Store::STATUS_FAILED;
break;
case 'in-progress':
case 'running':
case 'processing':
$status = ActionScheduler_Store::STATUS_RUNNING;
break;
case 'completed':
case 'complete':
case 'done':
default:
$status = ActionScheduler_Store::STATUS_COMPLETE;
}
return $status;
}
/**
* Convert a relative or absolute time string into a timestamp using the site's timezone.
*
* @access private
* @since 3.5
* @param string $time_string A string like "+10 minutes" or "tomorrow 5pm".
* @return int UTC timestamp adjusted to the WordPress timezone.
*/
private static function as_strtotime( $time_string ) {
$timezone = wp_timezone();
$datetime = new DateTimeImmutable( 'now', $timezone );
$modified = $datetime->modify( $time_string );
return $modified->getTimestamp();
}
/**
* Halt Action Scheduler.
*
* @access public
* @since 3.5
* @return void
*/
public static function halt() {
update_option( 'pmpro_as_halted', true );
}
/**
* Resume Action Scheduler.
*
* @access public
* @since 3.5
* @return void
*/
public static function resume() {
update_option( 'pmpro_as_halted', false );
}
/**
* Get the active source path for Action Scheduler.
*
* @access private
* @since 3.5
* @return string The path to the active source of Action Scheduler.
*/
public static function get_active_source_path() {
if ( class_exists( '\ActionScheduler_SystemInformation' ) ) {
return ActionScheduler_SystemInformation::active_source_path();
} else {
// This was deprecated in Action Scheduler v3.9,2 when the SystemInformation class was introduced.
return ActionScheduler_Versions::instance()->active_source_path();
}
}
/**
* Clear scheduled recurring tasks.
*
* This method is used to clear any previously scheduled recurring tasks, typically when the plugin is updated.
*
* @access public
* @since 3.5.3
*/
public static function clear_recurring_tasks() {
self::remove_actions( null, array(), 'pmpro_recurring_tasks', 'pending' );
}
/**
* Check if Action Scheduler has any health issues.
*
* @since 3.5.3
* @return array Array of issues found, empty if no issues.
*/
public static function check_action_scheduler_table_health() {
global $wpdb;
$issues = array();
// List of required Action Scheduler tables
$required_tables = array(
'actionscheduler_actions',
'actionscheduler_claims',
'actionscheduler_groups',
'actionscheduler_logs',
);
// Check if each table exists
foreach ( $required_tables as $table ) {
$full_table_name = $wpdb->prefix . $table;
$escaped_table_name = $wpdb->esc_like( $full_table_name );
$table_exists = $wpdb->get_var(
$wpdb->prepare(
'SHOW TABLES LIKE %s',
$escaped_table_name
)
);
if ( $table_exists !== $full_table_name ) {
$issues[] = sprintf( __( 'Missing table: %s', 'paid-memberships-pro' ), $full_table_name );
}
}
// Check for 'priority' column in 'actionscheduler_actions' table (3.6+ requirement)
$actions_table = $wpdb->prefix . 'actionscheduler_actions';
$escaped_actions_table = $wpdb->esc_like( $actions_table );
$actions_table_exists = $wpdb->get_var(
$wpdb->prepare(
'SHOW TABLES LIKE %s',
$escaped_actions_table
)
);
if ( $actions_table_exists === $actions_table ) {
$priority_column = $wpdb->get_results(
$wpdb->prepare(
"SHOW COLUMNS FROM `{$actions_table}` LIKE %s",
'priority'
)
);
if ( empty( $priority_column ) ) {
$issues[] = __( 'Missing priority column in actionscheduler_actions table (required for Action Scheduler 3.6+)', 'paid-memberships-pro' );
}
} else {
// Already flagged as missing earlier, but could be handled separately if needed
// $issues[] = __( 'actionscheduler_actions table does not exist.', 'paid-memberships-pro' );
}
return $issues;
}
}