From e40091aebecb2da001b8e0151a08baab73bfcbd3 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Fri, 2 Aug 2024 22:35:21 -0400 Subject: [PATCH 01/14] save --- classes/ActionScheduler_PastDueMonitor.php | 164 +++++++++++++++++++++ classes/abstracts/ActionScheduler.php | 6 + 2 files changed, 170 insertions(+) create mode 100644 classes/ActionScheduler_PastDueMonitor.php diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php new file mode 100644 index 000000000..969826417 --- /dev/null +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -0,0 +1,164 @@ +threshold_seconds = absint( apply_filters( 'action_scheduler_pastdue_actions_seconds', DAY_IN_SECONDS ) ); + $this->threshold_minimum = absint( apply_filters( 'action_scheduler_pastdue_actions_min', 1 ) ); + $this->check_interval = absint( apply_filters( 'action_scheduler_pastdue_actions_check_interval', ( $this->threshold_seconds / 4 ) ) ); + $this->email_interval_seconds = absint( apply_filters( 'action_scheduler_pastdue_actions_email_interval', HOUR_IN_SECONDS ) ); + + add_action( 'action_scheduler_stored_action', array( $this, 'on_action_stored' ), 0 ); + + if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || false == DOING_AJAX ) ) { + add_action( 'admin_notices', array( $this, 'action__admin_notices' ) ); + } + } + + /** + * Action: action_scheduler_stored_action + * + * Run check on number of past-due actions, + * and maybe send email notification. + */ + public function on_action_stored( $action_id ) { + if ( 'action_scheduler_stored_action' !== current_action() ) { + return; + } + + if ( ! $this->critical() ) { + return; + } + + $this->maybe_send_email(); + } + + protected function maybe_send_email() { + $transient = get_transient( 'action_scheduler_pastdue_actions_last_email' ); + + if ( ! empty( $transient ) ) { + return; + } + + $threshold = absint( apply_filters( 'action_scheduler_pastdue_actions_email_min', $this->threshold_minimum ) ); + + if ( $this->num_pastdue_actions < $threshold ) { + return; + } + + $to = get_bloginfo( 'admin_email' ); + $from = ''; + $subject = ''; + $message = ''; + + set_transient( 'action_scheduler_pastdue_actions_last_email', time(), $this->email_interval_seconds ); + + wp_mail( $to, $subject, $message, "From: $from" ); + } + + protected function critical() { + // Allow third-parties to preempt the default check logic. + $check = apply_filters( 'action_scheduler_pastdue_actions_check_pre', null ); + + // If no third-party preempted and there are no past-due actions, return early. + if ( ! is_null( $check ) ) { + return $check; + } + + $transient = get_transient( 'action_scheduler_pastdue_actions_critical' ); + + if ( ! empty( $transient ) ) { + return 'yes' === $transient; + } + + # Scheduled actions query arguments. + $query_args = array( + 'date' => as_get_datetime_object( time() - $threshold_seconds ), + 'status' => ActionScheduler_Store::STATUS_PENDING, + 'per_page' => $this->threshold_minimum, + ); + + $store = ActionScheduler_Store::instance(); + $this->num_pastdue_actions = ( int ) $store->query_actions( $query_args, 'count' ); + + # Check if past-due actions count is greater than or equal to threshold. + $check = ( $num_pastdue_actions >= $this->threshold_minimum ); + $check = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $check, $this->num_pastdue_actions, $this->threshold_seconds, $this->threshold_minimum ); + + $transient = $check ? 'yes' : 'no'; + set_transient( 'action_scheduler_pastdue_actions_critical', $transient, $this->check_interval ); + + return $check; + } + + /** + * Action: admin_notices + * + * Maybe check past-due actions, and print notice. + */ + public function action__admin_notices() { + if ( 'admin_notices' !== current_action() ) { + return; + } + + # Filter to prevent showing notice (ex: inappropriate user). + if ( ! apply_filters( 'action_scheduler_check_pastdue_actions', current_user_can( 'manage_options' ) ) ) { + return; + } + + if ( ! $this->critical() ) { + return; + } + + $actions_url = add_query_arg( array( + 'page' => 'action-scheduler', + 'status' => 'past-due', + 'order' => 'asc', + ), admin_url( 'tools.php' ) ); + + # Print notice. + echo '

'; + printf( + // translators: 1) is the number of affected actions, 2) is a link to an admin screen. + _n( + 'Action Scheduler: %1$d past-due action found; something may be wrong. Read documentation »', + 'Action Scheduler: %1$d past-due actions found; something may be wrong. Read documentation »', + $this->num_pastdue_actions, + 'action-scheduler' + ), + $this->num_pastdue_actions, + esc_attr( esc_url( $actions_url ) ) + ); + echo '

'; + + # Facilitate third-parties to evaluate and print notices. + do_action( 'action_scheduler_pastdue_actions_extra_notices', $query_args ); + } + +} diff --git a/classes/abstracts/ActionScheduler.php b/classes/abstracts/ActionScheduler.php index 0163f7072..e4128d28b 100644 --- a/classes/abstracts/ActionScheduler.php +++ b/classes/abstracts/ActionScheduler.php @@ -41,6 +41,10 @@ public static function admin_view() { return ActionScheduler_AdminView::instance(); } + public static function monitor() { + return ActionSchdeduler_PastDueMonitor::instance(); + } + /** * Get the absolute system path to the plugin directory, or a file therein * @static @@ -146,6 +150,7 @@ public static function init( $plugin_file ) { $logger = self::logger(); $runner = self::runner(); $admin_view = self::admin_view(); + $monitor = self::monitor(); // Ensure initialization on plugin activation. if ( ! did_action( 'init' ) ) { @@ -153,6 +158,7 @@ public static function init( $plugin_file ) { add_action( 'init', array( $store, 'init' ), 1, 0 ); add_action( 'init', array( $logger, 'init' ), 1, 0 ); add_action( 'init', array( $runner, 'init' ), 1, 0 ); + add_action( 'init', array( $monitor, 'init' ), 1, 0 ); add_action( 'init', From 672bf1da15389c1dda3721869c25acb5fdda50d8 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Fri, 2 Aug 2024 22:39:07 -0400 Subject: [PATCH 02/14] save --- classes/ActionScheduler_AdminView.php | 99 ------------------- .../ActionScheduler_AdminView_Deprecated.php | 20 +++- 2 files changed, 19 insertions(+), 100 deletions(-) diff --git a/classes/ActionScheduler_AdminView.php b/classes/ActionScheduler_AdminView.php index ed30950a7..fdb72cbf3 100644 --- a/classes/ActionScheduler_AdminView.php +++ b/classes/ActionScheduler_AdminView.php @@ -40,7 +40,6 @@ public function init() { } add_action( 'admin_menu', array( $this, 'register_menu' ) ); - add_action( 'admin_notices', array( $this, 'maybe_check_pastdue_actions' ) ); add_action( 'current_screen', array( $this, 'add_help_tabs' ) ); } } @@ -110,104 +109,6 @@ protected function get_list_table() { return $this->list_table; } - /** - * Action: admin_notices - * - * Maybe check past-due actions, and print notice. - * - * @uses $this->check_pastdue_actions() - */ - public function maybe_check_pastdue_actions() { - - # Filter to prevent checking actions (ex: inappropriate user). - if ( ! apply_filters( 'action_scheduler_check_pastdue_actions', current_user_can( 'manage_options' ) ) ) { - return; - } - - # Get last check transient. - $last_check = get_transient( 'action_scheduler_last_pastdue_actions_check' ); - - # If transient exists, we're within interval, so bail. - if ( ! empty( $last_check ) ) { - return; - } - - # Perform the check. - $this->check_pastdue_actions(); - } - - /** - * Check past-due actions, and print notice. - * - * @todo update $link_url to "Past-due" filter when released (see issue #510, PR #511) - */ - protected function check_pastdue_actions() { - - # Set thresholds. - $threshold_seconds = ( int ) apply_filters( 'action_scheduler_pastdue_actions_seconds', DAY_IN_SECONDS ); - $threshold_min = ( int ) apply_filters( 'action_scheduler_pastdue_actions_min', 1 ); - - // Set fallback value for past-due actions count. - $num_pastdue_actions = 0; - - // Allow third-parties to preempt the default check logic. - $check = apply_filters( 'action_scheduler_pastdue_actions_check_pre', null ); - - // If no third-party preempted and there are no past-due actions, return early. - if ( ! is_null( $check ) ) { - return; - } - - # Scheduled actions query arguments. - $query_args = array( - 'date' => as_get_datetime_object( time() - $threshold_seconds ), - 'status' => ActionScheduler_Store::STATUS_PENDING, - 'per_page' => $threshold_min, - ); - - # If no third-party preempted, run default check. - if ( is_null( $check ) ) { - $store = ActionScheduler_Store::instance(); - $num_pastdue_actions = ( int ) $store->query_actions( $query_args, 'count' ); - - # Check if past-due actions count is greater than or equal to threshold. - $check = ( $num_pastdue_actions >= $threshold_min ); - $check = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $check, $num_pastdue_actions, $threshold_seconds, $threshold_min ); - } - - # If check failed, set transient and abort. - if ( ! boolval( $check ) ) { - $interval = apply_filters( 'action_scheduler_pastdue_actions_check_interval', round( $threshold_seconds / 4 ), $threshold_seconds ); - set_transient( 'action_scheduler_last_pastdue_actions_check', time(), $interval ); - - return; - } - - $actions_url = add_query_arg( array( - 'page' => 'action-scheduler', - 'status' => 'past-due', - 'order' => 'asc', - ), admin_url( 'tools.php' ) ); - - # Print notice. - echo '

'; - printf( - // translators: 1) is the number of affected actions, 2) is a link to an admin screen. - _n( - 'Action Scheduler: %1$d past-due action found; something may be wrong. Read documentation »', - 'Action Scheduler: %1$d past-due actions found; something may be wrong. Read documentation »', - $num_pastdue_actions, - 'action-scheduler' - ), - $num_pastdue_actions, - esc_attr( esc_url( $actions_url ) ) - ); - echo '

'; - - # Facilitate third-parties to evaluate and print notices. - do_action( 'action_scheduler_pastdue_actions_extra_notices', $query_args ); - } - /** * Provide more information about the screen and its data in the help tab. */ diff --git a/deprecated/ActionScheduler_AdminView_Deprecated.php b/deprecated/ActionScheduler_AdminView_Deprecated.php index 62d964e0f..fb2b0c967 100644 --- a/deprecated/ActionScheduler_AdminView_Deprecated.php +++ b/deprecated/ActionScheduler_AdminView_Deprecated.php @@ -144,4 +144,22 @@ public function post_updated_messages( $messages ) { _deprecated_function( __METHOD__, '2.0.0' ); return $messages; } -} \ No newline at end of file + + /** + * Action: admin_notices + * + * Maybe check past-due actions, and print notice. + * + * @uses $this->check_pastdue_actions() + */ + public function maybe_check_pastdue_actions() { + _deprecated_function( __METHOD__, '3.8.2' ); + } + + /** + * Check past-due actions, and print notice. + */ + protected function check_pastdue_actions() { + _deprecated_function( __METHOD__, '3.8.2' ); + } +} From 1d7c248ab186f48690c303edab80faf9476f5c84 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Fri, 2 Aug 2024 22:54:26 -0400 Subject: [PATCH 03/14] save --- classes/ActionScheduler_PastDueMonitor.php | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index 969826417..4168d6ca6 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -10,8 +10,8 @@ class ActionScheduler_PastDueMonitor { protected $threshold_seconds; protected $threshold_minimum; - protected $check_interval; - protected $email_interval_seconds; + protected $interval_check; + protected $interval_email_seconds; protected $num_pastdue_actions = 0; /** @@ -29,10 +29,11 @@ public static function instance() { } public function init() { - $this->threshold_seconds = absint( apply_filters( 'action_scheduler_pastdue_actions_seconds', DAY_IN_SECONDS ) ); - $this->threshold_minimum = absint( apply_filters( 'action_scheduler_pastdue_actions_min', 1 ) ); - $this->check_interval = absint( apply_filters( 'action_scheduler_pastdue_actions_check_interval', ( $this->threshold_seconds / 4 ) ) ); - $this->email_interval_seconds = absint( apply_filters( 'action_scheduler_pastdue_actions_email_interval', HOUR_IN_SECONDS ) ); + $this->threshold_seconds = absint( apply_filters( 'action_scheduler_pastdue_actions_seconds', DAY_IN_SECONDS ) ); + $this->threshold_minimum = absint( apply_filters( 'action_scheduler_pastdue_actions_min', 1 ) ); + $this->interval_check = absint( apply_filters( 'action_scheduler_pastdue_actions_check_interval', ( $this->threshold_seconds / 4 ) ) ); + $this->interval_email_seconds = absint( apply_filters( 'action_scheduler_pastdue_actions_email_interval', HOUR_IN_SECONDS ) ); + $this->threshold_email_minimum = absint( apply_filters( 'action_scheduler_pastdue_actions_email_min', $this->threshold_minimum ) ) add_action( 'action_scheduler_stored_action', array( $this, 'on_action_stored' ), 0 ); @@ -66,9 +67,7 @@ protected function maybe_send_email() { return; } - $threshold = absint( apply_filters( 'action_scheduler_pastdue_actions_email_min', $this->threshold_minimum ) ); - - if ( $this->num_pastdue_actions < $threshold ) { + if ( $this->num_pastdue_actions < $this->threshold_email_min ) { return; } @@ -77,7 +76,7 @@ protected function maybe_send_email() { $subject = ''; $message = ''; - set_transient( 'action_scheduler_pastdue_actions_last_email', time(), $this->email_interval_seconds ); + set_transient( 'action_scheduler_pastdue_actions_last_email', time(), $this->interval_email_seconds ); wp_mail( $to, $subject, $message, "From: $from" ); } @@ -112,7 +111,7 @@ protected function critical() { $check = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $check, $this->num_pastdue_actions, $this->threshold_seconds, $this->threshold_minimum ); $transient = $check ? 'yes' : 'no'; - set_transient( 'action_scheduler_pastdue_actions_critical', $transient, $this->check_interval ); + set_transient( 'action_scheduler_pastdue_actions_critical', $transient, $this->interval_check ); return $check; } From 4e13f5fe689659157cf00dd35bd243e1c27e6d23 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Sat, 3 Aug 2024 02:17:03 -0400 Subject: [PATCH 04/14] save --- classes/ActionScheduler_PastDueMonitor.php | 34 ++++++++-------------- classes/abstracts/ActionScheduler.php | 3 +- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index 4168d6ca6..ed2bfb36e 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -2,6 +2,7 @@ /** * Class ActionScheduler_PastDueMonitor + * * @codeCoverageIgnore */ class ActionScheduler_PastDueMonitor { @@ -10,12 +11,15 @@ class ActionScheduler_PastDueMonitor { protected $threshold_seconds; protected $threshold_minimum; + protected $threshold_email_minimum; protected $interval_check; protected $interval_email_seconds; protected $num_pastdue_actions = 0; + protected $query_args = array(); /** * @return ActionScheduler_PastDueMonitor + * * @codeCoverageIgnore */ public static function instance() { @@ -33,34 +37,20 @@ public function init() { $this->threshold_minimum = absint( apply_filters( 'action_scheduler_pastdue_actions_min', 1 ) ); $this->interval_check = absint( apply_filters( 'action_scheduler_pastdue_actions_check_interval', ( $this->threshold_seconds / 4 ) ) ); $this->interval_email_seconds = absint( apply_filters( 'action_scheduler_pastdue_actions_email_interval', HOUR_IN_SECONDS ) ); - $this->threshold_email_minimum = absint( apply_filters( 'action_scheduler_pastdue_actions_email_min', $this->threshold_minimum ) ) + $this->threshold_email_minimum = absint( apply_filters( 'action_scheduler_pastdue_actions_email_min', $this->threshold_minimum ) ); - add_action( 'action_scheduler_stored_action', array( $this, 'on_action_stored' ), 0 ); + $this->maybe_send_email(); if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || false == DOING_AJAX ) ) { add_action( 'admin_notices', array( $this, 'action__admin_notices' ) ); } } - /** - * Action: action_scheduler_stored_action - * - * Run check on number of past-due actions, - * and maybe send email notification. - */ - public function on_action_stored( $action_id ) { - if ( 'action_scheduler_stored_action' !== current_action() ) { - return; - } - + protected function maybe_send_email() { if ( ! $this->critical() ) { return; } - $this->maybe_send_email(); - } - - protected function maybe_send_email() { $transient = get_transient( 'action_scheduler_pastdue_actions_last_email' ); if ( ! empty( $transient ) ) { @@ -97,17 +87,17 @@ protected function critical() { } # Scheduled actions query arguments. - $query_args = array( - 'date' => as_get_datetime_object( time() - $threshold_seconds ), + $this->query_args = array( + 'date' => as_get_datetime_object( time() - $this->threshold_seconds ), 'status' => ActionScheduler_Store::STATUS_PENDING, 'per_page' => $this->threshold_minimum, ); $store = ActionScheduler_Store::instance(); - $this->num_pastdue_actions = ( int ) $store->query_actions( $query_args, 'count' ); + $this->num_pastdue_actions = ( int ) $store->query_actions( $this->query_args, 'count' ); # Check if past-due actions count is greater than or equal to threshold. - $check = ( $num_pastdue_actions >= $this->threshold_minimum ); + $check = ( $this->num_pastdue_actions >= $this->threshold_minimum ); $check = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $check, $this->num_pastdue_actions, $this->threshold_seconds, $this->threshold_minimum ); $transient = $check ? 'yes' : 'no'; @@ -157,7 +147,7 @@ public function action__admin_notices() { echo '

'; # Facilitate third-parties to evaluate and print notices. - do_action( 'action_scheduler_pastdue_actions_extra_notices', $query_args ); + do_action( 'action_scheduler_pastdue_actions_extra_notices', $this->query_args ); } } diff --git a/classes/abstracts/ActionScheduler.php b/classes/abstracts/ActionScheduler.php index e4128d28b..0dab7f2ad 100644 --- a/classes/abstracts/ActionScheduler.php +++ b/classes/abstracts/ActionScheduler.php @@ -42,7 +42,7 @@ public static function admin_view() { } public static function monitor() { - return ActionSchdeduler_PastDueMonitor::instance(); + return ActionScheduler_PastDueMonitor::instance(); } /** @@ -186,6 +186,7 @@ function () { $store->init(); $logger->init(); $runner->init(); + $monitor->init(); self::$data_store_initialized = true; /** From 3ea707d35d25529289d81ca08b9069d78c0d5528 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Sat, 3 Aug 2024 02:31:29 -0400 Subject: [PATCH 05/14] save --- classes/ActionScheduler_PastDueMonitor.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index ed2bfb36e..f50ddabff 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -80,10 +80,14 @@ protected function critical() { return $check; } - $transient = get_transient( 'action_scheduler_pastdue_actions_critical' ); + if ( ! empty( $this->num_pastdue_actions ) ) { + return true; + } + + $transient = get_transient( 'action_scheduler_pastdue_actions_pause' ); if ( ! empty( $transient ) ) { - return 'yes' === $transient; + return false; } # Scheduled actions query arguments. @@ -100,8 +104,9 @@ protected function critical() { $check = ( $this->num_pastdue_actions >= $this->threshold_minimum ); $check = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $check, $this->num_pastdue_actions, $this->threshold_seconds, $this->threshold_minimum ); - $transient = $check ? 'yes' : 'no'; - set_transient( 'action_scheduler_pastdue_actions_critical', $transient, $this->interval_check ); + if ( ! $check ) { + set_transient( 'action_scheduler_pastdue_actions_pause', time(), $this->interval_check ); + } return $check; } From 0a01652fdfde9ac6e9034fd354658e818f0ee3ca Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Sat, 3 Aug 2024 02:50:35 -0400 Subject: [PATCH 06/14] save --- classes/ActionScheduler_PastDueMonitor.php | 37 ++++++++++++++-------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index f50ddabff..698151955 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -61,14 +61,21 @@ protected function maybe_send_email() { return; } - $to = get_bloginfo( 'admin_email' ); - $from = ''; - $subject = ''; - $message = ''; + $sitename = wp_parse_url( network_home_url(), PHP_URL_HOST ); + + if ( null !== $sitename ) { + if ( str_starts_with( $sitename, 'www.' ) ) { + $sitename = substr( $sitename, 4 ); + } + } + + $to = get_site_option( 'admin_email' ); + $subject = sprintf( '%s: past-due scheduled actions', $sitename ); + $message = $this->message(); set_transient( 'action_scheduler_pastdue_actions_last_email', time(), $this->interval_email_seconds ); - wp_mail( $to, $subject, $message, "From: $from" ); + wp_mail( $to, $subject, $message ); } protected function critical() { @@ -130,15 +137,23 @@ public function action__admin_notices() { return; } + # Print notice. + echo '

'; + echo $this->message(); + echo '

'; + + # Facilitate third-parties to evaluate and print notices. + do_action( 'action_scheduler_pastdue_actions_extra_notices', $this->query_args ); + } + + protected function message() { $actions_url = add_query_arg( array( 'page' => 'action-scheduler', 'status' => 'past-due', 'order' => 'asc', ), admin_url( 'tools.php' ) ); - # Print notice. - echo '

'; - printf( + return sprintf( // translators: 1) is the number of affected actions, 2) is a link to an admin screen. _n( 'Action Scheduler: %1$d past-due action found; something may be wrong. Read documentation »', @@ -147,12 +162,8 @@ public function action__admin_notices() { 'action-scheduler' ), $this->num_pastdue_actions, - esc_attr( esc_url( $actions_url ) ) + esc_url( $actions_url ) ); - echo '

'; - - # Facilitate third-parties to evaluate and print notices. - do_action( 'action_scheduler_pastdue_actions_extra_notices', $this->query_args ); } } From 16dafb30ac7426516c251d4598cd4b475375f02f Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Sat, 3 Aug 2024 02:54:45 -0400 Subject: [PATCH 07/14] save --- classes/ActionScheduler_PastDueMonitor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index 698151955..8137769d5 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -57,7 +57,7 @@ protected function maybe_send_email() { return; } - if ( $this->num_pastdue_actions < $this->threshold_email_min ) { + if ( $this->num_pastdue_actions < $this->threshold_email_minimum ) { return; } @@ -70,7 +70,7 @@ protected function maybe_send_email() { } $to = get_site_option( 'admin_email' ); - $subject = sprintf( '%s: past-due scheduled actions', $sitename ); + $subject = sprintf( 'Action Scheduler: past-due scheduled actions (%s)', $sitename ); $message = $this->message(); set_transient( 'action_scheduler_pastdue_actions_last_email', time(), $this->interval_email_seconds ); From 70b028e0117fd4db5ac4b352345db05877157ef7 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Mon, 5 Aug 2024 19:24:33 -0400 Subject: [PATCH 08/14] save --- classes/ActionScheduler_PastDueMonitor.php | 130 +++++++++++++-------- 1 file changed, 83 insertions(+), 47 deletions(-) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index 8137769d5..e37639c6d 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -7,6 +7,9 @@ */ class ActionScheduler_PastDueMonitor { + const TRANSIENT_LAST_EMAIL = 'action_scheduler_pastdue_actions_last_email'; + const TRANSIENT_CHECK_INTERVAL = 'action_scheduler_last_pastdue_actions_check'; + private static $monitor = null; protected $threshold_seconds; @@ -32,6 +35,11 @@ public static function instance() { return self::$monitor; } + /** + * Initialize. + * + * @return void + */ public function init() { $this->threshold_seconds = absint( apply_filters( 'action_scheduler_pastdue_actions_seconds', DAY_IN_SECONDS ) ); $this->threshold_minimum = absint( apply_filters( 'action_scheduler_pastdue_actions_min', 1 ) ); @@ -39,83 +47,98 @@ public function init() { $this->interval_email_seconds = absint( apply_filters( 'action_scheduler_pastdue_actions_email_interval', HOUR_IN_SECONDS ) ); $this->threshold_email_minimum = absint( apply_filters( 'action_scheduler_pastdue_actions_email_min', $this->threshold_minimum ) ); - $this->maybe_send_email(); + add_action( 'action_scheduler_stored_action', array( $this, 'maybe_send_email' ) ); if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || false == DOING_AJAX ) ) { add_action( 'admin_notices', array( $this, 'action__admin_notices' ) ); } } - protected function maybe_send_email() { - if ( ! $this->critical() ) { - return; - } - - $transient = get_transient( 'action_scheduler_pastdue_actions_last_email' ); + /** + * Check if threshold for past-due actions is flooded. + * + * @return bool + */ + protected function flooded() { + $transient = get_transient( self::TRANSIENT_CHECK_INTERVAL ); if ( ! empty( $transient ) ) { - return; - } - - if ( $this->num_pastdue_actions < $this->threshold_email_minimum ) { - return; + return false; } - $sitename = wp_parse_url( network_home_url(), PHP_URL_HOST ); + # Scheduled actions query arguments. + $this->query_args = array( + 'date' => as_get_datetime_object( time() - $this->threshold_seconds ), + 'status' => ActionScheduler_Store::STATUS_PENDING, + 'per_page' => $this->threshold_minimum, + ); - if ( null !== $sitename ) { - if ( str_starts_with( $sitename, 'www.' ) ) { - $sitename = substr( $sitename, 4 ); - } - } + $store = ActionScheduler_Store::instance(); + $this->num_pastdue_actions = ( int ) $store->query_actions( $this->query_args, 'count' ); - $to = get_site_option( 'admin_email' ); - $subject = sprintf( 'Action Scheduler: past-due scheduled actions (%s)', $sitename ); - $message = $this->message(); + # Check if past-due actions count is greater than or equal to threshold. + $flooded = ( $this->num_pastdue_actions >= $this->threshold_minimum ); + $flooded = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $flooded, $this->num_pastdue_actions, $this->threshold_seconds, $this->threshold_minimum ); - set_transient( 'action_scheduler_pastdue_actions_last_email', time(), $this->interval_email_seconds ); + # If not flooded, delay next check. + if ( ! $flooded ) { + set_transient( self::TRANSIENT_CHECK_INTERVAL, time(), $this->interval_check ); + } - wp_mail( $to, $subject, $message ); + return $flooded; } - protected function critical() { + /** + * Action: action_scheduler_stored_action + * + * Maybe send email notice of past-due actions over threshold. + * + * @return void + */ + public function maybe_send_email() { + if ( 'action_scheduler_stored_action' !== current_action() ) { + return; + } + // Allow third-parties to preempt the default check logic. - $check = apply_filters( 'action_scheduler_pastdue_actions_check_pre', null ); + $pre = apply_filters( 'action_scheduler_pastdue_actions_check_pre', null ); // If no third-party preempted and there are no past-due actions, return early. - if ( ! is_null( $check ) ) { - return $check; + if ( ! is_null( $pre ) ) { + return; } - if ( ! empty( $this->num_pastdue_actions ) ) { - return true; + if ( ! $this->flooded() ) { + return; } - $transient = get_transient( 'action_scheduler_pastdue_actions_pause' ); + $transient = get_transient( self::TRANSIENT_LAST_EMAIL ); if ( ! empty( $transient ) ) { - return false; + return; } - # Scheduled actions query arguments. - $this->query_args = array( - 'date' => as_get_datetime_object( time() - $this->threshold_seconds ), - 'status' => ActionScheduler_Store::STATUS_PENDING, - 'per_page' => $this->threshold_minimum, - ); - - $store = ActionScheduler_Store::instance(); - $this->num_pastdue_actions = ( int ) $store->query_actions( $this->query_args, 'count' ); + if ( $this->num_pastdue_actions < $this->threshold_email_minimum ) { + return; + } - # Check if past-due actions count is greater than or equal to threshold. - $check = ( $this->num_pastdue_actions >= $this->threshold_minimum ); - $check = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $check, $this->num_pastdue_actions, $this->threshold_seconds, $this->threshold_minimum ); + $sitename = wp_parse_url( network_home_url(), PHP_URL_HOST ); - if ( ! $check ) { - set_transient( 'action_scheduler_pastdue_actions_pause', time(), $this->interval_check ); + if ( null !== $sitename ) { + if ( str_starts_with( $sitename, 'www.' ) ) { + $sitename = substr( $sitename, 4 ); + } } - return $check; + $to = get_site_option( 'admin_email' ); + $subject = sprintf( 'Action Scheduler: past-due scheduled actions (%s)', $sitename ); + $message = $this->message(); + + set_transient( self::TRANSIENT_LAST_EMAIL, time(), $this->interval_email_seconds ); + + wp_mail( $to, $subject, $message, array( + 'Content-type: text/html; charset=UTF-8', + ) ); } /** @@ -133,7 +156,15 @@ public function action__admin_notices() { return; } - if ( ! $this->critical() ) { + // Allow third-parties to preempt the default check logic. + $pre = apply_filters( 'action_scheduler_pastdue_actions_check_pre', null ); + + // If no third-party preempted and there are no past-due actions, return early. + if ( ! is_null( $pre ) ) { + return; + } + + if ( ! $this->flooded() ) { return; } @@ -146,6 +177,11 @@ public function action__admin_notices() { do_action( 'action_scheduler_pastdue_actions_extra_notices', $this->query_args ); } + /** + * Message for admin notice and email notice. + * + * @return string + */ protected function message() { $actions_url = add_query_arg( array( 'page' => 'action-scheduler', From c2e42d4d56d252218318cdf179d6671a2a420ae5 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Mon, 5 Aug 2024 19:37:51 -0400 Subject: [PATCH 09/14] save --- classes/ActionScheduler_PastDueMonitor.php | 105 ++++++++++++++++----- 1 file changed, 84 insertions(+), 21 deletions(-) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index e37639c6d..8fe93dfc3 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -7,20 +7,69 @@ */ class ActionScheduler_PastDueMonitor { - const TRANSIENT_LAST_EMAIL = 'action_scheduler_pastdue_actions_last_email'; + const TRANSIENT_LAST_EMAIL = 'action_scheduler_pastdue_actions_last_email'; const TRANSIENT_CHECK_INTERVAL = 'action_scheduler_last_pastdue_actions_check'; + /** + * Instance. + * + * @var null|self + */ private static $monitor = null; + /** + * Number of seconds in the past to qualify as past-due action. + * + * @var int + */ protected $threshold_seconds; - protected $threshold_minimum; + + /** + * Number of minimum past-due actions to display admin notice. + * + * @var int + */ + protected $threshold_admin_minimum; + + /** + * Number of minimum past-due actions to send email notice. + * + * @var int + */ protected $threshold_email_minimum; + + /** + * Number of seconds before past-due actions check after + * negative (not flooded) check. + * + * @var int + */ protected $interval_check; + + /** + * Number of seconds between email notices. + * + * @var int + */ protected $interval_email_seconds; + + /** + * Number of past-due actions (determined by thresholds). + * + * @var int + */ protected $num_pastdue_actions = 0; + + /** + * Query arguments for past-due actions. + * + * @var array + */ protected $query_args = array(); /** + * Get singleton instance. + * * @return ActionScheduler_PastDueMonitor * * @codeCoverageIgnore @@ -28,7 +77,7 @@ class ActionScheduler_PastDueMonitor { public static function instance() { if ( empty( self::$monitor ) ) { - $class = apply_filters( 'action_scheduler_pastdue_actions_monitor_class', 'ActionScheduler_PastDueMonitor' ); + $class = apply_filters( 'action_scheduler_pastdue_actions_monitor_class', 'ActionScheduler_PastDueMonitor' ); self::$monitor = new $class(); } @@ -49,7 +98,7 @@ public function init() { add_action( 'action_scheduler_stored_action', array( $this, 'maybe_send_email' ) ); - if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || false == DOING_AJAX ) ) { + if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || empty( DOING_AJAX ) ) ) { add_action( 'admin_notices', array( $this, 'action__admin_notices' ) ); } } @@ -66,7 +115,7 @@ protected function flooded() { return false; } - # Scheduled actions query arguments. + // Scheduled actions query arguments. $this->query_args = array( 'date' => as_get_datetime_object( time() - $this->threshold_seconds ), 'status' => ActionScheduler_Store::STATUS_PENDING, @@ -74,13 +123,14 @@ protected function flooded() { ); $store = ActionScheduler_Store::instance(); - $this->num_pastdue_actions = ( int ) $store->query_actions( $this->query_args, 'count' ); - # Check if past-due actions count is greater than or equal to threshold. + $this->num_pastdue_actions = absint( $store->query_actions( $this->query_args, 'count' ) ); + + // Check if past-due actions count is greater than or equal to threshold. $flooded = ( $this->num_pastdue_actions >= $this->threshold_minimum ); - $flooded = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $flooded, $this->num_pastdue_actions, $this->threshold_seconds, $this->threshold_minimum ); + $flooded = (bool) apply_filters( 'action_scheduler_pastdue_actions_check', $flooded, $this->num_pastdue_actions, $this->threshold_seconds, $this->threshold_minimum ); - # If not flooded, delay next check. + // If not flooded, delay next check. if ( ! $flooded ) { set_transient( self::TRANSIENT_CHECK_INTERVAL, time(), $this->interval_check ); } @@ -133,12 +183,13 @@ public function maybe_send_email() { $to = get_site_option( 'admin_email' ); $subject = sprintf( 'Action Scheduler: past-due scheduled actions (%s)', $sitename ); $message = $this->message(); + $headers = array( + 'Content-type: text/html; charset=UTF-8', + ); set_transient( self::TRANSIENT_LAST_EMAIL, time(), $this->interval_email_seconds ); - wp_mail( $to, $subject, $message, array( - 'Content-type: text/html; charset=UTF-8', - ) ); + wp_mail( $to, $subject, $message, $headers ); } /** @@ -151,7 +202,7 @@ public function action__admin_notices() { return; } - # Filter to prevent showing notice (ex: inappropriate user). + // Filter to prevent showing notice (ex: inappropriate user). if ( ! apply_filters( 'action_scheduler_check_pastdue_actions', current_user_can( 'manage_options' ) ) ) { return; } @@ -168,12 +219,21 @@ public function action__admin_notices() { return; } - # Print notice. + // Print notice. echo '

'; - echo $this->message(); + echo wp_kses( + $this->message(), + array( + 'strong' => array(), + 'a' => array( + 'target' => true, + 'href' => true, + ), + ) + ); echo '

'; - # Facilitate third-parties to evaluate and print notices. + // Facilitate third-parties to evaluate and print notices. do_action( 'action_scheduler_pastdue_actions_extra_notices', $this->query_args ); } @@ -183,11 +243,14 @@ public function action__admin_notices() { * @return string */ protected function message() { - $actions_url = add_query_arg( array( - 'page' => 'action-scheduler', - 'status' => 'past-due', - 'order' => 'asc', - ), admin_url( 'tools.php' ) ); + $actions_url = add_query_arg( + array( + 'page' => 'action-scheduler', + 'status' => 'past-due', + 'order' => 'asc', + ), + admin_url( 'tools.php' ) + ); return sprintf( // translators: 1) is the number of affected actions, 2) is a link to an admin screen. From df102dc716507221b0794506a365abdf7ca97d37 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Mon, 5 Aug 2024 19:48:45 -0400 Subject: [PATCH 10/14] save --- classes/ActionScheduler_PastDueMonitor.php | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index 8fe93dfc3..3846aff1d 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -29,7 +29,7 @@ class ActionScheduler_PastDueMonitor { * * @var int */ - protected $threshold_admin_minimum; + protected $threshold_minimum; /** * Number of minimum past-due actions to send email notice. @@ -130,10 +130,7 @@ protected function flooded() { $flooded = ( $this->num_pastdue_actions >= $this->threshold_minimum ); $flooded = (bool) apply_filters( 'action_scheduler_pastdue_actions_check', $flooded, $this->num_pastdue_actions, $this->threshold_seconds, $this->threshold_minimum ); - // If not flooded, delay next check. - if ( ! $flooded ) { - set_transient( self::TRANSIENT_CHECK_INTERVAL, time(), $this->interval_check ); - } + set_transient( self::TRANSIENT_CHECK_INTERVAL, time(), $this->interval_check ); return $flooded; } @@ -141,11 +138,12 @@ protected function flooded() { /** * Action: action_scheduler_stored_action * - * Maybe send email notice of past-due actions over threshold. + * Delete check interval transient, perform flooded check, + * and maybe send email. * * @return void */ - public function maybe_send_email() { + public function on_action_stored() { if ( 'action_scheduler_stored_action' !== current_action() ) { return; } @@ -158,10 +156,21 @@ public function maybe_send_email() { return; } + delete_transient( self::TRANSIENT_CHECK_INTERVAL ); + if ( ! $this->flooded() ) { return; } + $this->maybe_send_email(); + } + + /** + * Maybe send email notice of past-due actions over threshold. + * + * @return void + */ + protected function maybe_send_email() { $transient = get_transient( self::TRANSIENT_LAST_EMAIL ); if ( ! empty( $transient ) ) { From fc8a8dcb66b13d88c5cf1a0c2b12d5389adf161b Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Mon, 5 Aug 2024 19:51:17 -0400 Subject: [PATCH 11/14] save --- classes/ActionScheduler_PastDueMonitor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index 3846aff1d..f37c64554 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -96,7 +96,7 @@ public function init() { $this->interval_email_seconds = absint( apply_filters( 'action_scheduler_pastdue_actions_email_interval', HOUR_IN_SECONDS ) ); $this->threshold_email_minimum = absint( apply_filters( 'action_scheduler_pastdue_actions_email_min', $this->threshold_minimum ) ); - add_action( 'action_scheduler_stored_action', array( $this, 'maybe_send_email' ) ); + add_action( 'action_scheduler_stored_action', array( $this, 'on_action_stored' ) ); if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || empty( DOING_AJAX ) ) ) { add_action( 'admin_notices', array( $this, 'action__admin_notices' ) ); From 36af56c9e55a2ec2b6392642a743113adf460889 Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Wed, 7 Aug 2024 22:17:31 -0400 Subject: [PATCH 12/14] save --- classes/ActionScheduler_PastDueMonitor.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index f37c64554..75ec5334b 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -158,6 +158,9 @@ public function on_action_stored() { delete_transient( self::TRANSIENT_CHECK_INTERVAL ); + // Removing the callback before querying actions is necessary to prevent loop. + remove_action( 'action_scheduler_stored_action', array( $this, 'on_action_stored' ) ); + if ( ! $this->flooded() ) { return; } From 25a186b9aedd9a51d495e05c5d0feb860319886a Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Fri, 6 Sep 2024 21:30:53 -0400 Subject: [PATCH 13/14] save --- classes/ActionScheduler_PastDueMonitor.php | 89 +++++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index 75ec5334b..0cea25b82 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -67,6 +67,20 @@ class ActionScheduler_PastDueMonitor { */ protected $query_args = array(); + /** + * Notification methods. + * + * @var array + */ + protected $notify_methods = array(); + + /** + * Email address to send notification. + * + * @var string + */ + protected $notify_email_to; + /** * Get singleton instance. * @@ -95,6 +109,16 @@ public function init() { $this->interval_check = absint( apply_filters( 'action_scheduler_pastdue_actions_check_interval', ( $this->threshold_seconds / 4 ) ) ); $this->interval_email_seconds = absint( apply_filters( 'action_scheduler_pastdue_actions_email_interval', HOUR_IN_SECONDS ) ); $this->threshold_email_minimum = absint( apply_filters( 'action_scheduler_pastdue_actions_email_min', $this->threshold_minimum ) ); + $this->notify_methods = $this->notify_methods(); + + $notify_email_to = apply_filters( 'action_scheduler_pastdue_monitor_notify_email_to', get_site_option( 'admin_email' ) ); + + if ( ! is_string( $notify_email_to ) || ! is_email( $notify_email_to ) ) { + trigger_error( 'Invalid email address provided for past-due actions flooded email notification: using site\'s administrator email address.', E_USER_WARNING ); + $notify_email_to = get_site_option( 'admin_email' ); + } + + $this->notify_email_to = $notify_email_to; add_action( 'action_scheduler_stored_action', array( $this, 'on_action_stored' ) ); @@ -103,6 +127,60 @@ public function init() { } } + /** + * Check if notification method enabled. + * + * @param string $method + * @return bool + */ + protected function notify( $method ) { + return ! empty( $this->notify_methods[ $method ] ); + } + + /** + * Get enabled notification methods. + * + * @return array + */ + protected function notify_methods() { + $all = array( + 'admin' => true, + 'email' => true, + ); + + $default = $all; + $default['admin'] = function_exists( 'current_user_can' ) && current_user_can( 'manage_options' ); + $methods = $default; + + // Apply deprecated filter. + if ( has_filter( 'action_scheduler_check_pastdue_actions' ) ) { + $methods['admin'] = apply_filters_deprecated( + 'action_scheduler_check_pastdue_actions', + array( $methods['admin'] ), + '', // todo: add version number + 'action_scheduler_pastdue_monitor_notify' + ); + } + + // Allow site devs to specify notification preference. + $methods = apply_filters( 'action_scheduler_pastdue_monitor_notify', $methods ); + + // Support for scalar and bool values (ex: `__return_false`). + if ( is_scalar( $methods ) || is_bool( $methods ) ) { + return boolval( $methods ) ? $all : array(); + } + + // Insist on an array. + if ( ! is_array( $methods ) ) { + return $default; + } + + // Clear out unsupported methods. + $methods = array_intersect_key( $methods, $all ); + + return $methods; + } + /** * Check if threshold for past-due actions is flooded. * @@ -174,6 +252,10 @@ public function on_action_stored() { * @return void */ protected function maybe_send_email() { + if ( ! $this->notify( 'email' ) ) { + return; + } + $transient = get_transient( self::TRANSIENT_LAST_EMAIL ); if ( ! empty( $transient ) ) { @@ -192,7 +274,7 @@ protected function maybe_send_email() { } } - $to = get_site_option( 'admin_email' ); + $to = $this->notify_email_to; $subject = sprintf( 'Action Scheduler: past-due scheduled actions (%s)', $sitename ); $message = $this->message(); $headers = array( @@ -202,6 +284,8 @@ protected function maybe_send_email() { set_transient( self::TRANSIENT_LAST_EMAIL, time(), $this->interval_email_seconds ); wp_mail( $to, $subject, $message, $headers ); + + do_action( 'action_scheduler_pastdue_monitor_notified_email' ); } /** @@ -214,8 +298,7 @@ public function action__admin_notices() { return; } - // Filter to prevent showing notice (ex: inappropriate user). - if ( ! apply_filters( 'action_scheduler_check_pastdue_actions', current_user_can( 'manage_options' ) ) ) { + if ( ! $this->notify( 'admin' ) ) { return; } From 4d643f5c28d9a2f8d2e590ff8e6b1e383e9da41e Mon Sep 17 00:00:00 2001 From: Caleb Stauffer Date: Tue, 17 Sep 2024 13:54:07 -0400 Subject: [PATCH 14/14] add documentation for filter --- classes/ActionScheduler_PastDueMonitor.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/classes/ActionScheduler_PastDueMonitor.php b/classes/ActionScheduler_PastDueMonitor.php index 0cea25b82..6b6932961 100644 --- a/classes/ActionScheduler_PastDueMonitor.php +++ b/classes/ActionScheduler_PastDueMonitor.php @@ -163,6 +163,16 @@ protected function notify_methods() { } // Allow site devs to specify notification preference. + /** + * Allow site devs to specify the active notification methods. + * + * Default: + * array( 'admin' => true, 'email' => true ) + * + * Supported: + * - array + * - boolean: true = enable all, false = disable all + */ $methods = apply_filters( 'action_scheduler_pastdue_monitor_notify', $methods ); // Support for scalar and bool values (ex: `__return_false`).