Your IP : 216.73.216.162


Current Path : /home/x/b/o/xbodynamge/namtation/wp-content/
Upload File :
Current File : /home/x/b/o/xbodynamge/namtation/wp-content/abstracts.tar

ActionScheduler_Abstract_Schema.php000066600000011443151142440700013441 0ustar00<?php


/**
 * Class ActionScheduler_Abstract_Schema
 *
 * @package Action_Scheduler
 *
 * @codeCoverageIgnore
 *
 * Utility class for creating/updating custom tables
 */
abstract class ActionScheduler_Abstract_Schema {

	/**
	 * Increment this value in derived class to trigger a schema update.
	 *
	 * @var int
	 */
	protected $schema_version = 1;

	/**
	 * Schema version stored in database.
	 *
	 * @var string
	 */
	protected $db_version;

	/**
	 * Names of tables that will be registered by this class.
	 *
	 * @var array
	 */
	protected $tables = array();

	/**
	 * Can optionally be used by concrete classes to carry out additional initialization work
	 * as needed.
	 */
	public function init() {}

	/**
	 * Register tables with WordPress, and create them if needed.
	 *
	 * @param bool $force_update Optional. Default false. Use true to always run the schema update.
	 *
	 * @return void
	 */
	public function register_tables( $force_update = false ) {
		global $wpdb;

		// make WP aware of our tables.
		foreach ( $this->tables as $table ) {
			$wpdb->tables[] = $table;
			$name           = $this->get_full_table_name( $table );
			$wpdb->$table   = $name;
		}

		// create the tables.
		if ( $this->schema_update_required() || $force_update ) {
			foreach ( $this->tables as $table ) {
				/**
				 * Allow custom processing before updating a table schema.
				 *
				 * @param string $table Name of table being updated.
				 * @param string $db_version Existing version of the table being updated.
				 */
				do_action( 'action_scheduler_before_schema_update', $table, $this->db_version );
				$this->update_table( $table );
			}
			$this->mark_schema_update_complete();
		}
	}

	/**
	 * Get table definition.
	 *
	 * @param string $table The name of the table.
	 *
	 * @return string The CREATE TABLE statement, suitable for passing to dbDelta
	 */
	abstract protected function get_table_definition( $table );

	/**
	 * Determine if the database schema is out of date
	 * by comparing the integer found in $this->schema_version
	 * with the option set in the WordPress options table
	 *
	 * @return bool
	 */
	private function schema_update_required() {
		$option_name      = 'schema-' . static::class;
		$this->db_version = get_option( $option_name, 0 );

		// Check for schema option stored by the Action Scheduler Custom Tables plugin in case site has migrated from that plugin with an older schema.
		if ( 0 === $this->db_version ) {

			$plugin_option_name = 'schema-';

			switch ( static::class ) {
				case 'ActionScheduler_StoreSchema':
					$plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Store_Table_Maker';
					break;
				case 'ActionScheduler_LoggerSchema':
					$plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Logger_Table_Maker';
					break;
			}

			$this->db_version = get_option( $plugin_option_name, 0 );

			delete_option( $plugin_option_name );
		}

		return version_compare( $this->db_version, $this->schema_version, '<' );
	}

	/**
	 * Update the option in WordPress to indicate that
	 * our schema is now up to date
	 *
	 * @return void
	 */
	private function mark_schema_update_complete() {
		$option_name = 'schema-' . static::class;

		// work around race conditions and ensure that our option updates.
		$value_to_save = (string) $this->schema_version . '.0.' . time();

		update_option( $option_name, $value_to_save );
	}

	/**
	 * Update the schema for the given table
	 *
	 * @param string $table The name of the table to update.
	 *
	 * @return void
	 */
	private function update_table( $table ) {
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		$definition = $this->get_table_definition( $table );
		if ( $definition ) {
			$updated = dbDelta( $definition );
			foreach ( $updated as $updated_table => $update_description ) {
				if ( strpos( $update_description, 'Created table' ) === 0 ) {
					do_action( 'action_scheduler/created_table', $updated_table, $table ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
				}
			}
		}
	}

	/**
	 * Get full table name.
	 *
	 * @param string $table Table name.
	 *
	 * @return string The full name of the table, including the
	 *                table prefix for the current blog
	 */
	protected function get_full_table_name( $table ) {
		return $GLOBALS['wpdb']->prefix . $table;
	}

	/**
	 * Confirms that all of the tables registered by this schema class have been created.
	 *
	 * @return bool
	 */
	public function tables_exist() {
		global $wpdb;

		$tables_exist = true;

		foreach ( $this->tables as $table_name ) {
			$table_name     = $wpdb->prefix . $table_name;
			$pattern        = str_replace( '_', '\\_', $table_name );
			$existing_table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pattern ) );

			if ( $existing_table !== $table_name ) {
				$tables_exist = false;
				break;
			}
		}

		return $tables_exist;
	}
}
ActionScheduler_Logger.php000066600000017525151142440700011644 0ustar00<?php

/**
 * Class ActionScheduler_Logger
 *
 * @codeCoverageIgnore
 */
abstract class ActionScheduler_Logger {

	/**
	 * Instance.
	 *
	 * @var null|self
	 */
	private static $logger = null;

	/**
	 * Get instance.
	 *
	 * @return ActionScheduler_Logger
	 */
	public static function instance() {
		if ( empty( self::$logger ) ) {
			$class        = apply_filters( 'action_scheduler_logger_class', 'ActionScheduler_wpCommentLogger' );
			self::$logger = new $class();
		}
		return self::$logger;
	}

	/**
	 * Create log entry.
	 *
	 * @param string        $action_id Action ID.
	 * @param string        $message   Log message.
	 * @param DateTime|null $date      Log date.
	 *
	 * @return string The log entry ID
	 */
	abstract public function log( $action_id, $message, ?DateTime $date = null );

	/**
	 * Get action's log entry.
	 *
	 * @param string $entry_id Entry ID.
	 *
	 * @return ActionScheduler_LogEntry
	 */
	abstract public function get_entry( $entry_id );

	/**
	 * Get action's logs.
	 *
	 * @param string $action_id Action ID.
	 *
	 * @return ActionScheduler_LogEntry[]
	 */
	abstract public function get_logs( $action_id );


	/**
	 * Initialize.
	 *
	 * @codeCoverageIgnore
	 */
	public function init() {
		$this->hook_stored_action();
		add_action( 'action_scheduler_canceled_action', array( $this, 'log_canceled_action' ), 10, 1 );
		add_action( 'action_scheduler_begin_execute', array( $this, 'log_started_action' ), 10, 2 );
		add_action( 'action_scheduler_after_execute', array( $this, 'log_completed_action' ), 10, 3 );
		add_action( 'action_scheduler_failed_execution', array( $this, 'log_failed_action' ), 10, 3 );
		add_action( 'action_scheduler_failed_action', array( $this, 'log_timed_out_action' ), 10, 2 );
		add_action( 'action_scheduler_unexpected_shutdown', array( $this, 'log_unexpected_shutdown' ), 10, 2 );
		add_action( 'action_scheduler_reset_action', array( $this, 'log_reset_action' ), 10, 1 );
		add_action( 'action_scheduler_execution_ignored', array( $this, 'log_ignored_action' ), 10, 2 );
		add_action( 'action_scheduler_failed_fetch_action', array( $this, 'log_failed_fetch_action' ), 10, 2 );
		add_action( 'action_scheduler_failed_to_schedule_next_instance', array( $this, 'log_failed_schedule_next_instance' ), 10, 2 );
		add_action( 'action_scheduler_bulk_cancel_actions', array( $this, 'bulk_log_cancel_actions' ), 10, 1 );
	}

	/**
	 * Register callback for storing action.
	 */
	public function hook_stored_action() {
		add_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ) );
	}

	/**
	 * Unhook callback for storing action.
	 */
	public function unhook_stored_action() {
		remove_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ) );
	}

	/**
	 * Log action stored.
	 *
	 * @param int $action_id Action ID.
	 */
	public function log_stored_action( $action_id ) {
		$this->log( $action_id, __( 'action created', 'action-scheduler' ) );
	}

	/**
	 * Log action cancellation.
	 *
	 * @param int $action_id Action ID.
	 */
	public function log_canceled_action( $action_id ) {
		$this->log( $action_id, __( 'action canceled', 'action-scheduler' ) );
	}

	/**
	 * Log action start.
	 *
	 * @param int    $action_id Action ID.
	 * @param string $context Action execution context.
	 */
	public function log_started_action( $action_id, $context = '' ) {
		if ( ! empty( $context ) ) {
			/* translators: %s: context */
			$message = sprintf( __( 'action started via %s', 'action-scheduler' ), $context );
		} else {
			$message = __( 'action started', 'action-scheduler' );
		}
		$this->log( $action_id, $message );
	}

	/**
	 * Log action completion.
	 *
	 * @param int                         $action_id Action ID.
	 * @param null|ActionScheduler_Action $action Action.
	 * @param string                      $context Action execution context.
	 */
	public function log_completed_action( $action_id, $action = null, $context = '' ) {
		if ( ! empty( $context ) ) {
			/* translators: %s: context */
			$message = sprintf( __( 'action complete via %s', 'action-scheduler' ), $context );
		} else {
			$message = __( 'action complete', 'action-scheduler' );
		}
		$this->log( $action_id, $message );
	}

	/**
	 * Log action failure.
	 *
	 * @param int       $action_id Action ID.
	 * @param Exception $exception Exception.
	 * @param string    $context Action execution context.
	 */
	public function log_failed_action( $action_id, Exception $exception, $context = '' ) {
		if ( ! empty( $context ) ) {
			/* translators: 1: context 2: exception message */
			$message = sprintf( __( 'action failed via %1$s: %2$s', 'action-scheduler' ), $context, $exception->getMessage() );
		} else {
			/* translators: %s: exception message */
			$message = sprintf( __( 'action failed: %s', 'action-scheduler' ), $exception->getMessage() );
		}
		$this->log( $action_id, $message );
	}

	/**
	 * Log action timeout.
	 *
	 * @param int    $action_id  Action ID.
	 * @param string $timeout Timeout.
	 */
	public function log_timed_out_action( $action_id, $timeout ) {
		/* translators: %s: amount of time */
		$this->log( $action_id, sprintf( __( 'action marked as failed after %s seconds. Unknown error occurred. Check server, PHP and database error logs to diagnose cause.', 'action-scheduler' ), $timeout ) );
	}

	/**
	 * Log unexpected shutdown.
	 *
	 * @param int     $action_id Action ID.
	 * @param mixed[] $error     Error.
	 */
	public function log_unexpected_shutdown( $action_id, $error ) {
		if ( ! empty( $error ) ) {
			/* translators: 1: error message 2: filename 3: line */
			$this->log( $action_id, sprintf( __( 'unexpected shutdown: PHP Fatal error %1$s in %2$s on line %3$s', 'action-scheduler' ), $error['message'], $error['file'], $error['line'] ) );
		}
	}

	/**
	 * Log action reset.
	 *
	 * @param int $action_id Action ID.
	 */
	public function log_reset_action( $action_id ) {
		$this->log( $action_id, __( 'action reset', 'action-scheduler' ) );
	}

	/**
	 * Log ignored action.
	 *
	 * @param int    $action_id Action ID.
	 * @param string $context Action execution context.
	 */
	public function log_ignored_action( $action_id, $context = '' ) {
		if ( ! empty( $context ) ) {
			/* translators: %s: context */
			$message = sprintf( __( 'action ignored via %s', 'action-scheduler' ), $context );
		} else {
			$message = __( 'action ignored', 'action-scheduler' );
		}
		$this->log( $action_id, $message );
	}

	/**
	 * Log the failure of fetching the action.
	 *
	 * @param string         $action_id Action ID.
	 * @param null|Exception $exception The exception which occurred when fetching the action. NULL by default for backward compatibility.
	 */
	public function log_failed_fetch_action( $action_id, ?Exception $exception = null ) {

		if ( ! is_null( $exception ) ) {
			/* translators: %s: exception message */
			$log_message = sprintf( __( 'There was a failure fetching this action: %s', 'action-scheduler' ), $exception->getMessage() );
		} else {
			$log_message = __( 'There was a failure fetching this action', 'action-scheduler' );
		}

		$this->log( $action_id, $log_message );
	}

	/**
	 * Log the failure of scheduling the action's next instance.
	 *
	 * @param int       $action_id Action ID.
	 * @param Exception $exception Exception object.
	 */
	public function log_failed_schedule_next_instance( $action_id, Exception $exception ) {
		/* translators: %s: exception message */
		$this->log( $action_id, sprintf( __( 'There was a failure scheduling the next instance of this action: %s', 'action-scheduler' ), $exception->getMessage() ) );
	}

	/**
	 * Bulk add cancel action log entries.
	 *
	 * Implemented here for backward compatibility. Should be implemented in parent loggers
	 * for more performant bulk logging.
	 *
	 * @param array $action_ids List of action ID.
	 */
	public function bulk_log_cancel_actions( $action_ids ) {
		if ( empty( $action_ids ) ) {
			return;
		}

		foreach ( $action_ids as $action_id ) {
			$this->log_canceled_action( $action_id );
		}
	}
}
ActionScheduler_Abstract_Schedule.php000066600000003547151142440700014003 0ustar00<?php

