From 31b44c07b9167cfe1a8b504a1928d6d856372eed Mon Sep 17 00:00:00 2001 From: Dale Mugford Date: Thu, 26 Feb 2026 15:05:07 -0500 Subject: [PATCH 1/5] First run at Pause Mode refactor into modules. Signed-off-by: Dale Mugford --- classes/class-pmpro-pause-mode.php | 1076 ++++++++++++++++++++++++++++ includes/admin.php | 86 +++ paid-memberships-pro.php | 6 + 3 files changed, 1168 insertions(+) create mode 100644 classes/class-pmpro-pause-mode.php diff --git a/classes/class-pmpro-pause-mode.php b/classes/class-pmpro-pause-mode.php new file mode 100644 index 000000000..c5db9f781 --- /dev/null +++ b/classes/class-pmpro-pause-mode.php @@ -0,0 +1,1076 @@ +register_module( new PMPro_Pause_Module_Mutations() ); + $this->register_module( new PMPro_Pause_Module_Gateways() ); + $this->register_module( new PMPro_Pause_Module_Mail() ); + $this->register_module( new PMPro_Pause_Module_Schedules() ); + $this->register_module( new PMPro_Pause_Module_Frontend() ); + $this->register_module( new PMPro_Pause_Module_Sessions() ); + + // Always register the email replay callback so AS can process queued emails after resume. + add_action( 'pmpro_pause_mode_send_queued_email', array( $this, 'send_queued_email' ) ); + + // Re-activate modules if pause mode is active. + $state = $this->get_state(); + if ( ! empty( $state['enabled'] ) && ! empty( $state['modules'] ) ) { + foreach ( $this->get_ordered_modules( $state['modules'] ) as $slug ) { + if ( isset( $this->modules[ $slug ] ) ) { + $this->modules[ $slug ]->activate(); + } + } + } + } + + /** + * Register a module. + * + * @param PMPro_Pause_Module_Interface $module The module to register. + */ + public function register_module( PMPro_Pause_Module_Interface $module ) { + $this->modules[ $module->get_slug() ] = $module; + } + + /** + * Activate pause mode with the given modules. + * + * @param string[] $modules Array of module slugs to enable. + * @param string $activated_by Who/what activated pause mode. + * @return bool + */ + public function pause( $modules = array(), $activated_by = 'manual' ) { + // Validate module slugs. + $modules = array_filter( $modules, function( $slug ) { + return isset( $this->modules[ $slug ] ); + } ); + + if ( empty( $modules ) ) { + return false; + } + + // If already paused, merge modules. + $state = $this->get_state(); + if ( ! empty( $state['enabled'] ) ) { + $new_modules = array_unique( array_merge( $state['modules'], $modules ) ); + $modules_to_activate = array_diff( $modules, $state['modules'] ); + $state['modules'] = $new_modules; + update_option( self::OPTION_KEY, $state ); + $this->state = $state; + + // Activate only newly added modules. + foreach ( $this->get_ordered_modules( $modules_to_activate ) as $slug ) { + $this->modules[ $slug ]->activate(); + + /** Fires when a pause module is activated. */ + do_action( 'pmpro_pause_module_activated', $slug ); + } + + return true; + } + + // Build state. + $state = array( + 'enabled' => true, + 'modules' => array_values( $modules ), + 'activated_at' => time(), + 'activated_by' => $activated_by, + ); + + // Save state first so modules can read it. + update_option( self::OPTION_KEY, $state ); + $this->state = $state; + + // Activate modules in order. + foreach ( $this->get_ordered_modules( $modules ) as $slug ) { + $this->modules[ $slug ]->activate(); + do_action( 'pmpro_pause_module_activated', $slug ); + } + + $this->log( sprintf( 'Pause mode activated by %s with modules: %s', $activated_by, implode( ', ', $modules ) ) ); + + /** Fires after pause mode is fully activated. */ + do_action( 'pmpro_pause_mode_activated', $state ); + + return true; + } + + /** + * Resume all services. + * + * @return bool + */ + public function resume() { + $state = $this->get_state(); + if ( empty( $state['enabled'] ) ) { + return false; + } + + // Deactivate in reverse order. + $reverse = array_reverse( $this->get_ordered_modules( $state['modules'] ) ); + foreach ( $reverse as $slug ) { + if ( isset( $this->modules[ $slug ] ) ) { + $this->modules[ $slug ]->deactivate(); + do_action( 'pmpro_pause_module_deactivated', $slug ); + } + } + + // Run on_resume for each module. + foreach ( $state['modules'] as $slug ) { + if ( isset( $this->modules[ $slug ] ) ) { + $this->modules[ $slug ]->on_resume(); + } + } + + delete_option( self::OPTION_KEY ); + $this->state = null; + + $this->log( 'Pause mode deactivated.' ); + do_action( 'pmpro_pause_mode_deactivated', $state ); + + return true; + } + + /** + * Pause with a named preset. + * + * @param string $preset_name The preset name. + * @return bool + */ + public function pause_with_preset( $preset_name ) { + $presets = self::get_presets(); + if ( ! isset( $presets[ $preset_name ] ) ) { + return false; + } + return $this->pause( $presets[ $preset_name ]['modules'], $preset_name ); + } + + /** + * Enable a single module while paused. + * + * @param string $slug The module slug. + * @return bool + */ + public function enable_module( $slug ) { + $state = $this->get_state(); + if ( empty( $state['enabled'] ) || ! isset( $this->modules[ $slug ] ) ) { + return false; + } + + if ( in_array( $slug, $state['modules'], true ) ) { + return true; + } + + $state['modules'][] = $slug; + update_option( self::OPTION_KEY, $state ); + $this->state = $state; + + $this->modules[ $slug ]->activate(); + do_action( 'pmpro_pause_module_activated', $slug ); + + return true; + } + + /** + * Disable a single module while paused. + * + * @param string $slug The module slug. + * @return bool + */ + public function disable_module( $slug ) { + $state = $this->get_state(); + if ( empty( $state['enabled'] ) || ! isset( $this->modules[ $slug ] ) ) { + return false; + } + + $state['modules'] = array_values( array_diff( $state['modules'], array( $slug ) ) ); + update_option( self::OPTION_KEY, $state ); + $this->state = $state; + + $this->modules[ $slug ]->deactivate(); + $this->modules[ $slug ]->on_resume(); + do_action( 'pmpro_pause_module_deactivated', $slug ); + + // If no modules remain, fully resume. + if ( empty( $state['modules'] ) ) { + return $this->resume(); + } + + return true; + } + + /** + * Check if pause mode is active. + * + * @return bool + */ + public function is_paused() { + $state = $this->get_state(); + return ! empty( $state['enabled'] ); + } + + /** + * Check if a specific module is active. + * + * @param string $slug Module slug. + * @return bool + */ + public function is_module_active( $slug ) { + return isset( $this->modules[ $slug ] ) && $this->modules[ $slug ]->is_active(); + } + + /** + * Get the current state. + * + * @return array + */ + public function get_state() { + if ( is_null( $this->state ) ) { + $this->state = get_option( self::OPTION_KEY, array() ); + } + return $this->state; + } + + /** + * Get active module slugs. + * + * @return string[] + */ + public function get_active_modules() { + $state = $this->get_state(); + return ! empty( $state['modules'] ) ? $state['modules'] : array(); + } + + /** + * Get available presets. + * + * @return array + */ + public static function get_presets() { + $presets = array( + 'migration' => array( + 'label' => __( 'Migration (Full Lockdown)', 'paid-memberships-pro' ), + 'modules' => array( + 'pmpro_mutations', + 'pmpro_gateways', + 'pmpro_mail', + 'background_schedules', + 'frontend_block', + 'logged_in_sessions', + ), + ), + 'maintenance' => array( + 'label' => __( 'Maintenance', 'paid-memberships-pro' ), + 'modules' => array( + 'pmpro_mutations', + 'pmpro_mail', + 'background_schedules', + ), + ), + ); + + /** + * Filter available pause mode presets. + * + * @param array $presets Preset definitions. + */ + return apply_filters( 'pmpro_pause_mode_presets', $presets ); + } + + /** + * Sort modules into activation order. + * + * @param string[] $slugs Module slugs. + * @return string[] + */ + private function get_ordered_modules( $slugs ) { + $ordered = array(); + foreach ( self::$activation_order as $slug ) { + if ( in_array( $slug, $slugs, true ) ) { + $ordered[] = $slug; + } + } + // Append any custom modules not in the default order. + foreach ( $slugs as $slug ) { + if ( ! in_array( $slug, $ordered, true ) ) { + $ordered[] = $slug; + } + } + return $ordered; + } + + /** + * Check if the current user can bypass pause mode. + * + * @return bool + */ + public static function current_user_can_bypass() { + /** + * Filter whether the current user can bypass pause mode. + * + * @param bool $can_bypass Whether the user can bypass. + */ + return apply_filters( 'pmpro_pause_mode_admin_bypass', current_user_can( 'pmpro_manage_pause_mode' ) ); + } + + /** + * Send a queued email (Action Scheduler callback). + * + * @param array $email_data Serialized email data. + */ + public function send_queued_email( $email_data ) { + if ( empty( $email_data['to'] ) || empty( $email_data['subject'] ) ) { + return; + } + + $headers = ! empty( $email_data['headers'] ) ? $email_data['headers'] : ''; + $attachments = ! empty( $email_data['attachments'] ) ? $email_data['attachments'] : array(); + + wp_mail( $email_data['to'], $email_data['subject'], $email_data['message'], $headers, $attachments ); + } + + /** + * Log a pause mode event. + * + * @param string $message The message to log. + */ + private function log( $message ) { + error_log( '[PMPro Pause Mode] ' . $message ); + + /** Fires when a pause mode event is logged. */ + do_action( 'pmpro_pause_mode_log', $message ); + } +} + +/** + * Module A: Freeze PMPro state changes. + * + * @since TBD + */ +class PMPro_Pause_Module_Mutations implements PMPro_Pause_Module_Interface { + + private $active = false; + + public function get_slug() { + return 'pmpro_mutations'; + } + + public function get_label() { + return __( 'Freeze Membership Changes', 'paid-memberships-pro' ); + } + + public function activate() { + if ( $this->active ) { + return; + } + $this->active = true; + + add_filter( 'pmpro_checkout_checks', array( $this, 'block_checkout' ), 1 ); + add_filter( 'pmpro_change_level', array( $this, 'block_level_change' ), 1, 4 ); + add_filter( 'pmpro_checkout_order_creation_checks', array( $this, 'block_order_creation' ), 1 ); + } + + public function deactivate() { + if ( ! $this->active ) { + return; + } + $this->active = false; + + remove_filter( 'pmpro_checkout_checks', array( $this, 'block_checkout' ), 1 ); + remove_filter( 'pmpro_change_level', array( $this, 'block_level_change' ), 1 ); + remove_filter( 'pmpro_checkout_order_creation_checks', array( $this, 'block_order_creation' ), 1 ); + } + + public function is_active() { + return $this->active; + } + + public function on_resume() { + // No cleanup needed. + } + + /** + * Block checkout for non-admins. + */ + public function block_checkout( $value ) { + if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + return $value; + } + + global $pmpro_msg, $pmpro_msgt; + $pmpro_msg = __( 'This site is currently in maintenance mode. Please try again later.', 'paid-memberships-pro' ); + $pmpro_msgt = 'pmpro_error'; + return false; + } + + /** + * Block level changes for non-admins. + */ + public function block_level_change( $level, $user_id, $old_level_status, $cancel_level ) { + if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + return $level; + } + return false; + } + + /** + * Block order creation for non-admins. + */ + public function block_order_creation( $value ) { + if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + return $value; + } + return false; + } +} + +/** + * Module B: Block gateway communication. + * + * @since TBD + */ +class PMPro_Pause_Module_Gateways implements PMPro_Pause_Module_Interface { + + private $active = false; + + /** + * Known gateway API domains to block outbound requests to. + * + * @var string[] + */ + private static $gateway_domains = array( + 'api.stripe.com', + 'api.paypal.com', + 'api.sandbox.paypal.com', + 'api-3t.paypal.com', + 'api-3t.sandbox.paypal.com', + 'ipnpb.paypal.com', + 'ipnpb.sandbox.paypal.com', + 'api.braintreegateway.com', + 'payments.braintree-api.com', + 'apitest.authorize.net', + 'api2.authorize.net', + 'api.authorize.net', + ); + + /** + * Webhook AJAX actions to intercept. + * + * @var string[] + */ + private static $webhook_actions = array( + 'wp_ajax_nopriv_stripe_webhook', + 'wp_ajax_nopriv_ipnhandler', + 'wp_ajax_nopriv_authnet_silent_post', + 'wp_ajax_nopriv_braintree_webhook', + 'wp_ajax_nopriv_twocheckout-ins', + ); + + public function get_slug() { + return 'pmpro_gateways'; + } + + public function get_label() { + return __( 'Block Gateway Communication', 'paid-memberships-pro' ); + } + + public function activate() { + if ( $this->active ) { + return; + } + $this->active = true; + + add_action( 'pmpro_checkout_before_processing', array( $this, 'block_gateway_outbound' ), 0 ); + add_filter( 'pre_http_request', array( $this, 'block_outbound_http' ), 1, 3 ); + + /** Filter whether to block inbound webhooks with 503. */ + if ( apply_filters( 'pmpro_pause_mode_block_inbound_webhooks', true ) ) { + foreach ( self::$webhook_actions as $action ) { + add_action( $action, array( $this, 'block_inbound_webhook' ), 0 ); + } + } + } + + public function deactivate() { + if ( ! $this->active ) { + return; + } + $this->active = false; + + remove_action( 'pmpro_checkout_before_processing', array( $this, 'block_gateway_outbound' ), 0 ); + remove_filter( 'pre_http_request', array( $this, 'block_outbound_http' ), 1 ); + + foreach ( self::$webhook_actions as $action ) { + remove_action( $action, array( $this, 'block_inbound_webhook' ), 0 ); + } + } + + public function is_active() { + return $this->active; + } + + public function on_resume() { + // No cleanup needed. + } + + /** + * Block checkout from reaching the gateway. + */ + public function block_gateway_outbound() { + global $pmpro_msg, $pmpro_msgt; + $pmpro_msg = __( 'Payment processing is temporarily suspended. Please try again later.', 'paid-memberships-pro' ); + $pmpro_msgt = 'pmpro_error'; + + wp_die( + esc_html( $pmpro_msg ), + esc_html__( 'Service Unavailable', 'paid-memberships-pro' ), + array( 'response' => 503 ) + ); + } + + /** + * Block inbound webhooks with 503. + */ + public function block_inbound_webhook() { + $retry_after = apply_filters( 'pmpro_pause_mode_retry_after', 3600 ); + + status_header( 503 ); + header( 'Retry-After: ' . intval( $retry_after ) ); + header( 'Content-Type: text/plain; charset=utf-8' ); + echo 'Service temporarily unavailable. Retry later.'; + exit; + } + + /** + * Block outbound HTTP requests to gateway domains. + * + * @param false|array|WP_Error $response Whether to preempt the request. + * @param array $parsed_args Request arguments. + * @param string $url The request URL. + * @return false|array|WP_Error + */ + public function block_outbound_http( $response, $parsed_args, $url ) { + $host = wp_parse_url( $url, PHP_URL_HOST ); + if ( empty( $host ) ) { + return $response; + } + + /** + * Filter the list of blocked gateway domains. + * + * @param string[] $domains Gateway API domains. + */ + $blocked_domains = apply_filters( 'pmpro_pause_mode_blocked_gateway_domains', self::$gateway_domains ); + + if ( in_array( $host, $blocked_domains, true ) ) { + return new WP_Error( + 'pmpro_pause_mode_blocked', + __( 'Outbound gateway request blocked during pause mode.', 'paid-memberships-pro' ) + ); + } + + return $response; + } +} + +/** + * Module C: Queue all outgoing email. + * + * @since TBD + */ +class PMPro_Pause_Module_Mail implements PMPro_Pause_Module_Interface { + + private $active = false; + + public function get_slug() { + return 'pmpro_mail'; + } + + public function get_label() { + return __( 'Queue Outgoing Email', 'paid-memberships-pro' ); + } + + public function activate() { + if ( $this->active ) { + return; + } + $this->active = true; + + add_filter( 'pre_wp_mail', array( $this, 'intercept_email' ), 999, 2 ); + } + + public function deactivate() { + if ( ! $this->active ) { + return; + } + $this->active = false; + + remove_filter( 'pre_wp_mail', array( $this, 'intercept_email' ), 999 ); + } + + public function is_active() { + return $this->active; + } + + public function on_resume() { + // Queued emails will be processed by AS naturally once Module D is deactivated. + } + + /** + * Intercept outgoing email, queue it in AS. + * + * @param null|bool $return Short-circuit return value. + * @param array $atts Email attributes (to, subject, message, headers, attachments). + * @return false + */ + public function intercept_email( $return, $atts ) { + $email_data = array( + 'to' => $atts['to'], + 'subject' => $atts['subject'], + 'message' => $atts['message'], + 'headers' => $atts['headers'], + 'attachments' => $atts['attachments'], + 'queued_at' => time(), + ); + + PMPro_Action_Scheduler::instance()->maybe_add_task( + 'pmpro_pause_mode_send_queued_email', + array( $email_data ), + 'pmpro_pause_mode_email_queue' + ); + + // Return false to prevent sending. + return false; + } +} + +/** + * Module D: Halt background processing. + * + * @since TBD + */ +class PMPro_Pause_Module_Schedules implements PMPro_Pause_Module_Interface { + + private $active = false; + + public function get_slug() { + return 'background_schedules'; + } + + public function get_label() { + return __( 'Halt Background Processing', 'paid-memberships-pro' ); + } + + public function activate() { + if ( $this->active ) { + return; + } + $this->active = true; + + PMPro_Action_Scheduler::halt(); + add_filter( 'action_scheduler_before_execute', '__return_false', 999 ); + add_filter( 'spawn_cron', '__return_false', 999 ); + } + + public function deactivate() { + if ( ! $this->active ) { + return; + } + $this->active = false; + + PMPro_Action_Scheduler::resume(); + remove_filter( 'action_scheduler_before_execute', '__return_false', 999 ); + remove_filter( 'spawn_cron', '__return_false', 999 ); + } + + public function is_active() { + return $this->active; + } + + public function on_resume() { + // AS resume is handled in deactivate(). + } +} + +/** + * Module E: Block non-admin frontend traffic. + * + * @since TBD + */ +class PMPro_Pause_Module_Frontend implements PMPro_Pause_Module_Interface { + + private $active = false; + + public function get_slug() { + return 'frontend_block'; + } + + public function get_label() { + return __( 'Block Frontend Access', 'paid-memberships-pro' ); + } + + public function activate() { + if ( $this->active ) { + return; + } + $this->active = true; + + add_action( 'template_redirect', array( $this, 'block_frontend' ), 0 ); + add_filter( 'rest_authentication_errors', array( $this, 'block_rest_api' ), 0 ); + add_action( 'init', array( $this, 'block_post_requests' ), 0 ); + add_action( 'admin_init', array( $this, 'block_nopriv_ajax' ), 0 ); + add_filter( 'authenticate', array( $this, 'block_non_admin_login' ), 999, 2 ); + } + + public function deactivate() { + if ( ! $this->active ) { + return; + } + $this->active = false; + + remove_action( 'template_redirect', array( $this, 'block_frontend' ), 0 ); + remove_filter( 'rest_authentication_errors', array( $this, 'block_rest_api' ), 0 ); + remove_action( 'init', array( $this, 'block_post_requests' ), 0 ); + remove_action( 'admin_init', array( $this, 'block_nopriv_ajax' ), 0 ); + remove_filter( 'authenticate', array( $this, 'block_non_admin_login' ), 999 ); + } + + public function is_active() { + return $this->active; + } + + public function on_resume() { + // No cleanup needed. + } + + /** + * Block frontend for non-admins. + */ + public function block_frontend() { + if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + return; + } + + if ( is_admin() ) { + return; + } + + $this->show_maintenance_page(); + } + + /** + * Block REST API for non-admins. + * + * @param WP_Error|null|true $errors WP_Error if authentication error. + * @return WP_Error|null|true + */ + public function block_rest_api( $errors ) { + if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + return $errors; + } + + return new WP_Error( + 'pmpro_pause_mode_rest_blocked', + __( 'Site is temporarily unavailable for maintenance.', 'paid-memberships-pro' ), + array( 'status' => 503 ) + ); + } + + /** + * Block POST requests from non-admins. + */ + public function block_post_requests() { + if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + return; + } + + if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] && ! is_admin() ) { + wp_die( + esc_html__( 'This site is currently in maintenance mode. Please try again later.', 'paid-memberships-pro' ), + esc_html__( 'Service Unavailable', 'paid-memberships-pro' ), + array( 'response' => 503 ) + ); + } + } + + /** + * Block AJAX requests from non-logged-in users. + */ + public function block_nopriv_ajax() { + if ( ! wp_doing_ajax() || is_user_logged_in() ) { + return; + } + + wp_die( + esc_html__( 'Site temporarily unavailable.', 'paid-memberships-pro' ), + esc_html__( 'Service Unavailable', 'paid-memberships-pro' ), + array( 'response' => 503 ) + ); + } + + /** + * Block non-admin logins. + * + * @param WP_User|WP_Error|null $user The user object or error. + * @param string $username The username. + * @return WP_User|WP_Error|null + */ + public function block_non_admin_login( $user, $username ) { + if ( is_wp_error( $user ) ) { + return $user; + } + + if ( $user && ! user_can( $user, 'pmpro_manage_pause_mode' ) ) { + return new WP_Error( + 'pmpro_pause_mode_login_blocked', + __( 'This site is temporarily unavailable for maintenance. Only administrators can log in.', 'paid-memberships-pro' ) + ); + } + + return $user; + } + + /** + * Display the maintenance page. + */ + private function show_maintenance_page() { + $retry_after = apply_filters( 'pmpro_pause_mode_retry_after', 3600 ); + + $html = ' + + + ' . esc_html__( 'Site Under Maintenance', 'paid-memberships-pro' ) . ' + + + + +
+

