├── classes ├── ActionScheduler_Exception.php ├── actions │ ├── ActionScheduler_FinishedAction.php │ ├── ActionScheduler_NullAction.php │ ├── ActionScheduler_CanceledAction.php │ └── ActionScheduler_Action.php ├── ActionScheduler_NullLogEntry.php ├── schedules │ ├── ActionScheduler_Schedule.php │ ├── ActionScheduler_NullSchedule.php │ ├── ActionScheduler_CanceledSchedule.php │ ├── ActionScheduler_SimpleSchedule.php │ ├── ActionScheduler_IntervalSchedule.php │ └── ActionScheduler_CronSchedule.php ├── migration │ ├── DryRun_LogMigrator.php │ ├── DryRun_ActionMigrator.php │ ├── LogMigrator.php │ ├── ActionScheduler_DBStoreMigrator.php │ ├── BatchFetcher.php │ ├── Scheduler.php │ ├── ActionMigrator.php │ ├── Runner.php │ └── Config.php ├── ActionScheduler_ActionClaim.php ├── data-stores │ ├── ActionScheduler_wpPostStore_TaxonomyRegistrar.php │ ├── ActionScheduler_wpPostStore_PostStatusRegistrar.php │ ├── ActionScheduler_wpPostStore_PostTypeRegistrar.php │ └── ActionScheduler_DBLogger.php ├── ActionScheduler_InvalidActionException.php ├── abstracts │ ├── ActionScheduler_Lock.php │ ├── ActionScheduler_Abstract_Schedule.php │ ├── ActionScheduler_WPCLI_Command.php │ ├── ActionScheduler_Abstract_RecurringSchedule.php │ ├── ActionScheduler_TimezoneHelper.php │ └── ActionScheduler_Abstract_Schema.php ├── WP_CLI │ ├── Action │ │ ├── Next_Command.php │ │ ├── Get_Command.php │ │ ├── Delete_Command.php │ │ ├── List_Command.php │ │ ├── Cancel_Command.php │ │ ├── Generate_Command.php │ │ ├── Create_Command.php │ │ └── Run_Command.php │ ├── ProgressBar.php │ ├── ActionScheduler_WPCLI_Clean_Command.php │ └── Migration_Command.php ├── ActionScheduler_LogEntry.php ├── ActionScheduler_DateTime.php ├── ActionScheduler_AsyncRequest_QueueRunner.php ├── ActionScheduler_SystemInformation.php ├── ActionScheduler_FatalErrorMonitor.php ├── ActionScheduler_RecurringActionScheduler.php ├── schema │ ├── ActionScheduler_LoggerSchema.php │ └── ActionScheduler_StoreSchema.php ├── ActionScheduler_Compatibility.php ├── ActionScheduler_Versions.php ├── ActionScheduler_OptionLock.php ├── ActionScheduler_WPCommentCleaner.php └── ActionScheduler_wcSystemStatus.php ├── lib ├── cron-expression │ ├── CronExpression_MinutesField.php │ ├── CronExpression_YearField.php │ ├── LICENSE │ ├── CronExpression_FieldInterface.php │ ├── CronExpression_HoursField.php │ ├── CronExpression_MonthField.php │ ├── CronExpression_FieldFactory.php │ ├── CronExpression_AbstractField.php │ ├── README.md │ ├── CronExpression_DayOfMonthField.php │ └── CronExpression_DayOfWeekField.php └── WP_Async_Request.php ├── deprecated ├── ActionScheduler_Schedule_Deprecated.php ├── ActionScheduler_Abstract_QueueRunner_Deprecated.php ├── ActionScheduler_Store_Deprecated.php └── functions.php ├── README.md └── action-scheduler.php /classes/ActionScheduler_Exception.php: -------------------------------------------------------------------------------- 1 | set_schedule( new ActionScheduler_NullSchedule() ); 17 | } 18 | 19 | /** 20 | * Execute action. 21 | */ 22 | public function execute() { 23 | // don't execute. 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /classes/ActionScheduler_ActionClaim.php: -------------------------------------------------------------------------------- 1 | id = $id; 29 | $this->action_ids = $action_ids; 30 | } 31 | 32 | /** 33 | * Get claim ID. 34 | */ 35 | public function get_id() { 36 | return $this->id; 37 | } 38 | 39 | /** 40 | * Get IDs of claimed actions. 41 | */ 42 | public function get_actions() { 43 | return $this->action_ids; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /classes/schedules/ActionScheduler_NullSchedule.php: -------------------------------------------------------------------------------- 1 | scheduled_date = null; 22 | } 23 | 24 | /** 25 | * This schedule has no scheduled DateTime, so we need to override the parent __sleep(). 26 | * 27 | * @return array 28 | */ 29 | public function __sleep() { 30 | return array(); 31 | } 32 | 33 | /** 34 | * Wakeup. 35 | */ 36 | public function __wakeup() { 37 | $this->scheduled_date = null; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php: -------------------------------------------------------------------------------- 1 | taxonomy_args() ); 15 | } 16 | 17 | /** 18 | * Get taxonomy arguments. 19 | */ 20 | protected function taxonomy_args() { 21 | $args = array( 22 | 'label' => __( 'Action Group', 'action-scheduler' ), 23 | 'public' => false, 24 | 'hierarchical' => false, 25 | 'show_admin_column' => true, 26 | 'query_var' => false, 27 | 'rewrite' => false, 28 | ); 29 | 30 | $args = apply_filters( 'action_scheduler_taxonomy_args', $args ); 31 | return $args; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/cron-expression/CronExpression_MinutesField.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class CronExpression_MinutesField extends CronExpression_AbstractField 9 | { 10 | /** 11 | * {@inheritdoc} 12 | */ 13 | public function isSatisfiedBy(DateTime $date, $value) 14 | { 15 | return $this->isSatisfied($date->format('i'), $value); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function increment(DateTime $date, $invert = false) 22 | { 23 | if ($invert) { 24 | $date->modify('-1 minute'); 25 | } else { 26 | $date->modify('+1 minute'); 27 | } 28 | 29 | return $this; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function validate($value) 36 | { 37 | return (bool) preg_match('/[\*,\/\-0-9]+/', $value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /deprecated/ActionScheduler_Schedule_Deprecated.php: -------------------------------------------------------------------------------- 1 | get_date(); 19 | $replacement_method = 'get_date()'; 20 | } else { 21 | $return_value = $this->get_next( $after ); 22 | $replacement_method = 'get_next( $after )'; 23 | } 24 | 25 | _deprecated_function( __METHOD__, '3.0.0', __CLASS__ . '::' . $replacement_method ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 26 | 27 | return $return_value; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php: -------------------------------------------------------------------------------- 1 | set_schedule( new ActionScheduler_NullSchedule() ); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/cron-expression/CronExpression_YearField.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class CronExpression_YearField extends CronExpression_AbstractField 9 | { 10 | /** 11 | * {@inheritdoc} 12 | */ 13 | public function isSatisfiedBy(DateTime $date, $value) 14 | { 15 | return $this->isSatisfied($date->format('Y'), $value); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function increment(DateTime $date, $invert = false) 22 | { 23 | if ($invert) { 24 | $date->modify('-1 year'); 25 | $date->setDate($date->format('Y'), 12, 31); 26 | $date->setTime(23, 59, 0); 27 | } else { 28 | $date->modify('+1 year'); 29 | $date->setDate($date->format('Y'), 1, 1); 30 | $date->setTime(0, 0, 0); 31 | } 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function validate($value) 40 | { 41 | return (bool) preg_match('/[\*,\/\-0-9]+/', $value); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/cron-expression/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Michael Dowling and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /deprecated/ActionScheduler_Store_Deprecated.php: -------------------------------------------------------------------------------- 1 | mark_failure( $action_id ); 20 | } 21 | 22 | /** 23 | * Add base hooks 24 | * 25 | * @since 2.2.6 26 | */ 27 | protected static function hook() { 28 | _deprecated_function( __METHOD__, '3.0.0' ); 29 | } 30 | 31 | /** 32 | * Remove base hooks 33 | * 34 | * @since 2.2.6 35 | */ 36 | protected static function unhook() { 37 | _deprecated_function( __METHOD__, '3.0.0' ); 38 | } 39 | 40 | /** 41 | * Get the site's local time. 42 | * 43 | * @deprecated 2.1.0 44 | * @return DateTimeZone 45 | */ 46 | protected function get_local_timezone() { 47 | _deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' ); 48 | return ActionScheduler_TimezoneHelper::get_local_timezone(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/cron-expression/CronExpression_FieldInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface CronExpression_FieldInterface 9 | { 10 | /** 11 | * Check if the respective value of a DateTime field satisfies a CRON exp 12 | * 13 | * @param DateTime $date DateTime object to check 14 | * @param string $value CRON expression to test against 15 | * 16 | * @return bool Returns TRUE if satisfied, FALSE otherwise 17 | */ 18 | public function isSatisfiedBy(DateTime $date, $value); 19 | 20 | /** 21 | * When a CRON expression is not satisfied, this method is used to increment 22 | * or decrement a DateTime object by the unit of the cron field 23 | * 24 | * @param DateTime $date DateTime object to change 25 | * @param bool $invert (optional) Set to TRUE to decrement 26 | * 27 | * @return CronExpression_FieldInterface 28 | */ 29 | public function increment(DateTime $date, $invert = false); 30 | 31 | /** 32 | * Validates a CRON expression for a given field 33 | * 34 | * @param string $value CRON expression value to validate 35 | * 36 | * @return bool Returns TRUE if valid, FALSE otherwise 37 | */ 38 | public function validate($value); 39 | } 40 | -------------------------------------------------------------------------------- /lib/cron-expression/CronExpression_HoursField.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class CronExpression_HoursField extends CronExpression_AbstractField 9 | { 10 | /** 11 | * {@inheritdoc} 12 | */ 13 | public function isSatisfiedBy(DateTime $date, $value) 14 | { 15 | return $this->isSatisfied($date->format('H'), $value); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function increment(DateTime $date, $invert = false) 22 | { 23 | // Change timezone to UTC temporarily. This will 24 | // allow us to go back or forwards and hour even 25 | // if DST will be changed between the hours. 26 | $timezone = $date->getTimezone(); 27 | $date->setTimezone(new DateTimeZone('UTC')); 28 | if ($invert) { 29 | $date->modify('-1 hour'); 30 | $date->setTime($date->format('H'), 59); 31 | } else { 32 | $date->modify('+1 hour'); 33 | $date->setTime($date->format('H'), 0); 34 | } 35 | $date->setTimezone($timezone); 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function validate($value) 44 | { 45 | return (bool) preg_match('/[\*,\/\-0-9]+/', $value); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /classes/migration/LogMigrator.php: -------------------------------------------------------------------------------- 1 | source = $source_logger; 40 | $this->destination = $destination_logger; 41 | } 42 | 43 | /** 44 | * Migrate an action log. 45 | * 46 | * @param int $source_action_id Source logger object. 47 | * @param int $destination_action_id Destination logger object. 48 | */ 49 | public function migrate( $source_action_id, $destination_action_id ) { 50 | $logs = $this->source->get_logs( $source_action_id ); 51 | 52 | foreach ( $logs as $log ) { 53 | if ( absint( $log->get_action_id() ) === absint( $source_action_id ) ) { 54 | $this->destination->log( $destination_action_id, $log->get_message(), $log->get_date() ); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/cron-expression/CronExpression_MonthField.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class CronExpression_MonthField extends CronExpression_AbstractField 9 | { 10 | /** 11 | * {@inheritdoc} 12 | */ 13 | public function isSatisfiedBy(DateTime $date, $value) 14 | { 15 | // Convert text month values to integers 16 | $value = str_ireplace( 17 | array( 18 | 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 19 | 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC' 20 | ), 21 | range(1, 12), 22 | $value 23 | ); 24 | 25 | return $this->isSatisfied($date->format('m'), $value); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function increment(DateTime $date, $invert = false) 32 | { 33 | if ($invert) { 34 | // $date->modify('last day of previous month'); // remove for php 5.2 compat 35 | $date->modify('previous month'); 36 | $date->modify($date->format('Y-m-t')); 37 | $date->setTime(23, 59); 38 | } else { 39 | //$date->modify('first day of next month'); // remove for php 5.2 compat 40 | $date->modify('next month'); 41 | $date->modify($date->format('Y-m-01')); 42 | $date->setTime(0, 0); 43 | } 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function validate($value) 52 | { 53 | return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /classes/ActionScheduler_InvalidActionException.php: -------------------------------------------------------------------------------- 1 | __wakeup() for details. 10 | * 11 | * @var null 12 | */ 13 | private $timestamp = null; 14 | 15 | /** 16 | * Calculate when the next instance of this schedule would run based on a given date & time. 17 | * 18 | * @param DateTime $after Timestamp. 19 | * 20 | * @return DateTime|null 21 | */ 22 | public function calculate_next( DateTime $after ) { 23 | return null; 24 | } 25 | 26 | /** 27 | * Cancelled actions should never have a next schedule, even if get_next() 28 | * is called with $after < $this->scheduled_date. 29 | * 30 | * @param DateTime $after Timestamp. 31 | * @return DateTime|null 32 | */ 33 | public function get_next( DateTime $after ) { 34 | return null; 35 | } 36 | 37 | /** 38 | * Action is not recurring. 39 | * 40 | * @return bool 41 | */ 42 | public function is_recurring() { 43 | return false; 44 | } 45 | 46 | /** 47 | * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 48 | * 49 | * Prior to Action Scheduler 3.0.0, schedules used different property names to refer 50 | * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp 51 | * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 52 | * aligned properties and property names for better inheritance. To maintain backward 53 | * compatibility with schedules serialized and stored prior to 3.0, we need to correctly 54 | * map the old property names with matching visibility. 55 | */ 56 | public function __wakeup() { 57 | if ( ! is_null( $this->timestamp ) ) { 58 | $this->scheduled_timestamp = $this->timestamp; 59 | unset( $this->timestamp ); 60 | } 61 | parent::__wakeup(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/cron-expression/CronExpression_FieldFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * @link http://en.wikipedia.org/wiki/Cron 8 | */ 9 | class CronExpression_FieldFactory 10 | { 11 | /** 12 | * @var array Cache of instantiated fields 13 | */ 14 | private $fields = array(); 15 | 16 | /** 17 | * Get an instance of a field object for a cron expression position 18 | * 19 | * @param int $position CRON expression position value to retrieve 20 | * 21 | * @return CronExpression_FieldInterface 22 | * @throws InvalidArgumentException if a position is not valid 23 | */ 24 | public function getField($position) 25 | { 26 | if (!isset($this->fields[$position])) { 27 | switch ($position) { 28 | case 0: 29 | $this->fields[$position] = new CronExpression_MinutesField(); 30 | break; 31 | case 1: 32 | $this->fields[$position] = new CronExpression_HoursField(); 33 | break; 34 | case 2: 35 | $this->fields[$position] = new CronExpression_DayOfMonthField(); 36 | break; 37 | case 3: 38 | $this->fields[$position] = new CronExpression_MonthField(); 39 | break; 40 | case 4: 41 | $this->fields[$position] = new CronExpression_DayOfWeekField(); 42 | break; 43 | case 5: 44 | $this->fields[$position] = new CronExpression_YearField(); 45 | break; 46 | default: 47 | throw new InvalidArgumentException( 48 | $position . ' is not a valid position' 49 | ); 50 | } 51 | } 52 | 53 | return $this->fields[$position]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /classes/migration/ActionScheduler_DBStoreMigrator.php: -------------------------------------------------------------------------------- 1 | $this->get_scheduled_date_string( $action, $last_attempt_date ), 40 | 'last_attempt_local' => $this->get_scheduled_date_string_local( $action, $last_attempt_date ), 41 | ); 42 | 43 | $wpdb->update( $wpdb->actionscheduler_actions, $data, array( 'action_id' => $action_id ), array( '%s', '%s' ), array( '%d' ) ); 44 | } 45 | 46 | return $action_id; 47 | } catch ( \Exception $e ) { 48 | // translators: %s is an error message. 49 | throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 ); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /classes/abstracts/ActionScheduler_Lock.php: -------------------------------------------------------------------------------- 1 | get_expiration( $lock_type ) >= time() ); 32 | } 33 | 34 | /** 35 | * Set a lock. 36 | * 37 | * To prevent race conditions, implementations should avoid setting the lock if the lock is already held. 38 | * 39 | * @param string $lock_type A string to identify different lock types. 40 | * @return bool 41 | */ 42 | abstract public function set( $lock_type ); 43 | 44 | /** 45 | * If a lock is set, return the timestamp it was set to expiry. 46 | * 47 | * @param string $lock_type A string to identify different lock types. 48 | * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. 49 | */ 50 | abstract public function get_expiration( $lock_type ); 51 | 52 | /** 53 | * Get the amount of time to set for a given lock. 60 seconds by default. 54 | * 55 | * @param string $lock_type A string to identify different lock types. 56 | * @return int 57 | */ 58 | protected function get_duration( $lock_type ) { 59 | return apply_filters( 'action_scheduler_lock_duration', self::$lock_duration, $lock_type ); 60 | } 61 | 62 | /** 63 | * Get instance. 64 | * 65 | * @return ActionScheduler_Lock 66 | */ 67 | public static function instance() { 68 | if ( empty( self::$locker ) ) { 69 | $class = apply_filters( 'action_scheduler_lock_class', 'ActionScheduler_OptionLock' ); 70 | self::$locker = new $class(); 71 | } 72 | return self::$locker; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php: -------------------------------------------------------------------------------- 1 | post_status_args(), $this->post_status_running_labels() ) ); 15 | register_post_status( ActionScheduler_Store::STATUS_FAILED, array_merge( $this->post_status_args(), $this->post_status_failed_labels() ) ); 16 | } 17 | 18 | /** 19 | * Build the args array for the post type definition 20 | * 21 | * @return array 22 | */ 23 | protected function post_status_args() { 24 | $args = array( 25 | 'public' => false, 26 | 'exclude_from_search' => false, 27 | 'show_in_admin_all_list' => true, 28 | 'show_in_admin_status_list' => true, 29 | ); 30 | 31 | return apply_filters( 'action_scheduler_post_status_args', $args ); 32 | } 33 | 34 | /** 35 | * Build the args array for the post type definition 36 | * 37 | * @return array 38 | */ 39 | protected function post_status_failed_labels() { 40 | $labels = array( 41 | 'label' => _x( 'Failed', 'post', 'action-scheduler' ), 42 | /* translators: %s: count */ 43 | 'label_count' => _n_noop( 'Failed (%s)', 'Failed (%s)', 'action-scheduler' ), 44 | ); 45 | 46 | return apply_filters( 'action_scheduler_post_status_failed_labels', $labels ); 47 | } 48 | 49 | /** 50 | * Build the args array for the post type definition 51 | * 52 | * @return array 53 | */ 54 | protected function post_status_running_labels() { 55 | $labels = array( 56 | 'label' => _x( 'In-Progress', 'post', 'action-scheduler' ), 57 | /* translators: %s: count */ 58 | 'label_count' => _n_noop( 'In-Progress (%s)', 'In-Progress (%s)', 'action-scheduler' ), 59 | ); 60 | 61 | return apply_filters( 'action_scheduler_post_status_running_labels', $labels ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /classes/WP_CLI/Action/Next_Command.php: -------------------------------------------------------------------------------- 1 | args[0]; 21 | $group = get_flag_value( $this->assoc_args, 'group', '' ); 22 | $callback_args = get_flag_value( $this->assoc_args, 'args', null ); 23 | $raw = (bool) get_flag_value( $this->assoc_args, 'raw', false ); 24 | 25 | if ( ! empty( $callback_args ) ) { 26 | $callback_args = json_decode( $callback_args, true ); 27 | } 28 | 29 | if ( $raw ) { 30 | \WP_CLI::line( as_next_scheduled_action( $hook, $callback_args, $group ) ); 31 | return; 32 | } 33 | 34 | $params = array( 35 | 'hook' => $hook, 36 | 'orderby' => 'date', 37 | 'order' => 'ASC', 38 | 'group' => $group, 39 | ); 40 | 41 | if ( is_array( $callback_args ) ) { 42 | $params['args'] = $callback_args; 43 | } 44 | 45 | $params['status'] = \ActionScheduler_Store::STATUS_RUNNING; 46 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export 47 | \WP_CLI::debug( 'ActionScheduler()::store()->query_action( ' . var_export( $params, true ) . ' )' ); 48 | 49 | $store = \ActionScheduler::store(); 50 | $action_id = $store->query_action( $params ); 51 | 52 | if ( $action_id ) { 53 | echo $action_id; 54 | return; 55 | } 56 | 57 | $params['status'] = \ActionScheduler_Store::STATUS_PENDING; 58 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export 59 | \WP_CLI::debug( 'ActionScheduler()::store()->query_action( ' . var_export( $params, true ) . ' )' ); 60 | 61 | $action_id = $store->query_action( $params ); 62 | 63 | if ( $action_id ) { 64 | echo $action_id; 65 | return; 66 | } 67 | 68 | \WP_CLI::warning( 'No matching next action.' ); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /classes/migration/BatchFetcher.php: -------------------------------------------------------------------------------- 1 | store = $source_store; 31 | } 32 | 33 | /** 34 | * Retrieve a list of actions. 35 | * 36 | * @param int $count The number of actions to retrieve. 37 | * 38 | * @return int[] A list of action IDs 39 | */ 40 | public function fetch( $count = 10 ) { 41 | foreach ( $this->get_query_strategies( $count ) as $query ) { 42 | $action_ids = $this->store->query_actions( $query ); 43 | if ( ! empty( $action_ids ) ) { 44 | return $action_ids; 45 | } 46 | } 47 | 48 | return array(); 49 | } 50 | 51 | /** 52 | * Generate a list of prioritized of action search parameters. 53 | * 54 | * @param int $count Number of actions to find. 55 | * 56 | * @return array 57 | */ 58 | private function get_query_strategies( $count ) { 59 | $now = as_get_datetime_object(); 60 | $args = array( 61 | 'date' => $now, 62 | 'per_page' => $count, 63 | 'offset' => 0, 64 | 'orderby' => 'date', 65 | 'order' => 'ASC', 66 | ); 67 | 68 | $priorities = array( 69 | Store::STATUS_PENDING, 70 | Store::STATUS_FAILED, 71 | Store::STATUS_CANCELED, 72 | Store::STATUS_COMPLETE, 73 | Store::STATUS_RUNNING, 74 | '', // any other unanticipated status. 75 | ); 76 | 77 | foreach ( $priorities as $status ) { 78 | yield wp_parse_args( 79 | array( 80 | 'status' => $status, 81 | 'date_compare' => '<=', 82 | ), 83 | $args 84 | ); 85 | 86 | yield wp_parse_args( 87 | array( 88 | 'status' => $status, 89 | 'date_compare' => '>=', 90 | ), 91 | $args 92 | ); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php: -------------------------------------------------------------------------------- 1 | post_type_args() ); 14 | } 15 | 16 | /** 17 | * Build the args array for the post type definition 18 | * 19 | * @return array 20 | */ 21 | protected function post_type_args() { 22 | $args = array( 23 | 'label' => __( 'Scheduled Actions', 'action-scheduler' ), 24 | 'description' => __( 'Scheduled actions are hooks triggered on a certain date and time.', 'action-scheduler' ), 25 | 'public' => false, 26 | 'map_meta_cap' => true, 27 | 'hierarchical' => false, 28 | 'supports' => array( 'title', 'editor', 'comments' ), 29 | 'rewrite' => false, 30 | 'query_var' => false, 31 | 'can_export' => true, 32 | 'ep_mask' => EP_NONE, 33 | 'labels' => array( 34 | 'name' => __( 'Scheduled Actions', 'action-scheduler' ), 35 | 'singular_name' => __( 'Scheduled Action', 'action-scheduler' ), 36 | 'menu_name' => _x( 'Scheduled Actions', 'Admin menu name', 'action-scheduler' ), 37 | 'add_new' => __( 'Add', 'action-scheduler' ), 38 | 'add_new_item' => __( 'Add New Scheduled Action', 'action-scheduler' ), 39 | 'edit' => __( 'Edit', 'action-scheduler' ), 40 | 'edit_item' => __( 'Edit Scheduled Action', 'action-scheduler' ), 41 | 'new_item' => __( 'New Scheduled Action', 'action-scheduler' ), 42 | 'view' => __( 'View Action', 'action-scheduler' ), 43 | 'view_item' => __( 'View Action', 'action-scheduler' ), 44 | 'search_items' => __( 'Search Scheduled Actions', 'action-scheduler' ), 45 | 'not_found' => __( 'No actions found', 'action-scheduler' ), 46 | 'not_found_in_trash' => __( 'No actions found in trash', 'action-scheduler' ), 47 | ), 48 | ); 49 | 50 | $args = apply_filters( 'action_scheduler_post_type_args', $args ); 51 | return $args; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /classes/ActionScheduler_LogEntry.php: -------------------------------------------------------------------------------- 1 | comment_type 40 | * to ActionScheduler_LogEntry::__construct(), goodness knows why, and the Follow-up Emails plugin 41 | * hard-codes loading its own version of ActionScheduler_wpCommentLogger with that out-dated method, 42 | * goodness knows why, so we need to guard against that here instead of using a DateTime type declaration 43 | * for the constructor's 3rd param of $date and causing a fatal error with older versions of FUE. 44 | */ 45 | if ( null !== $date && ! is_a( $date, 'DateTime' ) ) { 46 | _doing_it_wrong( __METHOD__, 'The third parameter must be a valid DateTime instance, or null.', '2.0.0' ); 47 | $date = null; 48 | } 49 | 50 | $this->action_id = $action_id; 51 | $this->message = $message; 52 | $this->date = $date ? $date : new Datetime(); 53 | } 54 | 55 | /** 56 | * Returns the date when this log entry was created 57 | * 58 | * @return Datetime 59 | */ 60 | public function get_date() { 61 | return $this->date; 62 | } 63 | 64 | /** 65 | * Get action ID of log entry. 66 | */ 67 | public function get_action_id() { 68 | return $this->action_id; 69 | } 70 | 71 | /** 72 | * Get log entry message. 73 | */ 74 | public function get_message() { 75 | return $this->message; 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /classes/abstracts/ActionScheduler_Abstract_Schedule.php: -------------------------------------------------------------------------------- 1 | scheduled_date 17 | * 18 | * @var int 19 | */ 20 | protected $scheduled_timestamp = null; 21 | 22 | /** 23 | * Construct. 24 | * 25 | * @param DateTime $date The date & time to run the action. 26 | */ 27 | public function __construct( DateTime $date ) { 28 | $this->scheduled_date = $date; 29 | } 30 | 31 | /** 32 | * Check if a schedule should recur. 33 | * 34 | * @return bool 35 | */ 36 | abstract public function is_recurring(); 37 | 38 | /** 39 | * Calculate when the next instance of this schedule would run based on a given date & time. 40 | * 41 | * @param DateTime $after Start timestamp. 42 | * @return DateTime 43 | */ 44 | abstract protected function calculate_next( DateTime $after ); 45 | 46 | /** 47 | * Get the next date & time when this schedule should run after a given date & time. 48 | * 49 | * @param DateTime $after Start timestamp. 50 | * @return DateTime|null 51 | */ 52 | public function get_next( DateTime $after ) { 53 | $after = clone $after; 54 | if ( $after > $this->scheduled_date ) { 55 | $after = $this->calculate_next( $after ); 56 | return $after; 57 | } 58 | return clone $this->scheduled_date; 59 | } 60 | 61 | /** 62 | * Get the date & time the schedule is set to run. 63 | * 64 | * @return DateTime|null 65 | */ 66 | public function get_date() { 67 | return $this->scheduled_date; 68 | } 69 | 70 | /** 71 | * For PHP 5.2 compat, because DateTime objects can't be serialized 72 | * 73 | * @return array 74 | */ 75 | public function __sleep() { 76 | $this->scheduled_timestamp = $this->scheduled_date->getTimestamp(); 77 | return array( 78 | 'scheduled_timestamp', 79 | ); 80 | } 81 | 82 | /** 83 | * Wakeup. 84 | */ 85 | public function __wakeup() { 86 | $this->scheduled_date = as_get_datetime_object( $this->scheduled_timestamp ); 87 | unset( $this->scheduled_timestamp ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /classes/ActionScheduler_DateTime.php: -------------------------------------------------------------------------------- 1 | format( 'U' ); 30 | } 31 | 32 | /** 33 | * Set the UTC offset. 34 | * 35 | * This represents a fixed offset instead of a timezone setting. 36 | * 37 | * @param string|int $offset UTC offset value. 38 | */ 39 | public function setUtcOffset( $offset ) { 40 | $this->utcOffset = intval( $offset ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 41 | } 42 | 43 | /** 44 | * Returns the timezone offset. 45 | * 46 | * @return int 47 | * @link http://php.net/manual/en/datetime.getoffset.php 48 | */ 49 | #[\ReturnTypeWillChange] 50 | public function getOffset() { 51 | return $this->utcOffset ? $this->utcOffset : parent::getOffset(); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 52 | } 53 | 54 | /** 55 | * Set the TimeZone associated with the DateTime 56 | * 57 | * @param DateTimeZone $timezone Timezone object. 58 | * 59 | * @return static 60 | * @link http://php.net/manual/en/datetime.settimezone.php 61 | */ 62 | #[\ReturnTypeWillChange] 63 | public function setTimezone( $timezone ) { 64 | $this->utcOffset = 0; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 65 | parent::setTimezone( $timezone ); 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Get the timestamp with the WordPress timezone offset added or subtracted. 72 | * 73 | * @since 3.0.0 74 | * @return int 75 | */ 76 | public function getOffsetTimestamp() { 77 | return $this->getTimestamp() + $this->getOffset(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /classes/abstracts/ActionScheduler_WPCLI_Command.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | protected $assoc_args; 23 | 24 | /** 25 | * Construct. 26 | * 27 | * @param string[] $args Positional arguments. 28 | * @param array $assoc_args Keyed arguments. 29 | * @throws \Exception When loading a CLI command file outside of WP CLI context. 30 | */ 31 | public function __construct( array $args, array $assoc_args ) { 32 | if ( ! defined( 'WP_CLI' ) || ! constant( 'WP_CLI' ) ) { 33 | /* translators: %s php class name */ 34 | throw new \Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'action-scheduler' ), get_class( $this ) ) ); 35 | } 36 | 37 | $this->args = $args; 38 | $this->assoc_args = $assoc_args; 39 | } 40 | 41 | /** 42 | * Execute command. 43 | */ 44 | abstract public function execute(); 45 | 46 | /** 47 | * Get the scheduled date in a human friendly format. 48 | * 49 | * @see ActionScheduler_ListTable::get_schedule_display_string() 50 | * @param ActionScheduler_Schedule $schedule Schedule. 51 | * @return string 52 | */ 53 | protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) { 54 | 55 | $schedule_display_string = ''; 56 | 57 | if ( ! $schedule->get_date() ) { 58 | return '0000-00-00 00:00:00'; 59 | } 60 | 61 | $next_timestamp = $schedule->get_date()->getTimestamp(); 62 | 63 | $schedule_display_string .= $schedule->get_date()->format( static::DATE_FORMAT ); 64 | 65 | return $schedule_display_string; 66 | } 67 | 68 | /** 69 | * Transforms arguments with '__' from CSV into expected arrays. 70 | * 71 | * @see \WP_CLI\CommandWithDBObject::process_csv_arguments_to_arrays() 72 | * @link https://github.com/wp-cli/entity-command/blob/c270cc9a2367cb8f5845f26a6b5e203397c91392/src/WP_CLI/CommandWithDBObject.php#L99 73 | * @return void 74 | */ 75 | protected function process_csv_arguments_to_arrays() { 76 | foreach ( $this->assoc_args as $k => $v ) { 77 | if ( false !== strpos( $k, '__' ) ) { 78 | $this->assoc_args[ $k ] = explode( ',', $v ); 79 | } 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /classes/WP_CLI/Action/Get_Command.php: -------------------------------------------------------------------------------- 1 | args[0]; 17 | $store = \ActionScheduler::store(); 18 | $logger = \ActionScheduler::logger(); 19 | $action = $store->fetch_action( $action_id ); 20 | 21 | if ( is_a( $action, ActionScheduler_NullAction::class ) ) { 22 | /* translators: %d is action ID. */ 23 | \WP_CLI::error( sprintf( esc_html__( 'Unable to retrieve action %d.', 'action-scheduler' ), $action_id ) ); 24 | } 25 | 26 | $only_logs = ! empty( $this->assoc_args['field'] ) && 'log_entries' === $this->assoc_args['field']; 27 | $only_logs = $only_logs || ( ! empty( $this->assoc_args['fields'] ) && 'log_entries' === $this->assoc_args['fields'] ); 28 | $log_entries = array(); 29 | 30 | foreach ( $logger->get_logs( $action_id ) as $log_entry ) { 31 | $log_entries[] = array( 32 | 'date' => $log_entry->get_date()->format( static::DATE_FORMAT ), 33 | 'message' => $log_entry->get_message(), 34 | ); 35 | } 36 | 37 | if ( $only_logs ) { 38 | $args = array( 39 | 'format' => \WP_CLI\Utils\get_flag_value( $this->assoc_args, 'format', 'table' ), 40 | ); 41 | 42 | $formatter = new \WP_CLI\Formatter( $args, array( 'date', 'message' ) ); 43 | $formatter->display_items( $log_entries ); 44 | 45 | return; 46 | } 47 | 48 | try { 49 | $status = $store->get_status( $action_id ); 50 | } catch ( \Exception $e ) { 51 | \WP_CLI::error( $e->getMessage() ); 52 | } 53 | 54 | $action_arr = array( 55 | 'id' => $this->args[0], 56 | 'hook' => $action->get_hook(), 57 | 'status' => $status, 58 | 'args' => $action->get_args(), 59 | 'group' => $action->get_group(), 60 | 'recurring' => $action->get_schedule()->is_recurring() ? 'yes' : 'no', 61 | 'scheduled_date' => $this->get_schedule_display_string( $action->get_schedule() ), 62 | 'log_entries' => $log_entries, 63 | ); 64 | 65 | $fields = array_keys( $action_arr ); 66 | 67 | if ( ! empty( $this->assoc_args['fields'] ) ) { 68 | $fields = explode( ',', $this->assoc_args['fields'] ); 69 | } 70 | 71 | $formatter = new \WP_CLI\Formatter( $this->assoc_args, $fields ); 72 | $formatter->display_item( $action_arr ); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /classes/ActionScheduler_AsyncRequest_QueueRunner.php: -------------------------------------------------------------------------------- 1 | store = $store; 39 | } 40 | 41 | /** 42 | * Handle async requests 43 | * 44 | * Run a queue, and maybe dispatch another async request to run another queue 45 | * if there are still pending actions after completing a queue in this request. 46 | */ 47 | protected function handle() { 48 | do_action( 'action_scheduler_run_queue', 'Async Request' ); // run a queue in the same way as WP Cron, but declare the Async Request context. 49 | 50 | $sleep_seconds = $this->get_sleep_seconds(); 51 | 52 | if ( $sleep_seconds ) { 53 | sleep( $sleep_seconds ); 54 | } 55 | 56 | $this->maybe_dispatch(); 57 | } 58 | 59 | /** 60 | * If the async request runner is needed and allowed to run, dispatch a request. 61 | */ 62 | public function maybe_dispatch() { 63 | if ( ! $this->allow() ) { 64 | return; 65 | } 66 | 67 | $this->dispatch(); 68 | ActionScheduler_QueueRunner::instance()->unhook_dispatch_async_request(); 69 | } 70 | 71 | /** 72 | * Only allow async requests when needed. 73 | * 74 | * Also allow 3rd party code to disable running actions via async requests. 75 | */ 76 | protected function allow() { 77 | 78 | if ( ! has_action( 'action_scheduler_run_queue' ) || ActionScheduler::runner()->has_maximum_concurrent_batches() || ! $this->store->has_pending_actions_due() ) { 79 | $allow = false; 80 | } else { 81 | $allow = true; 82 | } 83 | 84 | return apply_filters( 'action_scheduler_allow_async_request_runner', $allow ); 85 | } 86 | 87 | /** 88 | * Chaining async requests can crash MySQL. A brief sleep call in PHP prevents that. 89 | */ 90 | protected function get_sleep_seconds() { 91 | return apply_filters( 'action_scheduler_async_request_sleep_seconds', 5, $this ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /classes/schedules/ActionScheduler_SimpleSchedule.php: -------------------------------------------------------------------------------- 1 | __wakeup() for details. 10 | * 11 | * @var null|DateTime 12 | */ 13 | private $timestamp = null; 14 | 15 | /** 16 | * Calculate when this schedule should start after a given date & time using 17 | * the number of seconds between recurrences. 18 | * 19 | * @param DateTime $after Timestamp. 20 | * 21 | * @return DateTime|null 22 | */ 23 | public function calculate_next( DateTime $after ) { 24 | return null; 25 | } 26 | 27 | /** 28 | * Schedule is not recurring. 29 | * 30 | * @return bool 31 | */ 32 | public function is_recurring() { 33 | return false; 34 | } 35 | 36 | /** 37 | * Serialize schedule with data required prior to AS 3.0.0 38 | * 39 | * Prior to Action Scheduler 3.0.0, schedules used different property names to refer 40 | * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp 41 | * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 42 | * aligned properties and property names for better inheritance. To guard against the 43 | * scheduled date for single actions always being seen as "now" if downgrading to 44 | * Action Scheduler < 3.0.0, we need to also store the data with the old property names 45 | * so if it's unserialized in AS < 3.0, the schedule doesn't end up with a null recurrence. 46 | * 47 | * @return array 48 | */ 49 | public function __sleep() { 50 | 51 | $sleep_params = parent::__sleep(); 52 | 53 | $this->timestamp = $this->scheduled_timestamp; 54 | 55 | return array_merge( 56 | $sleep_params, 57 | array( 58 | 'timestamp', 59 | ) 60 | ); 61 | } 62 | 63 | /** 64 | * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 65 | * 66 | * Prior to Action Scheduler 3.0.0, schedules used different property names to refer 67 | * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp 68 | * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 69 | * aligned properties and property names for better inheritance. To maintain backward 70 | * compatibility with schedules serialized and stored prior to 3.0, we need to correctly 71 | * map the old property names with matching visibility. 72 | */ 73 | public function __wakeup() { 74 | 75 | if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->timestamp ) ) { 76 | $this->scheduled_timestamp = $this->timestamp; 77 | unset( $this->timestamp ); 78 | } 79 | parent::__wakeup(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Action Scheduler - Job Queue for WordPress [![Build Status](https://travis-ci.org/woocommerce/action-scheduler.png?branch=master)](https://travis-ci.org/woocommerce/action-scheduler) [![codecov](https://codecov.io/gh/woocommerce/action-scheduler/branch/master/graph/badge.svg)](https://codecov.io/gh/woocommerce/action-scheduler) 2 | 3 | Action Scheduler is a scalable, traceable job queue for background processing large sets of actions in WordPress. It's specially designed to be distributed in WordPress plugins. 4 | 5 | Action Scheduler works by triggering an action hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occasions. 6 | 7 | Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook. 8 | 9 | ## Battle-Tested Background Processing 10 | 11 | Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins. 12 | 13 | It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, at a sustained rate of over 10,000 / hour without negatively impacting normal site operations. 14 | 15 | This is all on infrastructure and WordPress sites outside the control of the plugin author. 16 | 17 | If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help. 18 | 19 | ## Learn More 20 | 21 | To learn more about how to Action Scheduler works, and how to use it in your plugin, check out the docs on [ActionScheduler.org](https://actionscheduler.org). 22 | 23 | There you will find: 24 | 25 | * [Usage guide](https://actionscheduler.org/usage/): instructions on installing and using Action Scheduler 26 | * [WP CLI guide](https://actionscheduler.org/wp-cli/): instructions on running Action Scheduler at scale via WP CLI 27 | * [API Reference](https://actionscheduler.org/api/): complete reference guide for all API functions 28 | * [Administration Guide](https://actionscheduler.org/admin/): guide to managing scheduled actions via the administration screen 29 | * [Guide to Background Processing at Scale](https://actionscheduler.org/perf/): instructions for running Action Scheduler at scale via the default WP Cron queue runner 30 | 31 | ## Credits 32 | 33 | Action Scheduler is developed and maintained by [Automattic](http://automattic.com/) with significant early development completed by [Flightless](https://flightless.us/). 34 | 35 | Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/woocommerce/action-scheduler/pulls) welcome. 36 | -------------------------------------------------------------------------------- /classes/ActionScheduler_SystemInformation.php: -------------------------------------------------------------------------------- 1 | 'plugin', # or 'theme' 17 | * 'name' => 'Name', 18 | * ] 19 | * 20 | * @return array 21 | */ 22 | public static function active_source(): array { 23 | $plugins = get_plugins(); 24 | $plugin_files = array_keys( $plugins ); 25 | 26 | foreach ( $plugin_files as $plugin_file ) { 27 | $plugin_path = trailingslashit( WP_PLUGIN_DIR ) . dirname( $plugin_file ); 28 | $plugin_file = trailingslashit( WP_PLUGIN_DIR ) . $plugin_file; 29 | 30 | if ( 0 !== strpos( dirname( __DIR__ ), $plugin_path ) ) { 31 | continue; 32 | } 33 | 34 | $plugin_data = get_plugin_data( $plugin_file ); 35 | 36 | if ( ! is_array( $plugin_data ) || empty( $plugin_data['Name'] ) ) { 37 | continue; 38 | } 39 | 40 | return array( 41 | 'type' => 'plugin', 42 | 'name' => $plugin_data['Name'], 43 | ); 44 | } 45 | 46 | $themes = (array) search_theme_directories(); 47 | 48 | foreach ( $themes as $slug => $data ) { 49 | $needle = trailingslashit( $data['theme_root'] ) . $slug . '/'; 50 | 51 | if ( 0 !== strpos( __FILE__, $needle ) ) { 52 | continue; 53 | } 54 | 55 | $theme = wp_get_theme( $slug ); 56 | 57 | if ( ! is_object( $theme ) || ! is_a( $theme, \WP_Theme::class ) ) { 58 | continue; 59 | } 60 | 61 | return array( 62 | 'type' => 'theme', 63 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 64 | 'name' => $theme->Name, 65 | ); 66 | } 67 | 68 | return array(); 69 | } 70 | 71 | /** 72 | * Returns the directory path for the currently active installation of Action Scheduler. 73 | * 74 | * @return string 75 | */ 76 | public static function active_source_path(): string { 77 | return trailingslashit( dirname( __DIR__ ) ); 78 | } 79 | 80 | /** 81 | * Get registered sources. 82 | * 83 | * It is not always possible to obtain this information. For instance, if earlier versions (<=3.9.0) of 84 | * Action Scheduler register themselves first, then the necessary data about registered sources will 85 | * not be available. 86 | * 87 | * @return array 88 | */ 89 | public static function get_sources() { 90 | $versions = ActionScheduler_Versions::instance(); 91 | return method_exists( $versions, 'get_sources' ) ? $versions->get_sources() : array(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /classes/ActionScheduler_FatalErrorMonitor.php: -------------------------------------------------------------------------------- 1 | store = $store; 36 | } 37 | 38 | /** 39 | * Start monitoring. 40 | * 41 | * @param ActionScheduler_ActionClaim $claim Claimed actions. 42 | */ 43 | public function attach( ActionScheduler_ActionClaim $claim ) { 44 | $this->claim = $claim; 45 | add_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) ); 46 | add_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0, 1 ); 47 | add_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0, 0 ); 48 | add_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0, 0 ); 49 | add_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0, 0 ); 50 | } 51 | 52 | /** 53 | * Stop monitoring. 54 | */ 55 | public function detach() { 56 | $this->claim = null; 57 | $this->untrack_action(); 58 | remove_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) ); 59 | remove_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0 ); 60 | remove_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0 ); 61 | remove_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0 ); 62 | remove_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0 ); 63 | } 64 | 65 | /** 66 | * Track specified action. 67 | * 68 | * @param int $action_id Action ID to track. 69 | */ 70 | public function track_current_action( $action_id ) { 71 | $this->action_id = $action_id; 72 | } 73 | 74 | /** 75 | * Un-track action. 76 | */ 77 | public function untrack_action() { 78 | $this->action_id = 0; 79 | } 80 | 81 | /** 82 | * Handle unexpected shutdown. 83 | */ 84 | public function handle_unexpected_shutdown() { 85 | $error = error_get_last(); 86 | 87 | if ( $error ) { 88 | if ( in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) { 89 | if ( ! empty( $this->action_id ) ) { 90 | $this->store->mark_failure( $this->action_id ); 91 | do_action( 'action_scheduler_unexpected_shutdown', $this->action_id, $error ); 92 | } 93 | } 94 | 95 | $this->store->release_claim( $this->claim ); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /classes/schedules/ActionScheduler_IntervalSchedule.php: -------------------------------------------------------------------------------- 1 | __wakeup() for details. 10 | * 11 | * @var null 12 | */ 13 | private $start_timestamp = null; 14 | 15 | /** 16 | * Deprecated property @see $this->__wakeup() for details. 17 | * 18 | * @var null 19 | */ 20 | private $interval_in_seconds = null; 21 | 22 | /** 23 | * Calculate when this schedule should start after a given date & time using 24 | * the number of seconds between recurrences. 25 | * 26 | * @param DateTime $after Timestamp. 27 | * @return DateTime 28 | */ 29 | protected function calculate_next( DateTime $after ) { 30 | $after->modify( '+' . (int) $this->get_recurrence() . ' seconds' ); 31 | return $after; 32 | } 33 | 34 | /** 35 | * Schedule interval in seconds. 36 | * 37 | * @return int 38 | */ 39 | public function interval_in_seconds() { 40 | _deprecated_function( __METHOD__, '3.0.0', '(int)ActionScheduler_Abstract_RecurringSchedule::get_recurrence()' ); 41 | return (int) $this->get_recurrence(); 42 | } 43 | 44 | /** 45 | * Serialize interval schedules with data required prior to AS 3.0.0 46 | * 47 | * Prior to Action Scheduler 3.0.0, recurring schedules used different property names to 48 | * refer to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp 49 | * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 50 | * aligned properties and property names for better inheritance. To guard against the 51 | * possibility of infinite loops if downgrading to Action Scheduler < 3.0.0, we need to 52 | * also store the data with the old property names so if it's unserialized in AS < 3.0, 53 | * the schedule doesn't end up with a null/false/0 recurrence. 54 | * 55 | * @return array 56 | */ 57 | public function __sleep() { 58 | 59 | $sleep_params = parent::__sleep(); 60 | 61 | $this->start_timestamp = $this->scheduled_timestamp; 62 | $this->interval_in_seconds = $this->recurrence; 63 | 64 | return array_merge( 65 | $sleep_params, 66 | array( 67 | 'start_timestamp', 68 | 'interval_in_seconds', 69 | ) 70 | ); 71 | } 72 | 73 | /** 74 | * Unserialize interval schedules serialized/stored prior to AS 3.0.0 75 | * 76 | * For more background, @see ActionScheduler_Abstract_RecurringSchedule::__wakeup(). 77 | */ 78 | public function __wakeup() { 79 | if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->start_timestamp ) ) { 80 | $this->scheduled_timestamp = $this->start_timestamp; 81 | unset( $this->start_timestamp ); 82 | } 83 | 84 | if ( is_null( $this->recurrence ) && ! is_null( $this->interval_in_seconds ) ) { 85 | $this->recurrence = $this->interval_in_seconds; 86 | unset( $this->interval_in_seconds ); 87 | } 88 | parent::__wakeup(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/cron-expression/CronExpression_AbstractField.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | abstract class CronExpression_AbstractField implements CronExpression_FieldInterface 9 | { 10 | /** 11 | * Check to see if a field is satisfied by a value 12 | * 13 | * @param string $dateValue Date value to check 14 | * @param string $value Value to test 15 | * 16 | * @return bool 17 | */ 18 | public function isSatisfied($dateValue, $value) 19 | { 20 | if ($this->isIncrementsOfRanges($value)) { 21 | return $this->isInIncrementsOfRanges($dateValue, $value); 22 | } elseif ($this->isRange($value)) { 23 | return $this->isInRange($dateValue, $value); 24 | } 25 | 26 | return $value == '*' || $dateValue == $value; 27 | } 28 | 29 | /** 30 | * Check if a value is a range 31 | * 32 | * @param string $value Value to test 33 | * 34 | * @return bool 35 | */ 36 | public function isRange($value) 37 | { 38 | return strpos($value, '-') !== false; 39 | } 40 | 41 | /** 42 | * Check if a value is an increments of ranges 43 | * 44 | * @param string $value Value to test 45 | * 46 | * @return bool 47 | */ 48 | public function isIncrementsOfRanges($value) 49 | { 50 | return strpos($value, '/') !== false; 51 | } 52 | 53 | /** 54 | * Test if a value is within a range 55 | * 56 | * @param string $dateValue Set date value 57 | * @param string $value Value to test 58 | * 59 | * @return bool 60 | */ 61 | public function isInRange($dateValue, $value) 62 | { 63 | $parts = array_map('trim', explode('-', $value, 2)); 64 | 65 | return $dateValue >= $parts[0] && $dateValue <= $parts[1]; 66 | } 67 | 68 | /** 69 | * Test if a value is within an increments of ranges (offset[-to]/step size) 70 | * 71 | * @param string $dateValue Set date value 72 | * @param string $value Value to test 73 | * 74 | * @return bool 75 | */ 76 | public function isInIncrementsOfRanges($dateValue, $value) 77 | { 78 | $parts = array_map('trim', explode('/', $value, 2)); 79 | $stepSize = isset($parts[1]) ? $parts[1] : 0; 80 | if ($parts[0] == '*' || $parts[0] === '0') { 81 | return (int) $dateValue % $stepSize == 0; 82 | } 83 | 84 | $range = explode('-', $parts[0], 2); 85 | $offset = $range[0]; 86 | $to = isset($range[1]) ? $range[1] : $dateValue; 87 | // Ensure that the date value is within the range 88 | if ($dateValue < $offset || $dateValue > $to) { 89 | return false; 90 | } 91 | 92 | for ($i = $offset; $i <= $to; $i+= $stepSize) { 93 | if ($i == $dateValue) { 94 | return true; 95 | } 96 | } 97 | 98 | return false; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /classes/WP_CLI/Action/Delete_Command.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | protected $action_counts = array( 23 | 'deleted' => 0, 24 | 'failed' => 0, 25 | 'total' => 0, 26 | ); 27 | 28 | /** 29 | * Construct. 30 | * 31 | * @param string[] $args Positional arguments. 32 | * @param array $assoc_args Keyed arguments. 33 | */ 34 | public function __construct( array $args, array $assoc_args ) { 35 | parent::__construct( $args, $assoc_args ); 36 | 37 | $this->action_ids = array_map( 'absint', $args ); 38 | $this->action_counts['total'] = count( $this->action_ids ); 39 | 40 | add_action( 'action_scheduler_deleted_action', array( $this, 'on_action_deleted' ) ); 41 | } 42 | 43 | /** 44 | * Execute. 45 | * 46 | * @return void 47 | */ 48 | public function execute() { 49 | $store = \ActionScheduler::store(); 50 | 51 | $progress_bar = \WP_CLI\Utils\make_progress_bar( 52 | sprintf( 53 | /* translators: %d: number of actions to be deleted */ 54 | _n( 'Deleting %d action', 'Deleting %d actions', $this->action_counts['total'], 'action-scheduler' ), 55 | number_format_i18n( $this->action_counts['total'] ) 56 | ), 57 | $this->action_counts['total'] 58 | ); 59 | 60 | foreach ( $this->action_ids as $action_id ) { 61 | try { 62 | $store->delete_action( $action_id ); 63 | } catch ( \Exception $e ) { 64 | $this->action_counts['failed']++; 65 | \WP_CLI::warning( $e->getMessage() ); 66 | } 67 | 68 | $progress_bar->tick(); 69 | } 70 | 71 | $progress_bar->finish(); 72 | 73 | /* translators: %1$d: number of actions deleted */ 74 | $format = _n( 'Deleted %1$d action', 'Deleted %1$d actions', $this->action_counts['deleted'], 'action-scheduler' ) . ', '; 75 | /* translators: %2$d: number of actions deletions failed */ 76 | $format .= _n( '%2$d failure.', '%2$d failures.', $this->action_counts['failed'], 'action-scheduler' ); 77 | 78 | \WP_CLI::success( 79 | sprintf( 80 | $format, 81 | number_format_i18n( $this->action_counts['deleted'] ), 82 | number_format_i18n( $this->action_counts['failed'] ) 83 | ) 84 | ); 85 | } 86 | 87 | /** 88 | * Action: action_scheduler_deleted_action 89 | * 90 | * @param int $action_id Action ID. 91 | * @return void 92 | */ 93 | public function on_action_deleted( $action_id ) { 94 | if ( 'action_scheduler_deleted_action' !== current_action() ) { 95 | return; 96 | } 97 | 98 | $action_id = absint( $action_id ); 99 | 100 | if ( ! in_array( $action_id, $this->action_ids, true ) ) { 101 | return; 102 | } 103 | 104 | $this->action_counts['deleted']++; 105 | \WP_CLI::debug( sprintf( 'Action %d was deleted.', $action_id ) ); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /action-scheduler.php: -------------------------------------------------------------------------------- 1 | . 28 | * 29 | * @package ActionScheduler 30 | */ 31 | 32 | if ( ! function_exists( 'action_scheduler_register_3_dot_9_dot_3' ) && function_exists( 'add_action' ) ) { // WRCS: DEFINED_VERSION. 33 | 34 | if ( ! class_exists( 'ActionScheduler_Versions', false ) ) { 35 | require_once __DIR__ . '/classes/ActionScheduler_Versions.php'; 36 | add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 ); 37 | } 38 | 39 | add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_9_dot_3', 0, 0 ); // WRCS: DEFINED_VERSION. 40 | 41 | // phpcs:disable Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace 42 | /** 43 | * Registers this version of Action Scheduler. 44 | */ 45 | function action_scheduler_register_3_dot_9_dot_3() { // WRCS: DEFINED_VERSION. 46 | $versions = ActionScheduler_Versions::instance(); 47 | $versions->register( '3.9.3', 'action_scheduler_initialize_3_dot_9_dot_3' ); // WRCS: DEFINED_VERSION. 48 | } 49 | 50 | // phpcs:disable Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace 51 | /** 52 | * Initializes this version of Action Scheduler. 53 | */ 54 | function action_scheduler_initialize_3_dot_9_dot_3() { // WRCS: DEFINED_VERSION. 55 | // A final safety check is required even here, because historic versions of Action Scheduler 56 | // followed a different pattern (in some unusual cases, we could reach this point and the 57 | // ActionScheduler class is already defined—so we need to guard against that). 58 | if ( ! class_exists( 'ActionScheduler', false ) ) { 59 | require_once __DIR__ . '/classes/abstracts/ActionScheduler.php'; 60 | ActionScheduler::init( __FILE__ ); 61 | } 62 | } 63 | 64 | // Support usage in themes - load this version if no plugin has loaded a version yet. 65 | if ( did_action( 'plugins_loaded' ) && ! doing_action( 'plugins_loaded' ) && ! class_exists( 'ActionScheduler', false ) ) { 66 | action_scheduler_initialize_3_dot_9_dot_3(); // WRCS: DEFINED_VERSION. 67 | do_action( 'action_scheduler_pre_theme_init' ); 68 | ActionScheduler_Versions::initialize_latest_version(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /classes/ActionScheduler_RecurringActionScheduler.php: -------------------------------------------------------------------------------- 1 | total_ticks = 0; 71 | $this->message = $message; 72 | $this->count = $count; 73 | $this->interval = $interval; 74 | } 75 | 76 | /** 77 | * Increment the progress bar ticks. 78 | */ 79 | public function tick() { 80 | if ( null === $this->progress_bar ) { 81 | $this->setup_progress_bar(); 82 | } 83 | 84 | $this->progress_bar->tick(); 85 | $this->total_ticks++; 86 | 87 | do_action( 'action_scheduler/progress_tick', $this->total_ticks ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 88 | } 89 | 90 | /** 91 | * Get the progress bar tick count. 92 | * 93 | * @return int 94 | */ 95 | public function current() { 96 | return $this->progress_bar ? $this->progress_bar->current() : 0; 97 | } 98 | 99 | /** 100 | * Finish the current progress bar. 101 | */ 102 | public function finish() { 103 | if ( null !== $this->progress_bar ) { 104 | $this->progress_bar->finish(); 105 | } 106 | 107 | $this->progress_bar = null; 108 | } 109 | 110 | /** 111 | * Set the message used when creating the progress bar. 112 | * 113 | * @param string $message The message to be used when the next progress bar is created. 114 | */ 115 | public function set_message( $message ) { 116 | $this->message = $message; 117 | } 118 | 119 | /** 120 | * Set the count for a new progress bar. 121 | * 122 | * @param integer $count The total number of ticks expected to complete. 123 | */ 124 | public function set_count( $count ) { 125 | $this->count = $count; 126 | $this->finish(); 127 | } 128 | 129 | /** 130 | * Set up the progress bar. 131 | */ 132 | protected function setup_progress_bar() { 133 | $this->progress_bar = \WP_CLI\Utils\make_progress_bar( 134 | $this->message, 135 | $this->count, 136 | $this->interval 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /classes/schema/ActionScheduler_LoggerSchema.php: -------------------------------------------------------------------------------- 1 | tables = array( 27 | self::LOG_TABLE, 28 | ); 29 | } 30 | 31 | /** 32 | * Performs additional setup work required to support this schema. 33 | */ 34 | public function init() { 35 | add_action( 'action_scheduler_before_schema_update', array( $this, 'update_schema_3_0' ), 10, 2 ); 36 | } 37 | 38 | /** 39 | * Get table definition. 40 | * 41 | * @param string $table Table name. 42 | */ 43 | protected function get_table_definition( $table ) { 44 | global $wpdb; 45 | $table_name = $wpdb->$table; 46 | $charset_collate = $wpdb->get_charset_collate(); 47 | switch ( $table ) { 48 | 49 | case self::LOG_TABLE: 50 | $default_date = ActionScheduler_StoreSchema::DEFAULT_DATE; 51 | return "CREATE TABLE $table_name ( 52 | log_id bigint(20) unsigned NOT NULL auto_increment, 53 | action_id bigint(20) unsigned NOT NULL, 54 | message text NOT NULL, 55 | log_date_gmt datetime NULL default '{$default_date}', 56 | log_date_local datetime NULL default '{$default_date}', 57 | PRIMARY KEY (log_id), 58 | KEY action_id (action_id), 59 | KEY log_date_gmt (log_date_gmt) 60 | ) $charset_collate"; 61 | 62 | default: 63 | return ''; 64 | } 65 | } 66 | 67 | /** 68 | * Update the logs table schema, allowing datetime fields to be NULL. 69 | * 70 | * This is needed because the NOT NULL constraint causes a conflict with some versions of MySQL 71 | * configured with sql_mode=NO_ZERO_DATE, which can for instance lead to tables not being created. 72 | * 73 | * Most other schema updates happen via ActionScheduler_Abstract_Schema::update_table(), however 74 | * that method relies on dbDelta() and this change is not possible when using that function. 75 | * 76 | * @param string $table Name of table being updated. 77 | * @param string $db_version The existing schema version of the table. 78 | */ 79 | public function update_schema_3_0( $table, $db_version ) { 80 | global $wpdb; 81 | 82 | if ( 'actionscheduler_logs' !== $table || version_compare( $db_version, '3', '>=' ) ) { 83 | return; 84 | } 85 | 86 | // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared 87 | $table_name = $wpdb->prefix . 'actionscheduler_logs'; 88 | $table_list = $wpdb->get_col( "SHOW TABLES LIKE '{$table_name}'" ); 89 | $default_date = ActionScheduler_StoreSchema::DEFAULT_DATE; 90 | 91 | if ( ! empty( $table_list ) ) { 92 | $query = " 93 | ALTER TABLE {$table_name} 94 | MODIFY COLUMN log_date_gmt datetime NULL default '{$default_date}', 95 | MODIFY COLUMN log_date_local datetime NULL default '{$default_date}' 96 | "; 97 | $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 98 | } 99 | // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/cron-expression/README.md: -------------------------------------------------------------------------------- 1 | PHP Cron Expression Parser 2 | ========================== 3 | 4 | [![Latest Stable Version](https://poser.pugx.org/mtdowling/cron-expression/v/stable.png)](https://packagist.org/packages/mtdowling/cron-expression) [![Total Downloads](https://poser.pugx.org/mtdowling/cron-expression/downloads.png)](https://packagist.org/packages/mtdowling/cron-expression) [![Build Status](https://secure.travis-ci.org/mtdowling/cron-expression.png)](http://travis-ci.org/mtdowling/cron-expression) 5 | 6 | The PHP cron expression parser can parse a CRON expression, determine if it is 7 | due to run, calculate the next run date of the expression, and calculate the previous 8 | run date of the expression. You can calculate dates far into the future or past by 9 | skipping n number of matching dates. 10 | 11 | The parser can handle increments of ranges (e.g. */12, 2-59/3), intervals (e.g. 0-9), 12 | lists (e.g. 1,2,3), W to find the nearest weekday for a given day of the month, L to 13 | find the last day of the month, L to find the last given weekday of a month, and hash 14 | (#) to find the nth weekday of a given month. 15 | 16 | Credits 17 | ========== 18 | 19 | Created by Micheal Dowling. Ported to PHP 5.2 by Flightless, Inc. 20 | Based on version 1.0.3: https://github.com/mtdowling/cron-expression/tree/v1.0.3 21 | 22 | Installing 23 | ========== 24 | 25 | Add the following to your project's composer.json: 26 | 27 | ```javascript 28 | { 29 | "require": { 30 | "mtdowling/cron-expression": "1.0.*" 31 | } 32 | } 33 | ``` 34 | 35 | Usage 36 | ===== 37 | ```php 38 | isDue(); 45 | echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); 46 | echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s'); 47 | 48 | // Works with complex expressions 49 | $cron = Cron\CronExpression::factory('3-59/15 2,6-12 */15 1 2-5'); 50 | echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); 51 | 52 | // Calculate a run date two iterations into the future 53 | $cron = Cron\CronExpression::factory('@daily'); 54 | echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s'); 55 | 56 | // Calculate a run date relative to a specific time 57 | $cron = Cron\CronExpression::factory('@monthly'); 58 | echo $cron->getNextRunDate('2010-01-12 00:00:00')->format('Y-m-d H:i:s'); 59 | ``` 60 | 61 | CRON Expressions 62 | ================ 63 | 64 | A CRON expression is a string representing the schedule for a particular command to execute. The parts of a CRON schedule are as follows: 65 | 66 | * * * * * * 67 | - - - - - - 68 | | | | | | | 69 | | | | | | + year [optional] 70 | | | | | +----- day of week (0 - 7) (Sunday=0 or 7) 71 | | | | +---------- month (1 - 12) 72 | | | +--------------- day of month (1 - 31) 73 | | +-------------------- hour (0 - 23) 74 | +------------------------- min (0 - 59) 75 | 76 | Requirements 77 | ============ 78 | 79 | - PHP 5.3+ 80 | - PHPUnit is required to run the unit tests 81 | - Composer is required to run the unit tests 82 | 83 | CHANGELOG 84 | ========= 85 | 86 | 1.0.3 (2013-11-23) 87 | ------------------ 88 | 89 | * Only set default timezone if the given $currentTime is not a DateTime instance (#34) 90 | * Fixes issue #28 where PHP increments of ranges were failing due to PHP casting hyphens to 0 91 | * Now supports expressions with any number of extra spaces, tabs, or newlines 92 | * Using static instead of self in `CronExpression::factory` 93 | -------------------------------------------------------------------------------- /classes/migration/Scheduler.php: -------------------------------------------------------------------------------- 1 | get_migration_runner(); 41 | $count = $migration_runner->run( $this->get_batch_size() ); 42 | 43 | if ( 0 === $count ) { 44 | $this->mark_complete(); 45 | } else { 46 | $this->schedule_migration( time() + $this->get_schedule_interval() ); 47 | } 48 | } 49 | 50 | /** 51 | * Mark the migration complete. 52 | */ 53 | public function mark_complete() { 54 | $this->unschedule_migration(); 55 | 56 | \ActionScheduler_DataController::mark_migration_complete(); 57 | do_action( 'action_scheduler/migration_complete' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 58 | } 59 | 60 | /** 61 | * Get a flag indicating whether the migration is scheduled. 62 | * 63 | * @return bool Whether there is a pending action in the store to handle the migration 64 | */ 65 | public function is_migration_scheduled() { 66 | $next = as_next_scheduled_action( self::HOOK ); 67 | 68 | return ! empty( $next ); 69 | } 70 | 71 | /** 72 | * Schedule the migration. 73 | * 74 | * @param int $when Optional timestamp to run the next migration batch. Defaults to now. 75 | * 76 | * @return string The action ID 77 | */ 78 | public function schedule_migration( $when = 0 ) { 79 | $next = as_next_scheduled_action( self::HOOK ); 80 | 81 | if ( ! empty( $next ) ) { 82 | return $next; 83 | } 84 | 85 | if ( empty( $when ) ) { 86 | $when = time() + MINUTE_IN_SECONDS; 87 | } 88 | 89 | return as_schedule_single_action( $when, self::HOOK, array(), self::GROUP ); 90 | } 91 | 92 | /** 93 | * Remove the scheduled migration action. 94 | */ 95 | public function unschedule_migration() { 96 | as_unschedule_action( self::HOOK, null, self::GROUP ); 97 | } 98 | 99 | /** 100 | * Get migration batch schedule interval. 101 | * 102 | * @return int Seconds between migration runs. Defaults to 0 seconds to allow chaining migration via Async Runners. 103 | */ 104 | private function get_schedule_interval() { 105 | return (int) apply_filters( 'action_scheduler/migration_interval', 0 ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 106 | } 107 | 108 | /** 109 | * Get migration batch size. 110 | * 111 | * @return int Number of actions to migrate in each batch. Defaults to 250. 112 | */ 113 | private function get_batch_size() { 114 | return (int) apply_filters( 'action_scheduler/migration_batch_size', 250 ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 115 | } 116 | 117 | /** 118 | * Get migration runner object. 119 | * 120 | * @return Runner 121 | */ 122 | private function get_migration_runner() { 123 | $config = Controller::instance()->get_migration_config_object(); 124 | 125 | return new Runner( $config ); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /classes/WP_CLI/Action/List_Command.php: -------------------------------------------------------------------------------- 1 | process_csv_arguments_to_arrays(); 47 | 48 | if ( ! empty( $this->assoc_args['fields'] ) ) { 49 | $fields = $this->assoc_args['fields']; 50 | } 51 | 52 | $formatter = new \WP_CLI\Formatter( $this->assoc_args, $fields ); 53 | $query_args = $this->assoc_args; 54 | 55 | /** 56 | * The `claimed` parameter expects a boolean or integer: 57 | * check for string 'false', and set explicitly to `false` boolean. 58 | */ 59 | if ( array_key_exists( 'claimed', $query_args ) && 'false' === strtolower( $query_args['claimed'] ) ) { 60 | $query_args['claimed'] = false; 61 | } 62 | 63 | $return_format = 'OBJECT'; 64 | 65 | if ( in_array( $formatter->format, array( 'ids', 'count' ), true ) ) { 66 | $return_format = '\'ids\''; 67 | } 68 | 69 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export 70 | $params = var_export( $query_args, true ); 71 | 72 | if ( empty( $query_args ) ) { 73 | $params = 'array()'; 74 | } 75 | 76 | \WP_CLI::debug( 77 | sprintf( 78 | 'as_get_scheduled_actions( %s, %s )', 79 | $params, 80 | $return_format 81 | ) 82 | ); 83 | 84 | if ( ! empty( $query_args['args'] ) ) { 85 | $query_args['args'] = json_decode( $query_args['args'], true ); 86 | } 87 | 88 | switch ( $formatter->format ) { 89 | 90 | case 'ids': 91 | $actions = as_get_scheduled_actions( $query_args, 'ids' ); 92 | echo implode( ' ', $actions ); 93 | break; 94 | 95 | case 'count': 96 | $actions = as_get_scheduled_actions( $query_args, 'ids' ); 97 | $formatter->display_items( $actions ); 98 | break; 99 | 100 | default: 101 | $actions = as_get_scheduled_actions( $query_args, OBJECT ); 102 | 103 | $actions_arr = array(); 104 | 105 | foreach ( $actions as $action_id => $action ) { 106 | $action_arr = array( 107 | 'id' => $action_id, 108 | 'hook' => $action->get_hook(), 109 | 'status' => $store->get_status( $action_id ), 110 | 'args' => $action->get_args(), 111 | 'group' => $action->get_group(), 112 | 'recurring' => $action->get_schedule()->is_recurring() ? 'yes' : 'no', 113 | 'scheduled_date' => $this->get_schedule_display_string( $action->get_schedule() ), 114 | 'log_entries' => array(), 115 | ); 116 | 117 | foreach ( $logger->get_logs( $action_id ) as $log_entry ) { 118 | $action_arr['log_entries'][] = array( 119 | 'date' => $log_entry->get_date()->format( static::DATE_FORMAT ), 120 | 'message' => $log_entry->get_message(), 121 | ); 122 | } 123 | 124 | $actions_arr[] = $action_arr; 125 | } 126 | 127 | $formatter->display_items( $actions_arr ); 128 | break; 129 | 130 | } 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php: -------------------------------------------------------------------------------- 1 | start - and logic to calculate the next run date after 13 | * that - @see $this->calculate_next(). The $first_date property also keeps a record of when the very 14 | * first instance of this chain of schedules ran. 15 | * 16 | * @var DateTime 17 | */ 18 | private $first_date = null; 19 | 20 | /** 21 | * Timestamp equivalent of @see $this->first_date 22 | * 23 | * @var int 24 | */ 25 | protected $first_timestamp = null; 26 | 27 | /** 28 | * The recurrence between each time an action is run using this schedule. 29 | * Used to calculate the start date & time. Can be a number of seconds, in the 30 | * case of ActionScheduler_IntervalSchedule, or a cron expression, as in the 31 | * case of ActionScheduler_CronSchedule. Or something else. 32 | * 33 | * @var mixed 34 | */ 35 | protected $recurrence; 36 | 37 | /** 38 | * Construct. 39 | * 40 | * @param DateTime $date The date & time to run the action. 41 | * @param mixed $recurrence The data used to determine the schedule's recurrence. 42 | * @param DateTime|null $first (Optional) The date & time the first instance of this interval schedule ran. Default null, meaning this is the first instance. 43 | */ 44 | public function __construct( DateTime $date, $recurrence, ?DateTime $first = null ) { 45 | parent::__construct( $date ); 46 | $this->first_date = empty( $first ) ? $date : $first; 47 | $this->recurrence = $recurrence; 48 | } 49 | 50 | /** 51 | * Schedule is recurring. 52 | * 53 | * @return bool 54 | */ 55 | public function is_recurring() { 56 | return true; 57 | } 58 | 59 | /** 60 | * Get the date & time of the first schedule in this recurring series. 61 | * 62 | * @return DateTime|null 63 | */ 64 | public function get_first_date() { 65 | return clone $this->first_date; 66 | } 67 | 68 | /** 69 | * Get the schedule's recurrence. 70 | * 71 | * @return string 72 | */ 73 | public function get_recurrence() { 74 | return $this->recurrence; 75 | } 76 | 77 | /** 78 | * For PHP 5.2 compat, since DateTime objects can't be serialized 79 | * 80 | * @return array 81 | */ 82 | public function __sleep() { 83 | $sleep_params = parent::__sleep(); 84 | $this->first_timestamp = $this->first_date->getTimestamp(); 85 | return array_merge( 86 | $sleep_params, 87 | array( 88 | 'first_timestamp', 89 | 'recurrence', 90 | ) 91 | ); 92 | } 93 | 94 | /** 95 | * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 96 | * 97 | * Prior to Action Scheduler 3.0.0, schedules used different property names to refer 98 | * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp 99 | * was the same as ActionScheduler_SimpleSchedule::timestamp. This was addressed in 100 | * Action Scheduler 3.0.0, where properties and property names were aligned for better 101 | * inheritance. To maintain backward compatibility with scheduled serialized and stored 102 | * prior to 3.0, we need to correctly map the old property names. 103 | */ 104 | public function __wakeup() { 105 | parent::__wakeup(); 106 | if ( $this->first_timestamp > 0 ) { 107 | $this->first_date = as_get_datetime_object( $this->first_timestamp ); 108 | } else { 109 | $this->first_date = $this->get_date(); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /classes/WP_CLI/Action/Cancel_Command.php: -------------------------------------------------------------------------------- 1 | assoc_args, 'group', '' ); 20 | $callback_args = get_flag_value( $this->assoc_args, 'args', null ); 21 | $all = get_flag_value( $this->assoc_args, 'all', false ); 22 | 23 | if ( ! empty( $this->args[0] ) ) { 24 | $hook = $this->args[0]; 25 | } 26 | 27 | if ( ! empty( $callback_args ) ) { 28 | $callback_args = json_decode( $callback_args, true ); 29 | } 30 | 31 | if ( $all ) { 32 | $this->cancel_all( $hook, $callback_args, $group ); 33 | return; 34 | } 35 | 36 | $this->cancel_single( $hook, $callback_args, $group ); 37 | } 38 | 39 | /** 40 | * Cancel single action. 41 | * 42 | * @param string $hook The hook that the job will trigger. 43 | * @param array $callback_args Args that would have been passed to the job. 44 | * @param string $group The group the job is assigned to. 45 | * @return void 46 | */ 47 | protected function cancel_single( $hook, $callback_args, $group ) { 48 | if ( empty( $hook ) ) { 49 | \WP_CLI::error( __( 'Please specify hook of action to cancel.', 'action-scheduler' ) ); 50 | } 51 | 52 | try { 53 | $result = as_unschedule_action( $hook, $callback_args, $group ); 54 | } catch ( \Exception $e ) { 55 | $this->print_error( $e, false ); 56 | } 57 | 58 | if ( null === $result ) { 59 | $e = new \Exception( __( 'Unable to cancel scheduled action: check the logs.', 'action-scheduler' ) ); 60 | $this->print_error( $e, false ); 61 | } 62 | 63 | $this->print_success( false ); 64 | } 65 | 66 | /** 67 | * Cancel all actions. 68 | * 69 | * @param string $hook The hook that the job will trigger. 70 | * @param array $callback_args Args that would have been passed to the job. 71 | * @param string $group The group the job is assigned to. 72 | * @return void 73 | */ 74 | protected function cancel_all( $hook, $callback_args, $group ) { 75 | if ( empty( $hook ) && empty( $group ) ) { 76 | \WP_CLI::error( __( 'Please specify hook and/or group of actions to cancel.', 'action-scheduler' ) ); 77 | } 78 | 79 | try { 80 | $result = as_unschedule_all_actions( $hook, $callback_args, $group ); 81 | } catch ( \Exception $e ) { 82 | $this->print_error( $e, true ); 83 | } 84 | 85 | /** 86 | * Because as_unschedule_all_actions() does not provide a result, 87 | * neither confirm or deny actions cancelled. 88 | */ 89 | \WP_CLI::success( __( 'Request to cancel scheduled actions completed.', 'action-scheduler' ) ); 90 | } 91 | 92 | /** 93 | * Print a success message. 94 | * 95 | * @return void 96 | */ 97 | protected function print_success() { 98 | \WP_CLI::success( __( 'Scheduled action cancelled.', 'action-scheduler' ) ); 99 | } 100 | 101 | /** 102 | * Convert an exception into a WP CLI error. 103 | * 104 | * @param \Exception $e The error object. 105 | * @param bool $multiple Boolean if multiple actions. 106 | * @throws \WP_CLI\ExitException When an error occurs. 107 | * @return void 108 | */ 109 | protected function print_error( \Exception $e, $multiple ) { 110 | \WP_CLI::error( 111 | sprintf( 112 | /* translators: %1$s: singular or plural %2$s: refers to the exception error message. */ 113 | __( 'There was an error cancelling the %1$s: %2$s', 'action-scheduler' ), 114 | $multiple ? __( 'scheduled actions', 'action-scheduler' ) : __( 'scheduled action', 'action-scheduler' ), 115 | $e->getMessage() 116 | ) 117 | ); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /lib/cron-expression/CronExpression_DayOfMonthField.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class CronExpression_DayOfMonthField extends CronExpression_AbstractField 22 | { 23 | /** 24 | * Get the nearest day of the week for a given day in a month 25 | * 26 | * @param int $currentYear Current year 27 | * @param int $currentMonth Current month 28 | * @param int $targetDay Target day of the month 29 | * 30 | * @return DateTime Returns the nearest date 31 | */ 32 | private static function getNearestWeekday($currentYear, $currentMonth, $targetDay) 33 | { 34 | $tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT); 35 | $target = new DateTime("$currentYear-$currentMonth-$tday"); 36 | $currentWeekday = (int) $target->format('N'); 37 | 38 | if ($currentWeekday < 6) { 39 | return $target; 40 | } 41 | 42 | $lastDayOfMonth = $target->format('t'); 43 | 44 | foreach (array(-1, 1, -2, 2) as $i) { 45 | $adjusted = $targetDay + $i; 46 | if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) { 47 | $target->setDate($currentYear, $currentMonth, $adjusted); 48 | if ($target->format('N') < 6 && $target->format('m') == $currentMonth) { 49 | return $target; 50 | } 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function isSatisfiedBy(DateTime $date, $value) 59 | { 60 | // ? states that the field value is to be skipped 61 | if ($value == '?') { 62 | return true; 63 | } 64 | 65 | $fieldValue = $date->format('d'); 66 | 67 | // Check to see if this is the last day of the month 68 | if ($value == 'L') { 69 | return $fieldValue == $date->format('t'); 70 | } 71 | 72 | // Check to see if this is the nearest weekday to a particular value 73 | if (strpos($value, 'W')) { 74 | // Parse the target day 75 | $targetDay = substr($value, 0, strpos($value, 'W')); 76 | // Find out if the current day is the nearest day of the week 77 | return $date->format('j') == self::getNearestWeekday( 78 | $date->format('Y'), 79 | $date->format('m'), 80 | $targetDay 81 | )->format('j'); 82 | } 83 | 84 | return $this->isSatisfied($date->format('d'), $value); 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function increment(DateTime $date, $invert = false) 91 | { 92 | if ($invert) { 93 | $date->modify('previous day'); 94 | $date->setTime(23, 59); 95 | } else { 96 | $date->modify('next day'); 97 | $date->setTime(0, 0); 98 | } 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * {@inheritdoc} 105 | */ 106 | public function validate($value) 107 | { 108 | return (bool) preg_match('/[\*,\/\-\?LW0-9A-Za-z]+/', $value); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /classes/WP_CLI/Action/Generate_Command.php: -------------------------------------------------------------------------------- 1 | args[0]; 19 | $schedule_start = $this->args[1]; 20 | $callback_args = get_flag_value( $this->assoc_args, 'args', array() ); 21 | $group = get_flag_value( $this->assoc_args, 'group', '' ); 22 | $interval = (int) get_flag_value( $this->assoc_args, 'interval', 0 ); // avoid absint() to support negative intervals 23 | $count = absint( get_flag_value( $this->assoc_args, 'count', 1 ) ); 24 | 25 | if ( ! empty( $callback_args ) ) { 26 | $callback_args = json_decode( $callback_args, true ); 27 | } 28 | 29 | $schedule_start = as_get_datetime_object( $schedule_start ); 30 | 31 | $function_args = array( 32 | 'start' => absint( $schedule_start->format( 'U' ) ), 33 | 'interval' => $interval, 34 | 'count' => $count, 35 | 'hook' => $hook, 36 | 'callback_args' => $callback_args, 37 | 'group' => $group, 38 | ); 39 | 40 | $function_args = array_values( $function_args ); 41 | 42 | try { 43 | $actions_added = $this->generate( ...$function_args ); 44 | } catch ( \Exception $e ) { 45 | $this->print_error( $e ); 46 | } 47 | 48 | $num_actions_added = count( (array) $actions_added ); 49 | 50 | $this->print_success( $num_actions_added, 'single' ); 51 | } 52 | 53 | /** 54 | * Schedule multiple single actions. 55 | * 56 | * @param int $schedule_start Starting timestamp of first action. 57 | * @param int $interval How long to wait between runs. 58 | * @param int $count Limit number of actions to schedule. 59 | * @param string $hook The hook to trigger. 60 | * @param array $args Arguments to pass when the hook triggers. 61 | * @param string $group The group to assign this job to. 62 | * @return int[] IDs of actions added. 63 | */ 64 | protected function generate( $schedule_start, $interval, $count, $hook, array $args = array(), $group = '' ) { 65 | $actions_added = array(); 66 | 67 | $progress_bar = \WP_CLI\Utils\make_progress_bar( 68 | sprintf( 69 | /* translators: %d is number of actions to create */ 70 | _n( 'Creating %d action', 'Creating %d actions', $count, 'action-scheduler' ), 71 | number_format_i18n( $count ) 72 | ), 73 | $count 74 | ); 75 | 76 | for ( $i = 0; $i < $count; $i++ ) { 77 | $actions_added[] = as_schedule_single_action( $schedule_start + ( $i * $interval ), $hook, $args, $group ); 78 | $progress_bar->tick(); 79 | } 80 | 81 | $progress_bar->finish(); 82 | 83 | return $actions_added; 84 | } 85 | 86 | /** 87 | * Print a success message with the action ID. 88 | * 89 | * @param int $actions_added Number of actions generated. 90 | * @param string $action_type Type of actions scheduled. 91 | * @return void 92 | */ 93 | protected function print_success( $actions_added, $action_type ) { 94 | \WP_CLI::success( 95 | sprintf( 96 | /* translators: %1$d refers to the total number of tasks added, %2$s is the action type */ 97 | _n( '%1$d %2$s action scheduled.', '%1$d %2$s actions scheduled.', $actions_added, 'action-scheduler' ), 98 | number_format_i18n( $actions_added ), 99 | $action_type 100 | ) 101 | ); 102 | } 103 | 104 | /** 105 | * Convert an exception into a WP CLI error. 106 | * 107 | * @param \Exception $e The error object. 108 | * @throws \WP_CLI\ExitException When an error occurs. 109 | * @return void 110 | */ 111 | protected function print_error( \Exception $e ) { 112 | \WP_CLI::error( 113 | sprintf( 114 | /* translators: %s refers to the exception error message. */ 115 | __( 'There was an error creating the scheduled action: %s', 'action-scheduler' ), 116 | $e->getMessage() 117 | ) 118 | ); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /classes/schedules/ActionScheduler_CronSchedule.php: -------------------------------------------------------------------------------- 1 | __wakeup() for details. 10 | * 11 | * @var null 12 | */ 13 | private $start_timestamp = null; 14 | 15 | /** 16 | * Deprecated property @see $this->__wakeup() for details. 17 | * 18 | * @var null 19 | */ 20 | private $cron = null; 21 | 22 | /** 23 | * Wrapper for parent constructor to accept a cron expression string and map it to a CronExpression for this 24 | * objects $recurrence property. 25 | * 26 | * @param DateTime $start The date & time to run the action at or after. If $start aligns with the CronSchedule passed via $recurrence, it will be used. If it does not align, the first matching date after it will be used. 27 | * @param CronExpression|string $recurrence The CronExpression used to calculate the schedule's next instance. 28 | * @param DateTime|null $first (Optional) The date & time the first instance of this interval schedule ran. Default null, meaning this is the first instance. 29 | */ 30 | public function __construct( DateTime $start, $recurrence, ?DateTime $first = null ) { 31 | if ( ! is_a( $recurrence, 'CronExpression' ) ) { 32 | $recurrence = CronExpression::factory( $recurrence ); 33 | } 34 | 35 | // For backward compatibility, we need to make sure the date is set to the first matching cron date, not whatever date is passed in. Importantly, by passing true as the 3rd param, if $start matches the cron expression, then it will be used. This was previously handled in the now deprecated next() method. 36 | $date = $recurrence->getNextRunDate( $start, 0, true ); 37 | 38 | // parent::__construct() will set this to $date by default, but that may be different to $start now. 39 | $first = empty( $first ) ? $start : $first; 40 | 41 | parent::__construct( $date, $recurrence, $first ); 42 | } 43 | 44 | /** 45 | * Calculate when an instance of this schedule would start based on a given 46 | * date & time using its the CronExpression. 47 | * 48 | * @param DateTime $after Timestamp. 49 | * @return DateTime 50 | */ 51 | protected function calculate_next( DateTime $after ) { 52 | return $this->recurrence->getNextRunDate( $after, 0, false ); 53 | } 54 | 55 | /** 56 | * Get the schedule's recurrence. 57 | * 58 | * @return string 59 | */ 60 | public function get_recurrence() { 61 | return strval( $this->recurrence ); 62 | } 63 | 64 | /** 65 | * Serialize cron schedules with data required prior to AS 3.0.0 66 | * 67 | * Prior to Action Scheduler 3.0.0, recurring schedules used different property names to 68 | * refer to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp 69 | * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 70 | * aligned properties and property names for better inheritance. To guard against the 71 | * possibility of infinite loops if downgrading to Action Scheduler < 3.0.0, we need to 72 | * also store the data with the old property names so if it's unserialized in AS < 3.0, 73 | * the schedule doesn't end up with a null recurrence. 74 | * 75 | * @return array 76 | */ 77 | public function __sleep() { 78 | 79 | $sleep_params = parent::__sleep(); 80 | 81 | $this->start_timestamp = $this->scheduled_timestamp; 82 | $this->cron = $this->recurrence; 83 | 84 | return array_merge( 85 | $sleep_params, 86 | array( 87 | 'start_timestamp', 88 | 'cron', 89 | ) 90 | ); 91 | } 92 | 93 | /** 94 | * Unserialize cron schedules serialized/stored prior to AS 3.0.0 95 | * 96 | * For more background, @see ActionScheduler_Abstract_RecurringSchedule::__wakeup(). 97 | */ 98 | public function __wakeup() { 99 | if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->start_timestamp ) ) { 100 | $this->scheduled_timestamp = $this->start_timestamp; 101 | unset( $this->start_timestamp ); 102 | } 103 | 104 | if ( is_null( $this->recurrence ) && ! is_null( $this->cron ) ) { 105 | $this->recurrence = $this->cron; 106 | unset( $this->cron ); 107 | } 108 | parent::__wakeup(); 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /classes/ActionScheduler_Compatibility.php: -------------------------------------------------------------------------------- 1 | $wp_max_limit_int && $filtered_limit_int > $current_limit_int ) ) { 68 | if ( false !== @ini_set( 'memory_limit', $filtered_limit ) ) { 69 | return $filtered_limit; 70 | } else { 71 | return false; 72 | } 73 | } elseif ( -1 === $wp_max_limit_int || $wp_max_limit_int > $current_limit_int ) { 74 | if ( false !== @ini_set( 'memory_limit', $wp_max_limit ) ) { 75 | return $wp_max_limit; 76 | } else { 77 | return false; 78 | } 79 | } 80 | 81 | // phpcs:enable 82 | 83 | return false; 84 | } 85 | 86 | /** 87 | * Attempts to raise the PHP timeout for time intensive processes. 88 | * 89 | * Only allows raising the existing limit and prevents lowering it. Wrapper for wc_set_time_limit(), when available. 90 | * 91 | * @param int $limit The time limit in seconds. 92 | */ 93 | public static function raise_time_limit( $limit = 0 ) { 94 | $limit = (int) $limit; 95 | $max_execution_time = (int) ini_get( 'max_execution_time' ); 96 | 97 | // If the max execution time is already set to zero (unlimited), there is no reason to make a further change. 98 | if ( 0 === $max_execution_time ) { 99 | return; 100 | } 101 | 102 | // Whichever of $max_execution_time or $limit is higher is the amount by which we raise the time limit. 103 | $raise_by = 0 === $limit || $limit > $max_execution_time ? $limit : $max_execution_time; 104 | 105 | if ( function_exists( 'wc_set_time_limit' ) ) { 106 | wc_set_time_limit( $raise_by ); 107 | } elseif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved 108 | @set_time_limit( $raise_by ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php: -------------------------------------------------------------------------------- 1 | ] 13 | * : The maximum number of actions to delete per batch. Defaults to 20. 14 | * 15 | * [--batches=] 16 | * : Limit execution to a number of batches. Defaults to 0, meaning batches will continue all eligible actions are deleted. 17 | * 18 | * [--status=] 19 | * : Only clean actions with the specified status. Defaults to Canceled, Completed. Define multiple statuses as a comma separated string (without spaces), e.g. `--status=complete,failed,canceled` 20 | * 21 | * [--before=] 22 | * : Only delete actions with scheduled date older than this. Defaults to 31 days. e.g `--before='7 days ago'`, `--before='02-Feb-2020 20:20:20'` 23 | * 24 | * [--pause=] 25 | * : The number of seconds to pause between batches. Default no pause. 26 | * 27 | * @param array $args Positional arguments. 28 | * @param array $assoc_args Keyed arguments. 29 | * @throws \WP_CLI\ExitException When an error occurs. 30 | * 31 | * @subcommand clean 32 | */ 33 | public function clean( $args, $assoc_args ) { 34 | // Handle passed arguments. 35 | $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 20 ) ); 36 | $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); 37 | $status = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'status', '' ) ); 38 | $status = array_filter( array_map( 'trim', $status ) ); 39 | $before = \WP_CLI\Utils\get_flag_value( $assoc_args, 'before', '' ); 40 | $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); 41 | 42 | $batches_completed = 0; 43 | $actions_deleted = 0; 44 | $unlimited = 0 === $batches; 45 | try { 46 | $lifespan = as_get_datetime_object( $before ); 47 | } catch ( Exception $e ) { 48 | $lifespan = null; 49 | } 50 | 51 | try { 52 | // Custom queue cleaner instance. 53 | $cleaner = new ActionScheduler_QueueCleaner( null, $batch ); 54 | 55 | // Clean actions for as long as possible. 56 | while ( $unlimited || $batches_completed < $batches ) { 57 | if ( $sleep && $batches_completed > 0 ) { 58 | sleep( $sleep ); 59 | } 60 | 61 | $deleted = count( $cleaner->clean_actions( $status, $lifespan, null, 'CLI' ) ); 62 | if ( $deleted <= 0 ) { 63 | break; 64 | } 65 | $actions_deleted += $deleted; 66 | $batches_completed++; 67 | $this->print_success( $deleted ); 68 | } 69 | } catch ( Exception $e ) { 70 | $this->print_error( $e ); 71 | } 72 | 73 | $this->print_total_batches( $batches_completed ); 74 | if ( $batches_completed > 1 ) { 75 | $this->print_success( $actions_deleted ); 76 | } 77 | } 78 | 79 | /** 80 | * Print WP CLI message about how many batches of actions were processed. 81 | * 82 | * @param int $batches_processed Number of batches processed. 83 | */ 84 | protected function print_total_batches( int $batches_processed ) { 85 | WP_CLI::log( 86 | sprintf( 87 | /* translators: %d refers to the total number of batches processed */ 88 | _n( '%d batch processed.', '%d batches processed.', $batches_processed, 'action-scheduler' ), 89 | $batches_processed 90 | ) 91 | ); 92 | } 93 | 94 | /** 95 | * Convert an exception into a WP CLI error. 96 | * 97 | * @param Exception $e The error object. 98 | */ 99 | protected function print_error( Exception $e ) { 100 | WP_CLI::error( 101 | sprintf( 102 | /* translators: %s refers to the exception error message */ 103 | __( 'There was an error deleting an action: %s', 'action-scheduler' ), 104 | $e->getMessage() 105 | ) 106 | ); 107 | } 108 | 109 | /** 110 | * Print a success message with the number of completed actions. 111 | * 112 | * @param int $actions_deleted Number of deleted actions. 113 | */ 114 | protected function print_success( int $actions_deleted ) { 115 | WP_CLI::success( 116 | sprintf( 117 | /* translators: %d refers to the total number of actions deleted */ 118 | _n( '%d action deleted.', '%d actions deleted.', $actions_deleted, 'action-scheduler' ), 119 | $actions_deleted 120 | ) 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /classes/ActionScheduler_Versions.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private $versions = array(); 20 | 21 | /** 22 | * Registered sources. 23 | * 24 | * @var array 25 | */ 26 | private $sources = array(); 27 | 28 | /** 29 | * Register version's callback. 30 | * 31 | * @param string $version_string Action Scheduler version. 32 | * @param callable $initialization_callback Callback to initialize the version. 33 | */ 34 | public function register( $version_string, $initialization_callback ) { 35 | if ( isset( $this->versions[ $version_string ] ) ) { 36 | return false; 37 | } 38 | 39 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace 40 | $backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); 41 | $source = $backtrace[0]['file']; 42 | 43 | $this->versions[ $version_string ] = $initialization_callback; 44 | $this->sources[ $source ] = $version_string; 45 | return true; 46 | } 47 | 48 | /** 49 | * Get all versions. 50 | */ 51 | public function get_versions() { 52 | return $this->versions; 53 | } 54 | 55 | /** 56 | * Get registered sources. 57 | * 58 | * Use with caution: this method is only available as of Action Scheduler's 3.9.1 59 | * release and, owing to the way Action Scheduler is loaded, it's possible that the 60 | * class definition used at runtime will belong to an earlier version. 61 | * 62 | * @since 3.9.1 63 | * 64 | * @return array 65 | */ 66 | public function get_sources() { 67 | return $this->sources; 68 | } 69 | 70 | /** 71 | * Get latest version registered. 72 | */ 73 | public function latest_version() { 74 | $keys = array_keys( $this->versions ); 75 | if ( empty( $keys ) ) { 76 | return false; 77 | } 78 | uasort( $keys, 'version_compare' ); 79 | return end( $keys ); 80 | } 81 | 82 | /** 83 | * Get callback for latest registered version. 84 | */ 85 | public function latest_version_callback() { 86 | $latest = $this->latest_version(); 87 | 88 | if ( empty( $latest ) || ! isset( $this->versions[ $latest ] ) ) { 89 | return '__return_null'; 90 | } 91 | 92 | return $this->versions[ $latest ]; 93 | } 94 | 95 | /** 96 | * Get instance. 97 | * 98 | * @return ActionScheduler_Versions 99 | * @codeCoverageIgnore 100 | */ 101 | public static function instance() { 102 | if ( empty( self::$instance ) ) { 103 | self::$instance = new self(); 104 | } 105 | return self::$instance; 106 | } 107 | 108 | /** 109 | * Initialize. 110 | * 111 | * @codeCoverageIgnore 112 | */ 113 | public static function initialize_latest_version() { 114 | $self = self::instance(); 115 | call_user_func( $self->latest_version_callback() ); 116 | } 117 | 118 | /** 119 | * Returns information about the plugin or theme which contains the current active version 120 | * of Action Scheduler. 121 | * 122 | * If this cannot be determined, or if Action Scheduler is being loaded via some other 123 | * method, then it will return an empty array. Otherwise, if populated, the array will 124 | * look like the following: 125 | * 126 | * [ 127 | * 'type' => 'plugin', # or 'theme' 128 | * 'name' => 'Name', 129 | * ] 130 | * 131 | * @deprecated 3.9.2 Use ActionScheduler_SystemInformation::active_source(). 132 | * 133 | * @return array 134 | */ 135 | public function active_source(): array { 136 | _deprecated_function( __METHOD__, '3.9.2', 'ActionScheduler_SystemInformation::active_source()' ); 137 | return ActionScheduler_SystemInformation::active_source(); 138 | } 139 | 140 | /** 141 | * Returns the directory path for the currently active installation of Action Scheduler. 142 | * 143 | * @deprecated 3.9.2 Use ActionScheduler_SystemInformation::active_source_path(). 144 | * 145 | * @return string 146 | */ 147 | public function active_source_path(): string { 148 | _deprecated_function( __METHOD__, '3.9.2', 'ActionScheduler_SystemInformation::active_source_path()' ); 149 | return ActionScheduler_SystemInformation::active_source_path(); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/cron-expression/CronExpression_DayOfWeekField.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class CronExpression_DayOfWeekField extends CronExpression_AbstractField 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function isSatisfiedBy(DateTime $date, $value) 24 | { 25 | if ($value == '?') { 26 | return true; 27 | } 28 | 29 | // Convert text day of the week values to integers 30 | $value = str_ireplace( 31 | array('SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'), 32 | range(0, 6), 33 | $value 34 | ); 35 | 36 | $currentYear = $date->format('Y'); 37 | $currentMonth = $date->format('m'); 38 | $lastDayOfMonth = $date->format('t'); 39 | 40 | // Find out if this is the last specific weekday of the month 41 | if (strpos($value, 'L')) { 42 | $weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L'))); 43 | $tdate = clone $date; 44 | $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); 45 | while ($tdate->format('w') != $weekday) { 46 | $tdate->setDate($currentYear, $currentMonth, --$lastDayOfMonth); 47 | } 48 | 49 | return $date->format('j') == $lastDayOfMonth; 50 | } 51 | 52 | // Handle # hash tokens 53 | if (strpos($value, '#')) { 54 | list($weekday, $nth) = explode('#', $value); 55 | // Validate the hash fields 56 | if ($weekday < 1 || $weekday > 5) { 57 | throw new InvalidArgumentException("Weekday must be a value between 1 and 5. {$weekday} given"); 58 | } 59 | if ($nth > 5) { 60 | throw new InvalidArgumentException('There are never more than 5 of a given weekday in a month'); 61 | } 62 | // The current weekday must match the targeted weekday to proceed 63 | if ($date->format('N') != $weekday) { 64 | return false; 65 | } 66 | 67 | $tdate = clone $date; 68 | $tdate->setDate($currentYear, $currentMonth, 1); 69 | $dayCount = 0; 70 | $currentDay = 1; 71 | while ($currentDay < $lastDayOfMonth + 1) { 72 | if ($tdate->format('N') == $weekday) { 73 | if (++$dayCount >= $nth) { 74 | break; 75 | } 76 | } 77 | $tdate->setDate($currentYear, $currentMonth, ++$currentDay); 78 | } 79 | 80 | return $date->format('j') == $currentDay; 81 | } 82 | 83 | // Handle day of the week values 84 | if (strpos($value, '-')) { 85 | $parts = explode('-', $value); 86 | if ($parts[0] == '7') { 87 | $parts[0] = '0'; 88 | } elseif ($parts[1] == '0') { 89 | $parts[1] = '7'; 90 | } 91 | $value = implode('-', $parts); 92 | } 93 | 94 | // Test to see which Sunday to use -- 0 == 7 == Sunday 95 | $format = in_array(7, str_split($value)) ? 'N' : 'w'; 96 | $fieldValue = $date->format($format); 97 | 98 | return $this->isSatisfied($fieldValue, $value); 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function increment(DateTime $date, $invert = false) 105 | { 106 | if ($invert) { 107 | $date->modify('-1 day'); 108 | $date->setTime(23, 59, 0); 109 | } else { 110 | $date->modify('+1 day'); 111 | $date->setTime(0, 0, 0); 112 | } 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public function validate($value) 121 | { 122 | return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/WP_Async_Request.php: -------------------------------------------------------------------------------- 1 | identifier = $this->prefix . '_' . $this->action; 64 | 65 | add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); 66 | add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); 67 | } 68 | 69 | /** 70 | * Set data used during the request 71 | * 72 | * @param array $data Data. 73 | * 74 | * @return $this 75 | */ 76 | public function data( $data ) { 77 | $this->data = $data; 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Dispatch the async request 84 | * 85 | * @return array|WP_Error 86 | */ 87 | public function dispatch() { 88 | $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); 89 | $args = $this->get_post_args(); 90 | 91 | return wp_remote_post( esc_url_raw( $url ), $args ); 92 | } 93 | 94 | /** 95 | * Get query args 96 | * 97 | * @return array 98 | */ 99 | protected function get_query_args() { 100 | if ( property_exists( $this, 'query_args' ) ) { 101 | return $this->query_args; 102 | } 103 | 104 | $args = array( 105 | 'action' => $this->identifier, 106 | 'nonce' => wp_create_nonce( $this->identifier ), 107 | ); 108 | 109 | /** 110 | * Filters the post arguments used during an async request. 111 | * 112 | * @param array $url 113 | */ 114 | return apply_filters( $this->identifier . '_query_args', $args ); 115 | } 116 | 117 | /** 118 | * Get query URL 119 | * 120 | * @return string 121 | */ 122 | protected function get_query_url() { 123 | if ( property_exists( $this, 'query_url' ) ) { 124 | return $this->query_url; 125 | } 126 | 127 | $url = admin_url( 'admin-ajax.php' ); 128 | 129 | /** 130 | * Filters the post arguments used during an async request. 131 | * 132 | * @param string $url 133 | */ 134 | return apply_filters( $this->identifier . '_query_url', $url ); 135 | } 136 | 137 | /** 138 | * Get post args 139 | * 140 | * @return array 141 | */ 142 | protected function get_post_args() { 143 | if ( property_exists( $this, 'post_args' ) ) { 144 | return $this->post_args; 145 | } 146 | 147 | $args = array( 148 | 'timeout' => 0.01, 149 | 'blocking' => false, 150 | 'body' => $this->data, 151 | 'cookies' => $_COOKIE, 152 | 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), 153 | ); 154 | 155 | /** 156 | * Filters the post arguments used during an async request. 157 | * 158 | * @param array $args 159 | */ 160 | return apply_filters( $this->identifier . '_post_args', $args ); 161 | } 162 | 163 | /** 164 | * Maybe handle 165 | * 166 | * Check for correct nonce and pass to handler. 167 | */ 168 | public function maybe_handle() { 169 | // Don't lock up other requests while processing. 170 | session_write_close(); 171 | 172 | check_ajax_referer( $this->identifier, 'nonce' ); 173 | 174 | $this->handle(); 175 | 176 | wp_die(); 177 | } 178 | 179 | /** 180 | * Handle 181 | * 182 | * Override this method to perform any actions required 183 | * during the async request. 184 | */ 185 | abstract protected function handle(); 186 | 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /classes/ActionScheduler_OptionLock.php: -------------------------------------------------------------------------------- 1 | maybe_dispatch_async_request() uses a lock to avoid 20 | * calling ActionScheduler_QueueRunner->has_maximum_concurrent_batches() every time the 'shutdown', 21 | * hook is triggered, because that method calls ActionScheduler_QueueRunner->store->get_claim_count() 22 | * to find the current number of claims in the database. 23 | * 24 | * @param string $lock_type A string to identify different lock types. 25 | * @bool True if lock value has changed, false if not or if set failed. 26 | */ 27 | public function set( $lock_type ) { 28 | global $wpdb; 29 | 30 | $lock_key = $this->get_key( $lock_type ); 31 | $existing_lock_value = $this->get_existing_lock( $lock_type ); 32 | $new_lock_value = $this->new_lock_value( $lock_type ); 33 | 34 | // The lock may not exist yet, or may have been deleted. 35 | if ( empty( $existing_lock_value ) ) { 36 | return (bool) $wpdb->insert( 37 | $wpdb->options, 38 | array( 39 | 'option_name' => $lock_key, 40 | 'option_value' => $new_lock_value, 41 | 'autoload' => 'no', 42 | ) 43 | ); 44 | } 45 | 46 | if ( $this->get_expiration_from( $existing_lock_value ) >= time() ) { 47 | return false; 48 | } 49 | 50 | // Otherwise, try to obtain the lock. 51 | return (bool) $wpdb->update( 52 | $wpdb->options, 53 | array( 'option_value' => $new_lock_value ), 54 | array( 55 | 'option_name' => $lock_key, 56 | 'option_value' => $existing_lock_value, 57 | ) 58 | ); 59 | } 60 | 61 | /** 62 | * If a lock is set, return the timestamp it was set to expiry. 63 | * 64 | * @param string $lock_type A string to identify different lock types. 65 | * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. 66 | */ 67 | public function get_expiration( $lock_type ) { 68 | return $this->get_expiration_from( $this->get_existing_lock( $lock_type ) ); 69 | } 70 | 71 | /** 72 | * Given the lock string, derives the lock expiration timestamp (or false if it cannot be determined). 73 | * 74 | * @param string $lock_value String containing a timestamp, or pipe-separated combination of unique value and timestamp. 75 | * 76 | * @return false|int 77 | */ 78 | private function get_expiration_from( $lock_value ) { 79 | $lock_string = explode( '|', $lock_value ); 80 | 81 | // Old style lock? 82 | if ( count( $lock_string ) === 1 && is_numeric( $lock_string[0] ) ) { 83 | return (int) $lock_string[0]; 84 | } 85 | 86 | // New style lock? 87 | if ( count( $lock_string ) === 2 && is_numeric( $lock_string[1] ) ) { 88 | return (int) $lock_string[1]; 89 | } 90 | 91 | return false; 92 | } 93 | 94 | /** 95 | * Get the key to use for storing the lock in the transient 96 | * 97 | * @param string $lock_type A string to identify different lock types. 98 | * @return string 99 | */ 100 | protected function get_key( $lock_type ) { 101 | return sprintf( 'action_scheduler_lock_%s', $lock_type ); 102 | } 103 | 104 | /** 105 | * Supplies the existing lock value, or an empty string if not set. 106 | * 107 | * @param string $lock_type A string to identify different lock types. 108 | * 109 | * @return string 110 | */ 111 | private function get_existing_lock( $lock_type ) { 112 | global $wpdb; 113 | 114 | // Now grab the existing lock value, if there is one. 115 | return (string) $wpdb->get_var( 116 | $wpdb->prepare( 117 | "SELECT option_value FROM $wpdb->options WHERE option_name = %s", 118 | $this->get_key( $lock_type ) 119 | ) 120 | ); 121 | } 122 | 123 | /** 124 | * Supplies a lock value consisting of a unique value and the current timestamp, which are separated by a pipe 125 | * character. 126 | * 127 | * Example: (string) "649de012e6b262.09774912|1688068114" 128 | * 129 | * @param string $lock_type A string to identify different lock types. 130 | * 131 | * @return string 132 | */ 133 | private function new_lock_value( $lock_type ) { 134 | return uniqid( '', true ) . '|' . ( time() + $this->get_duration( $lock_type ) ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /classes/migration/ActionMigrator.php: -------------------------------------------------------------------------------- 1 | source = $source_store; 46 | $this->destination = $destination_store; 47 | $this->log_migrator = $log_migrator; 48 | } 49 | 50 | /** 51 | * Migrate an action. 52 | * 53 | * @param int $source_action_id Action ID. 54 | * 55 | * @return int 0|new action ID 56 | * @throws \RuntimeException When unable to delete action from the source store. 57 | */ 58 | public function migrate( $source_action_id ) { 59 | try { 60 | $action = $this->source->fetch_action( $source_action_id ); 61 | $status = $this->source->get_status( $source_action_id ); 62 | } catch ( \Exception $e ) { 63 | $action = null; 64 | $status = ''; 65 | } 66 | 67 | if ( is_null( $action ) || empty( $status ) || ! $action->get_schedule()->get_date() ) { 68 | // null action or empty status means the fetch operation failed or the action didn't exist. 69 | // null schedule means it's missing vital data. 70 | // delete it and move on. 71 | try { 72 | $this->source->delete_action( $source_action_id ); 73 | } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch 74 | // nothing to do, it didn't exist in the first place. 75 | } 76 | do_action( 'action_scheduler/no_action_to_migrate', $source_action_id, $this->source, $this->destination ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 77 | 78 | return 0; 79 | } 80 | 81 | try { 82 | 83 | // Make sure the last attempt date is set correctly for completed and failed actions. 84 | $last_attempt_date = ( \ActionScheduler_Store::STATUS_PENDING !== $status ) ? $this->source->get_date( $source_action_id ) : null; 85 | 86 | $destination_action_id = $this->destination->save_action( $action, null, $last_attempt_date ); 87 | } catch ( \Exception $e ) { 88 | do_action( 'action_scheduler/migrate_action_failed', $source_action_id, $this->source, $this->destination ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 89 | 90 | return 0; // could not save the action in the new store. 91 | } 92 | 93 | try { 94 | switch ( $status ) { 95 | case \ActionScheduler_Store::STATUS_FAILED: 96 | $this->destination->mark_failure( $destination_action_id ); 97 | break; 98 | case \ActionScheduler_Store::STATUS_CANCELED: 99 | $this->destination->cancel_action( $destination_action_id ); 100 | break; 101 | } 102 | 103 | $this->log_migrator->migrate( $source_action_id, $destination_action_id ); 104 | $this->source->delete_action( $source_action_id ); 105 | 106 | $test_action = $this->source->fetch_action( $source_action_id ); 107 | if ( ! is_a( $test_action, 'ActionScheduler_NullAction' ) ) { 108 | // translators: %s is an action ID. 109 | throw new \RuntimeException( sprintf( __( 'Unable to remove source migrated action %s', 'action-scheduler' ), $source_action_id ) ); 110 | } 111 | do_action( 'action_scheduler/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 112 | 113 | return $destination_action_id; 114 | } catch ( \Exception $e ) { 115 | // could not delete from the old store. 116 | $this->source->mark_migrated( $source_action_id ); 117 | 118 | // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores 119 | do_action( 'action_scheduler/migrate_action_incomplete', $source_action_id, $destination_action_id, $this->source, $this->destination ); 120 | do_action( 'action_scheduler/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination ); 121 | // phpcs:enable 122 | 123 | return $destination_action_id; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /classes/actions/ActionScheduler_Action.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | protected $args = array(); 20 | 21 | /** 22 | * Action's schedule. 23 | * 24 | * @var ActionScheduler_Schedule 25 | */ 26 | protected $schedule = null; 27 | 28 | /** 29 | * Action's group. 30 | * 31 | * @var string 32 | */ 33 | protected $group = ''; 34 | 35 | /** 36 | * Priorities are conceptually similar to those used for regular WordPress actions. 37 | * Like those, a lower priority takes precedence over a higher priority and the default 38 | * is 10. 39 | * 40 | * Unlike regular WordPress actions, the priority of a scheduled action is strictly an 41 | * integer and should be kept within the bounds 0-255 (anything outside the bounds will 42 | * be brought back into the acceptable range). 43 | * 44 | * @var int 45 | */ 46 | protected $priority = 10; 47 | 48 | /** 49 | * Construct. 50 | * 51 | * @param string $hook Action's hook. 52 | * @param mixed[] $args Action's arguments. 53 | * @param null|ActionScheduler_Schedule $schedule Action's schedule. 54 | * @param string $group Action's group. 55 | */ 56 | public function __construct( $hook, array $args = array(), ?ActionScheduler_Schedule $schedule = null, $group = '' ) { 57 | $schedule = empty( $schedule ) ? new ActionScheduler_NullSchedule() : $schedule; 58 | $this->set_hook( $hook ); 59 | $this->set_schedule( $schedule ); 60 | $this->set_args( $args ); 61 | $this->set_group( $group ); 62 | } 63 | 64 | /** 65 | * Executes the action. 66 | * 67 | * If no callbacks are registered, an exception will be thrown and the action will not be 68 | * fired. This is useful to help detect cases where the code responsible for setting up 69 | * a scheduled action no longer exists. 70 | * 71 | * @throws Exception If no callbacks are registered for this action. 72 | */ 73 | public function execute() { 74 | $hook = $this->get_hook(); 75 | 76 | if ( ! has_action( $hook ) ) { 77 | throw new Exception( 78 | sprintf( 79 | /* translators: 1: action hook. */ 80 | __( 'Scheduled action for %1$s will not be executed as no callbacks are registered.', 'action-scheduler' ), 81 | $hook 82 | ) 83 | ); 84 | } 85 | 86 | do_action_ref_array( $hook, array_values( $this->get_args() ) ); 87 | } 88 | 89 | /** 90 | * Set action's hook. 91 | * 92 | * @param string $hook Action's hook. 93 | */ 94 | protected function set_hook( $hook ) { 95 | $this->hook = $hook; 96 | } 97 | 98 | /** 99 | * Get action's hook. 100 | */ 101 | public function get_hook() { 102 | return $this->hook; 103 | } 104 | 105 | /** 106 | * Set action's schedule. 107 | * 108 | * @param ActionScheduler_Schedule $schedule Action's schedule. 109 | */ 110 | protected function set_schedule( ActionScheduler_Schedule $schedule ) { 111 | $this->schedule = $schedule; 112 | } 113 | 114 | /** 115 | * Action's schedule. 116 | * 117 | * @return ActionScheduler_Schedule 118 | */ 119 | public function get_schedule() { 120 | return $this->schedule; 121 | } 122 | 123 | /** 124 | * Set action's args. 125 | * 126 | * @param mixed[] $args Action's arguments. 127 | */ 128 | protected function set_args( array $args ) { 129 | $this->args = $args; 130 | } 131 | 132 | /** 133 | * Get action's args. 134 | */ 135 | public function get_args() { 136 | return $this->args; 137 | } 138 | 139 | /** 140 | * Section action's group. 141 | * 142 | * @param string $group Action's group. 143 | */ 144 | protected function set_group( $group ) { 145 | $this->group = $group; 146 | } 147 | 148 | /** 149 | * Action's group. 150 | * 151 | * @return string 152 | */ 153 | public function get_group() { 154 | return $this->group; 155 | } 156 | 157 | /** 158 | * Action has not finished. 159 | * 160 | * @return bool 161 | */ 162 | public function is_finished() { 163 | return false; 164 | } 165 | 166 | /** 167 | * Sets the priority of the action. 168 | * 169 | * @param int $priority Priority level (lower is higher priority). Should be in the range 0-255. 170 | * 171 | * @return void 172 | */ 173 | public function set_priority( $priority ) { 174 | if ( $priority < 0 ) { 175 | $priority = 0; 176 | } elseif ( $priority > 255 ) { 177 | $priority = 255; 178 | } 179 | 180 | $this->priority = (int) $priority; 181 | } 182 | 183 | /** 184 | * Gets the action priority. 185 | * 186 | * @return int 187 | */ 188 | public function get_priority() { 189 | return $this->priority; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /classes/migration/Runner.php: -------------------------------------------------------------------------------- 1 | source_store = $config->get_source_store(); 79 | $this->destination_store = $config->get_destination_store(); 80 | $this->source_logger = $config->get_source_logger(); 81 | $this->destination_logger = $config->get_destination_logger(); 82 | 83 | $this->batch_fetcher = new BatchFetcher( $this->source_store ); 84 | if ( $config->get_dry_run() ) { 85 | $this->log_migrator = new DryRun_LogMigrator( $this->source_logger, $this->destination_logger ); 86 | $this->action_migrator = new DryRun_ActionMigrator( $this->source_store, $this->destination_store, $this->log_migrator ); 87 | } else { 88 | $this->log_migrator = new LogMigrator( $this->source_logger, $this->destination_logger ); 89 | $this->action_migrator = new ActionMigrator( $this->source_store, $this->destination_store, $this->log_migrator ); 90 | } 91 | 92 | if ( defined( 'WP_CLI' ) && WP_CLI ) { 93 | $this->progress_bar = $config->get_progress_bar(); 94 | } 95 | } 96 | 97 | /** 98 | * Run migration batch. 99 | * 100 | * @param int $batch_size Optional batch size. Default 10. 101 | * 102 | * @return int Size of batch processed. 103 | */ 104 | public function run( $batch_size = 10 ) { 105 | $batch = $this->batch_fetcher->fetch( $batch_size ); 106 | $batch_size = count( $batch ); 107 | 108 | if ( ! $batch_size ) { 109 | return 0; 110 | } 111 | 112 | if ( $this->progress_bar ) { 113 | /* translators: %d: amount of actions */ 114 | $this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), $batch_size ) ); 115 | $this->progress_bar->set_count( $batch_size ); 116 | } 117 | 118 | $this->migrate_actions( $batch ); 119 | 120 | return $batch_size; 121 | } 122 | 123 | /** 124 | * Migration a batch of actions. 125 | * 126 | * @param array $action_ids List of action IDs to migrate. 127 | */ 128 | public function migrate_actions( array $action_ids ) { 129 | do_action( 'action_scheduler/migration_batch_starting', $action_ids ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 130 | 131 | \ActionScheduler::logger()->unhook_stored_action(); 132 | $this->destination_logger->unhook_stored_action(); 133 | 134 | foreach ( $action_ids as $source_action_id ) { 135 | $destination_action_id = $this->action_migrator->migrate( $source_action_id ); 136 | if ( $destination_action_id ) { 137 | $this->destination_logger->log( 138 | $destination_action_id, 139 | sprintf( 140 | /* translators: 1: source action ID 2: source store class 3: destination action ID 4: destination store class */ 141 | __( 'Migrated action with ID %1$d in %2$s to ID %3$d in %4$s', 'action-scheduler' ), 142 | $source_action_id, 143 | get_class( $this->source_store ), 144 | $destination_action_id, 145 | get_class( $this->destination_store ) 146 | ) 147 | ); 148 | } 149 | 150 | if ( $this->progress_bar ) { 151 | $this->progress_bar->tick(); 152 | } 153 | } 154 | 155 | if ( $this->progress_bar ) { 156 | $this->progress_bar->finish(); 157 | } 158 | 159 | \ActionScheduler::logger()->hook_stored_action(); 160 | 161 | do_action( 'action_scheduler/migration_batch_complete', $action_ids ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 162 | } 163 | 164 | /** 165 | * Initialize destination store and logger. 166 | */ 167 | public function init_destination() { 168 | $this->destination_store->init(); 169 | $this->destination_logger->init(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /classes/ActionScheduler_WPCommentCleaner.php: -------------------------------------------------------------------------------- 1 | Status administration screen. 49 | add_action( 'load-tools_page_action-scheduler', array( __CLASS__, 'register_admin_notice' ) ); 50 | add_action( 'load-woocommerce_page_wc-status', array( __CLASS__, 'register_admin_notice' ) ); 51 | } 52 | 53 | /** 54 | * Determines if there are log entries in the wp comments table. 55 | * 56 | * Uses the flag set on migration completion set by @see self::maybe_schedule_cleanup(). 57 | * 58 | * @return boolean Whether there are scheduled action comments in the comments table. 59 | */ 60 | public static function has_logs() { 61 | return 'yes' === get_option( self::$has_logs_option_key ); 62 | } 63 | 64 | /** 65 | * Schedules the WP Post comment table cleanup to run in 6 months if it's not already scheduled. 66 | * Attached to the migration complete hook 'action_scheduler/migration_complete'. 67 | */ 68 | public static function maybe_schedule_cleanup() { 69 | $has_logs = 'no'; 70 | 71 | $args = array( 72 | 'type' => ActionScheduler_wpCommentLogger::TYPE, 73 | 'number' => 1, 74 | 'fields' => 'ids', 75 | ); 76 | 77 | if ( (bool) get_comments( $args ) ) { 78 | $has_logs = 'yes'; 79 | 80 | if ( ! as_next_scheduled_action( self::$cleanup_hook ) ) { 81 | as_schedule_single_action( gmdate( 'U' ) + ( 6 * MONTH_IN_SECONDS ), self::$cleanup_hook ); 82 | } 83 | } 84 | 85 | update_option( self::$has_logs_option_key, $has_logs, true ); 86 | } 87 | 88 | /** 89 | * Delete all action comments from the WP Comments table. 90 | */ 91 | public static function delete_all_action_comments() { 92 | global $wpdb; 93 | 94 | $wpdb->delete( 95 | $wpdb->comments, 96 | array( 97 | 'comment_type' => ActionScheduler_wpCommentLogger::TYPE, 98 | 'comment_agent' => ActionScheduler_wpCommentLogger::AGENT, 99 | ) 100 | ); 101 | 102 | update_option( self::$has_logs_option_key, 'no', true ); 103 | } 104 | 105 | /** 106 | * Registers admin notices about the orphaned action logs. 107 | */ 108 | public static function register_admin_notice() { 109 | add_action( 'admin_notices', array( __CLASS__, 'print_admin_notice' ) ); 110 | } 111 | 112 | /** 113 | * Prints details about the orphaned action logs and includes information on where to learn more. 114 | */ 115 | public static function print_admin_notice() { 116 | $next_cleanup_message = ''; 117 | $next_scheduled_cleanup_hook = as_next_scheduled_action( self::$cleanup_hook ); 118 | 119 | if ( $next_scheduled_cleanup_hook ) { 120 | /* translators: %s: date interval */ 121 | $next_cleanup_message = sprintf( __( 'This data will be deleted in %s.', 'action-scheduler' ), human_time_diff( gmdate( 'U' ), $next_scheduled_cleanup_hook ) ); 122 | } 123 | 124 | $notice = sprintf( 125 | /* translators: 1: next cleanup message 2: github issue URL */ 126 | __( 'Action Scheduler has migrated data to custom tables; however, orphaned log entries exist in the WordPress Comments table. %1$s Learn more »', 'action-scheduler' ), 127 | $next_cleanup_message, 128 | 'https://github.com/woocommerce/action-scheduler/issues/368' 129 | ); 130 | 131 | echo '

' . wp_kses_post( $notice ) . '

'; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /classes/migration/Config.php: -------------------------------------------------------------------------------- 1 | source_store ) ) { 77 | throw new \RuntimeException( __( 'Source store must be configured before running a migration', 'action-scheduler' ) ); 78 | } 79 | 80 | return $this->source_store; 81 | } 82 | 83 | /** 84 | * Set the configured source store. 85 | * 86 | * @param ActionScheduler_Store $store Source store object. 87 | */ 88 | public function set_source_store( Store $store ) { 89 | $this->source_store = $store; 90 | } 91 | 92 | /** 93 | * Get the configured source logger. 94 | * 95 | * @return ActionScheduler_Logger 96 | * @throws \RuntimeException When source logger is not configured. 97 | */ 98 | public function get_source_logger() { 99 | if ( empty( $this->source_logger ) ) { 100 | throw new \RuntimeException( __( 'Source logger must be configured before running a migration', 'action-scheduler' ) ); 101 | } 102 | 103 | return $this->source_logger; 104 | } 105 | 106 | /** 107 | * Set the configured source logger. 108 | * 109 | * @param ActionScheduler_Logger $logger Logger object. 110 | */ 111 | public function set_source_logger( Logger $logger ) { 112 | $this->source_logger = $logger; 113 | } 114 | 115 | /** 116 | * Get the configured destination store. 117 | * 118 | * @return ActionScheduler_Store 119 | * @throws \RuntimeException When destination store is not configured. 120 | */ 121 | public function get_destination_store() { 122 | if ( empty( $this->destination_store ) ) { 123 | throw new \RuntimeException( __( 'Destination store must be configured before running a migration', 'action-scheduler' ) ); 124 | } 125 | 126 | return $this->destination_store; 127 | } 128 | 129 | /** 130 | * Set the configured destination store. 131 | * 132 | * @param ActionScheduler_Store $store Action store object. 133 | */ 134 | public function set_destination_store( Store $store ) { 135 | $this->destination_store = $store; 136 | } 137 | 138 | /** 139 | * Get the configured destination logger. 140 | * 141 | * @return ActionScheduler_Logger 142 | * @throws \RuntimeException When destination logger is not configured. 143 | */ 144 | public function get_destination_logger() { 145 | if ( empty( $this->destination_logger ) ) { 146 | throw new \RuntimeException( __( 'Destination logger must be configured before running a migration', 'action-scheduler' ) ); 147 | } 148 | 149 | return $this->destination_logger; 150 | } 151 | 152 | /** 153 | * Set the configured destination logger. 154 | * 155 | * @param ActionScheduler_Logger $logger Logger object. 156 | */ 157 | public function set_destination_logger( Logger $logger ) { 158 | $this->destination_logger = $logger; 159 | } 160 | 161 | /** 162 | * Get flag indicating whether it's a dry run. 163 | * 164 | * @return bool 165 | */ 166 | public function get_dry_run() { 167 | return $this->dry_run; 168 | } 169 | 170 | /** 171 | * Set flag indicating whether it's a dry run. 172 | * 173 | * @param bool $dry_run Dry run toggle. 174 | */ 175 | public function set_dry_run( $dry_run ) { 176 | $this->dry_run = (bool) $dry_run; 177 | } 178 | 179 | /** 180 | * Get progress bar object. 181 | * 182 | * @return ActionScheduler\WPCLI\ProgressBar 183 | */ 184 | public function get_progress_bar() { 185 | return $this->progress_bar; 186 | } 187 | 188 | /** 189 | * Set progress bar object. 190 | * 191 | * @param ActionScheduler\WPCLI\ProgressBar $progress_bar Progress bar object. 192 | */ 193 | public function set_progress_bar( ProgressBar $progress_bar ) { 194 | $this->progress_bar = $progress_bar; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /classes/WP_CLI/Action/Create_Command.php: -------------------------------------------------------------------------------- 1 | args[0]; 21 | $schedule_start = $this->args[1]; 22 | $callback_args = get_flag_value( $this->assoc_args, 'args', array() ); 23 | $group = get_flag_value( $this->assoc_args, 'group', '' ); 24 | $interval = absint( get_flag_value( $this->assoc_args, 'interval', 0 ) ); 25 | $cron = get_flag_value( $this->assoc_args, 'cron', '' ); 26 | $unique = get_flag_value( $this->assoc_args, 'unique', false ); 27 | $priority = absint( get_flag_value( $this->assoc_args, 'priority', 10 ) ); 28 | 29 | if ( ! empty( $callback_args ) ) { 30 | $callback_args = json_decode( $callback_args, true ); 31 | } 32 | 33 | $function_args = array( 34 | 'start' => $schedule_start, 35 | 'cron' => $cron, 36 | 'interval' => $interval, 37 | 'hook' => $hook, 38 | 'callback_args' => $callback_args, 39 | 'group' => $group, 40 | 'unique' => $unique, 41 | 'priority' => $priority, 42 | ); 43 | 44 | try { 45 | // Generate schedule start if appropriate. 46 | if ( ! in_array( $schedule_start, static::ASYNC_OPTS, true ) ) { 47 | $schedule_start = as_get_datetime_object( $schedule_start ); 48 | $function_args['start'] = $schedule_start->format( 'U' ); 49 | } 50 | } catch ( \Exception $e ) { 51 | \WP_CLI::error( $e->getMessage() ); 52 | } 53 | 54 | // Default to creating single action. 55 | $action_type = 'single'; 56 | $function = 'as_schedule_single_action'; 57 | 58 | if ( ! empty( $interval ) ) { // Creating recurring action. 59 | $action_type = 'recurring'; 60 | $function = 'as_schedule_recurring_action'; 61 | 62 | $function_args = array_filter( 63 | $function_args, 64 | static function( $key ) { 65 | return in_array( $key, array( 'start', 'interval', 'hook', 'callback_args', 'group', 'unique', 'priority' ), true ); 66 | }, 67 | ARRAY_FILTER_USE_KEY 68 | ); 69 | } elseif ( ! empty( $cron ) ) { // Creating cron action. 70 | $action_type = 'cron'; 71 | $function = 'as_schedule_cron_action'; 72 | 73 | $function_args = array_filter( 74 | $function_args, 75 | static function( $key ) { 76 | return in_array( $key, array( 'start', 'cron', 'hook', 'callback_args', 'group', 'unique', 'priority' ), true ); 77 | }, 78 | ARRAY_FILTER_USE_KEY 79 | ); 80 | } elseif ( in_array( $function_args['start'], static::ASYNC_OPTS, true ) ) { // Enqueue async action. 81 | $action_type = 'async'; 82 | $function = 'as_enqueue_async_action'; 83 | 84 | $function_args = array_filter( 85 | $function_args, 86 | static function( $key ) { 87 | return in_array( $key, array( 'hook', 'callback_args', 'group', 'unique', 'priority' ), true ); 88 | }, 89 | ARRAY_FILTER_USE_KEY 90 | ); 91 | } else { // Enqueue single action. 92 | $function_args = array_filter( 93 | $function_args, 94 | static function( $key ) { 95 | return in_array( $key, array( 'start', 'hook', 'callback_args', 'group', 'unique', 'priority' ), true ); 96 | }, 97 | ARRAY_FILTER_USE_KEY 98 | ); 99 | } 100 | 101 | $function_args = array_values( $function_args ); 102 | 103 | try { 104 | $action_id = call_user_func_array( $function, $function_args ); 105 | } catch ( \Exception $e ) { 106 | $this->print_error( $e ); 107 | } 108 | 109 | if ( 0 === $action_id ) { 110 | $e = new \Exception( __( 'Unable to create a scheduled action.', 'action-scheduler' ) ); 111 | $this->print_error( $e ); 112 | } 113 | 114 | $this->print_success( $action_id, $action_type ); 115 | } 116 | 117 | /** 118 | * Print a success message with the action ID. 119 | * 120 | * @param int $action_id Created action ID. 121 | * @param string $action_type Type of action. 122 | * 123 | * @return void 124 | */ 125 | protected function print_success( $action_id, $action_type ) { 126 | \WP_CLI::success( 127 | sprintf( 128 | /* translators: %1$s: type of action, %2$d: ID of the created action */ 129 | __( '%1$s action (%2$d) scheduled.', 'action-scheduler' ), 130 | ucfirst( $action_type ), 131 | $action_id 132 | ) 133 | ); 134 | } 135 | 136 | /** 137 | * Convert an exception into a WP CLI error. 138 | * 139 | * @param \Exception $e The error object. 140 | * @throws \WP_CLI\ExitException When an error occurs. 141 | * @return void 142 | */ 143 | protected function print_error( \Exception $e ) { 144 | \WP_CLI::error( 145 | sprintf( 146 | /* translators: %s refers to the exception error message. */ 147 | __( 'There was an error creating the scheduled action: %s', 'action-scheduler' ), 148 | $e->getMessage() 149 | ) 150 | ); 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /classes/data-stores/ActionScheduler_DBLogger.php: -------------------------------------------------------------------------------- 1 | format( 'Y-m-d H:i:s' ); 29 | ActionScheduler_TimezoneHelper::set_local_timezone( $date ); 30 | $date_local = $date->format( 'Y-m-d H:i:s' ); 31 | 32 | /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort 33 | global $wpdb; 34 | $wpdb->insert( 35 | $wpdb->actionscheduler_logs, 36 | array( 37 | 'action_id' => $action_id, 38 | 'message' => $message, 39 | 'log_date_gmt' => $date_gmt, 40 | 'log_date_local' => $date_local, 41 | ), 42 | array( '%d', '%s', '%s', '%s' ) 43 | ); 44 | 45 | return $wpdb->insert_id; 46 | } 47 | 48 | /** 49 | * Retrieve an action log entry. 50 | * 51 | * @param int $entry_id Log entry ID. 52 | * 53 | * @return ActionScheduler_LogEntry 54 | */ 55 | public function get_entry( $entry_id ) { 56 | /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort 57 | global $wpdb; 58 | $entry = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE log_id=%d", $entry_id ) ); 59 | 60 | return $this->create_entry_from_db_record( $entry ); 61 | } 62 | 63 | /** 64 | * Create an action log entry from a database record. 65 | * 66 | * @param object $record Log entry database record object. 67 | * 68 | * @return ActionScheduler_LogEntry 69 | */ 70 | private function create_entry_from_db_record( $record ) { 71 | if ( empty( $record ) ) { 72 | return new ActionScheduler_NullLogEntry(); 73 | } 74 | 75 | if ( is_null( $record->log_date_gmt ) ) { 76 | $date = as_get_datetime_object( ActionScheduler_StoreSchema::DEFAULT_DATE ); 77 | } else { 78 | $date = as_get_datetime_object( $record->log_date_gmt ); 79 | } 80 | 81 | return new ActionScheduler_LogEntry( $record->action_id, $record->message, $date ); 82 | } 83 | 84 | /** 85 | * Retrieve an action's log entries from the database. 86 | * 87 | * @param int $action_id Action ID. 88 | * 89 | * @return ActionScheduler_LogEntry[] 90 | */ 91 | public function get_logs( $action_id ) { 92 | /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort 93 | global $wpdb; 94 | 95 | $records = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE action_id=%d", $action_id ) ); 96 | 97 | return array_map( array( $this, 'create_entry_from_db_record' ), $records ); 98 | } 99 | 100 | /** 101 | * Initialize the data store. 102 | * 103 | * @codeCoverageIgnore 104 | */ 105 | public function init() { 106 | $table_maker = new ActionScheduler_LoggerSchema(); 107 | $table_maker->init(); 108 | $table_maker->register_tables(); 109 | 110 | parent::init(); 111 | 112 | add_action( 'action_scheduler_deleted_action', array( $this, 'clear_deleted_action_logs' ), 10, 1 ); 113 | } 114 | 115 | /** 116 | * Delete the action logs for an action. 117 | * 118 | * @param int $action_id Action ID. 119 | */ 120 | public function clear_deleted_action_logs( $action_id ) { 121 | /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort 122 | global $wpdb; 123 | $wpdb->delete( $wpdb->actionscheduler_logs, array( 'action_id' => $action_id ), array( '%d' ) ); 124 | } 125 | 126 | /** 127 | * Bulk add cancel action log entries. 128 | * 129 | * @param array $action_ids List of action ID. 130 | */ 131 | public function bulk_log_cancel_actions( $action_ids ) { 132 | if ( empty( $action_ids ) ) { 133 | return; 134 | } 135 | 136 | /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort 137 | global $wpdb; 138 | $date = as_get_datetime_object(); 139 | $date_gmt = $date->format( 'Y-m-d H:i:s' ); 140 | ActionScheduler_TimezoneHelper::set_local_timezone( $date ); 141 | $date_local = $date->format( 'Y-m-d H:i:s' ); 142 | $message = __( 'action canceled', 'action-scheduler' ); 143 | $format = '(%d, ' . $wpdb->prepare( '%s, %s, %s', $message, $date_gmt, $date_local ) . ')'; 144 | $sql_query = "INSERT {$wpdb->actionscheduler_logs} (action_id, message, log_date_gmt, log_date_local) VALUES "; 145 | $value_rows = array(); 146 | 147 | foreach ( $action_ids as $action_id ) { 148 | $value_rows[] = $wpdb->prepare( $format, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 149 | } 150 | $sql_query .= implode( ',', $value_rows ); 151 | 152 | $wpdb->query( $sql_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /classes/abstracts/ActionScheduler_TimezoneHelper.php: -------------------------------------------------------------------------------- 1 | format( 'U' ) ); 29 | } 30 | 31 | if ( get_option( 'timezone_string' ) ) { 32 | $date->setTimezone( new DateTimeZone( self::get_local_timezone_string() ) ); 33 | } else { 34 | $date->setUtcOffset( self::get_local_timezone_offset() ); 35 | } 36 | 37 | return $date; 38 | } 39 | 40 | /** 41 | * Helper to retrieve the timezone string for a site until a WP core method exists 42 | * (see https://core.trac.wordpress.org/ticket/24730). 43 | * 44 | * Adapted from wc_timezone_string() and https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155. 45 | * 46 | * If no timezone string is set, and its not possible to match the UTC offset set for the site to a timezone 47 | * string, then an empty string will be returned, and the UTC offset should be used to set a DateTime's 48 | * timezone. 49 | * 50 | * @since 2.1.0 51 | * @param bool $reset Unused. 52 | * @return string PHP timezone string for the site or empty if no timezone string is available. 53 | */ 54 | protected static function get_local_timezone_string( $reset = false ) { 55 | // If site timezone string exists, return it. 56 | $timezone = get_option( 'timezone_string' ); 57 | if ( $timezone ) { 58 | return $timezone; 59 | } 60 | 61 | // Get UTC offset, if it isn't set then return UTC. 62 | $utc_offset = intval( get_option( 'gmt_offset', 0 ) ); 63 | if ( 0 === $utc_offset ) { 64 | return 'UTC'; 65 | } 66 | 67 | // Adjust UTC offset from hours to seconds. 68 | $utc_offset *= 3600; 69 | 70 | // Attempt to guess the timezone string from the UTC offset. 71 | $timezone = timezone_name_from_abbr( '', $utc_offset ); 72 | if ( $timezone ) { 73 | return $timezone; 74 | } 75 | 76 | // Last try, guess timezone string manually. 77 | foreach ( timezone_abbreviations_list() as $abbr ) { 78 | foreach ( $abbr as $city ) { 79 | if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- we are actually interested in the runtime timezone. 80 | return $city['timezone_id']; 81 | } 82 | } 83 | } 84 | 85 | // No timezone string. 86 | return ''; 87 | } 88 | 89 | /** 90 | * Get timezone offset in seconds. 91 | * 92 | * @since 2.1.0 93 | * @return float 94 | */ 95 | protected static function get_local_timezone_offset() { 96 | $timezone = get_option( 'timezone_string' ); 97 | 98 | if ( $timezone ) { 99 | $timezone_object = new DateTimeZone( $timezone ); 100 | return $timezone_object->getOffset( new DateTime( 'now' ) ); 101 | } else { 102 | return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS; 103 | } 104 | } 105 | 106 | /** 107 | * Get local timezone. 108 | * 109 | * @param bool $reset Toggle to discard stored value. 110 | * @deprecated 2.1.0 111 | */ 112 | public static function get_local_timezone( $reset = false ) { 113 | _deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' ); 114 | if ( $reset ) { 115 | self::$local_timezone = null; 116 | } 117 | if ( ! isset( self::$local_timezone ) ) { 118 | $tzstring = get_option( 'timezone_string' ); 119 | 120 | if ( empty( $tzstring ) ) { 121 | $gmt_offset = absint( get_option( 'gmt_offset' ) ); 122 | if ( 0 === $gmt_offset ) { 123 | $tzstring = 'UTC'; 124 | } else { 125 | $gmt_offset *= HOUR_IN_SECONDS; 126 | $tzstring = timezone_name_from_abbr( '', $gmt_offset, 1 ); 127 | 128 | // If there's no timezone string, try again with no DST. 129 | if ( false === $tzstring ) { 130 | $tzstring = timezone_name_from_abbr( '', $gmt_offset, 0 ); 131 | } 132 | 133 | // Try mapping to the first abbreviation we can find. 134 | if ( false === $tzstring ) { 135 | $is_dst = date( 'I' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- we are actually interested in the runtime timezone. 136 | foreach ( timezone_abbreviations_list() as $abbr ) { 137 | foreach ( $abbr as $city ) { 138 | if ( $city['dst'] === $is_dst && $city['offset'] === $gmt_offset ) { 139 | // If there's no valid timezone ID, keep looking. 140 | if ( is_null( $city['timezone_id'] ) ) { 141 | continue; 142 | } 143 | 144 | $tzstring = $city['timezone_id']; 145 | break 2; 146 | } 147 | } 148 | } 149 | } 150 | 151 | // If we still have no valid string, then fall back to UTC. 152 | if ( false === $tzstring ) { 153 | $tzstring = 'UTC'; 154 | } 155 | } 156 | } 157 | 158 | self::$local_timezone = new DateTimeZone( $tzstring ); 159 | } 160 | return self::$local_timezone; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /classes/WP_CLI/Action/Run_Command.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | protected $action_counts = array( 23 | 'executed' => 0, 24 | 'failed' => 0, 25 | 'ignored' => 0, 26 | 'invalid' => 0, 27 | 'total' => 0, 28 | ); 29 | 30 | /** 31 | * Construct. 32 | * 33 | * @param string[] $args Positional arguments. 34 | * @param array $assoc_args Keyed arguments. 35 | */ 36 | public function __construct( array $args, array $assoc_args ) { 37 | parent::__construct( $args, $assoc_args ); 38 | 39 | $this->action_ids = array_map( 'absint', $args ); 40 | $this->action_counts['total'] = count( $this->action_ids ); 41 | 42 | add_action( 'action_scheduler_execution_ignored', array( $this, 'on_action_ignored' ) ); 43 | add_action( 'action_scheduler_after_execute', array( $this, 'on_action_executed' ) ); 44 | add_action( 'action_scheduler_failed_execution', array( $this, 'on_action_failed' ), 10, 2 ); 45 | add_action( 'action_scheduler_failed_validation', array( $this, 'on_action_invalid' ), 10, 2 ); 46 | } 47 | 48 | /** 49 | * Execute. 50 | * 51 | * @return void 52 | */ 53 | public function execute() { 54 | $runner = \ActionScheduler::runner(); 55 | 56 | $progress_bar = \WP_CLI\Utils\make_progress_bar( 57 | sprintf( 58 | /* translators: %d: number of actions */ 59 | _n( 'Executing %d action', 'Executing %d actions', $this->action_counts['total'], 'action-scheduler' ), 60 | number_format_i18n( $this->action_counts['total'] ) 61 | ), 62 | $this->action_counts['total'] 63 | ); 64 | 65 | foreach ( $this->action_ids as $action_id ) { 66 | $runner->process_action( $action_id, 'Action Scheduler CLI' ); 67 | $progress_bar->tick(); 68 | } 69 | 70 | $progress_bar->finish(); 71 | 72 | foreach ( array( 73 | 'ignored', 74 | 'invalid', 75 | 'failed', 76 | ) as $type ) { 77 | $count = $this->action_counts[ $type ]; 78 | 79 | if ( empty( $count ) ) { 80 | continue; 81 | } 82 | 83 | /* 84 | * translators: 85 | * %1$d: count of actions evaluated. 86 | * %2$s: type of action evaluated. 87 | */ 88 | $format = _n( '%1$d action %2$s.', '%1$d actions %2$s.', $count, 'action-scheduler' ); 89 | 90 | \WP_CLI::warning( 91 | sprintf( 92 | $format, 93 | number_format_i18n( $count ), 94 | $type 95 | ) 96 | ); 97 | } 98 | 99 | \WP_CLI::success( 100 | sprintf( 101 | /* translators: %d: number of executed actions */ 102 | _n( 'Executed %d action.', 'Executed %d actions.', $this->action_counts['executed'], 'action-scheduler' ), 103 | number_format_i18n( $this->action_counts['executed'] ) 104 | ) 105 | ); 106 | } 107 | 108 | /** 109 | * Action: action_scheduler_execution_ignored 110 | * 111 | * @param int $action_id Action ID. 112 | * @return void 113 | */ 114 | public function on_action_ignored( $action_id ) { 115 | if ( 'action_scheduler_execution_ignored' !== current_action() ) { 116 | return; 117 | } 118 | 119 | $action_id = absint( $action_id ); 120 | 121 | if ( ! in_array( $action_id, $this->action_ids, true ) ) { 122 | return; 123 | } 124 | 125 | $this->action_counts['ignored']++; 126 | \WP_CLI::debug( sprintf( 'Action %d was ignored.', $action_id ) ); 127 | } 128 | 129 | /** 130 | * Action: action_scheduler_after_execute 131 | * 132 | * @param int $action_id Action ID. 133 | * @return void 134 | */ 135 | public function on_action_executed( $action_id ) { 136 | if ( 'action_scheduler_after_execute' !== current_action() ) { 137 | return; 138 | } 139 | 140 | $action_id = absint( $action_id ); 141 | 142 | if ( ! in_array( $action_id, $this->action_ids, true ) ) { 143 | return; 144 | } 145 | 146 | $this->action_counts['executed']++; 147 | \WP_CLI::debug( sprintf( 'Action %d was executed.', $action_id ) ); 148 | } 149 | 150 | /** 151 | * Action: action_scheduler_failed_execution 152 | * 153 | * @param int $action_id Action ID. 154 | * @param \Exception $e Exception. 155 | * @return void 156 | */ 157 | public function on_action_failed( $action_id, \Exception $e ) { 158 | if ( 'action_scheduler_failed_execution' !== current_action() ) { 159 | return; 160 | } 161 | 162 | $action_id = absint( $action_id ); 163 | 164 | if ( ! in_array( $action_id, $this->action_ids, true ) ) { 165 | return; 166 | } 167 | 168 | $this->action_counts['failed']++; 169 | \WP_CLI::debug( sprintf( 'Action %d failed execution: %s', $action_id, $e->getMessage() ) ); 170 | } 171 | 172 | /** 173 | * Action: action_scheduler_failed_validation 174 | * 175 | * @param int $action_id Action ID. 176 | * @param \Exception $e Exception. 177 | * @return void 178 | */ 179 | public function on_action_invalid( $action_id, \Exception $e ) { 180 | if ( 'action_scheduler_failed_validation' !== current_action() ) { 181 | return; 182 | } 183 | 184 | $action_id = absint( $action_id ); 185 | 186 | if ( ! in_array( $action_id, $this->action_ids, true ) ) { 187 | return; 188 | } 189 | 190 | $this->action_counts['invalid']++; 191 | \WP_CLI::debug( sprintf( 'Action %d failed validation: %s', $action_id, $e->getMessage() ) ); 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /deprecated/functions.php: -------------------------------------------------------------------------------- 1 | '' - the name of the action that will be triggered 108 | * 'args' => NULL - the args array that will be passed with the action 109 | * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. 110 | * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' 111 | * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. 112 | * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' 113 | * 'group' => '' - the group the action belongs to 114 | * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING 115 | * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID 116 | * 'per_page' => 5 - Number of results to return 117 | * 'offset' => 0 118 | * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date' 119 | * 'order' => 'ASC'. 120 | * @param string $return_format OBJECT, ARRAY_A, or ids. 121 | * 122 | * @deprecated 2.1.0 123 | * 124 | * @return array 125 | */ 126 | function wc_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { 127 | _deprecated_function( __FUNCTION__, '2.1.0', 'as_get_scheduled_actions()' ); 128 | return as_get_scheduled_actions( $args, $return_format ); 129 | } 130 | -------------------------------------------------------------------------------- /classes/abstracts/ActionScheduler_Abstract_Schema.php: -------------------------------------------------------------------------------- 1 | tables as $table ) { 54 | $wpdb->tables[] = $table; 55 | $name = $this->get_full_table_name( $table ); 56 | $wpdb->$table = $name; 57 | } 58 | 59 | // create the tables. 60 | if ( $this->schema_update_required() || $force_update ) { 61 | foreach ( $this->tables as $table ) { 62 | /** 63 | * Allow custom processing before updating a table schema. 64 | * 65 | * @param string $table Name of table being updated. 66 | * @param string $db_version Existing version of the table being updated. 67 | */ 68 | do_action( 'action_scheduler_before_schema_update', $table, $this->db_version ); 69 | $this->update_table( $table ); 70 | } 71 | $this->mark_schema_update_complete(); 72 | } 73 | } 74 | 75 | /** 76 | * Get table definition. 77 | * 78 | * @param string $table The name of the table. 79 | * 80 | * @return string The CREATE TABLE statement, suitable for passing to dbDelta 81 | */ 82 | abstract protected function get_table_definition( $table ); 83 | 84 | /** 85 | * Determine if the database schema is out of date 86 | * by comparing the integer found in $this->schema_version 87 | * with the option set in the WordPress options table 88 | * 89 | * @return bool 90 | */ 91 | private function schema_update_required() { 92 | $option_name = 'schema-' . static::class; 93 | $this->db_version = get_option( $option_name, 0 ); 94 | 95 | // Check for schema option stored by the Action Scheduler Custom Tables plugin in case site has migrated from that plugin with an older schema. 96 | if ( 0 === $this->db_version ) { 97 | 98 | $plugin_option_name = 'schema-'; 99 | 100 | switch ( static::class ) { 101 | case 'ActionScheduler_StoreSchema': 102 | $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Store_Table_Maker'; 103 | break; 104 | case 'ActionScheduler_LoggerSchema': 105 | $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Logger_Table_Maker'; 106 | break; 107 | } 108 | 109 | $this->db_version = get_option( $plugin_option_name, 0 ); 110 | 111 | delete_option( $plugin_option_name ); 112 | } 113 | 114 | return version_compare( $this->db_version, $this->schema_version, '<' ); 115 | } 116 | 117 | /** 118 | * Update the option in WordPress to indicate that 119 | * our schema is now up to date 120 | * 121 | * @return void 122 | */ 123 | private function mark_schema_update_complete() { 124 | $option_name = 'schema-' . static::class; 125 | 126 | // work around race conditions and ensure that our option updates. 127 | $value_to_save = (string) $this->schema_version . '.0.' . time(); 128 | 129 | update_option( $option_name, $value_to_save ); 130 | } 131 | 132 | /** 133 | * Update the schema for the given table 134 | * 135 | * @param string $table The name of the table to update. 136 | * 137 | * @return void 138 | */ 139 | private function update_table( $table ) { 140 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 141 | $definition = $this->get_table_definition( $table ); 142 | if ( $definition ) { 143 | $updated = dbDelta( $definition ); 144 | foreach ( $updated as $updated_table => $update_description ) { 145 | if ( strpos( $update_description, 'Created table' ) === 0 ) { 146 | do_action( 'action_scheduler/created_table', $updated_table, $table ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 147 | } 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * Get full table name. 154 | * 155 | * @param string $table Table name. 156 | * 157 | * @return string The full name of the table, including the 158 | * table prefix for the current blog 159 | */ 160 | protected function get_full_table_name( $table ) { 161 | return $GLOBALS['wpdb']->prefix . $table; 162 | } 163 | 164 | /** 165 | * Confirms that all of the tables registered by this schema class have been created. 166 | * 167 | * @return bool 168 | */ 169 | public function tables_exist() { 170 | global $wpdb; 171 | 172 | $tables_exist = true; 173 | 174 | foreach ( $this->tables as $table_name ) { 175 | $table_name = $wpdb->prefix . $table_name; 176 | $pattern = str_replace( '_', '\\_', $table_name ); 177 | $existing_table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pattern ) ); 178 | 179 | if ( $existing_table !== $table_name ) { 180 | $tables_exist = false; 181 | break; 182 | } 183 | } 184 | 185 | return $tables_exist; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /classes/schema/ActionScheduler_StoreSchema.php: -------------------------------------------------------------------------------- 1 | tables = array( 30 | self::ACTIONS_TABLE, 31 | self::CLAIMS_TABLE, 32 | self::GROUPS_TABLE, 33 | ); 34 | } 35 | 36 | /** 37 | * Performs additional setup work required to support this schema. 38 | */ 39 | public function init() { 40 | add_action( 'action_scheduler_before_schema_update', array( $this, 'update_schema_5_0' ), 10, 2 ); 41 | } 42 | 43 | /** 44 | * Get table definition. 45 | * 46 | * @param string $table Table name. 47 | */ 48 | protected function get_table_definition( $table ) { 49 | global $wpdb; 50 | $table_name = $wpdb->$table; 51 | $charset_collate = $wpdb->get_charset_collate(); 52 | $default_date = self::DEFAULT_DATE; 53 | // phpcs:ignore Squiz.PHP.CommentedOutCode 54 | $max_index_length = 191; // @see wp_get_db_schema() 55 | 56 | $hook_status_scheduled_date_gmt_max_index_length = $max_index_length - 20 - 8; // - status, - scheduled_date_gmt 57 | 58 | switch ( $table ) { 59 | 60 | case self::ACTIONS_TABLE: 61 | return "CREATE TABLE {$table_name} ( 62 | action_id bigint(20) unsigned NOT NULL auto_increment, 63 | hook varchar(191) NOT NULL, 64 | status varchar(20) NOT NULL, 65 | scheduled_date_gmt datetime NULL default '{$default_date}', 66 | scheduled_date_local datetime NULL default '{$default_date}', 67 | priority tinyint unsigned NOT NULL default '10', 68 | args varchar($max_index_length), 69 | schedule longtext, 70 | group_id bigint(20) unsigned NOT NULL default '0', 71 | attempts int(11) NOT NULL default '0', 72 | last_attempt_gmt datetime NULL default '{$default_date}', 73 | last_attempt_local datetime NULL default '{$default_date}', 74 | claim_id bigint(20) unsigned NOT NULL default '0', 75 | extended_args varchar(8000) DEFAULT NULL, 76 | PRIMARY KEY (action_id), 77 | KEY hook_status_scheduled_date_gmt (hook($hook_status_scheduled_date_gmt_max_index_length), status, scheduled_date_gmt), 78 | KEY status_scheduled_date_gmt (status, scheduled_date_gmt), 79 | KEY scheduled_date_gmt (scheduled_date_gmt), 80 | KEY args (args($max_index_length)), 81 | KEY group_id (group_id), 82 | KEY last_attempt_gmt (last_attempt_gmt), 83 | KEY `claim_id_status_priority_scheduled_date_gmt` (`claim_id`,`status`,`priority`,`scheduled_date_gmt`), 84 | KEY `status_last_attempt_gmt` (`status`,`last_attempt_gmt`), 85 | KEY `status_claim_id` (`status`,`claim_id`) 86 | ) $charset_collate"; 87 | 88 | case self::CLAIMS_TABLE: 89 | return "CREATE TABLE {$table_name} ( 90 | claim_id bigint(20) unsigned NOT NULL auto_increment, 91 | date_created_gmt datetime NULL default '{$default_date}', 92 | PRIMARY KEY (claim_id), 93 | KEY date_created_gmt (date_created_gmt) 94 | ) $charset_collate"; 95 | 96 | case self::GROUPS_TABLE: 97 | return "CREATE TABLE {$table_name} ( 98 | group_id bigint(20) unsigned NOT NULL auto_increment, 99 | slug varchar(255) NOT NULL, 100 | PRIMARY KEY (group_id), 101 | KEY slug (slug($max_index_length)) 102 | ) $charset_collate"; 103 | 104 | default: 105 | return ''; 106 | } 107 | } 108 | 109 | /** 110 | * Update the actions table schema, allowing datetime fields to be NULL. 111 | * 112 | * This is needed because the NOT NULL constraint causes a conflict with some versions of MySQL 113 | * configured with sql_mode=NO_ZERO_DATE, which can for instance lead to tables not being created. 114 | * 115 | * Most other schema updates happen via ActionScheduler_Abstract_Schema::update_table(), however 116 | * that method relies on dbDelta() and this change is not possible when using that function. 117 | * 118 | * @param string $table Name of table being updated. 119 | * @param string $db_version The existing schema version of the table. 120 | */ 121 | public function update_schema_5_0( $table, $db_version ) { 122 | global $wpdb; 123 | 124 | if ( 'actionscheduler_actions' !== $table || version_compare( $db_version, '5', '>=' ) ) { 125 | return; 126 | } 127 | 128 | // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared 129 | $table_name = $wpdb->prefix . 'actionscheduler_actions'; 130 | $table_list = $wpdb->get_col( "SHOW TABLES LIKE '{$table_name}'" ); 131 | $default_date = self::DEFAULT_DATE; 132 | 133 | if ( ! empty( $table_list ) ) { 134 | $query = " 135 | ALTER TABLE {$table_name} 136 | MODIFY COLUMN scheduled_date_gmt datetime NULL default '{$default_date}', 137 | MODIFY COLUMN scheduled_date_local datetime NULL default '{$default_date}', 138 | MODIFY COLUMN last_attempt_gmt datetime NULL default '{$default_date}', 139 | MODIFY COLUMN last_attempt_local datetime NULL default '{$default_date}' 140 | "; 141 | $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 142 | } 143 | // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /classes/WP_CLI/Migration_Command.php: -------------------------------------------------------------------------------- 1 | 'Migrates actions to the DB tables store', 44 | 'synopsis' => array( 45 | array( 46 | 'type' => 'assoc', 47 | 'name' => 'batch-size', 48 | 'optional' => true, 49 | 'default' => 100, 50 | 'description' => 'The number of actions to process in each batch', 51 | ), 52 | array( 53 | 'type' => 'assoc', 54 | 'name' => 'free-memory-on', 55 | 'optional' => true, 56 | 'default' => 50, 57 | 'description' => 'The number of actions to process between freeing memory. 0 disables freeing memory', 58 | ), 59 | array( 60 | 'type' => 'assoc', 61 | 'name' => 'pause', 62 | 'optional' => true, 63 | 'default' => 0, 64 | 'description' => 'The number of seconds to pause when freeing memory', 65 | ), 66 | array( 67 | 'type' => 'flag', 68 | 'name' => 'dry-run', 69 | 'optional' => true, 70 | 'description' => 'Reports on the actions that would have been migrated, but does not change any data', 71 | ), 72 | ), 73 | ) 74 | ); 75 | } 76 | 77 | /** 78 | * Process the data migration. 79 | * 80 | * @param array $positional_args Required for WP CLI. Not used in migration. 81 | * @param array $assoc_args Optional arguments. 82 | * 83 | * @return void 84 | */ 85 | public function migrate( $positional_args, $assoc_args ) { 86 | $this->init_logging(); 87 | 88 | $config = $this->get_migration_config( $assoc_args ); 89 | $runner = new Runner( $config ); 90 | $runner->init_destination(); 91 | 92 | $batch_size = isset( $assoc_args['batch-size'] ) ? (int) $assoc_args['batch-size'] : 100; 93 | $free_on = isset( $assoc_args['free-memory-on'] ) ? (int) $assoc_args['free-memory-on'] : 50; 94 | $sleep = isset( $assoc_args['pause'] ) ? (int) $assoc_args['pause'] : 0; 95 | \ActionScheduler_DataController::set_free_ticks( $free_on ); 96 | \ActionScheduler_DataController::set_sleep_time( $sleep ); 97 | 98 | do { 99 | $actions_processed = $runner->run( $batch_size ); 100 | $this->total_processed += $actions_processed; 101 | } while ( $actions_processed > 0 ); 102 | 103 | if ( ! $config->get_dry_run() ) { 104 | // let the scheduler know that there's nothing left to do. 105 | $scheduler = new Scheduler(); 106 | $scheduler->mark_complete(); 107 | } 108 | 109 | WP_CLI::success( sprintf( '%s complete. %d actions processed.', $config->get_dry_run() ? 'Dry run' : 'Migration', $this->total_processed ) ); 110 | } 111 | 112 | /** 113 | * Build the config object used to create the Runner 114 | * 115 | * @param array $args Optional arguments. 116 | * 117 | * @return ActionScheduler\Migration\Config 118 | */ 119 | private function get_migration_config( $args ) { 120 | $args = wp_parse_args( 121 | $args, 122 | array( 123 | 'dry-run' => false, 124 | ) 125 | ); 126 | 127 | $config = Controller::instance()->get_migration_config_object(); 128 | $config->set_dry_run( ! empty( $args['dry-run'] ) ); 129 | 130 | return $config; 131 | } 132 | 133 | /** 134 | * Hook command line logging into migration actions. 135 | */ 136 | private function init_logging() { 137 | add_action( 138 | 'action_scheduler/migrate_action_dry_run', 139 | function ( $action_id ) { 140 | WP_CLI::debug( sprintf( 'Dry-run: migrated action %d', $action_id ) ); 141 | } 142 | ); 143 | 144 | add_action( 145 | 'action_scheduler/no_action_to_migrate', 146 | function ( $action_id ) { 147 | WP_CLI::debug( sprintf( 'No action found to migrate for ID %d', $action_id ) ); 148 | } 149 | ); 150 | 151 | add_action( 152 | 'action_scheduler/migrate_action_failed', 153 | function ( $action_id ) { 154 | WP_CLI::warning( sprintf( 'Failed migrating action with ID %d', $action_id ) ); 155 | } 156 | ); 157 | 158 | add_action( 159 | 'action_scheduler/migrate_action_incomplete', 160 | function ( $source_id, $destination_id ) { 161 | WP_CLI::warning( sprintf( 'Unable to remove source action with ID %d after migrating to new ID %d', $source_id, $destination_id ) ); 162 | }, 163 | 10, 164 | 2 165 | ); 166 | 167 | add_action( 168 | 'action_scheduler/migrated_action', 169 | function ( $source_id, $destination_id ) { 170 | WP_CLI::debug( sprintf( 'Migrated source action with ID %d to new store with ID %d', $source_id, $destination_id ) ); 171 | }, 172 | 10, 173 | 2 174 | ); 175 | 176 | add_action( 177 | 'action_scheduler/migration_batch_starting', 178 | function ( $batch ) { 179 | WP_CLI::debug( 'Beginning migration of batch: ' . print_r( $batch, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r 180 | } 181 | ); 182 | 183 | add_action( 184 | 'action_scheduler/migration_batch_complete', 185 | function ( $batch ) { 186 | WP_CLI::log( sprintf( 'Completed migration of %d actions', count( $batch ) ) ); 187 | } 188 | ); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /classes/ActionScheduler_wcSystemStatus.php: -------------------------------------------------------------------------------- 1 | store = $store; 24 | } 25 | 26 | /** 27 | * Display action data, including number of actions grouped by status and the oldest & newest action in each status. 28 | * 29 | * Helpful to identify issues, like a clogged queue. 30 | */ 31 | public function render() { 32 | $action_counts = $this->store->action_counts(); 33 | $status_labels = $this->store->get_status_labels(); 34 | $oldest_and_newest = $this->get_oldest_and_newest( array_keys( $status_labels ) ); 35 | 36 | $this->get_template( $status_labels, $action_counts, $oldest_and_newest ); 37 | } 38 | 39 | /** 40 | * Get oldest and newest scheduled dates for a given set of statuses. 41 | * 42 | * @param array $status_keys Set of statuses to find oldest & newest action for. 43 | * @return array 44 | */ 45 | protected function get_oldest_and_newest( $status_keys ) { 46 | 47 | $oldest_and_newest = array(); 48 | 49 | foreach ( $status_keys as $status ) { 50 | $oldest_and_newest[ $status ] = array( 51 | 'oldest' => '–', 52 | 'newest' => '–', 53 | ); 54 | 55 | if ( 'in-progress' === $status ) { 56 | continue; 57 | } 58 | 59 | $oldest_and_newest[ $status ]['oldest'] = $this->get_action_status_date( $status, 'oldest' ); 60 | $oldest_and_newest[ $status ]['newest'] = $this->get_action_status_date( $status, 'newest' ); 61 | } 62 | 63 | return $oldest_and_newest; 64 | } 65 | 66 | /** 67 | * Get oldest or newest scheduled date for a given status. 68 | * 69 | * @param string $status Action status label/name string. 70 | * @param string $date_type Oldest or Newest. 71 | * @return DateTime 72 | */ 73 | protected function get_action_status_date( $status, $date_type = 'oldest' ) { 74 | 75 | $order = 'oldest' === $date_type ? 'ASC' : 'DESC'; 76 | 77 | $action = $this->store->query_actions( 78 | array( 79 | 'status' => $status, 80 | 'per_page' => 1, 81 | 'order' => $order, 82 | ) 83 | ); 84 | 85 | if ( ! empty( $action ) ) { 86 | $date_object = $this->store->get_date( $action[0] ); 87 | $action_date = $date_object->format( 'Y-m-d H:i:s O' ); 88 | } else { 89 | $action_date = '–'; 90 | } 91 | 92 | return $action_date; 93 | } 94 | 95 | /** 96 | * Get oldest or newest scheduled date for a given status. 97 | * 98 | * @param array $status_labels Set of statuses to find oldest & newest action for. 99 | * @param array $action_counts Number of actions grouped by status. 100 | * @param array $oldest_and_newest Date of the oldest and newest action with each status. 101 | */ 102 | protected function get_template( $status_labels, $action_counts, $oldest_and_newest ) { 103 | $as_version = ActionScheduler_Versions::instance()->latest_version(); 104 | $as_datastore = get_class( ActionScheduler_Store::instance() ); 105 | ?> 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | $count ) { 131 | // WC uses the 3rd column for export, so we need to display more data in that (hidden when viewed as part of the table) and add an empty 2nd column. 132 | printf( 133 | '', 134 | esc_html( $status_labels[ $status ] ), 135 | esc_html( number_format_i18n( $count ) ), 136 | esc_html( $oldest_and_newest[ $status ]['oldest'] ), 137 | esc_html( $oldest_and_newest[ $status ]['newest'] ) 138 | ); 139 | } 140 | ?> 141 | 142 |

 
%1$s %2$s, Oldest: %3$s, Newest: %4$s%3$s%4$s
143 | 144 |