/**
 * Class ActionScheduler_Abstract_Schedule
 */
abstract class ActionScheduler_Abstract_Schedule extends ActionScheduler_Schedule_Deprecated {

	/**
	 * The date & time the schedule is set to run.
	 *
	 * @var DateTime
	 */
	private $scheduled_date = null;

	/**
	 * Timestamp equivalent of @see $this->scheduled_date
	 *
	 * @var int
	 */
	protected $scheduled_timestamp = null;

	/**
	 * Construct.
	 *
	 * @param DateTime $date The date & time to run the action.
	 */
	public function __construct( DateTime $date ) {
		$this->scheduled_date = $date;
	}

	/**
	 * Check if a schedule should recur.
	 *
	 * @return bool
	 */
	abstract public function is_recurring();

	/**
	 * Calculate when the next instance of this schedule would run based on a given date & time.
	 *
	 * @param DateTime $after Start timestamp.
	 * @return DateTime
	 */
	abstract protected function calculate_next( DateTime $after );

	/**
	 * Get the next date & time when this schedule should run after a given date & time.
	 *
	 * @param DateTime $after Start timestamp.
	 * @return DateTime|null
	 */
	public function get_next( DateTime $after ) {
		$after = clone $after;
		if ( $after > $this->scheduled_date ) {
			$after = $this->calculate_next( $after );
			return $after;
		}
		return clone $this->scheduled_date;
	}

	/**
	 * Get the date & time the schedule is set to run.
	 *
	 * @return DateTime|null
	 */
	public function get_date() {
		return $this->scheduled_date;
	}

	/**
	 * For PHP 5.2 compat, because DateTime objects can't be serialized
	 *
	 * @return array
	 */
	public function __sleep() {
		$this->scheduled_timestamp = $this->scheduled_date->getTimestamp();
		return array(
			'scheduled_timestamp',
		);
	}

	/**
	 * Wakeup.
	 */
	public function __wakeup() {
		$this->scheduled_date = as_get_datetime_object( $this->scheduled_timestamp );
		unset( $this->scheduled_timestamp );
	}
}
ActionScheduler_WPCLI_Command.php000066600000004073151142440700012733 0ustar00<?php

/**
 * Abstract for WP-CLI commands.
 */
abstract class ActionScheduler_WPCLI_Command extends \WP_CLI_Command {

	const DATE_FORMAT = 'Y-m-d H:i:s O';

	/**
	 * Keyed arguments.
	 *
	 * @var string[]
	 */
	protected $args;

	/**
	 * Positional arguments.
	 *
	 * @var array<string, string>
	 */
	protected $assoc_args;

	/**
	 * Construct.
	 *
	 * @param string[]              $args       Positional arguments.
	 * @param array<string, string> $assoc_args Keyed arguments.
	 * @throws \Exception When loading a CLI command file outside of WP CLI context.
	 */
	public function __construct( array $args, array $assoc_args ) {
		if ( ! defined( 'WP_CLI' ) || ! constant( 'WP_CLI' ) ) {
			/* translators: %s php class name */
			throw new \Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'action-scheduler' ), get_class( $this ) ) );
		}

		$this->args       = $args;
		$this->assoc_args = $assoc_args;
	}

	/**
	 * Execute command.
	 */
	abstract public function execute();

	/**
	 * Get the scheduled date in a human friendly format.
	 *
	 * @see ActionScheduler_ListTable::get_schedule_display_string()
	 * @param ActionScheduler_Schedule $schedule Schedule.
	 * @return string
	 */
	protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) {

		$schedule_display_string = '';

		if ( ! $schedule->get_date() ) {
			return '0000-00-00 00:00:00';
		}

		$next_timestamp = $schedule->get_date()->getTimestamp();

		$schedule_display_string .= $schedule->get_date()->format( static::DATE_FORMAT );

		return $schedule_display_string;
	}

	/**
	 * Transforms arguments with '__' from CSV into expected arrays.
	 *
	 * @see \WP_CLI\CommandWithDBObject::process_csv_arguments_to_arrays()
	 * @link https://github.com/wp-cli/entity-command/blob/c270cc9a2367cb8f5845f26a6b5e203397c91392/src/WP_CLI/CommandWithDBObject.php#L99
	 * @return void
	 */
	protected function process_csv_arguments_to_arrays() {
		foreach ( $this->assoc_args as $k => $v ) {
			if ( false !== strpos( $k, '__' ) ) {
				$this->assoc_args[ $k ] = explode( ',', $v );
			}
		}
	}

}
ActionScheduler_Lock.php000066600000003437151142440700011312 0ustar00<?php

/**
 * Abstract class for setting a basic lock to throttle some action.
 *
 * Class ActionScheduler_Lock
 */
abstract class ActionScheduler_Lock {

	/**
	 * Instance.
	 *
	 * @var ActionScheduler_Lock
	 */
	private static $locker = null;

	/**
	 * Duration of lock.
	 *
	 * @var int
	 */
	protected static $lock_duration = MINUTE_IN_SECONDS;

	/**
	 * Check if a lock is set for a given lock type.
	 *
	 * @param string $lock_type A string to identify different lock types.
	 * @return bool
	 */
	public function is_locked( $lock_type ) {
		return ( $this->get_expiration( $lock_type ) >= time() );
	}

	/**
	 * Set a lock.
	 *
	 * To prevent race conditions, implementations should avoid setting the lock if the lock is already held.
	 *
	 * @param string $lock_type A string to identify different lock types.
	 * @return bool
	 */
	abstract public function set( $lock_type );

	/**
	 * If a lock is set, return the timestamp it was set to expiry.
	 *
	 * @param string $lock_type A string to identify different lock types.
	 * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire.
	 */
	abstract public function get_expiration( $lock_type );

	/**
	 * Get the amount of time to set for a given lock. 60 seconds by default.
	 *
	 * @param string $lock_type A string to identify different lock types.
	 * @return int
	 */
	protected function get_duration( $lock_type ) {
		return apply_filters( 'action_scheduler_lock_duration', self::$lock_duration, $lock_type );
	}

	/**
	 * Get instance.
	 *
	 * @return ActionScheduler_Lock
	 */
	public static function instance() {
		if ( empty( self::$locker ) ) {
			$class        = apply_filters( 'action_scheduler_lock_class', 'ActionScheduler_OptionLock' );
			self::$locker = new $class();
		}
		return self::$locker;
	}
}
ActionScheduler_TimezoneHelper.php000066600000011415151142440700013347 0ustar00<?php

/**
 * Class ActionScheduler_TimezoneHelper
 */
abstract class ActionScheduler_TimezoneHelper {

	/**
	 * DateTimeZone object.
	 *
	 * @var null|DateTimeZone
	 */
	private static $local_timezone = null;

	/**
	 * Set a DateTime's timezone to the WordPress site's timezone, or a UTC offset
	 * if no timezone string is available.
	 *
	 * @since  2.1.0
	 *
	 * @param DateTime $date Timestamp.
	 * @return ActionScheduler_DateTime
	 */
	public static function set_local_timezone( DateTime $date ) {

		// Accept a DateTime for easier backward compatibility, even though we require methods on ActionScheduler_DateTime.
		if ( ! is_a( $date, 'ActionScheduler_DateTime' ) ) {
			$date = as_get_datetime_object( $date->format( 'U' ) );
		}

		if ( get_option( 'timezone_string' ) ) {
			$date->setTimezone( new DateTimeZone( self::get_local_timezone_string() ) );
		} else {
			$date->setUtcOffset( self::get_local_timezone_offset() );
		}

		return $date;
	}

	/**
	 * Helper to retrieve the timezone string for a site until a WP core method exists
	 * (see https://core.trac.wordpress.org/ticket/24730).
	 *
	 * Adapted from wc_timezone_string() and https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155.
	 *
	 * If no timezone string is set, and its not possible to match the UTC offset set for the site to a timezone
	 * string, then an empty string will be returned, and the UTC offset should be used to set a DateTime's
	 * timezone.
	 *
	 * @since 2.1.0
	 * @param bool $reset Unused.
	 * @return string PHP timezone string for the site or empty if no timezone string is available.
	 */
	protected static function get_local_timezone_string( $reset = false ) {
		// If site timezone string exists, return it.
		$timezone = get_option( 'timezone_string' );
		if ( $timezone ) {
			return $timezone;
		}

		// Get UTC offset, if it isn't set then return UTC.
		$utc_offset = intval( get_option( 'gmt_offset', 0 ) );
		if ( 0 === $utc_offset ) {
			return 'UTC';
		}

		// Adjust UTC offset from hours to seconds.
		$utc_offset *= 3600;

		// Attempt to guess the timezone string from the UTC offset.
		$timezone = timezone_name_from_abbr( '', $utc_offset );
		if ( $timezone ) {
			return $timezone;
		}

		// Last try, guess timezone string manually.
		foreach ( timezone_abbreviations_list() as $abbr ) {
			foreach ( $abbr as $city ) {
				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.
					return $city['timezone_id'];
				}
			}
		}