' . esc_html__( 'Site Under Maintenance', 'paid-memberships-pro' ) . '

+

' . esc_html__( 'We are currently performing maintenance on this site. This process should be completed shortly.', 'paid-memberships-pro' ) . '

+

' . esc_html__( 'Thank you for your patience.', 'paid-memberships-pro' ) . '

+
+ +'; + + /** + * Filter the maintenance page HTML. + * + * @param string $html The maintenance page HTML. + */ + $html = apply_filters( 'pmpro_pause_mode_maintenance_template', $html ); + + status_header( 503 ); + header( 'Content-Type: text/html; charset=utf-8' ); + header( 'Retry-After: ' . intval( $retry_after ) ); + echo $html; + exit; + } +} + +/** + * Module F: Clear non-admin sessions. + * + * @since TBD + */ +class PMPro_Pause_Module_Sessions implements PMPro_Pause_Module_Interface { + + private $active = false; + + public function get_slug() { + return 'logged_in_sessions'; + } + + public function get_label() { + return __( 'Clear Non-Admin Sessions', 'paid-memberships-pro' ); + } + + public function activate() { + if ( $this->active ) { + return; + } + $this->active = true; + + // Clear non-admin sessions immediately. + $this->clear_non_admin_sessions(); + + // Block non-admin logins going forward. + add_filter( 'authenticate', array( $this, 'block_non_admin_login' ), 999, 2 ); + } + + public function deactivate() { + if ( ! $this->active ) { + return; + } + $this->active = false; + + remove_filter( 'authenticate', array( $this, 'block_non_admin_login' ), 999 ); + } + + public function is_active() { + return $this->active; + } + + public function on_resume() { + // Sessions regenerate naturally on login. + } + + /** + * Block non-admin logins. + * + * @param WP_User|WP_Error|null $user The user object or error. + * @param string $username The username. + * @return WP_User|WP_Error|null + */ + public function block_non_admin_login( $user, $username ) { + if ( is_wp_error( $user ) ) { + return $user; + } + + if ( $user && ! user_can( $user, 'pmpro_manage_pause_mode' ) ) { + return new WP_Error( + 'pmpro_pause_mode_login_blocked', + __( 'This site is temporarily unavailable for maintenance. Only administrators can log in.', 'paid-memberships-pro' ) + ); + } + + return $user; + } + + /** + * Clear sessions for all non-admin users. + */ + private function clear_non_admin_sessions() { + $admins = get_users( array( + 'capability' => 'pmpro_manage_pause_mode', + 'fields' => 'ID', + ) ); + + $users = get_users( array( + 'exclude' => $admins, + 'fields' => 'ID', + 'number' => 500, + ) ); + + foreach ( $users as $user_id ) { + $sessions = WP_Session_Tokens::get_instance( $user_id ); + $sessions->destroy_all(); + } + + // If there are more users, schedule a follow-up batch. + $total = count_users(); + if ( ( $total['total_users'] - count( $admins ) ) > 500 ) { + PMPro_Action_Scheduler::instance()->maybe_add_task( + 'pmpro_pause_mode_clear_sessions_batch', + array( $admins, 500 ), + 'pmpro_pause_mode_sessions' + ); + } + } +} + +/** + * Convenience function: check if the pause engine is active. + * + * @since TBD + * @return bool + */ +function pmpro_pause_engine_is_active() { + return PMPro_Pause_Mode::instance()->is_paused(); +} + +/** + * Convenience function: check if a specific pause module is active. + * + * @since TBD + * @param string $slug Module slug. + * @return bool + */ +function pmpro_pause_module_is_active( $slug ) { + return PMPro_Pause_Mode::instance()->is_module_active( $slug ); +} diff --git a/includes/admin.php b/includes/admin.php index e87156091..bb82d7309 100644 --- a/includes/admin.php +++ b/includes/admin.php @@ -202,6 +202,92 @@ function pmpro_pause_mode_notice() { } } +/** + * Display a notice when the pause engine is active. + * + * @since TBD + */ +function pmpro_pause_engine_notice() { + if ( ! pmpro_pause_engine_is_active() ) { + return; + } + + $state = PMPro_Pause_Mode::instance()->get_state(); + $modules = PMPro_Pause_Mode::instance()->get_active_modules(); + + // Build module label list. + $module_labels = array(); + foreach ( $modules as $slug ) { + if ( PMPro_Pause_Mode::instance()->is_module_active( $slug ) ) { + // Get the module label from the registered modules. + $all_presets = PMPro_Pause_Mode::get_presets(); + $module_labels[] = esc_html( $slug ); + } + } + + $activated_by = ! empty( $state['activated_by'] ) ? $state['activated_by'] : __( 'unknown', 'paid-memberships-pro' ); + $activated_at = ! empty( $state['activated_at'] ) ? human_time_diff( $state['activated_at'] ) . ' ' . __( 'ago', 'paid-memberships-pro' ) : __( 'unknown', 'paid-memberships-pro' ); + + ?> +
+
+ +
+
+