		// No timezone string.
		return '';
	}

	/**
	 * Get timezone offset in seconds.
	 *
	 * @since  2.1.0
	 * @return float
	 */
	protected static function get_local_timezone_offset() {
		$timezone = get_option( 'timezone_string' );

		if ( $timezone ) {
			$timezone_object = new DateTimeZone( $timezone );
			return $timezone_object->getOffset( new DateTime( 'now' ) );
		} else {
			return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS;
		}
	}

	/**
	 * Get local timezone.
	 *
	 * @param bool $reset Toggle to discard stored value.
	 * @deprecated 2.1.0
	 */
	public static function get_local_timezone( $reset = false ) {
		_deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' );
		if ( $reset ) {
			self::$local_timezone = null;
		}
		if ( ! isset( self::$local_timezone ) ) {
			$tzstring = get_option( 'timezone_string' );

			if ( empty( $tzstring ) ) {
				$gmt_offset = absint( get_option( 'gmt_offset' ) );
				if ( 0 === $gmt_offset ) {
					$tzstring = 'UTC';
				} else {
					$gmt_offset *= HOUR_IN_SECONDS;
					$tzstring    = timezone_name_from_abbr( '', $gmt_offset, 1 );

					// If there's no timezone string, try again with no DST.
					if ( false === $tzstring ) {
						$tzstring = timezone_name_from_abbr( '', $gmt_offset, 0 );
					}

					// Try mapping to the first abbreviation we can find.
					if ( false === $tzstring ) {
						$is_dst = date( 'I' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date	 -- we are actually interested in the runtime timezone.
						foreach ( timezone_abbreviations_list() as $abbr ) {
							foreach ( $abbr as $city ) {
								if ( $city['dst'] === $is_dst && $city['offset'] === $gmt_offset ) {
									// If there's no valid timezone ID, keep looking.
									if ( is_null( $city['timezone_id'] ) ) {
										continue;
									}

									$tzstring = $city['timezone_id'];
									break 2;
								}
							}
						}
					}

					// If we still have no valid string, then fall back to UTC.
					if ( false === $tzstring ) {
						$tzstring = 'UTC';
					}
				}
			}

			self::$local_timezone = new DateTimeZone( $tzstring );
		}
		return self::$local_timezone;
	}
}
ActionScheduler.php000066600000025610151142440700010337 0ustar00<?php

use Action_Scheduler\WP_CLI\Migration_Command;
use Action_Scheduler\Migration\Controller;

/**
 * Class ActionScheduler
 *
 * @codeCoverageIgnore
 */
abstract class ActionScheduler {

	/**
	 * Plugin file path.
	 *
	 * @var string
	 */
	private static $plugin_file = '';

	/**
	 * ActionScheduler_ActionFactory instance.
	 *
	 * @var ActionScheduler_ActionFactory
	 */
	private static $factory = null;

	/**
	 * Data store is initialized.
	 *
	 * @var bool
	 */
	private static $data_store_initialized = false;

	/**
	 * Factory.
	 */
	public static function factory() {
		if ( ! isset( self::$factory ) ) {
			self::$factory = new ActionScheduler_ActionFactory();
		}
		return self::$factory;
	}

	/**
	 * Get Store instance.
	 */
	public static function store() {
		return ActionScheduler_Store::instance();
	}

	/**
	 * Get Lock instance.
	 */
	public static function lock() {
		return ActionScheduler_Lock::instance();
	}

	/**
	 * Get Logger instance.
	 */
	public static function logger() {
		return ActionScheduler_Logger::instance();
	}

	/**
	 * Get QueueRunner instance.
	 */
	public static function runner() {
		return ActionScheduler_QueueRunner::instance();
	}

	/**
	 * Get AdminView instance.
	 */
	public static function admin_view() {
		return ActionScheduler_AdminView::instance();
	}

	/**
	 * Get the absolute system path to the plugin directory, or a file therein
	 *
	 * @static
	 * @param string $path Path relative to plugin directory.
	 * @return string
	 */
	public static function plugin_path( $path ) {
		$base = dirname( self::$plugin_file );
		if ( $path ) {
			return trailingslashit( $base ) . $path;
		} else {
			return untrailingslashit( $base );
		}
	}

	/**
	 * Get the absolute URL to the plugin directory, or a file therein
	 *
	 * @static
	 * @param string $path Path relative to plugin directory.
	 * @return string
	 */
	public static function plugin_url( $path ) {
		return plugins_url( $path, self::$plugin_file );
	}

	/**
	 * Autoload.
	 *
	 * @param string $class Class name.
	 */
	public static function autoload( $class ) {
		$d           = DIRECTORY_SEPARATOR;
		$classes_dir = self::plugin_path( 'classes' . $d );
		$separator   = strrpos( $class, '\\' );
		if ( false !== $separator ) {
			if ( 0 !== strpos( $class, 'Action_Scheduler' ) ) {
				return;
			}
			$class = substr( $class, $separator + 1 );
		}

		if ( 'Deprecated' === substr( $class, -10 ) ) {
			$dir = self::plugin_path( 'deprecated' . $d );
		} elseif ( self::is_class_abstract( $class ) ) {
			$dir = $classes_dir . 'abstracts' . $d;
		} elseif ( self::is_class_migration( $class ) ) {
			$dir = $classes_dir . 'migration' . $d;
		} elseif ( 'Schedule' === substr( $class, -8 ) ) {
			$dir = $classes_dir . 'schedules' . $d;
		} elseif ( 'Action' === substr( $class, -6 ) ) {
			$dir = $classes_dir . 'actions' . $d;
		} elseif ( 'Schema' === substr( $class, -6 ) ) {
			$dir = $classes_dir . 'schema' . $d;
		} elseif ( strpos( $class, 'ActionScheduler' ) === 0 ) {
			$segments = explode( '_', $class );
			$type     = isset( $segments[1] ) ? $segments[1] : '';

			switch ( $type ) {
				case 'WPCLI':
					$dir = $classes_dir . 'WP_CLI' . $d;
					break;
				case 'DBLogger':
				case 'DBStore':
				case 'HybridStore':
				case 'wpPostStore':
				case 'wpCommentLogger':
					$dir = $classes_dir . 'data-stores' . $d;
					break;
				default:
					$dir = $classes_dir;
					break;
			}
		} elseif ( self::is_class_cli( $class ) ) {
			$dir = $classes_dir . 'WP_CLI' . $d;
		} elseif ( strpos( $class, 'CronExpression' ) === 0 ) {
			$dir = self::plugin_path( 'lib' . $d . 'cron-expression' . $d );
		} elseif ( strpos( $class, 'WP_Async_Request' ) === 0 ) {
			$dir = self::plugin_path( 'lib' . $d );
		} else {
			return;
		}

		if ( file_exists( $dir . "{$class}.php" ) ) {
			include $dir . "{$class}.php";
			return;
		}
	}

	/**
	 * Initialize the plugin
	 *
	 * @static
	 * @param string $plugin_file Plugin file path.
	 */
	public static function init( $plugin_file ) {
		self::$plugin_file = $plugin_file;
		spl_autoload_register( array( __CLASS__, 'autoload' ) );

		/**
		 * Fires in the early stages of Action Scheduler init hook.
		 */
		do_action( 'action_scheduler_pre_init' );

		require_once self::plugin_path( 'functions.php' );
		ActionScheduler_DataController::init();

		$store      = self::store();
		$logger     = self::logger();
		$runner     = self::runner();
		$admin_view = self::admin_view();

		// Ensure initialization on plugin activation.
		if ( ! did_action( 'init' ) ) {
			// phpcs:ignore Squiz.PHP.CommentedOutCode
			add_action( 'init', array( $admin_view, 'init' ), 0, 0 ); // run before $store::init().
			add_action( 'init', array( $store, 'init' ), 1, 0 );
			add_action( 'init', array( $logger, 'init' ), 1, 0 );
			add_action( 'init', array( $runner, 'init' ), 1, 0 );

			add_action(
				'init',
				/**
				 * Runs after the active store's init() method has been called.
				 *
				 * It would probably be preferable to have $store->init() (or it's parent method) set this itself,
				 * once it has initialized, however that would cause problems in cases where a custom data store is in
				 * use and it has not yet been updated to follow that same logic.
				 */
				function () {
					self::$data_store_initialized = true;

					/**
					 * Fires when Action Scheduler is ready: it is safe to use the procedural API after this point.
					 *
					 * @since 3.5.5
					 */
					do_action( 'action_scheduler_init' );
				},
				1
			);
		} else {
			$admin_view->init();
			$store->init();
			$logger->init();
			$runner->init();
			self::$data_store_initialized = true;

			/**
			 * Fires when Action Scheduler is ready: it is safe to use the procedural API after this point.
			 *
			 * @since 3.5.5
			 */
			do_action( 'action_scheduler_init' );
		}

		if ( apply_filters( 'action_scheduler_load_deprecated_functions', true ) ) {
			require_once self::plugin_path( 'deprecated/functions.php' );
		}

		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Scheduler_command' );
			WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Clean_Command' );
			WP_CLI::add_command( 'action-scheduler action', '\Action_Scheduler\WP_CLI\Action_Command' );
			WP_CLI::add_command( 'action-scheduler', '\Action_Scheduler\WP_CLI\System_Command' );
			if ( ! ActionScheduler_DataController::is_migration_complete() && Controller::instance()->allow_migration() ) {
				$command = new Migration_Command();
				$command->register();
			}
		}

		/**
		 * Handle WP comment cleanup after migration.
		 */
		if ( is_a( $logger, 'ActionScheduler_DBLogger' ) && ActionScheduler_DataController::is_migration_complete() && ActionScheduler_WPCommentCleaner::has_logs() ) {
			ActionScheduler_WPCommentCleaner::init();
		}