+

+ ' . esc_html( $activated_by ) . '', + esc_html( $activated_at ) + ); + ?> +

+

+ ' . implode( ', ', $module_labels ) . '' + ); + ?> +

+ +

+ +

+ +
+
+ resume(); + + wp_safe_redirect( admin_url( 'admin.php?page=pmpro-dashboard' ) ); + exit; +} +add_action( 'admin_init', 'pmpro_handle_pause_engine_actions' ); + /** * Maybe display a notice about spam protection being disabled. * diff --git a/paid-memberships-pro.php b/paid-memberships-pro.php index ca9399799..ed4eceee2 100644 --- a/paid-memberships-pro.php +++ b/paid-memberships-pro.php @@ -188,6 +188,12 @@ } ); +// Load the Pause Mode engine. +require_once PMPRO_DIR . '/classes/class-pmpro-pause-mode.php'; +add_action( 'plugins_loaded', function() { + PMPro_Pause_Mode::instance(); +}, 5 ); + // Add On Management (Deprecated in 3.6, to be removed in 4.0.0) require_once( PMPRO_DIR . '/includes/addons.php' ); From dd110d9d5ebb66154b54480325d14341ab2d67e9 Mon Sep 17 00:00:00 2001 From: Dale Mugford Date: Thu, 26 Feb 2026 15:43:30 -0500 Subject: [PATCH 2/5] Rename references to Pause Mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For better clarity, use “pause engine” vs. pause mode. (PMPro_Pause_Engine(), etc.). Signed-off-by: Dale Mugford --- ...-mode.php => class-pmpro-pause-engine.php} | 92 +++--- includes/admin.php | 10 +- paid-memberships-pro.php | 6 +- tests/test-pause-engine.php | 267 ++++++++++++++++++ 4 files changed, 321 insertions(+), 54 deletions(-) rename classes/{class-pmpro-pause-mode.php => class-pmpro-pause-engine.php} (90%) create mode 100644 tests/test-pause-engine.php diff --git a/classes/class-pmpro-pause-mode.php b/classes/class-pmpro-pause-engine.php similarity index 90% rename from classes/class-pmpro-pause-mode.php rename to classes/class-pmpro-pause-engine.php index c5db9f781..3aff4ea76 100644 --- a/classes/class-pmpro-pause-mode.php +++ b/classes/class-pmpro-pause-engine.php @@ -1,6 +1,6 @@ register_module( new PMPro_Pause_Module_Sessions() ); // Always register the email replay callback so AS can process queued emails after resume. - add_action( 'pmpro_pause_mode_send_queued_email', array( $this, 'send_queued_email' ) ); + add_action( 'pmpro_pause_engine_send_queued_email', array( $this, 'send_queued_email' ) ); - // Re-activate modules if pause mode is active. + // Re-activate modules if pause engine is active. $state = $this->get_state(); if ( ! empty( $state['enabled'] ) && ! empty( $state['modules'] ) ) { foreach ( $this->get_ordered_modules( $state['modules'] ) as $slug ) { @@ -114,10 +114,10 @@ public function register_module( PMPro_Pause_Module_Interface $module ) { } /** - * Activate pause mode with the given modules. + * Activate pause engine with the given modules. * * @param string[] $modules Array of module slugs to enable. - * @param string $activated_by Who/what activated pause mode. + * @param string $activated_by Who/what activated pause engine. * @return bool */ public function pause( $modules = array(), $activated_by = 'manual' ) { @@ -170,8 +170,8 @@ public function pause( $modules = array(), $activated_by = 'manual' ) { $this->log( sprintf( 'Pause mode activated by %s with modules: %s', $activated_by, implode( ', ', $modules ) ) ); - /** Fires after pause mode is fully activated. */ - do_action( 'pmpro_pause_mode_activated', $state ); + /** Fires after pause engine is fully activated. */ + do_action( 'pmpro_pause_engine_activated', $state ); return true; } @@ -207,7 +207,7 @@ public function resume() { $this->state = null; $this->log( 'Pause mode deactivated.' ); - do_action( 'pmpro_pause_mode_deactivated', $state ); + do_action( 'pmpro_pause_engine_deactivated', $state ); return true; } @@ -281,7 +281,7 @@ public function disable_module( $slug ) { } /** - * Check if pause mode is active. + * Check if pause engine is active. * * @return bool */ @@ -351,11 +351,11 @@ public static function get_presets() { ); /** - * Filter available pause mode presets. + * Filter available pause engine presets. * * @param array $presets Preset definitions. */ - return apply_filters( 'pmpro_pause_mode_presets', $presets ); + return apply_filters( 'pmpro_pause_engine_presets', $presets ); } /** @@ -381,17 +381,17 @@ private function get_ordered_modules( $slugs ) { } /** - * Check if the current user can bypass pause mode. + * Check if the current user can bypass pause engine. * * @return bool */ public static function current_user_can_bypass() { /** - * Filter whether the current user can bypass pause mode. + * Filter whether the current user can bypass pause engine. * * @param bool $can_bypass Whether the user can bypass. */ - return apply_filters( 'pmpro_pause_mode_admin_bypass', current_user_can( 'pmpro_manage_pause_mode' ) ); + return apply_filters( 'pmpro_pause_engine_admin_bypass', current_user_can( 'pmpro_manage_pause_mode' ) ); } /** @@ -411,15 +411,15 @@ public function send_queued_email( $email_data ) { } /** - * Log a pause mode event. + * Log a pause engine event. * * @param string $message The message to log. */ private function log( $message ) { - error_log( '[PMPro Pause Mode] ' . $message ); + error_log( '[PMPro Pause Engine] ' . $message ); - /** Fires when a pause mode event is logged. */ - do_action( 'pmpro_pause_mode_log', $message ); + /** Fires when a pause engine event is logged. */ + do_action( 'pmpro_pause_engine_log', $message ); } } @@ -474,7 +474,7 @@ public function on_resume() { * Block checkout for non-admins. */ public function block_checkout( $value ) { - if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + if ( PMPro_Pause_Engine::current_user_can_bypass() ) { return $value; } @@ -488,7 +488,7 @@ public function block_checkout( $value ) { * Block level changes for non-admins. */ public function block_level_change( $level, $user_id, $old_level_status, $cancel_level ) { - if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + if ( PMPro_Pause_Engine::current_user_can_bypass() ) { return $level; } return false; @@ -498,7 +498,7 @@ public function block_level_change( $level, $user_id, $old_level_status, $cancel * Block order creation for non-admins. */ public function block_order_creation( $value ) { - if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + if ( PMPro_Pause_Engine::current_user_can_bypass() ) { return $value; } return false; @@ -565,7 +565,7 @@ public function activate() { add_filter( 'pre_http_request', array( $this, 'block_outbound_http' ), 1, 3 ); /** Filter whether to block inbound webhooks with 503. */ - if ( apply_filters( 'pmpro_pause_mode_block_inbound_webhooks', true ) ) { + if ( apply_filters( 'pmpro_pause_engine_block_inbound_webhooks', true ) ) { foreach ( self::$webhook_actions as $action ) { add_action( $action, array( $this, 'block_inbound_webhook' ), 0 ); } @@ -613,7 +613,7 @@ public function block_gateway_outbound() { * Block inbound webhooks with 503. */ public function block_inbound_webhook() { - $retry_after = apply_filters( 'pmpro_pause_mode_retry_after', 3600 ); + $retry_after = apply_filters( 'pmpro_pause_engine_retry_after', 3600 ); status_header( 503 ); header( 'Retry-After: ' . intval( $retry_after ) ); @@ -641,12 +641,12 @@ public function block_outbound_http( $response, $parsed_args, $url ) { * * @param string[] $domains Gateway API domains. */ - $blocked_domains = apply_filters( 'pmpro_pause_mode_blocked_gateway_domains', self::$gateway_domains ); + $blocked_domains = apply_filters( 'pmpro_pause_engine_blocked_gateway_domains', self::$gateway_domains ); if ( in_array( $host, $blocked_domains, true ) ) { return new WP_Error( - 'pmpro_pause_mode_blocked', - __( 'Outbound gateway request blocked during pause mode.', 'paid-memberships-pro' ) + 'pmpro_pause_engine_blocked', + __( 'Outbound gateway request blocked during pause engine.', 'paid-memberships-pro' ) ); } @@ -715,9 +715,9 @@ public function intercept_email( $return, $atts ) { ); PMPro_Action_Scheduler::instance()->maybe_add_task( - 'pmpro_pause_mode_send_queued_email', + 'pmpro_pause_engine_send_queued_email', array( $email_data ), - 'pmpro_pause_mode_email_queue' + 'pmpro_pause_engine_email_queue' ); // Return false to prevent sending. @@ -828,7 +828,7 @@ public function on_resume() { * Block frontend for non-admins. */ public function block_frontend() { - if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + if ( PMPro_Pause_Engine::current_user_can_bypass() ) { return; } @@ -846,12 +846,12 @@ public function block_frontend() { * @return WP_Error|null|true */ public function block_rest_api( $errors ) { - if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + if ( PMPro_Pause_Engine::current_user_can_bypass() ) { return $errors; } return new WP_Error( - 'pmpro_pause_mode_rest_blocked', + 'pmpro_pause_engine_rest_blocked', __( 'Site is temporarily unavailable for maintenance.', 'paid-memberships-pro' ), array( 'status' => 503 ) ); @@ -861,7 +861,7 @@ public function block_rest_api( $errors ) { * Block POST requests from non-admins. */ public function block_post_requests() { - if ( PMPro_Pause_Mode::current_user_can_bypass() ) { + if ( PMPro_Pause_Engine::current_user_can_bypass() ) { return; } @@ -903,7 +903,7 @@ public function block_non_admin_login( $user, $username ) { if ( $user && ! user_can( $user, 'pmpro_manage_pause_mode' ) ) { return new WP_Error( - 'pmpro_pause_mode_login_blocked', + 'pmpro_pause_engine_login_blocked', __( 'This site is temporarily unavailable for maintenance. Only administrators can log in.', 'paid-memberships-pro' ) ); } @@ -915,7 +915,7 @@ public function block_non_admin_login( $user, $username ) { * Display the maintenance page. */ private function show_maintenance_page() { - $retry_after = apply_filters( 'pmpro_pause_mode_retry_after', 3600 ); + $retry_after = apply_filters( 'pmpro_pause_engine_retry_after', 3600 ); $html = ' @@ -943,7 +943,7 @@ private function show_maintenance_page() { * * @param string $html The maintenance page HTML. */ - $html = apply_filters( 'pmpro_pause_mode_maintenance_template', $html ); + $html = apply_filters( 'pmpro_pause_engine_maintenance_template', $html ); status_header( 503 ); header( 'Content-Type: text/html; charset=utf-8' ); @@ -1014,7 +1014,7 @@ public function block_non_admin_login( $user, $username ) { if ( $user && ! user_can( $user, 'pmpro_manage_pause_mode' ) ) { return new WP_Error( - 'pmpro_pause_mode_login_blocked', + 'pmpro_pause_engine_login_blocked', __( 'This site is temporarily unavailable for maintenance. Only administrators can log in.', 'paid-memberships-pro' ) ); } @@ -1046,9 +1046,9 @@ private function clear_non_admin_sessions() { $total = count_users(); if ( ( $total['total_users'] - count( $admins ) ) > 500 ) { PMPro_Action_Scheduler::instance()->maybe_add_task( - 'pmpro_pause_mode_clear_sessions_batch', + 'pmpro_pause_engine_clear_sessions_batch', array( $admins, 500 ), - 'pmpro_pause_mode_sessions' + 'pmpro_pause_engine_sessions' ); } } @@ -1061,7 +1061,7 @@ private function clear_non_admin_sessions() { * @return bool */ function pmpro_pause_engine_is_active() { - return PMPro_Pause_Mode::instance()->is_paused(); + return PMPro_Pause_Engine::instance()->is_paused(); } /** @@ -1072,5 +1072,5 @@ function pmpro_pause_engine_is_active() { * @return bool */ function pmpro_pause_module_is_active( $slug ) { - return PMPro_Pause_Mode::instance()->is_module_active( $slug ); + return PMPro_Pause_Engine::instance()->is_module_active( $slug ); } diff --git a/includes/admin.php b/includes/admin.php index bb82d7309..f075cbf55 100644 --- a/includes/admin.php +++ b/includes/admin.php @@ -212,15 +212,15 @@ function pmpro_pause_engine_notice() { return; } - $state = PMPro_Pause_Mode::instance()->get_state(); - $modules = PMPro_Pause_Mode::instance()->get_active_modules(); + $state = PMPro_Pause_Engine::instance()->get_state(); + $modules = PMPro_Pause_Engine::instance()->get_active_modules(); // Build module label list. $module_labels = array(); foreach ( $modules as $slug ) { - if ( PMPro_Pause_Mode::instance()->is_module_active( $slug ) ) { + if ( PMPro_Pause_Engine::instance()->is_module_active( $slug ) ) { // Get the module label from the registered modules. - $all_presets = PMPro_Pause_Mode::get_presets(); + $all_presets = PMPro_Pause_Engine::get_presets(); $module_labels[] = esc_html( $slug ); } } @@ -281,7 +281,7 @@ function pmpro_handle_pause_engine_actions() { check_admin_referer( 'pmpro_resume_pause_engine' ); - PMPro_Pause_Mode::instance()->resume(); + PMPro_Pause_Engine::instance()->resume(); wp_safe_redirect( admin_url( 'admin.php?page=pmpro-dashboard' ) ); exit; diff --git a/paid-memberships-pro.php b/paid-memberships-pro.php index ed4eceee2..91fd58d2d 100644 --- a/paid-memberships-pro.php +++ b/paid-memberships-pro.php @@ -188,10 +188,10 @@ } ); -// Load the Pause Mode engine. -require_once PMPRO_DIR . '/classes/class-pmpro-pause-mode.php'; +// Load the Pause Engine. +require_once PMPRO_DIR . '/classes/class-pmpro-pause-engine.php'; add_action( 'plugins_loaded', function() { - PMPro_Pause_Mode::instance(); + PMPro_Pause_Engine::instance(); }, 5 ); // Add On Management (Deprecated in 3.6, to be removed in 4.0.0) diff --git a/tests/test-pause-engine.php b/tests/test-pause-engine.php new file mode 100644 index 000000000..c95651336 --- /dev/null +++ b/tests/test-pause-engine.php @@ -0,0 +1,267 @@ +is_paused() ) { + $pm->resume(); +} +delete_option( PMPro_Pause_Engine::OPTION_KEY ); +update_option( 'pmpro_as_halted', false ); + +// ------------------------------------------------------------------ +WP_CLI::log( '' ); +WP_CLI::log( '=== Orchestrator ===' ); +// ------------------------------------------------------------------ + +pm_assert( ! $pm->is_paused(), 'Not paused initially' ); +pm_assert( $pm->resume() === false, 'Resume when not paused returns false' ); +pm_assert( $pm->pause( array( 'nonexistent' ) ) === false, 'Pause with invalid module returns false' ); +pm_assert( $pm->pause_with_preset( 'nonexistent' ) === false, 'Pause with invalid preset returns false' ); + +// Pause with specific modules. +$pm->pause( array( 'pmpro_mutations', 'background_schedules' ), 'test' ); +pm_assert( $pm->is_paused(), 'Paused after pause()' ); +pm_assert( $pm->is_module_active( 'pmpro_mutations' ), 'Mutations module active' ); +pm_assert( $pm->is_module_active( 'background_schedules' ), 'Schedules module active' ); +pm_assert( ! $pm->is_module_active( 'pmpro_mail' ), 'Mail module not active' ); + +$state = $pm->get_state(); +pm_assert( $state['enabled'] === true, 'State enabled is true' ); +pm_assert( $state['activated_by'] === 'test', 'State activated_by is test' ); +pm_assert( ! empty( $state['activated_at'] ), 'State has activated_at' ); +pm_assert( in_array( 'pmpro_mutations', $state['modules'], true ), 'State has mutations in modules' ); + +// Merge more modules. +$pm->pause( array( 'pmpro_mail' ), 'test' ); +pm_assert( $pm->is_module_active( 'pmpro_mail' ), 'Mail module active after merge' ); +pm_assert( $pm->is_module_active( 'pmpro_mutations' ), 'Mutations still active after merge' ); + +// Enable/disable at runtime. +pm_assert( $pm->enable_module( 'frontend_block' ), 'Enable frontend_block at runtime' ); +pm_assert( $pm->is_module_active( 'frontend_block' ), 'Frontend block now active' ); + +$pm->disable_module( 'frontend_block' ); +pm_assert( ! $pm->is_module_active( 'frontend_block' ), 'Frontend block disabled' ); + +// Resume. +$pm->resume(); +pm_assert( ! $pm->is_paused(), 'Not paused after resume' ); +pm_assert( empty( $pm->get_active_modules() ), 'No active modules after resume' ); +pm_assert( get_option( PMPro_Pause_Engine::OPTION_KEY ) === false, 'Option deleted after resume' ); + +// ------------------------------------------------------------------ +WP_CLI::log( '' ); +WP_CLI::log( '=== Convenience Functions ===' ); +// ------------------------------------------------------------------ + +pm_assert( ! pmpro_pause_engine_is_active(), 'pmpro_pause_engine_is_active false when not paused' ); +$pm->pause( array( 'pmpro_mutations' ), 'test' ); +pm_assert( pmpro_pause_engine_is_active(), 'pmpro_pause_engine_is_active true when paused' ); +pm_assert( pmpro_pause_module_is_active( 'pmpro_mutations' ), 'pmpro_pause_module_is_active true for active module' ); +pm_assert( ! pmpro_pause_module_is_active( 'pmpro_mail' ), 'pmpro_pause_module_is_active false for inactive module' ); +$pm->resume(); + +// ------------------------------------------------------------------ +WP_CLI::log( '' ); +WP_CLI::log( '=== Preset: Migration ===' ); +// ------------------------------------------------------------------ + +$pm->pause_with_preset( 'migration' ); +$modules = $pm->get_active_modules(); +pm_assert( in_array( 'pmpro_mutations', $modules, true ), 'Migration preset has mutations' ); +pm_assert( in_array( 'pmpro_gateways', $modules, true ), 'Migration preset has gateways' ); +pm_assert( in_array( 'pmpro_mail', $modules, true ), 'Migration preset has mail' ); +pm_assert( in_array( 'background_schedules', $modules, true ), 'Migration preset has schedules' ); +pm_assert( in_array( 'frontend_block', $modules, true ), 'Migration preset has frontend_block' ); +pm_assert( in_array( 'logged_in_sessions', $modules, true ), 'Migration preset has sessions' ); +$pm->resume(); + +// ------------------------------------------------------------------ +WP_CLI::log( '' ); +WP_CLI::log( '=== Module D: Schedules ===' ); +// ------------------------------------------------------------------ + +$pm->pause( array( 'background_schedules' ), 'test' ); +pm_assert( (bool) get_option( 'pmpro_as_halted', false ), 'AS halted when schedules module active' ); +pm_assert( has_filter( 'action_scheduler_before_execute' ) !== false, 'action_scheduler_before_execute filter attached' ); +pm_assert( has_filter( 'spawn_cron' ) !== false, 'spawn_cron filter attached' ); +$pm->resume(); +pm_assert( ! (bool) get_option( 'pmpro_as_halted', false ), 'AS resumed after schedules module deactivated' ); + +// ------------------------------------------------------------------ +WP_CLI::log( '' ); +WP_CLI::log( '=== Module B: Gateways (outbound block) ===' ); +// ------------------------------------------------------------------ + +$pm->pause( array( 'pmpro_gateways' ), 'test' ); + +$result = apply_filters( 'pre_http_request', false, array(), 'https://api.stripe.com/v1/charges' ); +pm_assert( is_wp_error( $result ), 'Outbound to Stripe blocked' ); +pm_assert( $result->get_error_code() === 'pmpro_pause_engine_blocked', 'Error code is pmpro_pause_engine_blocked' ); + +$result2 = apply_filters( 'pre_http_request', false, array(), 'https://api.wordpress.org/plugins/' ); +pm_assert( $result2 === false, 'Outbound to WordPress.org allowed' ); + +$result3 = apply_filters( 'pre_http_request', false, array(), 'https://api.braintreegateway.com/merchants' ); +pm_assert( is_wp_error( $result3 ), 'Outbound to Braintree blocked' ); + +$pm->resume(); + +// ------------------------------------------------------------------ +WP_CLI::log( '' ); +WP_CLI::log( '=== Module C: Mail (intercept) ===' ); +// ------------------------------------------------------------------ + +$pm->pause( array( 'pmpro_mail' ), 'test' ); + +pm_assert( has_filter( 'pre_wp_mail' ) !== false, 'pre_wp_mail filter attached' ); + +$atts = array( + 'to' => 'test@example.com', + 'subject' => 'Pause Engine Test ' . time(), + 'message' => 'Test body', + 'headers' => '', + 'attachments' => array(), +); +$result = apply_filters( 'pre_wp_mail', null, $atts ); +pm_assert( $result === false, 'pre_wp_mail returns false (email suppressed)' ); + +$pm->resume(); + +// ------------------------------------------------------------------ +WP_CLI::log( '' ); +WP_CLI::log( '=== Module E: Frontend (REST block) ===' ); +// ------------------------------------------------------------------ + +$pm->pause( array( 'frontend_block' ), 'test' ); + +// As a non-admin user. +$subscriber_id = wp_insert_user( array( + 'user_login' => 'pm_test_sub_' . time(), + 'user_pass' => wp_generate_password(), + 'role' => 'subscriber', +) ); +wp_set_current_user( $subscriber_id ); + +$result = apply_filters( 'rest_authentication_errors', null ); +pm_assert( is_wp_error( $result ), 'REST blocked for subscriber' ); + +// As admin. +$admin_id = wp_insert_user( array( + 'user_login' => 'pm_test_admin_' . time(), + 'user_pass' => wp_generate_password(), + 'role' => 'administrator', +) ); +wp_set_current_user( $admin_id ); + +$result2 = apply_filters( 'rest_authentication_errors', null ); +pm_assert( ! is_wp_error( $result2 ), 'REST allowed for admin' ); + +// Login block — call our callback directly to avoid triggering WP core auth hooks. +$frontend_module = new PMPro_Pause_Module_Frontend(); +$frontend_module->activate(); + +$sub_user = get_user_by( 'ID', $subscriber_id ); +$result3 = $frontend_module->block_non_admin_login( $sub_user, $sub_user->user_login ); +pm_assert( is_wp_error( $result3 ), 'Login blocked for subscriber' ); + +$admin_user = get_user_by( 'ID', $admin_id ); +$result4 = $frontend_module->block_non_admin_login( $admin_user, $admin_user->user_login ); +pm_assert( ! is_wp_error( $result4 ), 'Login allowed for admin' ); + +$frontend_module->deactivate(); + +$pm->resume(); + +// Cleanup test users. +wp_delete_user( $subscriber_id ); +wp_delete_user( $admin_id ); +wp_set_current_user( 0 ); + +// ------------------------------------------------------------------ +WP_CLI::log( '' ); +WP_CLI::log( '=== Module A: Mutations ===' ); +// ------------------------------------------------------------------ + +$pm->pause( array( 'pmpro_mutations' ), 'test' ); + +pm_assert( has_filter( 'pmpro_change_level' ) !== false, 'pmpro_change_level filter attached' ); +pm_assert( has_filter( 'pmpro_checkout_checks' ) !== false, 'pmpro_checkout_checks filter attached' ); + +// Non-admin: pmpro_change_level should return false. +wp_set_current_user( 0 ); +$filtered = apply_filters( 'pmpro_change_level', 1, 999, 'inactive', null ); +pm_assert( $filtered === false, 'Level change blocked for non-admin' ); + +$pm->resume(); + +pm_assert( has_filter( 'pmpro_change_level', '__return_false' ) === false, 'pmpro_change_level filter removed after resume' ); + +// ------------------------------------------------------------------ +WP_CLI::log( '' ); +WP_CLI::log( '=== Disable Last Module Auto-Resumes ===' ); +// ------------------------------------------------------------------ + +$pm->pause( array( 'pmpro_mutations' ), 'test' ); +$pm->disable_module( 'pmpro_mutations' ); +pm_assert( ! $pm->is_paused(), 'Auto-resumed when last module disabled' ); + +// ------------------------------------------------------------------ +WP_CLI::log( '' ); +WP_CLI::log( '=== Actions Fired ===' ); +// ------------------------------------------------------------------ + +$activated = false; +$deactivated = false; +$mod_activated = null; +$mod_deactivated = null; + +add_action( 'pmpro_pause_engine_activated', function() use ( &$activated ) { $activated = true; } ); +add_action( 'pmpro_pause_engine_deactivated', function() use ( &$deactivated ) { $deactivated = true; } ); +add_action( 'pmpro_pause_module_activated', function( $s ) use ( &$mod_activated ) { $mod_activated = $s; } ); +add_action( 'pmpro_pause_module_deactivated', function( $s ) use ( &$mod_deactivated ) { $mod_deactivated = $s; } ); + +$pm->pause( array( 'pmpro_mutations' ), 'test' ); +pm_assert( $activated, 'pmpro_pause_engine_activated action fired' ); +pm_assert( $mod_activated === 'pmpro_mutations', 'pmpro_pause_module_activated fired with correct slug' ); + +$pm->resume(); +pm_assert( $deactivated, 'pmpro_pause_engine_deactivated action fired' ); +pm_assert( $mod_deactivated === 'pmpro_mutations', 'pmpro_pause_module_deactivated fired with correct slug' ); + +// ------------------------------------------------------------------ +// Summary +// ------------------------------------------------------------------ +WP_CLI::log( '' ); +WP_CLI::log( sprintf( '=== Results: %d passed, %d failed ===', $pm_test_pass, $pm_test_fail ) ); + +if ( $pm_test_fail > 0 ) { + WP_CLI::error( 'Some tests failed.' ); +} else { + WP_CLI::success( 'All tests passed!' ); +} From 25951ce9ede66165a77e2b2a08dd7cbce89dea9b Mon Sep 17 00:00:00 2001 From: Dale Mugford Date: Thu, 26 Feb 2026 20:43:37 -0500 Subject: [PATCH 3/5] Fixes - Allow admins to login after pause engine started - Clear non-admin logged-in sessions inline using batch processing Signed-off-by: Dale Mugford --- classes/class-pmpro-pause-engine.php | 50 ++++++++++++++++------------ 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/classes/class-pmpro-pause-engine.php b/classes/class-pmpro-pause-engine.php index 3aff4ea76..e717afff3 100644 --- a/classes/class-pmpro-pause-engine.php +++ b/classes/class-pmpro-pause-engine.php @@ -865,6 +865,12 @@ public function block_post_requests() { return; } + // Allow login page POST so admins can authenticate. + global $pagenow; + if ( 'wp-login.php' === $pagenow ) { + return; + } + if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] && ! is_admin() ) { wp_die( esc_html__( 'This site is currently in maintenance mode. Please try again later.', 'paid-memberships-pro' ), @@ -976,8 +982,12 @@ public function activate() { } $this->active = true; - // Clear non-admin sessions immediately. - $this->clear_non_admin_sessions(); + // Clear non-admin sessions once WP is fully loaded (get_users needs $wpdb roles ready). + if ( did_action( 'init' ) ) { + $this->clear_non_admin_sessions(); + } else { + add_action( 'init', array( $this, 'clear_non_admin_sessions' ), 0 ); + } // Block non-admin logins going forward. add_filter( 'authenticate', array( $this, 'block_non_admin_login' ), 999, 2 ); @@ -1025,32 +1035,30 @@ public function block_non_admin_login( $user, $username ) { /** * Clear sessions for all non-admin users. */ - private function clear_non_admin_sessions() { + public function clear_non_admin_sessions() { $admins = get_users( array( 'capability' => 'pmpro_manage_pause_mode', 'fields' => 'ID', ) ); - $users = get_users( array( - 'exclude' => $admins, - 'fields' => 'ID', - 'number' => 500, - ) ); + $offset = 0; + $batch = 500; - foreach ( $users as $user_id ) { - $sessions = WP_Session_Tokens::get_instance( $user_id ); - $sessions->destroy_all(); - } + do { + $users = get_users( array( + 'exclude' => $admins, + 'fields' => 'ID', + 'number' => $batch, + 'offset' => $offset, + ) ); - // If there are more users, schedule a follow-up batch. - $total = count_users(); - if ( ( $total['total_users'] - count( $admins ) ) > 500 ) { - PMPro_Action_Scheduler::instance()->maybe_add_task( - 'pmpro_pause_engine_clear_sessions_batch', - array( $admins, 500 ), - 'pmpro_pause_engine_sessions' - ); - } + foreach ( $users as $user_id ) { + $sessions = WP_Session_Tokens::get_instance( $user_id ); + $sessions->destroy_all(); + } + + $offset += $batch; + } while ( count( $users ) === $batch ); } } From b8bf901e2ad1e716d5857da2ec6b29345e96ba28 Mon Sep 17 00:00:00 2001 From: Dale Mugford Date: Thu, 26 Feb 2026 21:10:30 -0500 Subject: [PATCH 4/5] Use admin tester method vs. assert programmatic Signed-off-by: Dale Mugford --- tests/test-pause-engine.php | 403 +++++++++++++----------------------- 1 file changed, 147 insertions(+), 256 deletions(-) diff --git a/tests/test-pause-engine.php b/tests/test-pause-engine.php index c95651336..4cdf0a36d 100644 --- a/tests/test-pause-engine.php +++ b/tests/test-pause-engine.php @@ -1,267 +1,158 @@ is_paused() ) { - $pm->resume(); -} -delete_option( PMPro_Pause_Engine::OPTION_KEY ); -update_option( 'pmpro_as_halted', false ); - -// ------------------------------------------------------------------ -WP_CLI::log( '' ); -WP_CLI::log( '=== Orchestrator ===' ); -// ------------------------------------------------------------------ - -pm_assert( ! $pm->is_paused(), 'Not paused initially' ); -pm_assert( $pm->resume() === false, 'Resume when not paused returns false' ); -pm_assert( $pm->pause( array( 'nonexistent' ) ) === false, 'Pause with invalid module returns false' ); -pm_assert( $pm->pause_with_preset( 'nonexistent' ) === false, 'Pause with invalid preset returns false' ); - -// Pause with specific modules. -$pm->pause( array( 'pmpro_mutations', 'background_schedules' ), 'test' ); -pm_assert( $pm->is_paused(), 'Paused after pause()' ); -pm_assert( $pm->is_module_active( 'pmpro_mutations' ), 'Mutations module active' ); -pm_assert( $pm->is_module_active( 'background_schedules' ), 'Schedules module active' ); -pm_assert( ! $pm->is_module_active( 'pmpro_mail' ), 'Mail module not active' ); - -$state = $pm->get_state(); -pm_assert( $state['enabled'] === true, 'State enabled is true' ); -pm_assert( $state['activated_by'] === 'test', 'State activated_by is test' ); -pm_assert( ! empty( $state['activated_at'] ), 'State has activated_at' ); -pm_assert( in_array( 'pmpro_mutations', $state['modules'], true ), 'State has mutations in modules' ); - -// Merge more modules. -$pm->pause( array( 'pmpro_mail' ), 'test' ); -pm_assert( $pm->is_module_active( 'pmpro_mail' ), 'Mail module active after merge' ); -pm_assert( $pm->is_module_active( 'pmpro_mutations' ), 'Mutations still active after merge' ); - -// Enable/disable at runtime. -pm_assert( $pm->enable_module( 'frontend_block' ), 'Enable frontend_block at runtime' ); -pm_assert( $pm->is_module_active( 'frontend_block' ), 'Frontend block now active' ); - -$pm->disable_module( 'frontend_block' ); -pm_assert( ! $pm->is_module_active( 'frontend_block' ), 'Frontend block disabled' ); - -// Resume. -$pm->resume(); -pm_assert( ! $pm->is_paused(), 'Not paused after resume' ); -pm_assert( empty( $pm->get_active_modules() ), 'No active modules after resume' ); -pm_assert( get_option( PMPro_Pause_Engine::OPTION_KEY ) === false, 'Option deleted after resume' ); - -// ------------------------------------------------------------------ -WP_CLI::log( '' ); -WP_CLI::log( '=== Convenience Functions ===' ); -// ------------------------------------------------------------------ - -pm_assert( ! pmpro_pause_engine_is_active(), 'pmpro_pause_engine_is_active false when not paused' ); -$pm->pause( array( 'pmpro_mutations' ), 'test' ); -pm_assert( pmpro_pause_engine_is_active(), 'pmpro_pause_engine_is_active true when paused' ); -pm_assert( pmpro_pause_module_is_active( 'pmpro_mutations' ), 'pmpro_pause_module_is_active true for active module' ); -pm_assert( ! pmpro_pause_module_is_active( 'pmpro_mail' ), 'pmpro_pause_module_is_active false for inactive module' ); -$pm->resume(); - -// ------------------------------------------------------------------ -WP_CLI::log( '' ); -WP_CLI::log( '=== Preset: Migration ===' ); -// ------------------------------------------------------------------ - -$pm->pause_with_preset( 'migration' ); -$modules = $pm->get_active_modules(); -pm_assert( in_array( 'pmpro_mutations', $modules, true ), 'Migration preset has mutations' ); -pm_assert( in_array( 'pmpro_gateways', $modules, true ), 'Migration preset has gateways' ); -pm_assert( in_array( 'pmpro_mail', $modules, true ), 'Migration preset has mail' ); -pm_assert( in_array( 'background_schedules', $modules, true ), 'Migration preset has schedules' ); -pm_assert( in_array( 'frontend_block', $modules, true ), 'Migration preset has frontend_block' ); -pm_assert( in_array( 'logged_in_sessions', $modules, true ), 'Migration preset has sessions' ); -$pm->resume(); - -// ------------------------------------------------------------------ -WP_CLI::log( '' ); -WP_CLI::log( '=== Module D: Schedules ===' ); -// ------------------------------------------------------------------ - -$pm->pause( array( 'background_schedules' ), 'test' ); -pm_assert( (bool) get_option( 'pmpro_as_halted', false ), 'AS halted when schedules module active' ); -pm_assert( has_filter( 'action_scheduler_before_execute' ) !== false, 'action_scheduler_before_execute filter attached' ); -pm_assert( has_filter( 'spawn_cron' ) !== false, 'spawn_cron filter attached' ); -$pm->resume(); -pm_assert( ! (bool) get_option( 'pmpro_as_halted', false ), 'AS resumed after schedules module deactivated' ); - -// ------------------------------------------------------------------ -WP_CLI::log( '' ); -WP_CLI::log( '=== Module B: Gateways (outbound block) ===' ); -// ------------------------------------------------------------------ - -$pm->pause( array( 'pmpro_gateways' ), 'test' ); - -$result = apply_filters( 'pre_http_request', false, array(), 'https://api.stripe.com/v1/charges' ); -pm_assert( is_wp_error( $result ), 'Outbound to Stripe blocked' ); -pm_assert( $result->get_error_code() === 'pmpro_pause_engine_blocked', 'Error code is pmpro_pause_engine_blocked' ); - -$result2 = apply_filters( 'pre_http_request', false, array(), 'https://api.wordpress.org/plugins/' ); -pm_assert( $result2 === false, 'Outbound to WordPress.org allowed' ); - -$result3 = apply_filters( 'pre_http_request', false, array(), 'https://api.braintreegateway.com/merchants' ); -pm_assert( is_wp_error( $result3 ), 'Outbound to Braintree blocked' ); - -$pm->resume(); - -// ------------------------------------------------------------------ -WP_CLI::log( '' ); -WP_CLI::log( '=== Module C: Mail (intercept) ===' ); -// ------------------------------------------------------------------ - -$pm->pause( array( 'pmpro_mail' ), 'test' ); -pm_assert( has_filter( 'pre_wp_mail' ) !== false, 'pre_wp_mail filter attached' ); - -$atts = array( - 'to' => 'test@example.com', - 'subject' => 'Pause Engine Test ' . time(), - 'message' => 'Test body', - 'headers' => '', - 'attachments' => array(), -); -$result = apply_filters( 'pre_wp_mail', null, $atts ); -pm_assert( $result === false, 'pre_wp_mail returns false (email suppressed)' ); - -$pm->resume(); - -// ------------------------------------------------------------------ -WP_CLI::log( '' ); -WP_CLI::log( '=== Module E: Frontend (REST block) ===' ); -// ------------------------------------------------------------------ - -$pm->pause( array( 'frontend_block' ), 'test' ); - -// As a non-admin user. -$subscriber_id = wp_insert_user( array( - 'user_login' => 'pm_test_sub_' . time(), - 'user_pass' => wp_generate_password(), - 'role' => 'subscriber', -) ); -wp_set_current_user( $subscriber_id ); - -$result = apply_filters( 'rest_authentication_errors', null ); -pm_assert( is_wp_error( $result ), 'REST blocked for subscriber' ); - -// As admin. -$admin_id = wp_insert_user( array( - 'user_login' => 'pm_test_admin_' . time(), - 'user_pass' => wp_generate_password(), - 'role' => 'administrator', -) ); -wp_set_current_user( $admin_id ); - -$result2 = apply_filters( 'rest_authentication_errors', null ); -pm_assert( ! is_wp_error( $result2 ), 'REST allowed for admin' ); - -// Login block — call our callback directly to avoid triggering WP core auth hooks. -$frontend_module = new PMPro_Pause_Module_Frontend(); -$frontend_module->activate(); - -$sub_user = get_user_by( 'ID', $subscriber_id ); -$result3 = $frontend_module->block_non_admin_login( $sub_user, $sub_user->user_login ); -pm_assert( is_wp_error( $result3 ), 'Login blocked for subscriber' ); - -$admin_user = get_user_by( 'ID', $admin_id ); -$result4 = $frontend_module->block_non_admin_login( $admin_user, $admin_user->user_login ); -pm_assert( ! is_wp_error( $result4 ), 'Login allowed for admin' ); - -$frontend_module->deactivate(); - -$pm->resume(); - -// Cleanup test users. -wp_delete_user( $subscriber_id ); -wp_delete_user( $admin_id ); -wp_set_current_user( 0 ); - -// ------------------------------------------------------------------ -WP_CLI::log( '' ); -WP_CLI::log( '=== Module A: Mutations ===' ); -// ------------------------------------------------------------------ - -$pm->pause( array( 'pmpro_mutations' ), 'test' ); - -pm_assert( has_filter( 'pmpro_change_level' ) !== false, 'pmpro_change_level filter attached' ); -pm_assert( has_filter( 'pmpro_checkout_checks' ) !== false, 'pmpro_checkout_checks filter attached' ); - -// Non-admin: pmpro_change_level should return false. -wp_set_current_user( 0 ); -$filtered = apply_filters( 'pmpro_change_level', 1, 999, 'inactive', null ); -pm_assert( $filtered === false, 'Level change blocked for non-admin' ); - -$pm->resume(); - -pm_assert( has_filter( 'pmpro_change_level', '__return_false' ) === false, 'pmpro_change_level filter removed after resume' ); - -// ------------------------------------------------------------------ -WP_CLI::log( '' ); -WP_CLI::log( '=== Disable Last Module Auto-Resumes ===' ); -// ------------------------------------------------------------------ - -$pm->pause( array( 'pmpro_mutations' ), 'test' ); -$pm->disable_module( 'pmpro_mutations' ); -pm_assert( ! $pm->is_paused(), 'Auto-resumed when last module disabled' ); - -// ------------------------------------------------------------------ -WP_CLI::log( '' ); -WP_CLI::log( '=== Actions Fired ===' ); -// ------------------------------------------------------------------ - -$activated = false; -$deactivated = false; -$mod_activated = null; -$mod_deactivated = null; - -add_action( 'pmpro_pause_engine_activated', function() use ( &$activated ) { $activated = true; } ); -add_action( 'pmpro_pause_engine_deactivated', function() use ( &$deactivated ) { $deactivated = true; } ); -add_action( 'pmpro_pause_module_activated', function( $s ) use ( &$mod_activated ) { $mod_activated = $s; } ); -add_action( 'pmpro_pause_module_deactivated', function( $s ) use ( &$mod_deactivated ) { $mod_deactivated = $s; } ); - -$pm->pause( array( 'pmpro_mutations' ), 'test' ); -pm_assert( $activated, 'pmpro_pause_engine_activated action fired' ); -pm_assert( $mod_activated === 'pmpro_mutations', 'pmpro_pause_module_activated fired with correct slug' ); - -$pm->resume(); -pm_assert( $deactivated, 'pmpro_pause_engine_deactivated action fired' ); -pm_assert( $mod_deactivated === 'pmpro_mutations', 'pmpro_pause_module_deactivated fired with correct slug' ); - -// ------------------------------------------------------------------ -// Summary -// ------------------------------------------------------------------ -WP_CLI::log( '' ); -WP_CLI::log( sprintf( '=== Results: %d passed, %d failed ===', $pm_test_pass, $pm_test_fail ) ); + $action = sanitize_text_field( $_GET['pmpro_pause'] ); + $engine = PMPro_Pause_Engine::instance(); + $page_title = 'PMPro Pause Engine Test Suite'; + + switch ( $action ) { + case 'migration': + case 'maintenance': + $result = $engine->pause_with_preset( $action ); + wp_die( + '

Pause Engine: ' . esc_html( $action ) . ' preset

' + . '

Result: ' . ( $result ? 'Activated' : 'Failed (already active or invalid)' ) . '

' + . '

View status | ' + . 'Resume

', + $page_title + ); + break; + + case 'custom': + $modules = isset( $_GET['modules'] ) ? array_map( 'trim', explode( ',', sanitize_text_field( $_GET['modules'] ) ) ) : array(); + $result = $engine->pause( $modules, 'manual-test' ); + wp_die( + '

Pause Engine: Custom modules

' + . '

Modules requested: ' . esc_html( implode( ', ', $modules ) ) . '

' + . '

Result: ' . ( $result ? 'Activated' : 'Failed (no valid modules)' ) . '

' + . '

View status | ' + . 'Resume

', + $page_title + ); + break; + + case 'resume': + $result = $engine->resume(); + wp_die( + '

Pause Engine: Resume

' + . '

Result: ' . ( $result ? 'Resumed' : 'Was not paused' ) . '

' + . '

View status

', + $page_title + ); + break; + + case 'status': + $state = $engine->get_state(); + $paused = $engine->is_paused(); + $output = '

Pause Engine Status

'; + $output .= '

Active: ' . ( $paused ? 'YES' : 'No' ) . '

'; + + if ( $paused ) { + $output .= '

Activated by: ' . esc_html( $state['activated_by'] ?? 'unknown' ) . '

'; + $output .= '

Activated at: ' . esc_html( date( 'Y-m-d H:i:s', $state['activated_at'] ?? 0 ) ) . '

'; + $output .= '

Active modules:

    '; + foreach ( $engine->get_active_modules() as $slug ) { + $output .= '
  • ' . esc_html( $slug ) . ' — is_active(): ' . ( $engine->is_module_active( $slug ) ? 'true' : 'false' ) . '
  • '; + } + $output .= '
'; + $output .= '

Resume

'; + + // Module diagnostics. + $output .= '

Module Diagnostics

'; + + // Mutations: try a level change. + if ( $engine->is_module_active( 'pmpro_mutations' ) ) { + $test_result = apply_filters( 'pmpro_change_level', 1, 0, 'active', 0 ); + $output .= '

pmpro_mutations: pmpro_change_level filter returns: ' . var_export( $test_result, true ) . ''; + $output .= ( false === $test_result ) ? ' — but admin bypass should allow it, so this returns the level ID since you are admin.' : ''; + $output .= '

'; + } + + // Gateways: test outbound HTTP to Stripe. + if ( $engine->is_module_active( 'pmpro_gateways' ) ) { + $test_response = wp_remote_get( 'https://api.stripe.com/v1/tokens', array( 'timeout' => 5 ) ); + if ( is_wp_error( $test_response ) && 'pmpro_pause_engine_blocked' === $test_response->get_error_code() ) { + $output .= '

pmpro_gateways: Outbound to api.stripe.com — BLOCKED (correct)

'; + } else { + $code = is_wp_error( $test_response ) ? $test_response->get_error_code() : wp_remote_retrieve_response_code( $test_response ); + $output .= '

pmpro_gateways: Outbound to api.stripe.com — NOT blocked (got: ' . esc_html( $code ) . ')

'; + } + } + + // Mail: check for queued emails in Action Scheduler. + if ( $engine->is_module_active( 'pmpro_mail' ) ) { + $output .= '

pmpro_mail: '; + if ( function_exists( 'as_get_scheduled_actions' ) ) { + $queued = as_get_scheduled_actions( array( + 'hook' => 'pmpro_pause_engine_send_queued_email', + 'status' => ActionScheduler_Store::STATUS_PENDING, + ) ); + $output .= count( $queued ) . ' email(s) queued in Action Scheduler.'; + $output .= ' Send a test email'; + } else { + $output .= 'Action Scheduler not available.'; + } + $output .= '

'; + } + + // Schedules: check cron and AS state. + if ( $engine->is_module_active( 'background_schedules' ) ) { + $cron_blocked = has_filter( 'spawn_cron', '__return_false' ); + $as_blocked = has_filter( 'action_scheduler_before_execute', '__return_false' ); + $output .= '

background_schedules: '; + $output .= 'spawn_cron blocked: ' . ( $cron_blocked ? 'YES' : 'NO' ); + $output .= ' | AS execution blocked: ' . ( $as_blocked ? 'YES' : 'NO' ); + $output .= '

'; + } + + } else { + $output .= '

Engine is idle.

'; + } + + $output .= '

Quick Actions

'; + + wp_die( $output, $page_title ); + break; + + case 'test_mail': + // Send a test email to trigger the mail queue. + wp_mail( get_option( 'admin_email' ), 'Pause Engine Test Email', 'This is a test email queued while the Pause Engine is active.' ); + wp_die( + '

Test Email Sent

' + . '

wp_mail() was called. If the mail module is active, it should be queued in Action Scheduler instead of sent.

' + . '

Back to status

', + $page_title + ); + break; + } +}); -if ( $pm_test_fail > 0 ) { - WP_CLI::error( 'Some tests failed.' ); -} else { - WP_CLI::success( 'All tests passed!' ); -} From a4ff22c72cbeb32c316312bdfcfc2c85b8ad8bb1 Mon Sep 17 00:00:00 2001 From: Dale Mugford Date: Thu, 26 Feb 2026 21:40:26 -0500 Subject: [PATCH 5/5] Copilot review: fixes Fixes for Copilot suggestions, tested. Signed-off-by: Dale Mugford --- classes/class-pmpro-pause-engine.php | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/classes/class-pmpro-pause-engine.php b/classes/class-pmpro-pause-engine.php index e717afff3..2886accde 100644 --- a/classes/class-pmpro-pause-engine.php +++ b/classes/class-pmpro-pause-engine.php @@ -1,6 +1,6 @@ register_module( new PMPro_Pause_Module_Frontend() ); $this->register_module( new PMPro_Pause_Module_Sessions() ); + /** + * Fires after built-in pause modules are registered. + * + * Use this to register custom modules via $engine->register_module(). + * + * @param PMPro_Pause_Engine $engine The pause engine instance. + */ + do_action( 'pmpro_pause_engine_register_modules', $this ); + // Always register the email replay callback so AS can process queued emails after resume. add_action( 'pmpro_pause_engine_send_queued_email', array( $this, 'send_queued_email' ) ); @@ -598,6 +607,10 @@ public function on_resume() { * Block checkout from reaching the gateway. */ public function block_gateway_outbound() { + if ( PMPro_Pause_Engine::current_user_can_bypass() ) { + return; + } + global $pmpro_msg, $pmpro_msgt; $pmpro_msg = __( 'Payment processing is temporarily suspended. Please try again later.', 'paid-memberships-pro' ); $pmpro_msgt = 'pmpro_error'; @@ -631,6 +644,10 @@ public function block_inbound_webhook() { * @return false|array|WP_Error */ public function block_outbound_http( $response, $parsed_args, $url ) { + if ( PMPro_Pause_Engine::current_user_can_bypass() ) { + return $response; + } + $host = wp_parse_url( $url, PHP_URL_HOST ); if ( empty( $host ) ) { return $response; @@ -705,6 +722,11 @@ public function on_resume() { * @return false */ public function intercept_email( $return, $atts ) { + // Don't re-intercept emails being replayed by the queue. + if ( doing_action( 'pmpro_pause_engine_send_queued_email' ) ) { + return $return; + } + $email_data = array( 'to' => $atts['to'], 'subject' => $atts['subject'],