		add_action( 'action_scheduler/migration_complete', 'ActionScheduler_WPCommentCleaner::maybe_schedule_cleanup' );
	}

	/**
	 * Check whether the AS data store has been initialized.
	 *
	 * @param string $function_name The name of the function being called. Optional. Default `null`.
	 * @return bool
	 */
	public static function is_initialized( $function_name = null ) {
		if ( ! self::$data_store_initialized && ! empty( $function_name ) ) {
			$message = sprintf(
				/* translators: %s function name. */
				__( '%s() was called before the Action Scheduler data store was initialized', 'action-scheduler' ),
				esc_attr( $function_name )
			);
			_doing_it_wrong( esc_html( $function_name ), esc_html( $message ), '3.1.6' );
		}

		return self::$data_store_initialized;
	}

	/**
	 * Determine if the class is one of our abstract classes.
	 *
	 * @since 3.0.0
	 *
	 * @param string $class The class name.
	 *
	 * @return bool
	 */
	protected static function is_class_abstract( $class ) {
		static $abstracts = array(
			'ActionScheduler'                            => true,
			'ActionScheduler_Abstract_ListTable'         => true,
			'ActionScheduler_Abstract_QueueRunner'       => true,
			'ActionScheduler_Abstract_Schedule'          => true,
			'ActionScheduler_Abstract_RecurringSchedule' => true,
			'ActionScheduler_Lock'                       => true,
			'ActionScheduler_Logger'                     => true,
			'ActionScheduler_Abstract_Schema'            => true,
			'ActionScheduler_Store'                      => true,
			'ActionScheduler_TimezoneHelper'             => true,
			'ActionScheduler_WPCLI_Command'              => true,
		);

		return isset( $abstracts[ $class ] ) && $abstracts[ $class ];
	}

	/**
	 * Determine if the class is one of our migration classes.
	 *
	 * @since 3.0.0
	 *
	 * @param string $class The class name.
	 *
	 * @return bool
	 */
	protected static function is_class_migration( $class ) {
		static $migration_segments = array(
			'ActionMigrator'  => true,
			'BatchFetcher'    => true,
			'DBStoreMigrator' => true,
			'DryRun'          => true,
			'LogMigrator'     => true,
			'Config'          => true,
			'Controller'      => true,
			'Runner'          => true,
			'Scheduler'       => true,
		);

		$segments = explode( '_', $class );
		$segment  = isset( $segments[1] ) ? $segments[1] : $class;

		return isset( $migration_segments[ $segment ] ) && $migration_segments[ $segment ];
	}

	/**
	 * Determine if the class is one of our WP CLI classes.
	 *
	 * @since 3.0.0
	 *
	 * @param string $class The class name.
	 *
	 * @return bool
	 */
	protected static function is_class_cli( $class ) {
		static $cli_segments = array(
			'QueueRunner'                             => true,
			'Command'                                 => true,
			'ProgressBar'                             => true,
			'\Action_Scheduler\WP_CLI\Action_Command' => true,
			'\Action_Scheduler\WP_CLI\System_Command' => true,
		);

		$segments = explode( '_', $class );
		$segment  = isset( $segments[1] ) ? $segments[1] : $class;

		return isset( $cli_segments[ $segment ] ) && $cli_segments[ $segment ];
	}

	/**
	 * Clone.
	 */
	final public function __clone() {
		trigger_error( 'Singleton. No cloning allowed!', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
	}

	/**
	 * Wakeup.
	 */
	final public function __wakeup() {
		trigger_error( 'Singleton. No serialization allowed!', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
	}

	/**
	 * Construct.
	 */
	final private function __construct() {}

	/** Deprecated **/

	/**
	 * Get DateTime object.
	 *
	 * @param null|string $when     Date/time string.
	 * @param string      $timezone Timezone string.
	 */
	public static function get_datetime_object( $when = null, $timezone = 'UTC' ) {
		_deprecated_function( __METHOD__, '2.0', 'wcs_add_months()' );
		return as_get_datetime_object( $when, $timezone );
	}

	/**
	 * Issue deprecated warning if an Action Scheduler function is called in the shutdown hook.
	 *
	 * @param string $function_name The name of the function being called.
	 * @deprecated 3.1.6.
	 */
	public static function check_shutdown_hook( $function_name ) {
		_deprecated_function( __FUNCTION__, '3.1.6' );
	}
}
ActionScheduler_Abstract_RecurringSchedule.php000066600000006346151142440700015664 0ustar00<?php

/**
 * Class ActionScheduler_Abstract_RecurringSchedule
 */
abstract class ActionScheduler_Abstract_RecurringSchedule extends ActionScheduler_Abstract_Schedule {

	/**
	 * The date & time the first instance of this schedule was setup to run (which may not be this instance).
	 *
	 * Schedule objects are attached to an action object. Each schedule stores the run date for that
	 * object as the start date - @see $this->start - and logic to calculate the next run date after
	 * that - @see $this->calculate_next(). The $first_date property also keeps a record of when the very
	 * first instance of this chain of schedules ran.
	 *
	 * @var DateTime
	 */
	private $first_date = null;

	/**
	 * Timestamp equivalent of @see $this->first_date
	 *
	 * @var int
	 */
	protected $first_timestamp = null;

	/**
	 * The recurrence between each time an action is run using this schedule.
	 * Used to calculate the start date & time. Can be a number of seconds, in the
	 * case of ActionScheduler_IntervalSchedule, or a cron expression, as in the
	 * case of ActionScheduler_CronSchedule. Or something else.
	 *
	 * @var mixed
	 */
	protected $recurrence;

	/**
	 * Construct.
	 *
	 * @param DateTime      $date The date & time to run the action.
	 * @param mixed         $recurrence The data used to determine the schedule's recurrence.
	 * @param DateTime|null $first (Optional) The date & time the first instance of this interval schedule ran. Default null, meaning this is the first instance.
	 */
	public function __construct( DateTime $date, $recurrence, ?DateTime $first = null ) {
		parent::__construct( $date );
		$this->first_date = empty( $first ) ? $date : $first;
		$this->recurrence = $recurrence;
	}

	/**
	 * Schedule is recurring.
	 *
	 * @return bool
	 */
	public function is_recurring() {
		return true;
	}

	/**
	 * Get the date & time of the first schedule in this recurring series.
	 *
	 * @return DateTime|null
	 */
	public function get_first_date() {
		return clone $this->first_date;
	}

	/**
	 * Get the schedule's recurrence.
	 *
	 * @return string
	 */
	public function get_recurrence() {
		return $this->recurrence;
	}

	/**
	 * For PHP 5.2 compat, since DateTime objects can't be serialized
	 *
	 * @return array
	 */
	public function __sleep() {
		$sleep_params          = parent::__sleep();
		$this->first_timestamp = $this->first_date->getTimestamp();
		return array_merge(
			$sleep_params,
			array(
				'first_timestamp',
				'recurrence',
			)
		);
	}

	/**
	 * Unserialize recurring schedules serialized/stored prior to AS 3.0.0
	 *
	 * Prior to Action Scheduler 3.0.0, schedules used different property names to refer
	 * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp
	 * was the same as ActionScheduler_SimpleSchedule::timestamp. This was addressed in
	 * Action Scheduler 3.0.0, where properties and property names were aligned for better
	 * inheritance. To maintain backward compatibility with scheduled serialized and stored
	 * prior to 3.0, we need to correctly map the old property names.
	 */
	public function __wakeup() {
		parent::__wakeup();
		if ( $this->first_timestamp > 0 ) {
			$this->first_date = as_get_datetime_object( $this->first_timestamp );
		} else {
			$this->first_date = $this->get_date();
		}
	}
}
ActionScheduler_Abstract_ListTable.php000066600000061057151142440700014132 0ustar00<?php

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * Action Scheduler Abstract List Table class
 *
 * This abstract class enhances WP_List_Table making it ready to use.
 *
 * By extending this class we can focus on describing how our table looks like,
 * which columns needs to be shown, filter, ordered by and more and forget about the details.
 *
 * This class supports:
 *  - Bulk actions
 *  - Search
 *  - Sortable columns
 *  - Automatic translations of the columns
 *
 * @codeCoverageIgnore
 * @since  2.0.0
 */
abstract class ActionScheduler_Abstract_ListTable extends WP_List_Table {

	/**
	 * The table name
	 *
	 * @var string
	 */
	protected $table_name;

	/**
	 * Package name, used to get options from WP_List_Table::get_items_per_page.
	 *
	 * @var string
	 */
	protected $package;

	/**
	 * How many items do we render per page?
	 *
	 * @var int
	 */
	protected $items_per_page = 10;

	/**
	 * Enables search in this table listing. If this array
	 * is empty it means the listing is not searchable.
	 *
	 * @var array
	 */
	protected $search_by = array();

	/**
	 * Columns to show in the table listing. It is a key => value pair. The
	 * key must much the table column name and the value is the label, which is
	 * automatically translated.
	 *
	 * @var array
	 */
	protected $columns = array();

	/**
	 * Defines the row-actions. It expects an array where the key
	 * is the column name and the value is an array of actions.
	 *
	 * The array of actions are key => value, where key is the method name
	 * (with the prefix row_action_<key>) and the value is the label
	 * and title.
	 *
	 * @var array
	 */
	protected $row_actions = array();

	/**
	 * The Primary key of our table
	 *
	 * @var string
	 */
	protected $ID = 'ID';

	/**
	 * Enables sorting, it expects an array
	 * of columns (the column names are the values)
	 *
	 * @var array
	 */
	protected $sort_by = array();

	/**
	 * The default sort order
	 *
	 * @var string
	 */
	protected $filter_by = array();

	/**
	 * The status name => count combinations for this table's items. Used to display status filters.
	 *
	 * @var array
	 */
	protected $status_counts = array();

	/**
	 * Notices to display when loading the table. Array of arrays of form array( 'class' => {updated|error}, 'message' => 'This is the notice text display.' ).
	 *
	 * @var array
	 */
	protected $admin_notices = array();

	/**
	 * Localised string displayed in the <h1> element above the able.
	 *
	 * @var string
	 */
	protected $table_header;

	/**
	 * Enables bulk actions. It must be an array where the key is the action name
	 * and the value is the label (which is translated automatically). It is important
	 * to notice that it will check that the method exists (`bulk_$name`) and will throw
	 * an exception if it does not exists.
	 *
	 * This class will automatically check if the current request has a bulk action, will do the
	 * validations and afterwards will execute the bulk method, with two arguments. The first argument
	 * is the array with primary keys, the second argument is a string with a list of the primary keys,
	 * escaped and ready to use (with `IN`).
	 *
	 * @var array
	 */
	protected $bulk_actions = array();

	/**
	 * Makes translation easier, it basically just wraps
	 * `_x` with some default (the package name).
	 *
	 * @param string $text The new text to translate.
	 * @param string $context The context of the text.
	 * @return string|void The translated text.
	 *
	 * @deprecated 3.0.0 Use `_x()` instead.
	 */
	protected function translate( $text, $context = '' ) {
		return $text;
	}

	/**
	 * Reads `$this->bulk_actions` and returns an array that WP_List_Table understands. It
	 * also validates that the bulk method handler exists. It throws an exception because
	 * this is a library meant for developers and missing a bulk method is a development-time error.
	 *
	 * @return array
	 *
	 * @throws RuntimeException Throws RuntimeException when the bulk action does not have a callback method.
	 */
	protected function get_bulk_actions() {
		$actions = array();

		foreach ( $this->bulk_actions as $action => $label ) {
			if ( ! is_callable( array( $this, 'bulk_' . $action ) ) ) {
				throw new RuntimeException( "The bulk action $action does not have a callback method" );
			}

			$actions[ $action ] = $label;
		}

		return $actions;
	}

	/**
	 * Checks if the current request has a bulk action. If that is the case it will validate and will
	 * execute the bulk method handler. Regardless if the action is valid or not it will redirect to
	 * the previous page removing the current arguments that makes this request a bulk action.
	 */
	protected function process_bulk_action() {
		global $wpdb;
		// Detect when a bulk action is being triggered.
		$action = $this->current_action();
		if ( ! $action ) {
			return;
		}

		check_admin_referer( 'bulk-' . $this->_args['plural'] );

		$method = 'bulk_' . $action;
		if ( array_key_exists( $action, $this->bulk_actions ) && is_callable( array( $this, $method ) ) && ! empty( $_GET['ID'] ) && is_array( $_GET['ID'] ) ) {
			$ids_sql = '(' . implode( ',', array_fill( 0, count( $_GET['ID'] ), '%s' ) ) . ')';
			$id      = array_map( 'absint', $_GET['ID'] );
			$this->$method( $id, $wpdb->prepare( $ids_sql, $id ) ); //phpcs:ignore WordPress.DB.PreparedSQL
		}

		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
			wp_safe_redirect(
				remove_query_arg(
					array( '_wp_http_referer', '_wpnonce', 'ID', 'action', 'action2' ),
					esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) )
				)
			);
			exit;
		}
	}

	/**
	 * Default code for deleting entries.
	 * validated already by process_bulk_action()
	 *
	 * @param array  $ids ids of the items to delete.
	 * @param string $ids_sql the sql for the ids.
	 * @return void
	 */
	protected function bulk_delete( array $ids, $ids_sql ) {
		$store = ActionScheduler::store();
		foreach ( $ids as $action_id ) {
			$store->delete( $action_id );
		}
	}

	/**
	 * Prepares the _column_headers property which is used by WP_Table_List at rendering.
	 * It merges the columns and the sortable columns.
	 */
	protected function prepare_column_headers() {
		$this->_column_headers = array(
			$this->get_columns(),
			get_hidden_columns( $this->screen ),
			$this->get_sortable_columns(),
		);
	}

	/**
	 * Reads $this->sort_by and returns the columns name in a format that WP_Table_List
	 * expects
	 */
	public function get_sortable_columns() {
		$sort_by = array();
		foreach ( $this->sort_by as $column ) {
			$sort_by[ $column ] = array( $column, true );
		}
		return $sort_by;
	}

	/**
	 * Returns the columns names for rendering. It adds a checkbox for selecting everything
	 * as the first column
	 */
	public function get_columns() {
		$columns = array_merge(
			array( 'cb' => '<input type="checkbox" />' ),
			$this->columns
		);

		return $columns;
	}

	/**
	 * Get prepared LIMIT clause for items query
	 *
	 * @global wpdb $wpdb
	 *
	 * @return string Prepared LIMIT clause for items query.
	 */
	protected function get_items_query_limit() {
		global $wpdb;

		$per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
		return $wpdb->prepare( 'LIMIT %d', $per_page );
	}

	/**
	 * Returns the number of items to offset/skip for this current view.
	 *
	 * @return int
	 */
	protected function get_items_offset() {
		$per_page     = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
		$current_page = $this->get_pagenum();
		if ( 1 < $current_page ) {
			$offset = $per_page * ( $current_page - 1 );
		} else {
			$offset = 0;
		}

		return $offset;
	}

	/**
	 * Get prepared OFFSET clause for items query
	 *
	 * @global wpdb $wpdb
	 *
	 * @return string Prepared OFFSET clause for items query.
	 */
	protected function get_items_query_offset() {
		global $wpdb;

		return $wpdb->prepare( 'OFFSET %d', $this->get_items_offset() );
	}

	/**
	 * Prepares the ORDER BY sql statement. It uses `$this->sort_by` to know which
	 * columns are sortable. This requests validates the orderby $_GET parameter is a valid
	 * column and sortable. It will also use order (ASC|DESC) using DESC by default.
	 */
	protected function get_items_query_order() {
		if ( empty( $this->sort_by ) ) {
			return '';
		}

		$orderby = esc_sql( $this->get_request_orderby() );
		$order   = esc_sql( $this->get_request_order() );

		return "ORDER BY {$orderby} {$order}";
	}

	/**
	 * Querystring arguments to persist between form submissions.
	 *
	 * @since 3.7.3
	 *
	 * @return string[]
	 */
	protected function get_request_query_args_to_persist() {
		return array_merge(
			$this->sort_by,
			array(
				'page',
				'status',
				'tab',
			)
		);
	}

	/**
	 * Return the sortable column specified for this request to order the results by, if any.
	 *
	 * @return string
	 */
	protected function get_request_orderby() {

		$valid_sortable_columns = array_values( $this->sort_by );

		if ( ! empty( $_GET['orderby'] ) && in_array( $_GET['orderby'], $valid_sortable_columns, true ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
		} else {
			$orderby = $valid_sortable_columns[0];
		}

		return $orderby;
	}

	/**
	 * Return the sortable column order specified for this request.
	 *
	 * @return string
	 */
	protected function get_request_order() {

		if ( ! empty( $_GET['order'] ) && 'desc' === strtolower( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$order = 'DESC';
		} else {
			$order = 'ASC';
		}

		return $order;
	}

	/**
	 * Return the status filter for this request, if any.
	 *
	 * @return string
	 */
	protected function get_request_status() {
		$status = ( ! empty( $_GET['status'] ) ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
		return $status;
	}

	/**
	 * Return the search filter for this request, if any.
	 *
	 * @return string
	 */
	protected function get_request_search_query() {
		$search_query = ( ! empty( $_GET['s'] ) ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
		return $search_query;
	}

	/**
	 * Process and return the columns name. This is meant for using with SQL, this means it
	 * always includes the primary key.
	 *
	 * @return array
	 */
	protected function get_table_columns() {
		$columns = array_keys( $this->columns );
		if ( ! in_array( $this->ID, $columns, true ) ) {
			$columns[] = $this->ID;
		}

		return $columns;
	}

	/**
	 * Check if the current request is doing a "full text" search. If that is the case
	 * prepares the SQL to search texts using LIKE.
	 *
	 * If the current request does not have any search or if this list table does not support
	 * that feature it will return an empty string.
	 *
	 * @return string
	 */
	protected function get_items_query_search() {
		global $wpdb;

		if ( empty( $_GET['s'] ) || empty( $this->search_by ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return '';
		}

		$search_string = sanitize_text_field( wp_unslash( $_GET['s'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended

		$filter = array();
		foreach ( $this->search_by as $column ) {
			$wild     = '%';
			$sql_like = $wild . $wpdb->esc_like( $search_string ) . $wild;
			$filter[] = $wpdb->prepare( '`' . $column . '` LIKE %s', $sql_like ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.NotPrepared
		}
		return implode( ' OR ', $filter );
	}

	/**
	 * Prepares the SQL to filter rows by the options defined at `$this->filter_by`. Before trusting
	 * any data sent by the user it validates that it is a valid option.
	 */
	protected function get_items_query_filters() {
		global $wpdb;

		if ( ! $this->filter_by || empty( $_GET['filter_by'] ) || ! is_array( $_GET['filter_by'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return '';
		}

		$filter = array();

		foreach ( $this->filter_by as $column => $options ) {
			if ( empty( $_GET['filter_by'][ $column ] ) || empty( $options[ $_GET['filter_by'][ $column ] ] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
				continue;
			}

			$filter[] = $wpdb->prepare( "`$column` = %s", sanitize_text_field( wp_unslash( $_GET['filter_by'][ $column ] ) ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		}

		return implode( ' AND ', $filter );

	}

	/**
	 * Prepares the data to feed WP_Table_List.
	 *
	 * This has the core for selecting, sorting and filtering data. To keep the code simple
	 * its logic is split among many methods (get_items_query_*).
	 *
	 * Beside populating the items this function will also count all the records that matches
	 * the filtering criteria and will do fill the pagination variables.
	 */
	public function prepare_items() {
		global $wpdb;

		$this->process_bulk_action();

		$this->process_row_actions();

		if ( ! empty( $_REQUEST['_wp_http_referer'] && ! empty( $_SERVER['REQUEST_URI'] ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
			// _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
			wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) );
			exit;
		}

		$this->prepare_column_headers();

		$limit   = $this->get_items_query_limit();
		$offset  = $this->get_items_query_offset();
		$order   = $this->get_items_query_order();
		$where   = array_filter(
			array(
				$this->get_items_query_search(),
				$this->get_items_query_filters(),
			)
		);
		$columns = '`' . implode( '`, `', $this->get_table_columns() ) . '`';

		if ( ! empty( $where ) ) {
			$where = 'WHERE (' . implode( ') AND (', $where ) . ')';
		} else {
			$where = '';
		}

		$sql = "SELECT $columns FROM {$this->table_name} {$where} {$order} {$limit} {$offset}";

		$this->set_items( $wpdb->get_results( $sql, ARRAY_A ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		$query_count = "SELECT COUNT({$this->ID}) FROM {$this->table_name} {$where}";
		$total_items = $wpdb->get_var( $query_count ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$per_page    = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
		$this->set_pagination_args(
			array(
				'total_items' => $total_items,
				'per_page'    => $per_page,
				'total_pages' => ceil( $total_items / $per_page ),
			)
		);
	}

	/**
	 * Display the table.
	 *
	 * @param string $which The name of the table.
	 */
	public function extra_tablenav( $which ) {
		if ( ! $this->filter_by || 'top' !== $which ) {
			return;
		}

		echo '<div class="alignleft actions">';

		foreach ( $this->filter_by as $id => $options ) {
			$default = ! empty( $_GET['filter_by'][ $id ] ) ? sanitize_text_field( wp_unslash( $_GET['filter_by'][ $id ] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
			if ( empty( $options[ $default ] ) ) {
				$default = '';
			}

			echo '<select name="filter_by[' . esc_attr( $id ) . ']" class="first" id="filter-by-' . esc_attr( $id ) . '">';

			foreach ( $options as $value => $label ) {
				echo '<option value="' . esc_attr( $value ) . '" ' . esc_html( $value === $default ? 'selected' : '' ) . '>'
					. esc_html( $label )
				. '</option>';
			}

			echo '</select>';
		}

		submit_button( esc_html__( 'Filter', 'action-scheduler' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
		echo '</div>';
	}

	/**
	 * Set the data for displaying. It will attempt to unserialize (There is a chance that some columns
	 * are serialized). This can be override in child classes for further data transformation.
	 *
	 * @param array $items Items array.
	 */
	protected function set_items( array $items ) {
		$this->items = array();
		foreach ( $items as $item ) {
			$this->items[ $item[ $this->ID ] ] = array_map( 'maybe_unserialize', $item );
		}
	}

	/**
	 * Renders the checkbox for each row, this is the first column and it is named ID regardless
	 * of how the primary key is named (to keep the code simpler). The bulk actions will do the proper
	 * name transformation though using `$this->ID`.
	 *
	 * @param array $row The row to render.
	 */
	public function column_cb( $row ) {
		return '<input name="ID[]" type="checkbox" value="' . esc_attr( $row[ $this->ID ] ) . '" />';
	}

	/**
	 * Renders the row-actions.
	 *
	 * This method renders the action menu, it reads the definition from the $row_actions property,
	 * and it checks that the row action method exists before rendering it.
	 *
	 * @param array  $row Row to be rendered.
	 * @param string $column_name Column name.
	 * @return string
	 */
	protected function maybe_render_actions( $row, $column_name ) {
		if ( empty( $this->row_actions[ $column_name ] ) ) {
			return;
		}

		$row_id = $row[ $this->ID ];

		$actions      = '<div class="row-actions">';
		$action_count = 0;
		foreach ( $this->row_actions[ $column_name ] as $action_key => $action ) {

			$action_count++;

			if ( ! method_exists( $this, 'row_action_' . $action_key ) ) {
				continue;
			}

			$action_link = ! empty( $action['link'] ) ? $action['link'] : add_query_arg(
				array(
					'row_action' => $action_key,
					'row_id'     => $row_id,
					'nonce'      => wp_create_nonce( $action_key . '::' . $row_id ),
				)
			);
			$span_class  = ! empty( $action['class'] ) ? $action['class'] : $action_key;
			$separator   = ( $action_count < count( $this->row_actions[ $column_name ] ) ) ? ' | ' : '';

			$actions .= sprintf( '<span class="%s">', esc_attr( $span_class ) );
			$actions .= sprintf( '<a href="%1$s" title="%2$s">%3$s</a>', esc_url( $action_link ), esc_attr( $action['desc'] ), esc_html( $action['name'] ) );
			$actions .= sprintf( '%s</span>', $separator );
		}
		$actions .= '</div>';
		return $actions;
	}

	/**
	 * Process the bulk actions.
	 *
	 * @return void
	 */
	protected function process_row_actions() {
		$parameters = array( 'row_action', 'row_id', 'nonce' );
		foreach ( $parameters as $parameter ) {
			if ( empty( $_REQUEST[ $parameter ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				return;
			}
		}

		$action = sanitize_text_field( wp_unslash( $_REQUEST['row_action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
		$row_id = sanitize_text_field( wp_unslash( $_REQUEST['row_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
		$nonce  = sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
		$method = 'row_action_' . $action; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		if ( wp_verify_nonce( $nonce, $action . '::' . $row_id ) && method_exists( $this, $method ) ) {
			$this->$method( sanitize_text_field( wp_unslash( $row_id ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}

		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
			wp_safe_redirect(
				remove_query_arg(
					array( 'row_id', 'row_action', 'nonce' ),
					esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) )
				)
			);
			exit;
		}
	}

	/**
	 * Default column formatting, it will escape everything for security.
	 *
	 * @param array  $item The item array.
	 * @param string $column_name Column name to display.
	 *
	 * @return string
	 */
	public function column_default( $item, $column_name ) {
		$column_html  = esc_html( $item[ $column_name ] );
		$column_html .= $this->maybe_render_actions( $item, $column_name );
		return $column_html;
	}

	/**
	 * Display the table heading and search query, if any
	 */
	protected function display_header() {
		echo '<h1 class="wp-heading-inline">' . esc_attr( $this->table_header ) . '</h1>';
		if ( $this->get_request_search_query() ) {
			/* translators: %s: search query */
			echo '<span class="subtitle">' . esc_attr( sprintf( __( 'Search results for "%s"', 'action-scheduler' ), $this->get_request_search_query() ) ) . '</span>';
		}
		echo '<hr class="wp-header-end">';
	}

	/**
	 * Display the table heading and search query, if any
	 */
	protected function display_admin_notices() {
		foreach ( $this->admin_notices as $notice ) {
			echo '<div id="message" class="' . esc_attr( $notice['class'] ) . '">';
			echo '	<p>' . wp_kses_post( $notice['message'] ) . '</p>';
			echo '</div>';
		}
	}

	/**
	 * Prints the available statuses so the user can click to filter.
	 */
	protected function display_filter_by_status() {

		$status_list_items = array();
		$request_status    = $this->get_request_status();

		// Helper to set 'all' filter when not set on status counts passed in.
		if ( ! isset( $this->status_counts['all'] ) ) {
			$all_count = array_sum( $this->status_counts );
			if ( isset( $this->status_counts['past-due'] ) ) {
				$all_count -= $this->status_counts['past-due'];
			}
			$this->status_counts = array( 'all' => $all_count ) + $this->status_counts;
		}

		// Translated status labels.
		$status_labels             = ActionScheduler_Store::instance()->get_status_labels();
		$status_labels['all']      = esc_html_x( 'All', 'status labels', 'action-scheduler' );
		$status_labels['past-due'] = esc_html_x( 'Past-due', 'status labels', 'action-scheduler' );

		foreach ( $this->status_counts as $status_slug => $count ) {

			if ( 0 === $count ) {
				continue;
			}

			if ( $status_slug === $request_status || ( empty( $request_status ) && 'all' === $status_slug ) ) {
				$status_list_item = '<li class="%1$s"><a href="%2$s" class="current">%3$s</a> (%4$d)</li>';
			} else {
				$status_list_item = '<li class="%1$s"><a href="%2$s">%3$s</a> (%4$d)</li>';
			}

			$status_name         = isset( $status_labels[ $status_slug ] ) ? $status_labels[ $status_slug ] : ucfirst( $status_slug );
			$status_filter_url   = ( 'all' === $status_slug ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_slug );
			$status_filter_url   = remove_query_arg( array( 'paged', 's' ), $status_filter_url );
			$status_list_items[] = sprintf( $status_list_item, esc_attr( $status_slug ), esc_url( $status_filter_url ), esc_html( $status_name ), absint( $count ) );
		}

		if ( $status_list_items ) {
			echo '<ul class="subsubsub">';
			echo implode( " | \n", $status_list_items ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo '</ul>';
		}
	}

	/**
	 * Renders the table list, we override the original class to render the table inside a form
	 * and to render any needed HTML (like the search box). By doing so the callee of a function can simple
	 * forget about any extra HTML.
	 */
	protected function display_table() {
		echo '<form id="' . esc_attr( $this->_args['plural'] ) . '-filter" method="get">';
		foreach ( $this->get_request_query_args_to_persist() as $arg ) {
			$arg_value = isset( $_GET[ $arg ] ) ? sanitize_text_field( wp_unslash( $_GET[ $arg ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			if ( ! $arg_value ) {
				continue;
			}

			echo '<input type="hidden" name="' . esc_attr( $arg ) . '" value="' . esc_attr( $arg_value ) . '" />';
		}

		if ( ! empty( $this->search_by ) ) {
			echo $this->search_box( $this->get_search_box_button_text(), 'plugin' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}
		parent::display();
		echo '</form>';
	}

	/**
	 * Process any pending actions.
	 */
	public function process_actions() {
		$this->process_bulk_action();
		$this->process_row_actions();

		if ( ! empty( $_REQUEST['_wp_http_referer'] ) && ! empty( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			// _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
			wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) );
			exit;
		}
	}

	/**
	 * Render the list table page, including header, notices, status filters and table.
	 */
	public function display_page() {
		$this->prepare_items();

		echo '<div class="wrap">';
		$this->display_header();
		$this->display_admin_notices();
		$this->display_filter_by_status();
		$this->display_table();
		echo '</div>';
	}

	/**
	 * Get the text to display in the search box on the list table.
	 */
	protected function get_search_box_placeholder() {
		return esc_html__( 'Search', 'action-scheduler' );
	}

	/**
	 * Gets the screen per_page option name.
	 *
	 * @return string
	 */
	protected function get_per_page_option_name() {
		return $this->package . '_items_per_page';
	}
}
ActionScheduler_Abstract_QueueRunner.php000066600000032710151142440700014517 0ustar00<?php

/**
 * Abstract class with common Queue Cleaner functionality.
 */
abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abstract_QueueRunner_Deprecated {

	/**
	 * ActionScheduler_QueueCleaner instance.
	 *
	 * @var ActionScheduler_QueueCleaner
	 */
	protected $cleaner;

	/**
	 * ActionScheduler_FatalErrorMonitor instance.
	 *
	 * @var ActionScheduler_FatalErrorMonitor
	 */
	protected $monitor;

	/**
	 * ActionScheduler_Store instance.
	 *
	 * @var ActionScheduler_Store
	 */
	protected $store;

	/**
	 * The created time.
	 *
	 * Represents when the queue runner was constructed and used when calculating how long a PHP request has been running.
	 * For this reason it should be as close as possible to the PHP request start time.
	 *
	 * @var int
	 */
	private $created_time;

	/**
	 * ActionScheduler_Abstract_QueueRunner constructor.
	 *
	 * @param ActionScheduler_Store|null             $store Store object.
	 * @param ActionScheduler_FatalErrorMonitor|null $monitor Monitor object.
	 * @param ActionScheduler_QueueCleaner|null      $cleaner Cleaner object.
	 */
	public function __construct( ?ActionScheduler_Store $store = null, ?ActionScheduler_FatalErrorMonitor $monitor = null, ?ActionScheduler_QueueCleaner $cleaner = null ) {

		$this->created_time = microtime( true );

		$this->store   = $store ? $store : ActionScheduler_Store::instance();
		$this->monitor = $monitor ? $monitor : new ActionScheduler_FatalErrorMonitor( $this->store );
		$this->cleaner = $cleaner ? $cleaner : new ActionScheduler_QueueCleaner( $this->store );
	}

	/**
	 * Process an individual action.
	 *
	 * @param int    $action_id The action ID to process.
	 * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron'
	 *                        Generally, this should be capitalised and not localised as it's a proper noun.
	 * @throws \Exception When error running action.
	 */
	public function process_action( $action_id, $context = '' ) {
		// Temporarily override the error handler while we process the current action.
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
		set_error_handler(
			/**
			 * Temporary error handler which can catch errors and convert them into exceptions. This facilitates more
			 * robust error handling across all supported PHP versions.
			 *
			 * @throws Exception
			 *
			 * @param int    $type    Error level expressed as an integer.
			 * @param string $message Error message.
			 */
			function ( $type, $message ) {
				throw new Exception( $message );
			},
			E_USER_ERROR | E_RECOVERABLE_ERROR
		);

		/*
		 * The nested try/catch structure is required because we potentially need to convert thrown errors into
		 * exceptions (and an exception thrown from a catch block cannot be caught by a later catch block in the *same*
		 * structure).
		 */
		try {
			try {
				$valid_action = false;
				do_action( 'action_scheduler_before_execute', $action_id, $context );

				if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) {
					do_action( 'action_scheduler_execution_ignored', $action_id, $context );
					return;
				}

				$valid_action = true;
				do_action( 'action_scheduler_begin_execute', $action_id, $context );

				$action = $this->store->fetch_action( $action_id );
				$this->store->log_execution( $action_id );
				$action->execute();
				do_action( 'action_scheduler_after_execute', $action_id, $action, $context );
				$this->store->mark_complete( $action_id );
			} catch ( Throwable $e ) {
				// Throwable is defined when executing under PHP 7.0 and up. We convert it to an exception, for
				// compatibility with ActionScheduler_Logger.
				throw new Exception( $e->getMessage(), $e->getCode(), $e );
			}
		} catch ( Exception $e ) {
			// This catch block exists for compatibility with PHP 5.6.
			$this->handle_action_error( $action_id, $e, $context, $valid_action );
		} finally {
			restore_error_handler();
		}

		if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) && $action->get_schedule()->is_recurring() ) {
			$this->schedule_next_instance( $action, $action_id );
		}
	}

	/**
	 * Marks actions as either having failed execution or failed validation, as appropriate.
	 *
	 * @param int       $action_id    Action ID.
	 * @param Exception $e            Exception instance.
	 * @param string    $context      Execution context.
	 * @param bool      $valid_action If the action is valid.
	 *
	 * @return void
	 */
	private function handle_action_error( $action_id, $e, $context, $valid_action ) {
		if ( $valid_action ) {
			$this->store->mark_failure( $action_id );
			/**
			 * Runs when action execution fails.
			 *
			 * @param int       $action_id Action ID.
			 * @param Exception $e         Exception instance.
			 * @param string    $context   Execution context.
			 */
			do_action( 'action_scheduler_failed_execution', $action_id, $e, $context );
		} else {
			/**
			 * Runs when action validation fails.
			 *
			 * @param int       $action_id Action ID.
			 * @param Exception $e         Exception instance.
			 * @param string    $context   Execution context.
			 */
			do_action( 'action_scheduler_failed_validation', $action_id, $e, $context );
		}
	}

	/**
	 * Schedule the next instance of the action if necessary.
	 *
	 * @param ActionScheduler_Action $action Action.
	 * @param int                    $action_id Action ID.
	 */
	protected function schedule_next_instance( ActionScheduler_Action $action, $action_id ) {
		// If a recurring action has been consistently failing, we may wish to stop rescheduling it.
		if (
			ActionScheduler_Store::STATUS_FAILED === $this->store->get_status( $action_id )
			&& $this->recurring_action_is_consistently_failing( $action, $action_id )
		) {
			ActionScheduler_Logger::instance()->log(
				$action_id,
				__( 'This action appears to be consistently failing. A new instance will not be scheduled.', 'action-scheduler' )
			);

			return;
		}

		try {
			ActionScheduler::factory()->repeat( $action );
		} catch ( Exception $e ) {
			do_action( 'action_scheduler_failed_to_schedule_next_instance', $action_id, $e, $action );
		}
	}

	/**
	 * Determine if the specified recurring action has been consistently failing.
	 *
	 * @param ActionScheduler_Action $action    The recurring action to be rescheduled.
	 * @param int                    $action_id The ID of the recurring action.
	 *
	 * @return bool
	 */
	private function recurring_action_is_consistently_failing( ActionScheduler_Action $action, $action_id ) {
		/**
		 * Controls the failure threshold for recurring actions.
		 *
		 * Before rescheduling a recurring action, we look at its status. If it failed, we then check if all of the most
		 * recent actions (upto the threshold set by this filter) sharing the same hook have also failed: if they have,
		 * that is considered consistent failure and a new instance of the action will not be scheduled.
		 *
		 * @param int $failure_threshold Number of actions of the same hook to examine for failure. Defaults to 5.
		 */
		$consistent_failure_threshold = (int) apply_filters( 'action_scheduler_recurring_action_failure_threshold', 5 );

		// This query should find the earliest *failing* action (for the hook we are interested in) within our threshold.
		$query_args = array(
			'hook'         => $action->get_hook(),
			'status'       => ActionScheduler_Store::STATUS_FAILED,
			'date'         => date_create( 'now', timezone_open( 'UTC' ) )->format( 'Y-m-d H:i:s' ),
			'date_compare' => '<',
			'per_page'     => 1,
			'offset'       => $consistent_failure_threshold - 1,
		);

		$first_failing_action_id = $this->store->query_actions( $query_args );

		// If we didn't retrieve an action ID, then there haven't been enough failures for us to worry about.
		if ( empty( $first_failing_action_id ) ) {
			return false;
		}

		// Now let's fetch the first action (having the same hook) of *any status* within the same window.
		unset( $query_args['status'] );
		$first_action_id_with_the_same_hook = $this->store->query_actions( $query_args );

		/**
		 * If a recurring action is assessed as consistently failing, it will not be rescheduled. This hook provides a
		 * way to observe and optionally override that assessment.
		 *
		 * @param bool                   $is_consistently_failing If the action is considered to be consistently failing.
		 * @param ActionScheduler_Action $action                  The action being assessed.
		 */
		return (bool) apply_filters(
			'action_scheduler_recurring_action_is_consistently_failing',
			$first_action_id_with_the_same_hook === $first_failing_action_id,
			$action
		);
	}

	/**
	 * Run the queue cleaner.
	 */
	protected function run_cleanup() {
		$this->cleaner->clean( 10 * $this->get_time_limit() );
	}

	/**
	 * Get the number of concurrent batches a runner allows.
	 *
	 * @return int
	 */
	public function get_allowed_concurrent_batches() {
		return apply_filters( 'action_scheduler_queue_runner_concurrent_batches', 1 );
	}

	/**
	 * Check if the number of allowed concurrent batches is met or exceeded.
	 *
	 * @return bool
	 */
	public function has_maximum_concurrent_batches() {
		return $this->store->get_claim_count() >= $this->get_allowed_concurrent_batches();
	}

	/**
	 * Get the maximum number of seconds a batch can run for.
	 *
	 * @return int The number of seconds.
	 */
	protected function get_time_limit() {

		$time_limit = 30;

		// Apply deprecated filter from deprecated get_maximum_execution_time() method.
		if ( has_filter( 'action_scheduler_maximum_execution_time' ) ) {
			_deprecated_function( 'action_scheduler_maximum_execution_time', '2.1.1', 'action_scheduler_queue_runner_time_limit' );
			$time_limit = apply_filters( 'action_scheduler_maximum_execution_time', $time_limit );
		}

		return absint( apply_filters( 'action_scheduler_queue_runner_time_limit', $time_limit ) );
	}

	/**
	 * Get the number of seconds the process has been running.
	 *
	 * @return int The number of seconds.
	 */
	protected function get_execution_time() {
		$execution_time = microtime( true ) - $this->created_time;

		// Get the CPU time if the hosting environment uses it rather than wall-clock time to calculate a process's execution time.
		if ( function_exists( 'getrusage' ) && apply_filters( 'action_scheduler_use_cpu_execution_time', defined( 'PANTHEON_ENVIRONMENT' ) ) ) {
			$resource_usages = getrusage();

			if ( isset( $resource_usages['ru_stime.tv_usec'], $resource_usages['ru_stime.tv_usec'] ) ) {
				$execution_time = $resource_usages['ru_stime.tv_sec'] + ( $resource_usages['ru_stime.tv_usec'] / 1000000 );
			}
		}

		return $execution_time;
	}

	/**
	 * Check if the host's max execution time is (likely) to be exceeded if processing more actions.
	 *
	 * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action.
	 * @return bool
	 */
	protected function time_likely_to_be_exceeded( $processed_actions ) {
		$execution_time     = $this->get_execution_time();
		$max_execution_time = $this->get_time_limit();

		// Safety against division by zero errors.
		if ( 0 === $processed_actions ) {
			return $execution_time >= $max_execution_time;
		}

		$time_per_action       = $execution_time / $processed_actions;
		$estimated_time        = $execution_time + ( $time_per_action * 3 );
		$likely_to_be_exceeded = $estimated_time > $max_execution_time;

		return apply_filters( 'action_scheduler_maximum_execution_time_likely_to_be_exceeded', $likely_to_be_exceeded, $this, $processed_actions, $execution_time, $max_execution_time );
	}

	/**
	 * Get memory limit
	 *
	 * Based on WP_Background_Process::get_memory_limit()
	 *
	 * @return int
	 */
	protected function get_memory_limit() {
		if ( function_exists( 'ini_get' ) ) {
			$memory_limit = ini_get( 'memory_limit' );
		} else {
			$memory_limit = '128M'; // Sensible default, and minimum required by WooCommerce.
		}

		if ( ! $memory_limit || -1 === $memory_limit || '-1' === $memory_limit ) {
			// Unlimited, set to 32GB.
			$memory_limit = '32G';
		}

		return ActionScheduler_Compatibility::convert_hr_to_bytes( $memory_limit );
	}

	/**
	 * Memory exceeded
	 *
	 * Ensures the batch process never exceeds 90% of the maximum WordPress memory.
	 *
	 * Based on WP_Background_Process::memory_exceeded()
	 *
	 * @return bool
	 */
	protected function memory_exceeded() {

		$memory_limit    = $this->get_memory_limit() * 0.90;
		$current_memory  = memory_get_usage( true );
		$memory_exceeded = $current_memory >= $memory_limit;

		return apply_filters( 'action_scheduler_memory_exceeded', $memory_exceeded, $this );
	}

	/**
	 * See if the batch limits have been exceeded, which is when memory usage is almost at
	 * the maximum limit, or the time to process more actions will exceed the max time limit.
	 *
	 * Based on WC_Background_Process::batch_limits_exceeded()
	 *
	 * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action.
	 * @return bool
	 */
	protected function batch_limits_exceeded( $processed_actions ) {
		return $this->memory_exceeded() || $this->time_likely_to_be_exceeded( $processed_actions );
	}

	/**
	 * Process actions in the queue.
	 *
	 * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron'
	 *        Generally, this should be capitalised and not localised as it's a proper noun.
	 * @return int The number of actions processed.
	 */
	abstract public function run( $context = '' );
}
ActionScheduler_Store.php000066600000034071151142440700011514 0ustar00<?php

/**
 * Class ActionScheduler_Store
 *
 * @codeCoverageIgnore
 */
abstract class ActionScheduler_Store extends ActionScheduler_Store_Deprecated {
	const STATUS_COMPLETE = 'complete';
	const STATUS_PENDING  = 'pending';
	const STATUS_RUNNING  = 'in-progress';
	const STATUS_FAILED   = 'failed';
	const STATUS_CANCELED = 'canceled';
	const DEFAULT_CLASS   = 'ActionScheduler_wpPostStore';

	/**
	 * ActionScheduler_Store instance.
	 *
	 * @var ActionScheduler_Store
	 */
	private static $store = null;

	/**
	 * Maximum length of args.
	 *
	 * @var int
	 */
	protected static $max_args_length = 191;

	/**
	 * Save action.
	 *
	 * @param ActionScheduler_Action $action Action to save.
	 * @param null|DateTime          $scheduled_date Optional Date of the first instance
	 *                                               to store. Otherwise uses the first date of the action's
	 *                                               schedule.
	 *
	 * @return int The action ID
	 */
	abstract public function save_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null );

	/**
	 * Get action.
	 *
	 * @param string $action_id Action ID.
	 *
	 * @return ActionScheduler_Action
	 */
	abstract public function fetch_action( $action_id );

	/**
	 * Find an action.
	 *
	 * Note: the query ordering changes based on the passed 'status' value.
	 *
	 * @param string $hook Action hook.
	 * @param array  $params Parameters of the action to find.
	 *
	 * @return string|null ID of the next action matching the criteria or NULL if not found.
	 */
	public function find_action( $hook, $params = array() ) {
		$params = wp_parse_args(
			$params,
			array(
				'args'   => null,
				'status' => self::STATUS_PENDING,
				'group'  => '',
			)
		);

		// These params are fixed for this method.
		$params['hook']     = $hook;
		$params['orderby']  = 'date';
		$params['per_page'] = 1;

		if ( ! empty( $params['status'] ) ) {
			if ( self::STATUS_PENDING === $params['status'] ) {
				$params['order'] = 'ASC'; // Find the next action that matches.
			} else {
				$params['order'] = 'DESC'; // Find the most recent action that matches.
			}
		}

		$results = $this->query_actions( $params );

		return empty( $results ) ? null : $results[0];
	}

	/**
	 * Query for action count or list of action IDs.
	 *
	 * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status.
	 *
	 * @param array  $query {
	 *      Query filtering options.
	 *
	 *      @type string       $hook             The name of the actions. Optional.
	 *      @type string|array $status           The status or statuses of the actions. Optional.
	 *      @type array        $args             The args array of the actions. Optional.
	 *      @type DateTime     $date             The scheduled date of the action. Used in UTC timezone. Optional.
	 *      @type string       $date_compare     Operator for selecting by $date param. Accepted values are '!=', '>', '>=', '<', '<=', '='. Defaults to '<='.
	 *      @type DateTime     $modified         The last modified date of the action. Used in UTC timezone. Optional.
	 *      @type string       $modified_compare Operator for comparing $modified param. Accepted values are '!=', '>', '>=', '<', '<=', '='. Defaults to '<='.
	 *      @type string       $group            The group the action belongs to. Optional.
	 *      @type bool|int     $claimed          TRUE to find claimed actions, FALSE to find unclaimed actions, an int to find a specific claim ID. Optional.
	 *      @type int          $per_page         Number of results to return. Defaults to 5.
	 *      @type int          $offset           The query pagination offset. Defaults to 0.
	 *      @type int          $orderby          Accepted values are 'hook', 'group', 'modified', 'date' or 'none'. Defaults to 'date'.
	 *      @type string       $order            Accepted values are 'ASC' or 'DESC'. Defaults to 'ASC'.
	 * }
	 * @param string $query_type Whether to select or count the results. Default, select.
	 *
	 * @return string|array|null The IDs of actions matching the query. Null on failure.
	 */
	abstract public function query_actions( $query = array(), $query_type = 'select' );

	/**
	 * Run query to get a single action ID.
	 *
	 * @since 3.3.0
	 *
	 * @see ActionScheduler_Store::query_actions for $query arg usage but 'per_page' and 'offset' can't be used.
	 *
	 * @param array $query Query parameters.
	 *
	 * @return int|null
	 */
	public function query_action( $query ) {
		$query['per_page'] = 1;
		$query['offset']   = 0;
		$results           = $this->query_actions( $query );

		if ( empty( $results ) ) {
			return null;
		} else {
			return (int) $results[0];
		}
	}

	/**
	 * Get a count of all actions in the store, grouped by status
	 *
	 * @return array
	 */
	abstract public function action_counts();

	/**
	 * Get additional action counts.
	 *
	 * - add past-due actions
	 *
	 * @return array
	 */
	public function extra_action_counts() {
		$extra_actions = array();

		$pastdue_action_counts = (int) $this->query_actions(
			array(
				'status' => self::STATUS_PENDING,
				'date'   => as_get_datetime_object(),
			),
			'count'
		);

		if ( $pastdue_action_counts ) {
			$extra_actions['past-due'] = $pastdue_action_counts;
		}

		/**
		 * Allows 3rd party code to add extra action counts (used in filters in the list table).
		 *
		 * @since 3.5.0
		 * @param $extra_actions array Array with format action_count_identifier => action count.
		 */
		return apply_filters( 'action_scheduler_extra_action_counts', $extra_actions );
	}

	/**
	 * Cancel action.
	 *
	 * @param string $action_id Action ID.
	 */
	abstract public function cancel_action( $action_id );

	/**
	 * Delete action.
	 *
	 * @param string $action_id Action ID.
	 */
	abstract public function delete_action( $action_id );

	/**
	 * Get action's schedule or run timestamp.
	 *
	 * @param string $action_id Action ID.
	 *
	 * @return DateTime The date the action is schedule to run, or the date that it ran.
	 */
	abstract public function get_date( $action_id );


	/**
	 * Make a claim.
	 *
	 * @param int           $max_actions Maximum number of actions to claim.
	 * @param DateTime|null $before_date Claim only actions schedule before the given date. Defaults to now.
	 * @param array         $hooks       Claim only actions with a hook or hooks.
	 * @param string        $group       Claim only actions in the given group.
	 *
	 * @return ActionScheduler_ActionClaim
	 */
	abstract public function stake_claim( $max_actions = 10, ?DateTime $before_date = null, $hooks = array(), $group = '' );

	/**
	 * Get claim count.
	 *
	 * @return int
	 */
	abstract public function get_claim_count();

	/**
	 * Release the claim.
	 *
	 * @param ActionScheduler_ActionClaim $claim Claim object.
	 */
	abstract public function release_claim( ActionScheduler_ActionClaim $claim );

	/**
	 * Un-claim the action.
	 *
	 * @param string $action_id Action ID.
	 */
	abstract public function unclaim_action( $action_id );

	/**
	 * Mark action as failed.
	 *
	 * @param string $action_id Action ID.
	 */
	abstract public function mark_failure( $action_id );

	/**
	 * Log action's execution.
	 *
	 * @param string $action_id Actoin ID.
	 */
	abstract public function log_execution( $action_id );

	/**
	 * Mark action as complete.
	 *
	 * @param string $action_id Action ID.
	 */
	abstract public function mark_complete( $action_id );

	/**
	 * Get action's status.
	 *
	 * @param string $action_id Action ID.
	 * @return string
	 */
	abstract public function get_status( $action_id );

	/**
	 * Get action's claim ID.
	 *
	 * @param string $action_id Action ID.
	 * @return mixed
	 */
	abstract public function get_claim_id( $action_id );

	/**
	 * Find actions by claim ID.
	 *
	 * @param string $claim_id Claim ID.
	 * @return array
	 */
	abstract public function find_actions_by_claim_id( $claim_id );

	/**
	 * Validate SQL operator.
	 *
	 * @param string $comparison_operator Operator.
	 * @return string
	 */
	protected function validate_sql_comparator( $comparison_operator ) {
		if ( in_array( $comparison_operator, array( '!=', '>', '>=', '<', '<=', '=' ), true ) ) {
			return $comparison_operator;
		}

		return '=';
	}

	/**
	 * Get the time MySQL formatted date/time string for an action's (next) scheduled date.
	 *
	 * @param ActionScheduler_Action $action Action.
	 * @param null|DateTime          $scheduled_date Action's schedule date (optional).
	 * @return string
	 */
	protected function get_scheduled_date_string( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) {
		$next = is_null( $scheduled_date ) ? $action->get_schedule()->get_date() : $scheduled_date;

		if ( ! $next ) {
			$next = date_create();
		}

		$next->setTimezone( new DateTimeZone( 'UTC' ) );

		return $next->format( 'Y-m-d H:i:s' );
	}

	/**
	 * Get the time MySQL formatted date/time string for an action's (next) scheduled date.
	 *
	 * @param ActionScheduler_Action|null $action Action.
	 * @param null|DateTime               $scheduled_date Action's scheduled date (optional).
	 * @return string
	 */
	protected function get_scheduled_date_string_local( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) {
		$next = is_null( $scheduled_date ) ? $action->get_schedule()->get_date() : $scheduled_date;

		if ( ! $next ) {
			$next = date_create();
		}

		ActionScheduler_TimezoneHelper::set_local_timezone( $next );
		return $next->format( 'Y-m-d H:i:s' );
	}

	/**
	 * Validate that we could decode action arguments.
	 *
	 * @param mixed $args      The decoded arguments.
	 * @param int   $action_id The action ID.
	 *
	 * @throws ActionScheduler_InvalidActionException When the decoded arguments are invalid.
	 */
	protected function validate_args( $args, $action_id ) {
		// Ensure we have an array of args.
		if ( ! is_array( $args ) ) {
			throw ActionScheduler_InvalidActionException::from_decoding_args( $action_id );
		}

		// Validate JSON decoding if possible.
		if ( function_exists( 'json_last_error' ) && JSON_ERROR_NONE !== json_last_error() ) {
			throw ActionScheduler_InvalidActionException::from_decoding_args( $action_id, $args );
		}
	}

	/**
	 * Validate a ActionScheduler_Schedule object.
	 *
	 * @param mixed $schedule  The unserialized ActionScheduler_Schedule object.
	 * @param int   $action_id The action ID.
	 *
	 * @throws ActionScheduler_InvalidActionException When the schedule is invalid.
	 */
	protected function validate_schedule( $schedule, $action_id ) {
		if ( empty( $schedule ) || ! is_a( $schedule, 'ActionScheduler_Schedule' ) ) {
			throw ActionScheduler_InvalidActionException::from_schedule( $action_id, $schedule );
		}
	}

	/**
	 * InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4.
	 *
	 * Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However,
	 * with custom tables, we use an indexed VARCHAR column instead.
	 *
	 * @param  ActionScheduler_Action $action Action to be validated.
	 * @throws InvalidArgumentException When json encoded args is too long.
	 */
	protected function validate_action( ActionScheduler_Action $action ) {
		if ( strlen( wp_json_encode( $action->get_args() ) ) > static::$max_args_length ) {
			// translators: %d is a number (maximum length of action arguments).
			throw new InvalidArgumentException( sprintf( __( 'ActionScheduler_Action::$args too long. To ensure the args column can be indexed, action args should not be more than %d characters when encoded as JSON.', 'action-scheduler' ), static::$max_args_length ) );
		}
	}

	/**
	 * Cancel pending actions by hook.
	 *
	 * @since 3.0.0
	 *
	 * @param string $hook Hook name.
	 *
	 * @return void
	 */
	public function cancel_actions_by_hook( $hook ) {
		$action_ids = true;
		while ( ! empty( $action_ids ) ) {
			$action_ids = $this->query_actions(
				array(
					'hook'     => $hook,
					'status'   => self::STATUS_PENDING,
					'per_page' => 1000,
					'orderby'  => 'none',
				)
			);

			$this->bulk_cancel_actions( $action_ids );
		}
	}

	/**
	 * Cancel pending actions by group.
	 *
	 * @since 3.0.0
	 *
	 * @param string $group Group slug.
	 *
	 * @return void
	 */
	public function cancel_actions_by_group( $group ) {
		$action_ids = true;
		while ( ! empty( $action_ids ) ) {
			$action_ids = $this->query_actions(
				array(
					'group'    => $group,
					'status'   => self::STATUS_PENDING,
					'per_page' => 1000,
					'orderby'  => 'none',
				)
			);

			$this->bulk_cancel_actions( $action_ids );
		}
	}

	/**
	 * Cancel a set of action IDs.
	 *
	 * @since 3.0.0
	 *
	 * @param int[] $action_ids List of action IDs.
	 *
	 * @return void
	 */
	private function bulk_cancel_actions( $action_ids ) {
		foreach ( $action_ids as $action_id ) {
			$this->cancel_action( $action_id );
		}

		do_action( 'action_scheduler_bulk_cancel_actions', $action_ids );
	}

	/**
	 * Get status labels.
	 *
	 * @return array<string, string>
	 */
	public function get_status_labels() {
		return array(
			self::STATUS_COMPLETE => __( 'Complete', 'action-scheduler' ),
			self::STATUS_PENDING  => __( 'Pending', 'action-scheduler' ),
			self::STATUS_RUNNING  => __( 'In-progress', 'action-scheduler' ),
			self::STATUS_FAILED   => __( 'Failed', 'action-scheduler' ),
			self::STATUS_CANCELED => __( 'Canceled', 'action-scheduler' ),
		);
	}

	/**
	 * Check if there are any pending scheduled actions due to run.
	 *
	 * @return string
	 */
	public function has_pending_actions_due() {
		$pending_actions = $this->query_actions(
			array(
				'per_page' => 1,
				'date'     => as_get_datetime_object(),
				'status'   => self::STATUS_PENDING,
				'orderby'  => 'none',
			),
			'count'
		);

		return ! empty( $pending_actions );
	}

	/**
	 * Callable initialization function optionally overridden in derived classes.
	 */
	public function init() {}

	/**
	 * Callable function to mark an action as migrated optionally overridden in derived classes.
	 *
	 * @param int $action_id Action ID.
	 */
	public function mark_migrated( $action_id ) {}

	/**
	 * Get instance.
	 *
	 * @return ActionScheduler_Store
	 */
	public static function instance() {
		if ( empty( self::$store ) ) {
			$class       = apply_filters( 'action_scheduler_store_class', self::DEFAULT_CLASS );
			self::$store = new $class();
		}
		return self::$store;
	}
}