khaihihi
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
abstract class ActionScheduler {
|
||||
private static $plugin_file = '';
|
||||
/** @var ActionScheduler_ActionFactory */
|
||||
private static $factory = NULL;
|
||||
|
||||
public static function factory() {
|
||||
if ( !isset(self::$factory) ) {
|
||||
self::$factory = new ActionScheduler_ActionFactory();
|
||||
}
|
||||
return self::$factory;
|
||||
}
|
||||
|
||||
public static function store() {
|
||||
return ActionScheduler_Store::instance();
|
||||
}
|
||||
|
||||
public static function logger() {
|
||||
return ActionScheduler_Logger::instance();
|
||||
}
|
||||
|
||||
public static function runner() {
|
||||
return ActionScheduler_QueueRunner::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
|
||||
* @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
|
||||
* @return string
|
||||
*/
|
||||
public static function plugin_url( $path ) {
|
||||
return plugins_url($path, self::$plugin_file);
|
||||
}
|
||||
|
||||
public static function autoload( $class ) {
|
||||
$d = DIRECTORY_SEPARATOR;
|
||||
if ( 'Deprecated' === substr( $class, -10 ) ) {
|
||||
$dir = self::plugin_path('deprecated'.$d);
|
||||
} elseif ( strpos( $class, 'ActionScheduler' ) === 0 ) {
|
||||
$dir = self::plugin_path('classes'.$d);
|
||||
} elseif ( strpos( $class, 'CronExpression' ) === 0 ) {
|
||||
$dir = self::plugin_path('lib'.$d.'cron-expression'.$d);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( file_exists( "{$dir}{$class}.php" ) ) {
|
||||
include( "{$dir}{$class}.php" );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the plugin
|
||||
*
|
||||
* @static
|
||||
* @param string $plugin_file
|
||||
*/
|
||||
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' );
|
||||
|
||||
$store = self::store();
|
||||
add_action( 'init', array( $store, 'init' ), 1, 0 );
|
||||
|
||||
$logger = self::logger();
|
||||
add_action( 'init', array( $logger, 'init' ), 1, 0 );
|
||||
|
||||
$runner = self::runner();
|
||||
add_action( 'init', array( $runner, 'init' ), 1, 0 );
|
||||
|
||||
$admin_view = self::admin_view();
|
||||
add_action( 'init', array( $admin_view, 'init' ), 0, 0 ); // run before $store::init()
|
||||
|
||||
require_once( self::plugin_path('functions.php') );
|
||||
|
||||
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' );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final public function __clone() {
|
||||
trigger_error("Singleton. No cloning allowed!", E_USER_ERROR);
|
||||
}
|
||||
|
||||
final public function __wakeup() {
|
||||
trigger_error("Singleton. No serialization allowed!", E_USER_ERROR);
|
||||
}
|
||||
|
||||
final private function __construct() {}
|
||||
|
||||
/** Deprecated **/
|
||||
|
||||
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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
<?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
|
||||
*/
|
||||
protected $table_name;
|
||||
|
||||
/**
|
||||
* Package name, used in translations
|
||||
*/
|
||||
protected $package;
|
||||
|
||||
/**
|
||||
* How many items do we render per page?
|
||||
*/
|
||||
protected $items_per_page = 10;
|
||||
|
||||
/**
|
||||
* Enables search in this table listing. If this array
|
||||
* is empty it means the listing is not searchable.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
protected $row_actions = array();
|
||||
|
||||
/**
|
||||
* The Primary key of our table
|
||||
*/
|
||||
protected $ID = 'ID';
|
||||
|
||||
/**
|
||||
* Enables sorting, it expects an array
|
||||
* of columns (the column names are the values)
|
||||
*/
|
||||
protected $sort_by = array();
|
||||
|
||||
protected $filter_by = array();
|
||||
|
||||
/**
|
||||
* @var array The status name => count combinations for this table's items. Used to display status filters.
|
||||
*/
|
||||
protected $status_counts = array();
|
||||
|
||||
/**
|
||||
* @var array Notices to display when loading the table. Array of arrays of form array( 'class' => {updated|error}, 'message' => 'This is the notice text display.' ).
|
||||
*/
|
||||
protected $admin_notices = array();
|
||||
|
||||
/**
|
||||
* @var string Localised string displayed in the <h1> element above the able.
|
||||
*/
|
||||
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`).
|
||||
*/
|
||||
protected $bulk_actions = array();
|
||||
|
||||
/**
|
||||
* Makes translation easier, it basically just wraps
|
||||
* `_x` with some default (the package name)
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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 ] = $this->translate( $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' ) ) . ')';
|
||||
$this->$method( $_GET['ID'], $wpdb->prepare( $ids_sql, $_GET['ID'] ) );
|
||||
}
|
||||
|
||||
wp_redirect( remove_query_arg(
|
||||
array( '_wp_http_referer', '_wpnonce', 'ID', 'action', 'action2' ),
|
||||
wp_unslash( $_SERVER['REQUEST_URI'] )
|
||||
) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default code for deleting entries. We trust ids_sql because it is
|
||||
* validated already by process_bulk_action()
|
||||
*/
|
||||
protected function bulk_delete( array $ids, $ids_sql ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->query( "DELETE FROM {$this->table_name} WHERE {$this->ID} IN $ids_sql" );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(),
|
||||
array(),
|
||||
$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" />' ),
|
||||
array_map( array( $this, 'translate' ), $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->package . '_items_per_page', $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->package . '_items_per_page', $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}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ) ) {
|
||||
$orderby = sanitize_text_field( $_GET['orderby'] );
|
||||
} 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( $_GET['order'] ) ) {
|
||||
$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'] ) ) ? $_GET['status'] : '';
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the search filter for this request, if any.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_request_search_query() {
|
||||
$search_query = ( ! empty( $_GET['s'] ) ) ? $_GET['s'] : '';
|
||||
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 ) ) {
|
||||
$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.
|
||||
*
|
||||
* TODO:
|
||||
* - Improve search doing LIKE by word rather than by phrases.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_items_query_search() {
|
||||
global $wpdb;
|
||||
|
||||
if ( empty( $_GET['s'] ) || empty( $this->search_by ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$filter = array();
|
||||
foreach ( $this->search_by as $column ) {
|
||||
$filter[] = '`' . $column . '` like "%' . $wpdb->esc_like( $_GET['s'] ) . '%"';
|
||||
}
|
||||
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'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$filter = array();
|
||||
|
||||
foreach ( $this->filter_by as $column => $options ) {
|
||||
if ( empty( $_GET['filter_by'][ $column ] ) || empty( $options[ $_GET['filter_by'][ $column ] ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filter[] = $wpdb->prepare( "`$column` = %s", $_GET['filter_by'][ $column ] );
|
||||
}
|
||||
|
||||
return implode( ' AND ', $filter );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the data to feed WP_Table_List.
|
||||
*
|
||||
* This has the core for selecting, sorting and filting 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'] ) ) {
|
||||
// _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
|
||||
wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), 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 ) );
|
||||
|
||||
$query_count = "SELECT COUNT({$this->ID}) FROM {$this->table_name} {$where}";
|
||||
$total_items = $wpdb->get_var( $query_count );
|
||||
$per_page = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page );
|
||||
$this->set_pagination_args( array(
|
||||
'total_items' => $total_items,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => ceil( $total_items / $per_page ),
|
||||
) );
|
||||
}
|
||||
|
||||
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 ] ) ? $_GET['filter_by'][ $id ] : '';
|
||||
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( $this->translate( $label ) )
|
||||
. '</option>';
|
||||
}
|
||||
|
||||
echo '</select>';
|
||||
}
|
||||
|
||||
submit_button( __( 'Filter', 'woocommerce' ), '', '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.
|
||||
*/
|
||||
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`.
|
||||
*/
|
||||
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 render
|
||||
* @param $column_name Current row
|
||||
* @return
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
protected function process_row_actions() {
|
||||
$parameters = array( 'row_action', 'row_id', 'nonce' );
|
||||
foreach ( $parameters as $parameter ) {
|
||||
if ( empty( $_REQUEST[ $parameter ] ) ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$method = 'row_action_' . $_REQUEST['row_action'];
|
||||
|
||||
if ( $_REQUEST['nonce'] === wp_create_nonce( $_REQUEST[ 'row_action' ] . '::' . $_REQUEST[ 'row_id' ] ) && method_exists( $this, $method ) ) {
|
||||
$this->$method( $_REQUEST['row_id'] );
|
||||
}
|
||||
|
||||
wp_redirect( remove_query_arg(
|
||||
array( 'row_id', 'row_action', 'nonce' ),
|
||||
wp_unslash( $_SERVER['REQUEST_URI'] )
|
||||
) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default column formatting, it will escape everything for security.
|
||||
*/
|
||||
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"', 'woocommerce' ), $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="' . $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'] ) ) {
|
||||
$this->status_counts = array( 'all' => array_sum( $this->status_counts ) ) + $this->status_counts;
|
||||
}
|
||||
|
||||
foreach ( $this->status_counts as $status_name => $count ) {
|
||||
|
||||
if ( 0 === $count ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $status_name === $request_status || ( empty( $request_status ) && 'all' === $status_name ) ) {
|
||||
$status_list_item = '<li class="%1$s"><strong>%3$s</strong> (%4$d)</li>';
|
||||
} else {
|
||||
$status_list_item = '<li class="%1$s"><a href="%2$s">%3$s</a> (%4$d)</li>';
|
||||
}
|
||||
|
||||
$status_filter_url = ( 'all' === $status_name ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_name );
|
||||
$status_list_items[] = sprintf( $status_list_item, esc_attr( $status_name ), esc_url( $status_filter_url ), esc_html( ucfirst( $status_name ) ), absint( $count ) );
|
||||
}
|
||||
|
||||
if ( $status_list_items ) {
|
||||
echo '<ul class="subsubsub">';
|
||||
echo implode( " | \n", $status_list_items );
|
||||
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 ( $_GET as $key => $value ) {
|
||||
if ( '_' === $key[0] || 'paged' === $key ) {
|
||||
continue;
|
||||
}
|
||||
echo '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( $value ) . '" />';
|
||||
}
|
||||
if ( ! empty( $this->search_by ) ) {
|
||||
echo $this->search_box( $this->get_search_box_button_text(), 'plugin' ); // WPCS: XSS OK
|
||||
}
|
||||
parent::display();
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 __( 'Search', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Abstract class with common Queue Cleaner functionality.
|
||||
*/
|
||||
abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abstract_QueueRunner_Deprecated {
|
||||
|
||||
/** @var ActionScheduler_QueueCleaner */
|
||||
protected $cleaner;
|
||||
|
||||
/** @var ActionScheduler_FatalErrorMonitor */
|
||||
protected $monitor;
|
||||
|
||||
/** @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 $store
|
||||
* @param ActionScheduler_FatalErrorMonitor $monitor
|
||||
* @param ActionScheduler_QueueCleaner $cleaner
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
public function process_action( $action_id ) {
|
||||
try {
|
||||
do_action( 'action_scheduler_before_execute', $action_id );
|
||||
|
||||
if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) {
|
||||
do_action( 'action_scheduler_execution_ignored', $action_id );
|
||||
return;
|
||||
}
|
||||
|
||||
$action = $this->store->fetch_action( $action_id );
|
||||
$this->store->log_execution( $action_id );
|
||||
$action->execute();
|
||||
do_action( 'action_scheduler_after_execute', $action_id, $action );
|
||||
$this->store->mark_complete( $action_id );
|
||||
} catch ( Exception $e ) {
|
||||
$this->store->mark_failure( $action_id );
|
||||
do_action( 'action_scheduler_failed_execution', $action_id, $e );
|
||||
}
|
||||
|
||||
if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) ) {
|
||||
$this->schedule_next_instance( $action );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the next instance of the action if necessary.
|
||||
*
|
||||
* @param ActionScheduler_Action $action
|
||||
*/
|
||||
protected function schedule_next_instance( ActionScheduler_Action $action ) {
|
||||
$schedule = $action->get_schedule();
|
||||
$next = $schedule->next( as_get_datetime_object() );
|
||||
|
||||
if ( ! is_null( $next ) && $schedule->is_recurring() ) {
|
||||
$this->store->save_action( $action, $next );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the queue cleaner.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*/
|
||||
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', 5 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
$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.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
* @return int The number of actions processed.
|
||||
*/
|
||||
abstract public function run();
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_Action
|
||||
*/
|
||||
class ActionScheduler_Action {
|
||||
protected $hook = '';
|
||||
protected $args = array();
|
||||
/** @var ActionScheduler_Schedule */
|
||||
protected $schedule = NULL;
|
||||
protected $group = '';
|
||||
|
||||
public function __construct( $hook, array $args = array(), ActionScheduler_Schedule $schedule = NULL, $group = '' ) {
|
||||
$schedule = empty( $schedule ) ? new ActionScheduler_NullSchedule() : $schedule;
|
||||
$this->set_hook($hook);
|
||||
$this->set_schedule($schedule);
|
||||
$this->set_args($args);
|
||||
$this->set_group($group);
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
return do_action_ref_array($this->get_hook(), $this->get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hook
|
||||
*/
|
||||
protected function set_hook( $hook ) {
|
||||
$this->hook = $hook;
|
||||
}
|
||||
|
||||
public function get_hook() {
|
||||
return $this->hook;
|
||||
}
|
||||
|
||||
protected function set_schedule( ActionScheduler_Schedule $schedule ) {
|
||||
$this->schedule = $schedule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ActionScheduler_Schedule
|
||||
*/
|
||||
public function get_schedule() {
|
||||
return $this->schedule;
|
||||
}
|
||||
|
||||
protected function set_args( array $args ) {
|
||||
$this->args = $args;
|
||||
}
|
||||
|
||||
public function get_args() {
|
||||
return $this->args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $group
|
||||
*/
|
||||
protected function set_group( $group ) {
|
||||
$this->group = $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function get_group() {
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool If the action has been finished
|
||||
*/
|
||||
public function is_finished() {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_ActionClaim
|
||||
*/
|
||||
class ActionScheduler_ActionClaim {
|
||||
private $id = '';
|
||||
private $action_ids = array();
|
||||
|
||||
public function __construct( $id, array $action_ids ) {
|
||||
$this->id = $id;
|
||||
$this->action_ids = $action_ids;
|
||||
}
|
||||
|
||||
public function get_id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function get_actions() {
|
||||
return $this->action_ids;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_ActionFactory
|
||||
*/
|
||||
class ActionScheduler_ActionFactory {
|
||||
|
||||
/**
|
||||
* @param string $status The action's status in the data store
|
||||
* @param string $hook The hook to trigger when this action runs
|
||||
* @param array $args Args to pass to callbacks when the hook is triggered
|
||||
* @param ActionScheduler_Schedule $schedule The action's schedule
|
||||
* @param string $group A group to put the action in
|
||||
*
|
||||
* @return ActionScheduler_Action An instance of the stored action
|
||||
*/
|
||||
public function get_stored_action( $status, $hook, array $args = array(), ActionScheduler_Schedule $schedule = null, $group = '' ) {
|
||||
|
||||
switch ( $status ) {
|
||||
case ActionScheduler_Store::STATUS_PENDING :
|
||||
$action_class = 'ActionScheduler_Action';
|
||||
break;
|
||||
case ActionScheduler_Store::STATUS_CANCELED :
|
||||
$action_class = 'ActionScheduler_CanceledAction';
|
||||
break;
|
||||
default :
|
||||
$action_class = 'ActionScheduler_FinishedAction';
|
||||
break;
|
||||
}
|
||||
|
||||
$action_class = apply_filters( 'action_scheduler_stored_action_class', $action_class, $status, $hook, $args, $schedule, $group );
|
||||
|
||||
$action = new $action_class( $hook, $args, $schedule, $group );
|
||||
|
||||
/**
|
||||
* Allow 3rd party code to change the instantiated action for a given hook, args, schedule and group.
|
||||
*
|
||||
* @param ActionScheduler_Action $action The instantiated action.
|
||||
* @param string $hook The instantiated action's hook.
|
||||
* @param array $args The instantiated action's args.
|
||||
* @param ActionScheduler_Schedule $schedule The instantiated action's schedule.
|
||||
* @param string $group The instantiated action's group.
|
||||
*/
|
||||
return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hook The hook to trigger when this action runs
|
||||
* @param array $args Args to pass when the hook is triggered
|
||||
* @param int $when Unix timestamp when the action will run
|
||||
* @param string $group A group to put the action in
|
||||
*
|
||||
* @return string The ID of the stored action
|
||||
*/
|
||||
public function single( $hook, $args = array(), $when = null, $group = '' ) {
|
||||
$date = as_get_datetime_object( $when );
|
||||
$schedule = new ActionScheduler_SimpleSchedule( $date );
|
||||
$action = new ActionScheduler_Action( $hook, $args, $schedule, $group );
|
||||
return $this->store( $action );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hook The hook to trigger when this action runs
|
||||
* @param array $args Args to pass when the hook is triggered
|
||||
* @param int $first Unix timestamp for the first run
|
||||
* @param int $interval Seconds between runs
|
||||
* @param string $group A group to put the action in
|
||||
*
|
||||
* @return string The ID of the stored action
|
||||
*/
|
||||
public function recurring( $hook, $args = array(), $first = null, $interval = null, $group = '' ) {
|
||||
if ( empty($interval) ) {
|
||||
return $this->single( $hook, $args, $first, $group );
|
||||
}
|
||||
$date = as_get_datetime_object( $first );
|
||||
$schedule = new ActionScheduler_IntervalSchedule( $date, $interval );
|
||||
$action = new ActionScheduler_Action( $hook, $args, $schedule, $group );
|
||||
return $this->store( $action );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $hook The hook to trigger when this action runs
|
||||
* @param array $args Args to pass when the hook is triggered
|
||||
* @param int $first Unix timestamp for the first run
|
||||
* @param int $schedule A cron definition string
|
||||
* @param string $group A group to put the action in
|
||||
*
|
||||
* @return string The ID of the stored action
|
||||
*/
|
||||
public function cron( $hook, $args = array(), $first = null, $schedule = null, $group = '' ) {
|
||||
if ( empty($schedule) ) {
|
||||
return $this->single( $hook, $args, $first, $group );
|
||||
}
|
||||
$date = as_get_datetime_object( $first );
|
||||
$cron = CronExpression::factory( $schedule );
|
||||
$schedule = new ActionScheduler_CronSchedule( $date, $cron );
|
||||
$action = new ActionScheduler_Action( $hook, $args, $schedule, $group );
|
||||
return $this->store( $action );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ActionScheduler_Action $action
|
||||
*
|
||||
* @return string The ID of the stored action
|
||||
*/
|
||||
protected function store( ActionScheduler_Action $action ) {
|
||||
$store = ActionScheduler_Store::instance();
|
||||
return $store->save_action( $action );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_AdminView
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ActionScheduler_AdminView extends ActionScheduler_AdminView_Deprecated {
|
||||
|
||||
private static $admin_view = NULL;
|
||||
|
||||
/**
|
||||
* @return ActionScheduler_QueueRunner
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function instance() {
|
||||
|
||||
if ( empty( self::$admin_view ) ) {
|
||||
$class = apply_filters('action_scheduler_admin_view_class', 'ActionScheduler_AdminView');
|
||||
self::$admin_view = new $class();
|
||||
}
|
||||
|
||||
return self::$admin_view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function init() {
|
||||
if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || false == DOING_AJAX ) ) {
|
||||
|
||||
if ( class_exists( 'WooCommerce' ) ) {
|
||||
add_action( 'woocommerce_admin_status_content_action-scheduler', array( $this, 'render_admin_ui' ) );
|
||||
add_action( 'woocommerce_system_status_report', array( $this, 'system_status_report' ) );
|
||||
add_filter( 'woocommerce_admin_status_tabs', array( $this, 'register_system_status_tab' ) );
|
||||
}
|
||||
|
||||
add_action( 'admin_menu', array( $this, 'register_menu' ) );
|
||||
}
|
||||
}
|
||||
|
||||
public function system_status_report() {
|
||||
$table = new ActionScheduler_wcSystemStatus( ActionScheduler::store() );
|
||||
$table->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers action-scheduler into WooCommerce > System status.
|
||||
*
|
||||
* @param array $tabs An associative array of tab key => label.
|
||||
* @return array $tabs An associative array of tab key => label, including Action Scheduler's tabs
|
||||
*/
|
||||
public function register_system_status_tab( array $tabs ) {
|
||||
$tabs['action-scheduler'] = __( 'Scheduled Actions', 'woocommerce' );
|
||||
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include Action Scheduler's administration under the Tools menu.
|
||||
*
|
||||
* A menu under the Tools menu is important for backward compatibility (as that's
|
||||
* where it started), and also provides more convenient access than the WooCommerce
|
||||
* System Status page, and for sites where WooCommerce isn't active.
|
||||
*/
|
||||
public function register_menu() {
|
||||
add_submenu_page(
|
||||
'tools.php',
|
||||
__( 'Scheduled Actions', 'woocommerce' ),
|
||||
__( 'Scheduled Actions', 'woocommerce' ),
|
||||
'manage_options',
|
||||
'action-scheduler',
|
||||
array( $this, 'render_admin_ui' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the Admin UI
|
||||
*/
|
||||
public function render_admin_ui() {
|
||||
$table = new ActionScheduler_ListTable( ActionScheduler::store(), ActionScheduler::logger(), ActionScheduler::runner() );
|
||||
$table->display_page();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_CanceledAction
|
||||
*
|
||||
* Stored action which was canceled and therefore acts like a finished action but should always return a null schedule,
|
||||
* regardless of schedule passed to its constructor.
|
||||
*/
|
||||
class ActionScheduler_CanceledAction extends ActionScheduler_FinishedAction {
|
||||
|
||||
/**
|
||||
* @param string $hook
|
||||
* @param array $args
|
||||
* @param ActionScheduler_Schedule $schedule
|
||||
* @param string $group
|
||||
*/
|
||||
public function __construct( $hook, array $args = array(), ActionScheduler_Schedule $schedule = null, $group = '' ) {
|
||||
parent::__construct( $hook, $args, $schedule, $group );
|
||||
$this->set_schedule( new ActionScheduler_NullSchedule() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_Compatibility
|
||||
*/
|
||||
class ActionScheduler_Compatibility {
|
||||
|
||||
/**
|
||||
* Converts a shorthand byte value to an integer byte value.
|
||||
*
|
||||
* Wrapper for wp_convert_hr_to_bytes(), moved to load.php in WordPress 4.6 from media.php
|
||||
*
|
||||
* @link https://secure.php.net/manual/en/function.ini-get.php
|
||||
* @link https://secure.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
|
||||
*
|
||||
* @param string $value A (PHP ini) byte value, either shorthand or ordinary.
|
||||
* @return int An integer byte value.
|
||||
*/
|
||||
public static function convert_hr_to_bytes( $value ) {
|
||||
if ( function_exists( 'wp_convert_hr_to_bytes' ) ) {
|
||||
return wp_convert_hr_to_bytes( $value );
|
||||
}
|
||||
|
||||
$value = strtolower( trim( $value ) );
|
||||
$bytes = (int) $value;
|
||||
|
||||
if ( false !== strpos( $value, 'g' ) ) {
|
||||
$bytes *= GB_IN_BYTES;
|
||||
} elseif ( false !== strpos( $value, 'm' ) ) {
|
||||
$bytes *= MB_IN_BYTES;
|
||||
} elseif ( false !== strpos( $value, 'k' ) ) {
|
||||
$bytes *= KB_IN_BYTES;
|
||||
}
|
||||
|
||||
// Deal with large (float) values which run into the maximum integer size.
|
||||
return min( $bytes, PHP_INT_MAX );
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to raise the PHP memory limit for memory intensive processes.
|
||||
*
|
||||
* Only allows raising the existing limit and prevents lowering it.
|
||||
*
|
||||
* Wrapper for wp_raise_memory_limit(), added in WordPress v4.6.0
|
||||
*
|
||||
* @return bool|int|string The limit that was set or false on failure.
|
||||
*/
|
||||
public static function raise_memory_limit() {
|
||||
if ( function_exists( 'wp_raise_memory_limit' ) ) {
|
||||
return wp_raise_memory_limit( 'admin' );
|
||||
}
|
||||
|
||||
$current_limit = @ini_get( 'memory_limit' );
|
||||
$current_limit_int = self::convert_hr_to_bytes( $current_limit );
|
||||
|
||||
if ( -1 === $current_limit_int ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$wp_max_limit = WP_MAX_MEMORY_LIMIT;
|
||||
$wp_max_limit_int = self::convert_hr_to_bytes( $wp_max_limit );
|
||||
$filtered_limit = apply_filters( 'admin_memory_limit', $wp_max_limit );
|
||||
$filtered_limit_int = self::convert_hr_to_bytes( $filtered_limit );
|
||||
|
||||
if ( -1 === $filtered_limit_int || ( $filtered_limit_int > $wp_max_limit_int && $filtered_limit_int > $current_limit_int ) ) {
|
||||
if ( false !== @ini_set( 'memory_limit', $filtered_limit ) ) {
|
||||
return $filtered_limit;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} elseif ( -1 === $wp_max_limit_int || $wp_max_limit_int > $current_limit_int ) {
|
||||
if ( false !== @ini_set( 'memory_limit', $wp_max_limit ) ) {
|
||||
return $wp_max_limit;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to raise the PHP timeout for time intensive processes.
|
||||
*
|
||||
* Only allows raising the existing limit and prevents lowering it. Wrapper for wc_set_time_limit(), when available.
|
||||
*
|
||||
* @param int The time limit in seconds.
|
||||
*/
|
||||
public static function raise_time_limit( $limit = 0 ) {
|
||||
if ( $limit < ini_get( 'max_execution_time' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( function_exists( 'wc_set_time_limit' ) ) {
|
||||
wc_set_time_limit( $limit );
|
||||
} elseif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) {
|
||||
@set_time_limit( $limit );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_CronSchedule
|
||||
*/
|
||||
class ActionScheduler_CronSchedule implements ActionScheduler_Schedule {
|
||||
/** @var DateTime */
|
||||
private $start = NULL;
|
||||
private $start_timestamp = 0;
|
||||
/** @var CronExpression */
|
||||
private $cron = NULL;
|
||||
|
||||
public function __construct( DateTime $start, CronExpression $cron ) {
|
||||
$this->start = $start;
|
||||
$this->cron = $cron;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $after
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function next( DateTime $after = NULL ) {
|
||||
$after = empty($after) ? clone $this->start : clone $after;
|
||||
return $this->cron->getNextRunDate($after, 0, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function is_recurring() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function get_recurrence() {
|
||||
return strval($this->cron);
|
||||
}
|
||||
|
||||
/**
|
||||
* For PHP 5.2 compat, since DateTime objects can't be serialized
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep() {
|
||||
$this->start_timestamp = $this->start->getTimestamp();
|
||||
return array(
|
||||
'start_timestamp',
|
||||
'cron'
|
||||
);
|
||||
}
|
||||
|
||||
public function __wakeup() {
|
||||
$this->start = as_get_datetime_object($this->start_timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ActionScheduler DateTime class.
|
||||
*
|
||||
* This is a custom extension to DateTime that
|
||||
*/
|
||||
class ActionScheduler_DateTime extends DateTime {
|
||||
|
||||
/**
|
||||
* UTC offset.
|
||||
*
|
||||
* Only used when a timezone is not set. When a timezone string is
|
||||
* used, this will be set to 0.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $utcOffset = 0;
|
||||
|
||||
/**
|
||||
* Get the unix timestamp of the current object.
|
||||
*
|
||||
* Missing in PHP 5.2 so just here so it can be supported consistently.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTimestamp() {
|
||||
return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the UTC offset.
|
||||
*
|
||||
* This represents a fixed offset instead of a timezone setting.
|
||||
*
|
||||
* @param $offset
|
||||
*/
|
||||
public function setUtcOffset( $offset ) {
|
||||
$this->utcOffset = intval( $offset );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timezone offset.
|
||||
*
|
||||
* @return int
|
||||
* @link http://php.net/manual/en/datetime.getoffset.php
|
||||
*/
|
||||
public function getOffset() {
|
||||
return $this->utcOffset ? $this->utcOffset : parent::getOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the TimeZone associated with the DateTime
|
||||
*
|
||||
* @param DateTimeZone $timezone
|
||||
*
|
||||
* @return static
|
||||
* @link http://php.net/manual/en/datetime.settimezone.php
|
||||
*/
|
||||
public function setTimezone( $timezone ) {
|
||||
$this->utcOffset = 0;
|
||||
parent::setTimezone( $timezone );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp with the WordPress timezone offset added or subtracted.
|
||||
*
|
||||
* @since 3.0.0
|
||||
* @return int
|
||||
*/
|
||||
public function getOffsetTimestamp() {
|
||||
return $this->getTimestamp() + $this->getOffset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ActionScheduler Exception Interface.
|
||||
*
|
||||
* Facilitates catching Exceptions unique to Action Scheduler.
|
||||
*
|
||||
* @package Prospress\ActionScheduler
|
||||
* @since %VERSION%
|
||||
*/
|
||||
interface ActionScheduler_Exception {}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_FatalErrorMonitor
|
||||
*/
|
||||
class ActionScheduler_FatalErrorMonitor {
|
||||
/** @var ActionScheduler_ActionClaim */
|
||||
private $claim = NULL;
|
||||
/** @var ActionScheduler_Store */
|
||||
private $store = NULL;
|
||||
private $action_id = 0;
|
||||
|
||||
public function __construct( ActionScheduler_Store $store ) {
|
||||
$this->store = $store;
|
||||
}
|
||||
|
||||
public function attach( ActionScheduler_ActionClaim $claim ) {
|
||||
$this->claim = $claim;
|
||||
add_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) );
|
||||
add_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0, 1 );
|
||||
add_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0, 0 );
|
||||
add_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0, 0 );
|
||||
add_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0, 0 );
|
||||
}
|
||||
|
||||
public function detach() {
|
||||
$this->claim = NULL;
|
||||
$this->untrack_action();
|
||||
remove_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) );
|
||||
remove_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0 );
|
||||
remove_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0 );
|
||||
remove_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0 );
|
||||
remove_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0 );
|
||||
}
|
||||
|
||||
public function track_current_action( $action_id ) {
|
||||
$this->action_id = $action_id;
|
||||
}
|
||||
|
||||
public function untrack_action() {
|
||||
$this->action_id = 0;
|
||||
}
|
||||
|
||||
public function handle_unexpected_shutdown() {
|
||||
if ( $error = error_get_last() ) {
|
||||
if ( in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ) ) ) {
|
||||
if ( !empty($this->action_id) ) {
|
||||
$this->store->mark_failure( $this->action_id );
|
||||
do_action( 'action_scheduler_unexpected_shutdown', $this->action_id, $error );
|
||||
}
|
||||
}
|
||||
$this->store->release_claim( $this->claim );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_FinishedAction
|
||||
*/
|
||||
class ActionScheduler_FinishedAction extends ActionScheduler_Action {
|
||||
|
||||
public function execute() {
|
||||
// don't execute
|
||||
}
|
||||
|
||||
public function is_finished() {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_IntervalSchedule
|
||||
*/
|
||||
class ActionScheduler_IntervalSchedule implements ActionScheduler_Schedule {
|
||||
/** @var DateTime */
|
||||
private $start = NULL;
|
||||
private $start_timestamp = 0;
|
||||
private $interval_in_seconds = 0;
|
||||
|
||||
public function __construct( DateTime $start, $interval ) {
|
||||
$this->start = $start;
|
||||
$this->interval_in_seconds = (int)$interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $after
|
||||
*
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function next( DateTime $after = NULL ) {
|
||||
$after = empty($after) ? as_get_datetime_object('@0') : clone $after;
|
||||
if ( $after > $this->start ) {
|
||||
$after->modify('+'.$this->interval_in_seconds.' seconds');
|
||||
return $after;
|
||||
}
|
||||
return clone $this->start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function is_recurring() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function interval_in_seconds() {
|
||||
return $this->interval_in_seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* For PHP 5.2 compat, since DateTime objects can't be serialized
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep() {
|
||||
$this->start_timestamp = $this->start->getTimestamp();
|
||||
return array(
|
||||
'start_timestamp',
|
||||
'interval_in_seconds'
|
||||
);
|
||||
}
|
||||
|
||||
public function __wakeup() {
|
||||
$this->start = as_get_datetime_object($this->start_timestamp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* InvalidAction Exception.
|
||||
*
|
||||
* Used for identifying actions that are invalid in some way.
|
||||
*
|
||||
* @package Prospress\ActionScheduler
|
||||
*/
|
||||
class ActionScheduler_InvalidActionException extends \InvalidArgumentException implements ActionScheduler_Exception {
|
||||
|
||||
/**
|
||||
* Create a new exception when the action's args cannot be decoded to an array.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*
|
||||
* @param string $action_id The action ID with bad args.
|
||||
* @return static
|
||||
*/
|
||||
public static function from_decoding_args( $action_id ) {
|
||||
$message = sprintf(
|
||||
__( 'Action [%s] has invalid arguments. It cannot be JSON decoded to an array.', 'woocommerce' ),
|
||||
$action_id
|
||||
);
|
||||
|
||||
return new static( $message );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Implements the admin view of the actions.
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
|
||||
|
||||
/**
|
||||
* The package name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $package = 'action-scheduler';
|
||||
|
||||
/**
|
||||
* Columns to show (name => label).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $columns = array();
|
||||
|
||||
/**
|
||||
* Actions (name => label).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $row_actions = array();
|
||||
|
||||
/**
|
||||
* The active data stores
|
||||
*
|
||||
* @var ActionScheduler_Store
|
||||
*/
|
||||
protected $store;
|
||||
|
||||
/**
|
||||
* A logger to use for getting action logs to display
|
||||
*
|
||||
* @var ActionScheduler_Logger
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* A ActionScheduler_QueueRunner runner instance (or child class)
|
||||
*
|
||||
* @var ActionScheduler_QueueRunner
|
||||
*/
|
||||
protected $runner;
|
||||
|
||||
/**
|
||||
* Bulk actions. The key of the array is the method name of the implementation:
|
||||
*
|
||||
* bulk_<key>(array $ids, string $sql_in).
|
||||
*
|
||||
* See the comments in the parent class for further details
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $bulk_actions = array();
|
||||
|
||||
/**
|
||||
* Flag variable to render our notifications, if any, once.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $did_notification = false;
|
||||
|
||||
/**
|
||||
* Array of seconds for common time periods, like week or month, alongside an internationalised string representation, i.e. "Day" or "Days"
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $time_periods;
|
||||
|
||||
/**
|
||||
* Sets the current data store object into `store->action` and initialises the object.
|
||||
*
|
||||
* @param ActionScheduler_Store $store
|
||||
* @param ActionScheduler_Logger $logger
|
||||
* @param ActionScheduler_QueueRunner $runner
|
||||
*/
|
||||
public function __construct( ActionScheduler_Store $store, ActionScheduler_Logger $logger, ActionScheduler_QueueRunner $runner ) {
|
||||
|
||||
$this->store = $store;
|
||||
$this->logger = $logger;
|
||||
$this->runner = $runner;
|
||||
|
||||
$this->table_header = __( 'Scheduled Actions', 'woocommerce' );
|
||||
|
||||
$this->bulk_actions = array(
|
||||
'delete' => __( 'Delete', 'woocommerce' ),
|
||||
);
|
||||
|
||||
$this->columns = array(
|
||||
'hook' => __( 'Hook', 'woocommerce' ),
|
||||
'status' => __( 'Status', 'woocommerce' ),
|
||||
'args' => __( 'Arguments', 'woocommerce' ),
|
||||
'group' => __( 'Group', 'woocommerce' ),
|
||||
'recurrence' => __( 'Recurrence', 'woocommerce' ),
|
||||
'schedule' => __( 'Scheduled Date', 'woocommerce' ),
|
||||
'log_entries' => __( 'Log', 'woocommerce' ),
|
||||
);
|
||||
|
||||
$this->sort_by = array(
|
||||
'schedule',
|
||||
'hook',
|
||||
'group',
|
||||
);
|
||||
|
||||
$this->search_by = array(
|
||||
'hook',
|
||||
'args',
|
||||
'claim_id',
|
||||
);
|
||||
|
||||
$request_status = $this->get_request_status();
|
||||
|
||||
if ( empty( $request_status ) ) {
|
||||
$this->sort_by[] = 'status';
|
||||
} elseif ( in_array( $request_status, array( 'in-progress', 'failed' ) ) ) {
|
||||
$this->columns += array( 'claim_id' => __( 'Claim ID', 'woocommerce' ) );
|
||||
$this->sort_by[] = 'claim_id';
|
||||
}
|
||||
|
||||
$this->row_actions = array(
|
||||
'hook' => array(
|
||||
'run' => array(
|
||||
'name' => __( 'Run', 'woocommerce' ),
|
||||
'desc' => __( 'Process the action now as if it were run as part of a queue', 'woocommerce' ),
|
||||
),
|
||||
'cancel' => array(
|
||||
'name' => __( 'Cancel', 'woocommerce' ),
|
||||
'desc' => __( 'Cancel the action now to avoid it being run in future', 'woocommerce' ),
|
||||
'class' => 'cancel trash',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
self::$time_periods = array(
|
||||
array(
|
||||
'seconds' => YEAR_IN_SECONDS,
|
||||
'names' => _n_noop( '%s year', '%s years', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
'seconds' => MONTH_IN_SECONDS,
|
||||
'names' => _n_noop( '%s month', '%s months', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
'seconds' => WEEK_IN_SECONDS,
|
||||
'names' => _n_noop( '%s week', '%s weeks', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
'seconds' => DAY_IN_SECONDS,
|
||||
'names' => _n_noop( '%s day', '%s days', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
'seconds' => HOUR_IN_SECONDS,
|
||||
'names' => _n_noop( '%s hour', '%s hours', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
'seconds' => MINUTE_IN_SECONDS,
|
||||
'names' => _n_noop( '%s minute', '%s minutes', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
'seconds' => 1,
|
||||
'names' => _n_noop( '%s second', '%s seconds', 'woocommerce' ),
|
||||
),
|
||||
);
|
||||
|
||||
parent::__construct( array(
|
||||
'singular' => 'action-scheduler',
|
||||
'plural' => 'action-scheduler',
|
||||
'ajax' => false,
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an interval of seconds into a two part human friendly string.
|
||||
*
|
||||
* The WordPress human_time_diff() function only calculates the time difference to one degree, meaning
|
||||
* even if an action is 1 day and 11 hours away, it will display "1 day". This function goes one step
|
||||
* further to display two degrees of accuracy.
|
||||
*
|
||||
* Inspired by the Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/
|
||||
*
|
||||
* @param int $interval A interval in seconds.
|
||||
* @param int $periods_to_include Depth of time periods to include, e.g. for an interval of 70, and $periods_to_include of 2, both minutes and seconds would be included. With a value of 1, only minutes would be included.
|
||||
* @return string A human friendly string representation of the interval.
|
||||
*/
|
||||
private static function human_interval( $interval, $periods_to_include = 2 ) {
|
||||
|
||||
if ( $interval <= 0 ) {
|
||||
return __( 'Now!', 'woocommerce' );
|
||||
}
|
||||
|
||||
$output = '';
|
||||
|
||||
for ( $time_period_index = 0, $periods_included = 0, $seconds_remaining = $interval; $time_period_index < count( self::$time_periods ) && $seconds_remaining > 0 && $periods_included < $periods_to_include; $time_period_index++ ) {
|
||||
|
||||
$periods_in_interval = floor( $seconds_remaining / self::$time_periods[ $time_period_index ]['seconds'] );
|
||||
|
||||
if ( $periods_in_interval > 0 ) {
|
||||
if ( ! empty( $output ) ) {
|
||||
$output .= ' ';
|
||||
}
|
||||
$output .= sprintf( _n( self::$time_periods[ $time_period_index ]['names'][0], self::$time_periods[ $time_period_index ]['names'][1], $periods_in_interval, 'woocommerce' ), $periods_in_interval );
|
||||
$seconds_remaining -= $periods_in_interval * self::$time_periods[ $time_period_index ]['seconds'];
|
||||
$periods_included++;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the recurrence of an action or 'Non-repeating'. The output is human readable.
|
||||
*
|
||||
* @param ActionScheduler_Action $action
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_recurrence( $action ) {
|
||||
$recurrence = $action->get_schedule();
|
||||
if ( $recurrence->is_recurring() ) {
|
||||
if ( method_exists( $recurrence, 'interval_in_seconds' ) ) {
|
||||
return sprintf( __( 'Every %s', 'woocommerce' ), self::human_interval( $recurrence->interval_in_seconds() ) );
|
||||
}
|
||||
|
||||
if ( method_exists( $recurrence, 'get_recurrence' ) ) {
|
||||
return sprintf( __( 'Cron %s', 'woocommerce' ), $recurrence->get_recurrence() );
|
||||
}
|
||||
}
|
||||
|
||||
return __( 'Non-repeating', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the argument of an action to render it in a human friendly format.
|
||||
*
|
||||
* @param array $row The array representation of the current row of the table
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_args( array $row ) {
|
||||
if ( empty( $row['args'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$row_html = '<ul>';
|
||||
foreach ( $row['args'] as $key => $value ) {
|
||||
$row_html .= sprintf( '<li><code>%s => %s</code></li>', esc_html( var_export( $key, true ) ), esc_html( var_export( $value, true ) ) );
|
||||
}
|
||||
$row_html .= '</ul>';
|
||||
|
||||
return apply_filters( 'action_scheduler_list_table_column_args', $row_html, $row );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
|
||||
*
|
||||
* @param array $row Action array.
|
||||
* @return string
|
||||
*/
|
||||
public function column_log_entries( array $row ) {
|
||||
|
||||
$log_entries_html = '<ol>';
|
||||
|
||||
$timezone = new DateTimezone( 'UTC' );
|
||||
|
||||
foreach ( $row['log_entries'] as $log_entry ) {
|
||||
$log_entries_html .= $this->get_log_entry_html( $log_entry, $timezone );
|
||||
}
|
||||
|
||||
$log_entries_html .= '</ol>';
|
||||
|
||||
return $log_entries_html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
|
||||
*
|
||||
* @param ActionScheduler_LogEntry $log_entry
|
||||
* @param DateTimezone $timezone
|
||||
* @return string
|
||||
*/
|
||||
protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) {
|
||||
$date = $log_entry->get_date();
|
||||
$date->setTimezone( $timezone );
|
||||
return sprintf( '<li><strong>%s</strong><br/>%s</li>', esc_html( $date->format( 'Y-m-d H:i:s O' ) ), esc_html( $log_entry->get_message() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Only display row actions for pending actions.
|
||||
*
|
||||
* @param array $row Row to render
|
||||
* @param string $column_name Current row
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function maybe_render_actions( $row, $column_name ) {
|
||||
if ( 'pending' === strtolower( $row['status'] ) ) {
|
||||
return parent::maybe_render_actions( $row, $column_name );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders admin notifications
|
||||
*
|
||||
* Notifications:
|
||||
* 1. When the maximum number of tasks are being executed simultaneously
|
||||
* 2. Notifications when a task us manually executed
|
||||
*/
|
||||
public function display_admin_notices() {
|
||||
|
||||
if ( $this->store->get_claim_count() >= $this->runner->get_allowed_concurrent_batches() ) {
|
||||
$this->admin_notices[] = array(
|
||||
'class' => 'updated',
|
||||
'message' => sprintf( __( 'Maximum simultaneous batches already in progress (%s queues). No actions will be processed until the current batches are complete.', 'woocommerce' ), $this->store->get_claim_count() ),
|
||||
);
|
||||
}
|
||||
|
||||
$notification = get_transient( 'action_scheduler_admin_notice' );
|
||||
|
||||
if ( is_array( $notification ) ) {
|
||||
delete_transient( 'action_scheduler_admin_notice' );
|
||||
|
||||
$action = $this->store->fetch_action( $notification['action_id'] );
|
||||
$action_hook_html = '<strong><code>' . $action->get_hook() . '</code></strong>';
|
||||
if ( 1 == $notification['success'] ) {
|
||||
$class = 'updated';
|
||||
switch ( $notification['row_action_type'] ) {
|
||||
case 'run' :
|
||||
$action_message_html = sprintf( __( 'Successfully executed action: %s', 'woocommerce' ), $action_hook_html );
|
||||
break;
|
||||
case 'cancel' :
|
||||
$action_message_html = sprintf( __( 'Successfully canceled action: %s', 'woocommerce' ), $action_hook_html );
|
||||
break;
|
||||
default :
|
||||
$action_message_html = sprintf( __( 'Successfully processed change for action: %s', 'woocommerce' ), $action_hook_html );
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$class = 'error';
|
||||
$action_message_html = sprintf( __( 'Could not process change for action: "%s" (ID: %d). Error: %s', 'woocommerce' ), $action_hook_html, esc_html( $notification['action_id'] ), esc_html( $notification['error_message'] ) );
|
||||
}
|
||||
|
||||
$action_message_html = apply_filters( 'action_scheduler_admin_notice_html', $action_message_html, $action, $notification );
|
||||
|
||||
$this->admin_notices[] = array(
|
||||
'class' => $class,
|
||||
'message' => $action_message_html,
|
||||
);
|
||||
}
|
||||
|
||||
parent::display_admin_notices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the scheduled date in a human friendly format.
|
||||
*
|
||||
* @param array $row The array representation of the current row of the table
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_schedule( $row ) {
|
||||
return $this->get_schedule_display_string( $row['schedule'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scheduled date in a human friendly format.
|
||||
*
|
||||
* @param ActionScheduler_Schedule $schedule
|
||||
* @return string
|
||||
*/
|
||||
protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) {
|
||||
|
||||
$schedule_display_string = '';
|
||||
|
||||
if ( ! $schedule->next() ) {
|
||||
return $schedule_display_string;
|
||||
}
|
||||
|
||||
$next_timestamp = $schedule->next()->getTimestamp();
|
||||
|
||||
$schedule_display_string .= $schedule->next()->format( 'Y-m-d H:i:s O' );
|
||||
$schedule_display_string .= '<br/>';
|
||||
|
||||
if ( gmdate( 'U' ) > $next_timestamp ) {
|
||||
$schedule_display_string .= sprintf( __( ' (%s ago)', 'woocommerce' ), self::human_interval( gmdate( 'U' ) - $next_timestamp ) );
|
||||
} else {
|
||||
$schedule_display_string .= sprintf( __( ' (%s)', 'woocommerce' ), self::human_interval( $next_timestamp - gmdate( 'U' ) ) );
|
||||
}
|
||||
|
||||
return $schedule_display_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk delete
|
||||
*
|
||||
* Deletes actions based on their ID. This is the handler for the bulk delete. It assumes the data
|
||||
* properly validated by the callee and it will delete the actions without any extra validation.
|
||||
*
|
||||
* @param array $ids
|
||||
* @param string $ids_sql Inherited and unused
|
||||
*/
|
||||
protected function bulk_delete( array $ids, $ids_sql ) {
|
||||
foreach ( $ids as $id ) {
|
||||
$this->store->delete_action( $id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
|
||||
* parameters are valid.
|
||||
*
|
||||
* @param int $action_id
|
||||
*/
|
||||
protected function row_action_cancel( $action_id ) {
|
||||
$this->process_row_action( $action_id, 'cancel' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
|
||||
* parameters are valid.
|
||||
*
|
||||
* @param int $action_id
|
||||
*/
|
||||
protected function row_action_run( $action_id ) {
|
||||
$this->process_row_action( $action_id, 'run' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the logic behind processing an action once an action link is clicked on the list table.
|
||||
*
|
||||
* @param int $action_id
|
||||
* @param string $row_action_type The type of action to perform on the action.
|
||||
*/
|
||||
protected function process_row_action( $action_id, $row_action_type ) {
|
||||
try {
|
||||
switch ( $row_action_type ) {
|
||||
case 'run' :
|
||||
$this->runner->process_action( $action_id );
|
||||
break;
|
||||
case 'cancel' :
|
||||
$this->store->cancel_action( $action_id );
|
||||
break;
|
||||
}
|
||||
$success = 1;
|
||||
$error_message = '';
|
||||
} catch ( Exception $e ) {
|
||||
$success = 0;
|
||||
$error_message = $e->getMessage();
|
||||
}
|
||||
|
||||
set_transient( 'action_scheduler_admin_notice', compact( 'action_id', 'success', 'error_message', 'row_action_type' ), 30 );
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function prepare_items() {
|
||||
$this->process_bulk_action();
|
||||
|
||||
$this->process_row_actions();
|
||||
|
||||
if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) {
|
||||
// _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
|
||||
wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->prepare_column_headers();
|
||||
|
||||
$per_page = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page );
|
||||
$query = array(
|
||||
'per_page' => $per_page,
|
||||
'offset' => $this->get_items_offset(),
|
||||
'status' => $this->get_request_status(),
|
||||
'orderby' => $this->get_request_orderby(),
|
||||
'order' => $this->get_request_order(),
|
||||
'search' => $this->get_request_search_query(),
|
||||
);
|
||||
|
||||
$this->items = array();
|
||||
|
||||
$total_items = $this->store->query_actions( $query, 'count' );
|
||||
|
||||
$status_labels = $this->store->get_status_labels();
|
||||
|
||||
foreach ( $this->store->query_actions( $query ) as $action_id ) {
|
||||
try {
|
||||
$action = $this->store->fetch_action( $action_id );
|
||||
} catch ( Exception $e ) {
|
||||
continue;
|
||||
}
|
||||
$this->items[ $action_id ] = array(
|
||||
'ID' => $action_id,
|
||||
'hook' => $action->get_hook(),
|
||||
'status' => $status_labels[ $this->store->get_status( $action_id ) ],
|
||||
'args' => $action->get_args(),
|
||||
'group' => $action->get_group(),
|
||||
'log_entries' => $this->logger->get_logs( $action_id ),
|
||||
'claim_id' => $this->store->get_claim_id( $action_id ),
|
||||
'recurrence' => $this->get_recurrence( $action ),
|
||||
'schedule' => $action->get_schedule(),
|
||||
);
|
||||
}
|
||||
|
||||
$this->set_pagination_args( array(
|
||||
'total_items' => $total_items,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => ceil( $total_items / $per_page ),
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the available statuses so the user can click to filter.
|
||||
*/
|
||||
protected function display_filter_by_status() {
|
||||
$this->status_counts = $this->store->action_counts();
|
||||
parent::display_filter_by_status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text to display in the search box on the list table.
|
||||
*/
|
||||
protected function get_search_box_button_text() {
|
||||
return __( 'Search hook, args and claim ID', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_LogEntry
|
||||
*/
|
||||
class ActionScheduler_LogEntry {
|
||||
|
||||
/**
|
||||
* @var int $action_id
|
||||
*/
|
||||
protected $action_id = '';
|
||||
|
||||
/**
|
||||
* @var string $message
|
||||
*/
|
||||
protected $message = '';
|
||||
|
||||
/**
|
||||
* @var Datetime $date
|
||||
*/
|
||||
protected $date;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param mixed $action_id Action ID
|
||||
* @param string $message Message
|
||||
* @param Datetime $date Datetime object with the time when this log entry was created. If this parameter is
|
||||
* not provided a new Datetime object (with current time) will be created.
|
||||
*/
|
||||
public function __construct( $action_id, $message, $date = null ) {
|
||||
|
||||
/*
|
||||
* ActionScheduler_wpCommentLogger::get_entry() previously passed a 3rd param of $comment->comment_type
|
||||
* to ActionScheduler_LogEntry::__construct(), goodness knows why, and the Follow-up Emails plugin
|
||||
* hard-codes loading its own version of ActionScheduler_wpCommentLogger with that out-dated method,
|
||||
* goodness knows why, so we need to guard against that here instead of using a DateTime type declaration
|
||||
* for the constructor's 3rd param of $date and causing a fatal error with older versions of FUE.
|
||||
*/
|
||||
if ( null !== $date && ! is_a( $date, 'DateTime' ) ) {
|
||||
_doing_it_wrong( __METHOD__, 'The third parameter must be a valid DateTime instance, or null.', '2.0.0' );
|
||||
$date = null;
|
||||
}
|
||||
|
||||
$this->action_id = $action_id;
|
||||
$this->message = $message;
|
||||
$this->date = $date ? $date : new Datetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the date when this log entry was created
|
||||
*
|
||||
* @return Datetime
|
||||
*/
|
||||
public function get_date() {
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function get_action_id() {
|
||||
return $this->action_id;
|
||||
}
|
||||
|
||||
public function get_message() {
|
||||
return $this->message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_Logger
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
abstract class ActionScheduler_Logger {
|
||||
private static $logger = NULL;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
* @param string $message
|
||||
* @param DateTime $date
|
||||
*
|
||||
* @return string The log entry ID
|
||||
*/
|
||||
abstract public function log( $action_id, $message, DateTime $date = NULL );
|
||||
|
||||
/**
|
||||
* @param string $entry_id
|
||||
*
|
||||
* @return ActionScheduler_LogEntry
|
||||
*/
|
||||
abstract public function get_entry( $entry_id );
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*
|
||||
* @return ActionScheduler_LogEntry[]
|
||||
*/
|
||||
abstract public function get_logs( $action_id );
|
||||
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ), 10, 1 );
|
||||
add_action( 'action_scheduler_canceled_action', array( $this, 'log_canceled_action' ), 10, 1 );
|
||||
add_action( 'action_scheduler_before_execute', array( $this, 'log_started_action' ), 10, 1 );
|
||||
add_action( 'action_scheduler_after_execute', array( $this, 'log_completed_action' ), 10, 1 );
|
||||
add_action( 'action_scheduler_failed_execution', array( $this, 'log_failed_action' ), 10, 2 );
|
||||
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, 1 );
|
||||
add_action( 'action_scheduler_failed_fetch_action', array( $this, 'log_failed_fetch_action' ), 10, 1 );
|
||||
}
|
||||
|
||||
public function log_stored_action( $action_id ) {
|
||||
$this->log( $action_id, __( 'action created', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
public function log_canceled_action( $action_id ) {
|
||||
$this->log( $action_id, __( 'action canceled', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
public function log_started_action( $action_id ) {
|
||||
$this->log( $action_id, __( 'action started', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
public function log_completed_action( $action_id ) {
|
||||
$this->log( $action_id, __( 'action complete', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
public function log_failed_action( $action_id, Exception $exception ) {
|
||||
$this->log( $action_id, sprintf( __( 'action failed: %s', 'woocommerce' ), $exception->getMessage() ) );
|
||||
}
|
||||
|
||||
public function log_timed_out_action( $action_id, $timeout ) {
|
||||
$this->log( $action_id, sprintf( __( 'action timed out after %s seconds', 'woocommerce' ), $timeout ) );
|
||||
}
|
||||
|
||||
public function log_unexpected_shutdown( $action_id, $error ) {
|
||||
if ( ! empty( $error ) ) {
|
||||
$this->log( $action_id, sprintf( __( 'unexpected shutdown: PHP Fatal error %s in %s on line %s', 'woocommerce' ), $error['message'], $error['file'], $error['line'] ) );
|
||||
}
|
||||
}
|
||||
|
||||
public function log_reset_action( $action_id ) {
|
||||
$this->log( $action_id, __( 'action reset', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
public function log_ignored_action( $action_id ) {
|
||||
$this->log( $action_id, __( 'action ignored', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
public function log_failed_fetch_action( $action_id ) {
|
||||
$this->log( $action_id, __( 'There was a failure fetching this action', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_NullAction
|
||||
*/
|
||||
class ActionScheduler_NullAction extends ActionScheduler_Action {
|
||||
|
||||
public function __construct( $hook = '', array $args = array(), ActionScheduler_Schedule $schedule = NULL ) {
|
||||
$this->set_schedule( new ActionScheduler_NullSchedule() );
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
// don't execute
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_NullLogEntry
|
||||
*/
|
||||
class ActionScheduler_NullLogEntry extends ActionScheduler_LogEntry {
|
||||
public function __construct( $action_id = '', $message = '' ) {
|
||||
// nothing to see here
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_NullSchedule
|
||||
*/
|
||||
class ActionScheduler_NullSchedule implements ActionScheduler_Schedule {
|
||||
|
||||
public function next( DateTime $after = NULL ) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function is_recurring() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_QueueCleaner
|
||||
*/
|
||||
class ActionScheduler_QueueCleaner {
|
||||
|
||||
/** @var int */
|
||||
protected $batch_size;
|
||||
|
||||
/** @var ActionScheduler_Store */
|
||||
private $store = null;
|
||||
|
||||
/**
|
||||
* 31 days in seconds.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $month_in_seconds = 2678400;
|
||||
|
||||
/**
|
||||
* ActionScheduler_QueueCleaner constructor.
|
||||
*
|
||||
* @param ActionScheduler_Store $store The store instance.
|
||||
* @param int $batch_size The batch size.
|
||||
*/
|
||||
public function __construct( ActionScheduler_Store $store = null, $batch_size = 20 ) {
|
||||
$this->store = $store ? $store : ActionScheduler_Store::instance();
|
||||
$this->batch_size = $batch_size;
|
||||
}
|
||||
|
||||
public function delete_old_actions() {
|
||||
$lifespan = apply_filters( 'action_scheduler_retention_period', $this->month_in_seconds );
|
||||
$cutoff = as_get_datetime_object($lifespan.' seconds ago');
|
||||
|
||||
$statuses_to_purge = array(
|
||||
ActionScheduler_Store::STATUS_COMPLETE,
|
||||
ActionScheduler_Store::STATUS_CANCELED,
|
||||
);
|
||||
|
||||
foreach ( $statuses_to_purge as $status ) {
|
||||
$actions_to_delete = $this->store->query_actions( array(
|
||||
'status' => $status,
|
||||
'modified' => $cutoff,
|
||||
'modified_compare' => '<=',
|
||||
'per_page' => $this->get_batch_size(),
|
||||
) );
|
||||
|
||||
foreach ( $actions_to_delete as $action_id ) {
|
||||
try {
|
||||
$this->store->delete_action( $action_id );
|
||||
} catch ( Exception $e ) {
|
||||
|
||||
/**
|
||||
* Notify 3rd party code of exceptions when deleting a completed action older than the retention period
|
||||
*
|
||||
* This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their
|
||||
* actions.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @param int $action_id The scheduled actions ID in the data store
|
||||
* @param Exception $e The exception thrown when attempting to delete the action from the data store
|
||||
* @param int $lifespan The retention period, in seconds, for old actions
|
||||
* @param int $count_of_actions_to_delete The number of old actions being deleted in this batch
|
||||
*/
|
||||
do_action( 'action_scheduler_failed_old_action_deletion', $action_id, $e, $lifespan, count( $actions_to_delete ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unclaim pending actions that have not been run within a given time limit.
|
||||
*
|
||||
* When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
|
||||
* as a parameter is 10x the time limit used for queue processing.
|
||||
*
|
||||
* @param int $time_limit The number of seconds to allow a queue to run before unclaiming its pending actions. Default 300 (5 minutes).
|
||||
*/
|
||||
public function reset_timeouts( $time_limit = 300 ) {
|
||||
$timeout = apply_filters( 'action_scheduler_timeout_period', $time_limit );
|
||||
if ( $timeout < 0 ) {
|
||||
return;
|
||||
}
|
||||
$cutoff = as_get_datetime_object($timeout.' seconds ago');
|
||||
$actions_to_reset = $this->store->query_actions( array(
|
||||
'status' => ActionScheduler_Store::STATUS_PENDING,
|
||||
'modified' => $cutoff,
|
||||
'modified_compare' => '<=',
|
||||
'claimed' => true,
|
||||
'per_page' => $this->get_batch_size(),
|
||||
) );
|
||||
|
||||
foreach ( $actions_to_reset as $action_id ) {
|
||||
$this->store->unclaim_action( $action_id );
|
||||
do_action( 'action_scheduler_reset_action', $action_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark actions that have been running for more than a given time limit as failed, based on
|
||||
* the assumption some uncatachable and unloggable fatal error occurred during processing.
|
||||
*
|
||||
* When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
|
||||
* as a parameter is 10x the time limit used for queue processing.
|
||||
*
|
||||
* @param int $time_limit The number of seconds to allow an action to run before it is considered to have failed. Default 300 (5 minutes).
|
||||
*/
|
||||
public function mark_failures( $time_limit = 300 ) {
|
||||
$timeout = apply_filters( 'action_scheduler_failure_period', $time_limit );
|
||||
if ( $timeout < 0 ) {
|
||||
return;
|
||||
}
|
||||
$cutoff = as_get_datetime_object($timeout.' seconds ago');
|
||||
$actions_to_reset = $this->store->query_actions( array(
|
||||
'status' => ActionScheduler_Store::STATUS_RUNNING,
|
||||
'modified' => $cutoff,
|
||||
'modified_compare' => '<=',
|
||||
'per_page' => $this->get_batch_size(),
|
||||
) );
|
||||
|
||||
foreach ( $actions_to_reset as $action_id ) {
|
||||
$this->store->mark_failure( $action_id );
|
||||
do_action( 'action_scheduler_failed_action', $action_id, $timeout );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do all of the cleaning actions.
|
||||
*
|
||||
* @param int $time_limit The number of seconds to use as the timeout and failure period. Default 300 (5 minutes).
|
||||
* @author Jeremy Pry
|
||||
*/
|
||||
public function clean( $time_limit = 300 ) {
|
||||
$this->delete_old_actions();
|
||||
$this->reset_timeouts( $time_limit );
|
||||
$this->mark_failures( $time_limit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the batch size for cleaning the queue.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
* @return int
|
||||
*/
|
||||
protected function get_batch_size() {
|
||||
/**
|
||||
* Filter the batch size when cleaning the queue.
|
||||
*
|
||||
* @param int $batch_size The number of actions to clean in one batch.
|
||||
*/
|
||||
return absint( apply_filters( 'action_scheduler_cleanup_batch_size', $this->batch_size ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_QueueRunner
|
||||
*/
|
||||
class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner {
|
||||
const WP_CRON_HOOK = 'action_scheduler_run_queue';
|
||||
|
||||
const WP_CRON_SCHEDULE = 'every_minute';
|
||||
|
||||
/** @var ActionScheduler_QueueRunner */
|
||||
private static $runner = null;
|
||||
|
||||
/**
|
||||
* @return ActionScheduler_QueueRunner
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function instance() {
|
||||
if ( empty(self::$runner) ) {
|
||||
$class = apply_filters('action_scheduler_queue_runner_class', 'ActionScheduler_QueueRunner');
|
||||
self::$runner = new $class();
|
||||
}
|
||||
return self::$runner;
|
||||
}
|
||||
|
||||
/**
|
||||
* ActionScheduler_QueueRunner constructor.
|
||||
*
|
||||
* @param ActionScheduler_Store $store
|
||||
* @param ActionScheduler_FatalErrorMonitor $monitor
|
||||
* @param ActionScheduler_QueueCleaner $cleaner
|
||||
*/
|
||||
public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) {
|
||||
parent::__construct( $store, $monitor, $cleaner );
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function init() {
|
||||
|
||||
add_filter( 'cron_schedules', array( self::instance(), 'add_wp_cron_schedule' ) );
|
||||
|
||||
if ( !wp_next_scheduled(self::WP_CRON_HOOK) ) {
|
||||
$schedule = apply_filters( 'action_scheduler_run_schedule', self::WP_CRON_SCHEDULE );
|
||||
wp_schedule_event( time(), $schedule, self::WP_CRON_HOOK );
|
||||
}
|
||||
|
||||
add_action( self::WP_CRON_HOOK, array( self::instance(), 'run' ) );
|
||||
}
|
||||
|
||||
public function run() {
|
||||
ActionScheduler_Compatibility::raise_memory_limit();
|
||||
ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() );
|
||||
do_action( 'action_scheduler_before_process_queue' );
|
||||
$this->run_cleanup();
|
||||
$processed_actions = 0;
|
||||
if ( $this->store->get_claim_count() < $this->get_allowed_concurrent_batches() ) {
|
||||
$batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 );
|
||||
do {
|
||||
$processed_actions_in_batch = $this->do_batch( $batch_size );
|
||||
$processed_actions += $processed_actions_in_batch;
|
||||
} while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $processed_actions ) ); // keep going until we run out of actions, time, or memory
|
||||
}
|
||||
|
||||
do_action( 'action_scheduler_after_process_queue' );
|
||||
return $processed_actions;
|
||||
}
|
||||
|
||||
protected function do_batch( $size = 100 ) {
|
||||
$claim = $this->store->stake_claim($size);
|
||||
$this->monitor->attach($claim);
|
||||
$processed_actions = 0;
|
||||
|
||||
foreach ( $claim->get_actions() as $action_id ) {
|
||||
// bail if we lost the claim
|
||||
if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $claim->get_id() ) ) ) {
|
||||
break;
|
||||
}
|
||||
$this->process_action( $action_id );
|
||||
$processed_actions++;
|
||||
|
||||
if ( $this->batch_limits_exceeded( $processed_actions ) ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->store->release_claim($claim);
|
||||
$this->monitor->detach();
|
||||
$this->clear_caches();
|
||||
return $processed_actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Running large batches can eat up memory, as WP adds data to its object cache.
|
||||
*
|
||||
* If using a persistent object store, this has the side effect of flushing that
|
||||
* as well, so this is disabled by default. To enable:
|
||||
*
|
||||
* add_filter( 'action_scheduler_queue_runner_flush_cache', '__return_true' );
|
||||
*/
|
||||
protected function clear_caches() {
|
||||
if ( ! wp_using_ext_object_cache() || apply_filters( 'action_scheduler_queue_runner_flush_cache', false ) ) {
|
||||
wp_cache_flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function add_wp_cron_schedule( $schedules ) {
|
||||
$schedules['every_minute'] = array(
|
||||
'interval' => 60, // in seconds
|
||||
'display' => __( 'Every minute', 'woocommerce' ),
|
||||
);
|
||||
|
||||
return $schedules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_Schedule
|
||||
*/
|
||||
interface ActionScheduler_Schedule {
|
||||
/**
|
||||
* @param DateTime $after
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function next( DateTime $after = NULL );
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function is_recurring();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_SimpleSchedule
|
||||
*/
|
||||
class ActionScheduler_SimpleSchedule implements ActionScheduler_Schedule {
|
||||
private $date = NULL;
|
||||
private $timestamp = 0;
|
||||
public function __construct( DateTime $date ) {
|
||||
$this->date = clone $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $after
|
||||
*
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function next( DateTime $after = NULL ) {
|
||||
$after = empty($after) ? as_get_datetime_object('@0') : $after;
|
||||
return ( $after > $this->date ) ? NULL : clone $this->date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function is_recurring() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* For PHP 5.2 compat, since DateTime objects can't be serialized
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep() {
|
||||
$this->timestamp = $this->date->getTimestamp();
|
||||
return array(
|
||||
'timestamp',
|
||||
);
|
||||
}
|
||||
|
||||
public function __wakeup() {
|
||||
$this->date = as_get_datetime_object($this->timestamp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_Store
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
abstract class ActionScheduler_Store {
|
||||
const STATUS_COMPLETE = 'complete';
|
||||
const STATUS_PENDING = 'pending';
|
||||
const STATUS_RUNNING = 'in-progress';
|
||||
const STATUS_FAILED = 'failed';
|
||||
const STATUS_CANCELED = 'canceled';
|
||||
|
||||
/** @var ActionScheduler_Store */
|
||||
private static $store = NULL;
|
||||
|
||||
/**
|
||||
* @param ActionScheduler_Action $action
|
||||
* @param DateTime $scheduled_date Optional Date of the first instance
|
||||
* to store. Otherwise uses the first date of the action's
|
||||
* schedule.
|
||||
*
|
||||
* @return string The action ID
|
||||
*/
|
||||
abstract public function save_action( ActionScheduler_Action $action, DateTime $scheduled_date = NULL );
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*
|
||||
* @return ActionScheduler_Action
|
||||
*/
|
||||
abstract public function fetch_action( $action_id );
|
||||
|
||||
/**
|
||||
* @param string $hook
|
||||
* @param array $params
|
||||
* @return string ID of the next action matching the criteria
|
||||
*/
|
||||
abstract public function find_action( $hook, $params = array() );
|
||||
|
||||
/**
|
||||
* @param array $query
|
||||
* @return array The IDs of actions matching the query
|
||||
*/
|
||||
abstract public function query_actions( $query = array() );
|
||||
|
||||
/**
|
||||
* Get a count of all actions in the store, grouped by status
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract public function action_counts();
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*/
|
||||
abstract public function cancel_action( $action_id );
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*/
|
||||
abstract public function delete_action( $action_id );
|
||||
|
||||
/**
|
||||
* @param string $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 );
|
||||
|
||||
|
||||
/**
|
||||
* @param int $max_actions
|
||||
* @param DateTime $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 = '' );
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
abstract public function get_claim_count();
|
||||
|
||||
/**
|
||||
* @param ActionScheduler_ActionClaim $claim
|
||||
*/
|
||||
abstract public function release_claim( ActionScheduler_ActionClaim $claim );
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*/
|
||||
abstract public function unclaim_action( $action_id );
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*/
|
||||
abstract public function mark_failure( $action_id );
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*/
|
||||
abstract public function log_execution( $action_id );
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*/
|
||||
abstract public function mark_complete( $action_id );
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function get_status( $action_id );
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function get_claim_id( $action_id );
|
||||
|
||||
/**
|
||||
* @param string $claim_id
|
||||
* @return array
|
||||
*/
|
||||
abstract public function find_actions_by_claim_id( $claim_id );
|
||||
|
||||
/**
|
||||
* @param string $comparison_operator
|
||||
* @return string
|
||||
*/
|
||||
protected function validate_sql_comparator( $comparison_operator ) {
|
||||
if ( in_array( $comparison_operator, array('!=', '>', '>=', '<', '<=', '=') ) ) {
|
||||
return $comparison_operator;
|
||||
}
|
||||
return '=';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time MySQL formated date/time string for an action's (next) scheduled date.
|
||||
*
|
||||
* @param ActionScheduler_Action $action
|
||||
* @param DateTime $scheduled_date (optional)
|
||||
* @return string
|
||||
*/
|
||||
protected function get_scheduled_date_string( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) {
|
||||
$next = null === $scheduled_date ? $action->get_schedule()->next() : $scheduled_date;
|
||||
if ( ! $next ) {
|
||||
throw new InvalidArgumentException( __( 'Invalid schedule. Cannot save action.', 'woocommerce' ) );
|
||||
}
|
||||
$next->setTimezone( new DateTimeZone( 'UTC' ) );
|
||||
|
||||
return $next->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time MySQL formated date/time string for an action's (next) scheduled date.
|
||||
*
|
||||
* @param ActionScheduler_Action $action
|
||||
* @param DateTime $scheduled_date (optional)
|
||||
* @return string
|
||||
*/
|
||||
protected function get_scheduled_date_string_local( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) {
|
||||
$next = null === $scheduled_date ? $action->get_schedule()->next() : $scheduled_date;
|
||||
if ( ! $next ) {
|
||||
throw new InvalidArgumentException( __( 'Invalid schedule. Cannot save action.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
ActionScheduler_TimezoneHelper::set_local_timezone( $next );
|
||||
return $next->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function get_status_labels() {
|
||||
return array(
|
||||
self::STATUS_COMPLETE => __( 'Complete', 'woocommerce' ),
|
||||
self::STATUS_PENDING => __( 'Pending', 'woocommerce' ),
|
||||
self::STATUS_RUNNING => __( 'In-progress', 'woocommerce' ),
|
||||
self::STATUS_FAILED => __( 'Failed', 'woocommerce' ),
|
||||
self::STATUS_CANCELED => __( 'Canceled', 'woocommerce' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function init() {}
|
||||
|
||||
/**
|
||||
* @return ActionScheduler_Store
|
||||
*/
|
||||
public static function instance() {
|
||||
if ( empty(self::$store) ) {
|
||||
$class = apply_filters('action_scheduler_store_class', 'ActionScheduler_wpPostStore');
|
||||
self::$store = new $class();
|
||||
}
|
||||
return self::$store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site's local time.
|
||||
*
|
||||
* @deprecated 2.1.0
|
||||
* @return DateTimeZone
|
||||
*/
|
||||
protected function get_local_timezone() {
|
||||
_deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' );
|
||||
return ActionScheduler_TimezoneHelper::get_local_timezone();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_TimezoneHelper
|
||||
*/
|
||||
abstract class ActionScheduler_TimezoneHelper {
|
||||
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
|
||||
* @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
|
||||
* @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 ) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = get_option('gmt_offset');
|
||||
if ( $gmt_offset == 0 ) {
|
||||
$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' );
|
||||
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 ( 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_Versions
|
||||
*/
|
||||
class ActionScheduler_Versions {
|
||||
/**
|
||||
* @var ActionScheduler_Versions
|
||||
*/
|
||||
private static $instance = NULL;
|
||||
|
||||
private $versions = array();
|
||||
|
||||
public function register( $version_string, $initialization_callback ) {
|
||||
if ( isset($this->versions[$version_string]) ) {
|
||||
return FALSE;
|
||||
}
|
||||
$this->versions[$version_string] = $initialization_callback;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function get_versions() {
|
||||
return $this->versions;
|
||||
}
|
||||
|
||||
public function latest_version() {
|
||||
$keys = array_keys($this->versions);
|
||||
if ( empty($keys) ) {
|
||||
return false;
|
||||
}
|
||||
uasort( $keys, 'version_compare' );
|
||||
return end($keys);
|
||||
}
|
||||
|
||||
public function latest_version_callback() {
|
||||
$latest = $this->latest_version();
|
||||
if ( empty($latest) || !isset($this->versions[$latest]) ) {
|
||||
return '__return_null';
|
||||
}
|
||||
return $this->versions[$latest];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ActionScheduler_Versions
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function instance() {
|
||||
if ( empty(self::$instance) ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function initialize_latest_version() {
|
||||
$self = self::instance();
|
||||
call_user_func($self->latest_version_callback());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* WP CLI Queue runner.
|
||||
*
|
||||
* This class can only be called from within a WP CLI instance.
|
||||
*/
|
||||
class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRunner {
|
||||
|
||||
/** @var array */
|
||||
protected $actions;
|
||||
|
||||
/** @var ActionScheduler_ActionClaim */
|
||||
protected $claim;
|
||||
|
||||
/** @var \cli\progress\Bar */
|
||||
protected $progress_bar;
|
||||
|
||||
/**
|
||||
* ActionScheduler_WPCLI_QueueRunner constructor.
|
||||
*
|
||||
* @param ActionScheduler_Store $store
|
||||
* @param ActionScheduler_FatalErrorMonitor $monitor
|
||||
* @param ActionScheduler_QueueCleaner $cleaner
|
||||
*
|
||||
* @throws Exception When this is not run within WP CLI
|
||||
*/
|
||||
public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) {
|
||||
if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) {
|
||||
/* translators: %s php class name */
|
||||
throw new Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'woocommerce' ), __CLASS__ ) );
|
||||
}
|
||||
|
||||
parent::__construct( $store, $monitor, $cleaner );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the Queue before processing.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*
|
||||
* @param int $batch_size The batch size to process.
|
||||
* @param array $hooks The hooks being used to filter the actions claimed in this batch.
|
||||
* @param string $group The group of actions to claim with this batch.
|
||||
* @param bool $force Whether to force running even with too many concurrent processes.
|
||||
*
|
||||
* @return int The number of actions that will be run.
|
||||
* @throws \WP_CLI\ExitException When there are too many concurrent batches.
|
||||
*/
|
||||
public function setup( $batch_size, $hooks = array(), $group = '', $force = false ) {
|
||||
$this->run_cleanup();
|
||||
$this->add_hooks();
|
||||
|
||||
// Check to make sure there aren't too many concurrent processes running.
|
||||
$claim_count = $this->store->get_claim_count();
|
||||
$too_many = $claim_count >= $this->get_allowed_concurrent_batches();
|
||||
if ( $too_many ) {
|
||||
if ( $force ) {
|
||||
WP_CLI::warning( __( 'There are too many concurrent batches, but the run is forced to continue.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::error( __( 'There are too many concurrent batches.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Stake a claim and store it.
|
||||
$this->claim = $this->store->stake_claim( $batch_size, null, $hooks, $group );
|
||||
$this->monitor->attach( $this->claim );
|
||||
$this->actions = $this->claim->get_actions();
|
||||
|
||||
return count( $this->actions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add our hooks to the appropriate actions.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*/
|
||||
protected function add_hooks() {
|
||||
add_action( 'action_scheduler_before_execute', array( $this, 'before_execute' ) );
|
||||
add_action( 'action_scheduler_after_execute', array( $this, 'after_execute' ), 10, 2 );
|
||||
add_action( 'action_scheduler_failed_execution', array( $this, 'action_failed' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the WP CLI progress bar.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*/
|
||||
protected function setup_progress_bar() {
|
||||
$count = count( $this->actions );
|
||||
$this->progress_bar = \WP_CLI\Utils\make_progress_bar(
|
||||
sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'woocommerce' ), number_format_i18n( $count ) ),
|
||||
$count
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process actions in the queue.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
* @return int The number of actions processed.
|
||||
*/
|
||||
public function run() {
|
||||
do_action( 'action_scheduler_before_process_queue' );
|
||||
$this->setup_progress_bar();
|
||||
foreach ( $this->actions as $action_id ) {
|
||||
// Error if we lost the claim.
|
||||
if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $this->claim->get_id() ) ) ) {
|
||||
WP_CLI::warning( __( 'The claim has been lost. Aborting current batch.', 'woocommerce' ) );
|
||||
break;
|
||||
}
|
||||
|
||||
$this->process_action( $action_id );
|
||||
$this->progress_bar->tick();
|
||||
$this->maybe_stop_the_insanity();
|
||||
}
|
||||
|
||||
$completed = $this->progress_bar->current();
|
||||
$this->progress_bar->finish();
|
||||
$this->store->release_claim( $this->claim );
|
||||
do_action( 'action_scheduler_after_process_queue' );
|
||||
|
||||
return $completed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle WP CLI message when the action is starting.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*
|
||||
* @param $action_id
|
||||
*/
|
||||
public function before_execute( $action_id ) {
|
||||
/* translators: %s refers to the action ID */
|
||||
WP_CLI::log( sprintf( __( 'Started processing action %s', 'woocommerce' ), $action_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle WP CLI message when the action has completed.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*
|
||||
* @param int $action_id
|
||||
* @param null|ActionScheduler_Action $action The instance of the action. Default to null for backward compatibility.
|
||||
*/
|
||||
public function after_execute( $action_id, $action = null ) {
|
||||
// backward compatibility
|
||||
if ( null === $action ) {
|
||||
$action = $this->store->fetch_action( $action_id );
|
||||
}
|
||||
/* translators: %s refers to the action ID */
|
||||
WP_CLI::log( sprintf( __( 'Completed processing action %s with hook: %s', 'woocommerce' ), $action_id, $action->get_hook() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle WP CLI message when the action has failed.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*
|
||||
* @param int $action_id
|
||||
* @param Exception $exception
|
||||
* @throws \WP_CLI\ExitException With failure message.
|
||||
*/
|
||||
public function action_failed( $action_id, $exception ) {
|
||||
WP_CLI::error(
|
||||
/* translators: %1$s refers to the action ID, %2$s refers to the Exception message */
|
||||
sprintf( __( 'Error processing action %1$s: %2$s', 'woocommerce' ), $action_id, $exception->getMessage() ),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep and help avoid hitting memory limit
|
||||
*
|
||||
* @param int $sleep_time Amount of seconds to sleep
|
||||
*/
|
||||
protected function stop_the_insanity( $sleep_time = 0 ) {
|
||||
if ( 0 < $sleep_time ) {
|
||||
/* translators: 1: sleep time 2: time unit */
|
||||
WP_CLI::warning( sprintf( __( 'Stopped the insanity for %$1d %$2s', 'woocommerce' ), $sleep_time, _n( 'second', 'seconds', $sleep_time, 'woocommerce' ) ) );
|
||||
sleep( $sleep_time );
|
||||
}
|
||||
|
||||
WP_CLI::warning( __( 'Attempting to reduce used memory...', 'woocommerce' ) );
|
||||
|
||||
/**
|
||||
* @var $wpdb \wpdb
|
||||
* @var $wp_object_cache \WP_Object_Cache
|
||||
*/
|
||||
global $wpdb, $wp_object_cache;
|
||||
|
||||
$wpdb->queries = array();
|
||||
|
||||
if ( ! is_object( $wp_object_cache ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wp_object_cache->group_ops = array();
|
||||
$wp_object_cache->stats = array();
|
||||
$wp_object_cache->memcache_debug = array();
|
||||
$wp_object_cache->cache = array();
|
||||
|
||||
if ( is_callable( array( $wp_object_cache, '__remoteset' ) ) ) {
|
||||
call_user_func( array( $wp_object_cache, '__remoteset' ) ); // important
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe trigger the stop_the_insanity() method to free up memory.
|
||||
*/
|
||||
protected function maybe_stop_the_insanity() {
|
||||
// The value returned by progress_bar->current() might be padded. Remove padding, and convert to int.
|
||||
$current_iteration = intval( trim( $this->progress_bar->current() ) );
|
||||
if ( 0 === $current_iteration % 50 ) {
|
||||
$this->stop_the_insanity();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Commands for the Action Scheduler by Prospress.
|
||||
*/
|
||||
class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Run the Action Scheduler
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--batch-size=<size>]
|
||||
* : The maximum number of actions to run. Defaults to 100.
|
||||
*
|
||||
* [--batches=<size>]
|
||||
* : Limit execution to a number of batches. Defaults to 0, meaning batches will continue being executed until all actions are complete.
|
||||
*
|
||||
* [--cleanup-batch-size=<size>]
|
||||
* : The maximum number of actions to clean up. Defaults to the value of --batch-size.
|
||||
*
|
||||
* [--hooks=<hooks>]
|
||||
* : Only run actions with the specified hook. Omitting this option runs actions with any hook. Define multiple hooks as a comma separated string (without spaces), e.g. `--hooks=hook_one,hook_two,hook_three`
|
||||
*
|
||||
* [--group=<group>]
|
||||
* : Only run actions from the specified group. Omitting this option runs actions from all groups.
|
||||
*
|
||||
* [--force]
|
||||
* : Whether to force execution despite the maximum number of concurrent processes being exceeded.
|
||||
*
|
||||
* @param array $args Positional arguments.
|
||||
* @param array $assoc_args Keyed arguments.
|
||||
* @throws \WP_CLI\ExitException When an error occurs.
|
||||
*/
|
||||
public function run( $args, $assoc_args ) {
|
||||
// Handle passed arguments.
|
||||
$batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) );
|
||||
$batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) );
|
||||
$clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) );
|
||||
$hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) );
|
||||
$hooks = array_filter( array_map( 'trim', $hooks ) );
|
||||
$group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' );
|
||||
$force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false );
|
||||
|
||||
$batches_completed = 0;
|
||||
$actions_completed = 0;
|
||||
$unlimited = $batches === 0;
|
||||
|
||||
try {
|
||||
// Custom queue cleaner instance.
|
||||
$cleaner = new ActionScheduler_QueueCleaner( null, $clean );
|
||||
|
||||
// Get the queue runner instance
|
||||
$runner = new ActionScheduler_WPCLI_QueueRunner( null, null, $cleaner );
|
||||
|
||||
// Determine how many tasks will be run in the first batch.
|
||||
$total = $runner->setup( $batch, $hooks, $group, $force );
|
||||
|
||||
// Run actions for as long as possible.
|
||||
while ( $total > 0 ) {
|
||||
$this->print_total_actions( $total );
|
||||
$actions_completed += $runner->run();
|
||||
$batches_completed++;
|
||||
|
||||
// Maybe set up tasks for the next batch.
|
||||
$total = ( $unlimited || $batches_completed < $batches ) ? $runner->setup( $batch, $hooks, $group, $force ) : 0;
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
$this->print_error( $e );
|
||||
}
|
||||
|
||||
$this->print_total_batches( $batches_completed );
|
||||
$this->print_success( $actions_completed );
|
||||
}
|
||||
|
||||
/**
|
||||
* Print WP CLI message about how many actions are about to be processed.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*
|
||||
* @param int $total
|
||||
*/
|
||||
protected function print_total_actions( $total ) {
|
||||
WP_CLI::log(
|
||||
sprintf(
|
||||
/* translators: %d refers to how many scheduled taks were found to run */
|
||||
_n( 'Found %d scheduled task', 'Found %d scheduled tasks', $total, 'woocommerce' ),
|
||||
number_format_i18n( $total )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print WP CLI message about how many batches of actions were processed.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*
|
||||
* @param int $batches_completed
|
||||
*/
|
||||
protected function print_total_batches( $batches_completed ) {
|
||||
WP_CLI::log(
|
||||
sprintf(
|
||||
/* translators: %d refers to the total number of batches executed */
|
||||
_n( '%d batch executed.', '%d batches executed.', $batches_completed, 'woocommerce' ),
|
||||
number_format_i18n( $batches_completed )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an exception into a WP CLI error.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*
|
||||
* @param Exception $e The error object.
|
||||
*
|
||||
* @throws \WP_CLI\ExitException
|
||||
*/
|
||||
protected function print_error( Exception $e ) {
|
||||
WP_CLI::error(
|
||||
sprintf(
|
||||
/* translators: %s refers to the exception error message. */
|
||||
__( 'There was an error running the action scheduler: %s', 'woocommerce' ),
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a success message with the number of completed actions.
|
||||
*
|
||||
* @author Jeremy Pry
|
||||
*
|
||||
* @param int $actions_completed
|
||||
*/
|
||||
protected function print_success( $actions_completed ) {
|
||||
WP_CLI::success(
|
||||
sprintf(
|
||||
/* translators: %d refers to the total number of taskes completed */
|
||||
_n( '%d scheduled task completed.', '%d scheduled tasks completed.', $actions_completed, 'woocommerce' ),
|
||||
number_format_i18n( $actions_completed )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_wcSystemStatus
|
||||
*/
|
||||
class ActionScheduler_wcSystemStatus {
|
||||
|
||||
/**
|
||||
* The active data stores
|
||||
*
|
||||
* @var ActionScheduler_Store
|
||||
*/
|
||||
protected $store;
|
||||
|
||||
function __construct( $store ) {
|
||||
$this->store = $store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display action data, including number of actions grouped by status and the oldest & newest action in each status.
|
||||
*
|
||||
* Helpful to identify issues, like a clogged queue.
|
||||
*/
|
||||
public function render() {
|
||||
$action_counts = $this->store->action_counts();
|
||||
$status_labels = $this->store->get_status_labels();
|
||||
$oldest_and_newest = $this->get_oldest_and_newest( array_keys( $status_labels ) );
|
||||
|
||||
$this->get_template( $status_labels, $action_counts, $oldest_and_newest );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get oldest and newest scheduled dates for a given set of statuses.
|
||||
*
|
||||
* @param array $status_keys Set of statuses to find oldest & newest action for.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_oldest_and_newest( $status_keys ) {
|
||||
|
||||
$oldest_and_newest = array();
|
||||
|
||||
foreach ( $status_keys as $status ) {
|
||||
$oldest_and_newest[ $status ] = array(
|
||||
'oldest' => '–',
|
||||
'newest' => '–',
|
||||
);
|
||||
|
||||
if ( 'in-progress' === $status ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldest_and_newest[ $status ]['oldest'] = $this->get_action_status_date( $status, 'oldest' );
|
||||
$oldest_and_newest[ $status ]['newest'] = $this->get_action_status_date( $status, 'newest' );
|
||||
}
|
||||
|
||||
return $oldest_and_newest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get oldest or newest scheduled date for a given status.
|
||||
*
|
||||
* @param string $status Action status label/name string.
|
||||
* @param string $date_type Oldest or Newest.
|
||||
* @return DateTime
|
||||
*/
|
||||
protected function get_action_status_date( $status, $date_type = 'oldest' ) {
|
||||
|
||||
$order = 'oldest' === $date_type ? 'ASC' : 'DESC';
|
||||
|
||||
$action = $this->store->query_actions( array(
|
||||
'claimed' => false,
|
||||
'status' => $status,
|
||||
'per_page' => 1,
|
||||
'order' => $order,
|
||||
) );
|
||||
|
||||
if ( ! empty( $action ) ) {
|
||||
$date_object = $this->store->get_date( $action[0] );
|
||||
$action_date = $date_object->format( 'Y-m-d H:i:s O' );
|
||||
} else {
|
||||
$action_date = '–';
|
||||
}
|
||||
|
||||
return $action_date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get oldest or newest scheduled date for a given status.
|
||||
*
|
||||
* @param array $status_labels Set of statuses to find oldest & newest action for.
|
||||
* @param array $action_counts Number of actions grouped by status.
|
||||
* @param array $oldest_and_newest Date of the oldest and newest action with each status.
|
||||
*/
|
||||
protected function get_template( $status_labels, $action_counts, $oldest_and_newest ) {
|
||||
?>
|
||||
|
||||
<table class="wc_status_table widefat" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="5" data-export-label="Action Scheduler"><h2><?php esc_html_e( 'Action Scheduler', 'woocommerce' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows scheduled action counts.', 'woocommerce' ) ); ?></h2></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php esc_html_e( 'Action Status', 'woocommerce' ); ?></strong></td>
|
||||
<td class="help"> </td>
|
||||
<td><strong><?php esc_html_e( 'Count', 'woocommerce' ); ?></strong></td>
|
||||
<td><strong><?php esc_html_e( 'Oldest Scheduled Date', 'woocommerce' ); ?></strong></td>
|
||||
<td><strong><?php esc_html_e( 'Newest Scheduled Date', 'woocommerce' ); ?></strong></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
foreach ( $action_counts as $status => $count ) {
|
||||
// WC uses the 3rd column for export, so we need to display more data in that (hidden when viewed as part of the table) and add an empty 2nd column.
|
||||
printf(
|
||||
'<tr><td>%1$s</td><td> </td><td>%2$s<span style="display: none;">, Oldest: %3$s, Newest: %4$s</span></td><td>%3$s</td><td>%4$s</td></tr>',
|
||||
esc_html( $status_labels[ $status ] ),
|
||||
number_format_i18n( $count ),
|
||||
$oldest_and_newest[ $status ]['oldest'],
|
||||
$oldest_and_newest[ $status ]['newest']
|
||||
);
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* is triggered when invoking inaccessible methods in an object context.
|
||||
*
|
||||
* @param $name string
|
||||
* @param $arguments array
|
||||
*
|
||||
* @return mixed
|
||||
* @link https://php.net/manual/en/language.oop5.overloading.php#language.oop5.overloading.methods
|
||||
*/
|
||||
public function __call( $name, $arguments ) {
|
||||
switch ( $name ) {
|
||||
case 'print':
|
||||
_deprecated_function( __CLASS__ . '::print()', '2.2.4', __CLASS__ . '::render()' );
|
||||
return call_user_func_array( array( $this, 'render' ), $arguments );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_wpCommentLogger
|
||||
*/
|
||||
class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger {
|
||||
const AGENT = 'ActionScheduler';
|
||||
const TYPE = 'action_log';
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
* @param string $message
|
||||
* @param DateTime $date
|
||||
*
|
||||
* @return string The log entry ID
|
||||
*/
|
||||
public function log( $action_id, $message, DateTime $date = NULL ) {
|
||||
if ( empty($date) ) {
|
||||
$date = as_get_datetime_object();
|
||||
} else {
|
||||
$date = as_get_datetime_object( clone $date );
|
||||
}
|
||||
$comment_id = $this->create_wp_comment( $action_id, $message, $date );
|
||||
return $comment_id;
|
||||
}
|
||||
|
||||
protected function create_wp_comment( $action_id, $message, DateTime $date ) {
|
||||
|
||||
$comment_date_gmt = $date->format('Y-m-d H:i:s');
|
||||
ActionScheduler_TimezoneHelper::set_local_timezone( $date );
|
||||
$comment_data = array(
|
||||
'comment_post_ID' => $action_id,
|
||||
'comment_date' => $date->format('Y-m-d H:i:s'),
|
||||
'comment_date_gmt' => $comment_date_gmt,
|
||||
'comment_author' => self::AGENT,
|
||||
'comment_content' => $message,
|
||||
'comment_agent' => self::AGENT,
|
||||
'comment_type' => self::TYPE,
|
||||
);
|
||||
return wp_insert_comment($comment_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entry_id
|
||||
*
|
||||
* @return ActionScheduler_LogEntry
|
||||
*/
|
||||
public function get_entry( $entry_id ) {
|
||||
$comment = $this->get_comment( $entry_id );
|
||||
if ( empty($comment) || $comment->comment_type != self::TYPE ) {
|
||||
return new ActionScheduler_NullLogEntry();
|
||||
}
|
||||
|
||||
$date = as_get_datetime_object( $comment->comment_date_gmt );
|
||||
ActionScheduler_TimezoneHelper::set_local_timezone( $date );
|
||||
return new ActionScheduler_LogEntry( $comment->comment_post_ID, $comment->comment_content, $date );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*
|
||||
* @return ActionScheduler_LogEntry[]
|
||||
*/
|
||||
public function get_logs( $action_id ) {
|
||||
$status = 'all';
|
||||
if ( get_post_status($action_id) == 'trash' ) {
|
||||
$status = 'post-trashed';
|
||||
}
|
||||
$comments = get_comments(array(
|
||||
'post_id' => $action_id,
|
||||
'orderby' => 'comment_date_gmt',
|
||||
'order' => 'ASC',
|
||||
'type' => self::TYPE,
|
||||
'status' => $status,
|
||||
));
|
||||
$logs = array();
|
||||
foreach ( $comments as $c ) {
|
||||
$entry = $this->get_entry( $c );
|
||||
if ( !empty($entry) ) {
|
||||
$logs[] = $entry;
|
||||
}
|
||||
}
|
||||
return $logs;
|
||||
}
|
||||
|
||||
protected function get_comment( $comment_id ) {
|
||||
return get_comment( $comment_id );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param WP_Comment_Query $query
|
||||
*/
|
||||
public function filter_comment_queries( $query ) {
|
||||
foreach ( array('ID', 'parent', 'post_author', 'post_name', 'post_parent', 'type', 'post_type', 'post_id', 'post_ID') as $key ) {
|
||||
if ( !empty($query->query_vars[$key]) ) {
|
||||
return; // don't slow down queries that wouldn't include action_log comments anyway
|
||||
}
|
||||
}
|
||||
$query->query_vars['action_log_filter'] = TRUE;
|
||||
add_filter( 'comments_clauses', array( $this, 'filter_comment_query_clauses' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $clauses
|
||||
* @param WP_Comment_Query $query
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function filter_comment_query_clauses( $clauses, $query ) {
|
||||
if ( !empty($query->query_vars['action_log_filter']) ) {
|
||||
$clauses['where'] .= $this->get_where_clause();
|
||||
}
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure Action Scheduler logs are excluded from comment feeds, which use WP_Query, not
|
||||
* the WP_Comment_Query class handled by @see self::filter_comment_queries().
|
||||
*
|
||||
* @param string $where
|
||||
* @param WP_Query $query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function filter_comment_feed( $where, $query ) {
|
||||
if ( is_comment_feed() ) {
|
||||
$where .= $this->get_where_clause();
|
||||
}
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a SQL clause to exclude Action Scheduler comments.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_where_clause() {
|
||||
global $wpdb;
|
||||
return sprintf( " AND {$wpdb->comments}.comment_type != '%s'", self::TYPE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove action log entries from wp_count_comments()
|
||||
*
|
||||
* @param array $stats
|
||||
* @param int $post_id
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function filter_comment_count( $stats, $post_id ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( 0 === $post_id ) {
|
||||
$stats = $this->get_comment_count();
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the comment counts from our cache, or the database if the cached version isn't set.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
protected function get_comment_count() {
|
||||
global $wpdb;
|
||||
|
||||
$stats = get_transient( 'as_comment_count' );
|
||||
|
||||
if ( ! $stats ) {
|
||||
$stats = array();
|
||||
|
||||
$count = $wpdb->get_results( "SELECT comment_approved, COUNT( * ) AS num_comments FROM {$wpdb->comments} WHERE comment_type NOT IN('order_note','action_log') GROUP BY comment_approved", ARRAY_A );
|
||||
|
||||
$total = 0;
|
||||
$stats = array();
|
||||
$approved = array( '0' => 'moderated', '1' => 'approved', 'spam' => 'spam', 'trash' => 'trash', 'post-trashed' => 'post-trashed' );
|
||||
|
||||
foreach ( (array) $count as $row ) {
|
||||
// Don't count post-trashed toward totals
|
||||
if ( 'post-trashed' != $row['comment_approved'] && 'trash' != $row['comment_approved'] ) {
|
||||
$total += $row['num_comments'];
|
||||
}
|
||||
if ( isset( $approved[ $row['comment_approved'] ] ) ) {
|
||||
$stats[ $approved[ $row['comment_approved'] ] ] = $row['num_comments'];
|
||||
}
|
||||
}
|
||||
|
||||
$stats['total_comments'] = $total;
|
||||
$stats['all'] = $total;
|
||||
|
||||
foreach ( $approved as $key ) {
|
||||
if ( empty( $stats[ $key ] ) ) {
|
||||
$stats[ $key ] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$stats = (object) $stats;
|
||||
set_transient( 'as_comment_count', $stats );
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete comment count cache whenever there is new comment or the status of a comment changes. Cache
|
||||
* will be regenerated next time ActionScheduler_wpCommentLogger::filter_comment_count() is called.
|
||||
*/
|
||||
public function delete_comment_count_cache() {
|
||||
delete_transient( 'as_comment_count' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'action_scheduler_before_process_queue', array( $this, 'disable_comment_counting' ), 10, 0 );
|
||||
add_action( 'action_scheduler_after_process_queue', array( $this, 'enable_comment_counting' ), 10, 0 );
|
||||
|
||||
parent::init();
|
||||
|
||||
add_action( 'pre_get_comments', array( $this, 'filter_comment_queries' ), 10, 1 );
|
||||
add_action( 'wp_count_comments', array( $this, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs
|
||||
add_action( 'comment_feed_where', array( $this, 'filter_comment_feed' ), 10, 2 );
|
||||
|
||||
// Delete comments count cache whenever there is a new comment or a comment status changes
|
||||
add_action( 'wp_insert_comment', array( $this, 'delete_comment_count_cache' ) );
|
||||
add_action( 'wp_set_comment_status', array( $this, 'delete_comment_count_cache' ) );
|
||||
}
|
||||
|
||||
public function disable_comment_counting() {
|
||||
wp_defer_comment_counting(true);
|
||||
}
|
||||
public function enable_comment_counting() {
|
||||
wp_defer_comment_counting(false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,821 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_wpPostStore
|
||||
*/
|
||||
class ActionScheduler_wpPostStore extends ActionScheduler_Store {
|
||||
const POST_TYPE = 'scheduled-action';
|
||||
const GROUP_TAXONOMY = 'action-group';
|
||||
const SCHEDULE_META_KEY = '_action_manager_schedule';
|
||||
|
||||
/** @var DateTimeZone */
|
||||
protected $local_timezone = NULL;
|
||||
|
||||
/** @var int */
|
||||
private static $max_index_length = 191;
|
||||
|
||||
public function save_action( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ){
|
||||
try {
|
||||
$this->validate_action( $action );
|
||||
$post_array = $this->create_post_array( $action, $scheduled_date );
|
||||
$post_id = $this->save_post_array( $post_array );
|
||||
$schedule = $action->get_schedule();
|
||||
|
||||
if ( ! is_null( $scheduled_date ) && $schedule->is_recurring() ) {
|
||||
$schedule = new ActionScheduler_IntervalSchedule( $scheduled_date, $schedule->interval_in_seconds() );
|
||||
}
|
||||
|
||||
$this->save_post_schedule( $post_id, $schedule );
|
||||
$this->save_action_group( $post_id, $action->get_group() );
|
||||
do_action( 'action_scheduler_stored_action', $post_id );
|
||||
return $post_id;
|
||||
} catch ( Exception $e ) {
|
||||
throw new RuntimeException( sprintf( __('Error saving action: %s', 'woocommerce'), $e->getMessage() ), 0 );
|
||||
}
|
||||
}
|
||||
|
||||
protected function create_post_array( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) {
|
||||
$post = array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_title' => $action->get_hook(),
|
||||
'post_content' => json_encode($action->get_args()),
|
||||
'post_status' => ( $action->is_finished() ? 'publish' : 'pending' ),
|
||||
'post_date_gmt' => $this->get_scheduled_date_string( $action, $scheduled_date ),
|
||||
'post_date' => $this->get_scheduled_date_string_local( $action, $scheduled_date ),
|
||||
);
|
||||
return $post;
|
||||
}
|
||||
|
||||
protected function save_post_array( $post_array ) {
|
||||
add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 );
|
||||
add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
|
||||
$post_id = wp_insert_post($post_array);
|
||||
remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 );
|
||||
remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
|
||||
|
||||
if ( is_wp_error($post_id) || empty($post_id) ) {
|
||||
throw new RuntimeException(__('Unable to save action.', 'woocommerce'));
|
||||
}
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
public function filter_insert_post_data( $postdata ) {
|
||||
if ( $postdata['post_type'] == self::POST_TYPE ) {
|
||||
$postdata['post_author'] = 0;
|
||||
if ( $postdata['post_status'] == 'future' ) {
|
||||
$postdata['post_status'] = 'publish';
|
||||
}
|
||||
}
|
||||
return $postdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a (probably unique) post name for scheduled actions in a more performant manner than wp_unique_post_slug().
|
||||
*
|
||||
* When an action's post status is transitioned to something other than 'draft', 'pending' or 'auto-draft, like 'publish'
|
||||
* or 'failed' or 'trash', WordPress will find a unique slug (stored in post_name column) using the wp_unique_post_slug()
|
||||
* function. This is done to ensure URL uniqueness. The approach taken by wp_unique_post_slug() is to iterate over existing
|
||||
* post_name values that match, and append a number 1 greater than the largest. This makes sense when manually creating a
|
||||
* post from the Edit Post screen. It becomes a bottleneck when automatically processing thousands of actions, with a
|
||||
* database containing thousands of related post_name values.
|
||||
*
|
||||
* WordPress 5.1 introduces the 'pre_wp_unique_post_slug' filter for plugins to address this issue.
|
||||
*
|
||||
* We can short-circuit WordPress's wp_unique_post_slug() approach using the 'pre_wp_unique_post_slug' filter. This
|
||||
* method is available to be used as a callback on that filter. It provides a more scalable approach to generating a
|
||||
* post_name/slug that is probably unique. Because Action Scheduler never actually uses the post_name field, or an
|
||||
* action's slug, being probably unique is good enough.
|
||||
*
|
||||
* For more backstory on this issue, see:
|
||||
* - https://github.com/Prospress/action-scheduler/issues/44 and
|
||||
* - https://core.trac.wordpress.org/ticket/21112
|
||||
*
|
||||
* @param string $override_slug Short-circuit return value.
|
||||
* @param string $slug The desired slug (post_name).
|
||||
* @param int $post_ID Post ID.
|
||||
* @param string $post_status The post status.
|
||||
* @param string $post_type Post type.
|
||||
* @return string
|
||||
*/
|
||||
public function set_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type ) {
|
||||
if ( self::POST_TYPE == $post_type ) {
|
||||
$override_slug = uniqid( self::POST_TYPE . '-', true ) . '-' . wp_generate_password( 32, false );
|
||||
}
|
||||
return $override_slug;
|
||||
}
|
||||
|
||||
protected function save_post_schedule( $post_id, $schedule ) {
|
||||
update_post_meta( $post_id, self::SCHEDULE_META_KEY, $schedule );
|
||||
}
|
||||
|
||||
protected function save_action_group( $post_id, $group ) {
|
||||
if ( empty($group) ) {
|
||||
wp_set_object_terms( $post_id, array(), self::GROUP_TAXONOMY, FALSE );
|
||||
} else {
|
||||
wp_set_object_terms( $post_id, array($group), self::GROUP_TAXONOMY, FALSE );
|
||||
}
|
||||
}
|
||||
|
||||
public function fetch_action( $action_id ) {
|
||||
$post = $this->get_post( $action_id );
|
||||
if ( empty($post) || $post->post_type != self::POST_TYPE ) {
|
||||
return $this->get_null_action();
|
||||
}
|
||||
return $this->make_action_from_post($post);
|
||||
}
|
||||
|
||||
protected function get_post( $action_id ) {
|
||||
if ( empty($action_id) ) {
|
||||
return NULL;
|
||||
}
|
||||
return get_post($action_id);
|
||||
}
|
||||
|
||||
protected function get_null_action() {
|
||||
return new ActionScheduler_NullAction();
|
||||
}
|
||||
|
||||
protected function make_action_from_post( $post ) {
|
||||
$hook = $post->post_title;
|
||||
|
||||
try {
|
||||
$args = json_decode( $post->post_content, true );
|
||||
$this->validate_args( $args, $post->ID );
|
||||
|
||||
$schedule = get_post_meta( $post->ID, self::SCHEDULE_META_KEY, true );
|
||||
if ( empty( $schedule ) || ! is_a( $schedule, 'ActionScheduler_Schedule' ) ) {
|
||||
throw ActionScheduler_InvalidActionException::from_decoding_args( $post->ID );
|
||||
}
|
||||
} catch ( ActionScheduler_InvalidActionException $exception ) {
|
||||
$schedule = new ActionScheduler_NullSchedule();
|
||||
$args = array();
|
||||
do_action( 'action_scheduler_failed_fetch_action', $post->ID );
|
||||
}
|
||||
|
||||
$group = wp_get_object_terms( $post->ID, self::GROUP_TAXONOMY, array('fields' => 'names') );
|
||||
$group = empty( $group ) ? '' : reset($group);
|
||||
|
||||
return ActionScheduler::factory()->get_stored_action( $this->get_action_status_by_post_status( $post->post_status ), $hook, $args, $schedule, $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $post_status
|
||||
*
|
||||
* @throws InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels()
|
||||
* @return string
|
||||
*/
|
||||
protected function get_action_status_by_post_status( $post_status ) {
|
||||
|
||||
switch ( $post_status ) {
|
||||
case 'publish' :
|
||||
$action_status = self::STATUS_COMPLETE;
|
||||
break;
|
||||
case 'trash' :
|
||||
$action_status = self::STATUS_CANCELED;
|
||||
break;
|
||||
default :
|
||||
if ( ! array_key_exists( $post_status, $this->get_status_labels() ) ) {
|
||||
throw new InvalidArgumentException( sprintf( 'Invalid post status: "%s". No matching action status available.', $post_status ) );
|
||||
}
|
||||
$action_status = $post_status;
|
||||
break;
|
||||
}
|
||||
|
||||
return $action_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action_status
|
||||
* @throws InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels()
|
||||
* @return string
|
||||
*/
|
||||
protected function get_post_status_by_action_status( $action_status ) {
|
||||
|
||||
switch ( $action_status ) {
|
||||
case self::STATUS_COMPLETE :
|
||||
$post_status = 'publish';
|
||||
break;
|
||||
case self::STATUS_CANCELED :
|
||||
$post_status = 'trash';
|
||||
break;
|
||||
default :
|
||||
if ( ! array_key_exists( $action_status, $this->get_status_labels() ) ) {
|
||||
throw new InvalidArgumentException( sprintf( 'Invalid action status: "%s".', $action_status ) );
|
||||
}
|
||||
$post_status = $action_status;
|
||||
break;
|
||||
}
|
||||
|
||||
return $post_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hook
|
||||
* @param array $params
|
||||
*
|
||||
* @return string 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' => ActionScheduler_Store::STATUS_PENDING,
|
||||
'group' => '',
|
||||
));
|
||||
/** @var wpdb $wpdb */
|
||||
global $wpdb;
|
||||
$query = "SELECT p.ID FROM {$wpdb->posts} p";
|
||||
$args = array();
|
||||
if ( !empty($params['group']) ) {
|
||||
$query .= " INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID";
|
||||
$query .= " INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id";
|
||||
$query .= " INNER JOIN {$wpdb->terms} t ON tt.term_id=t.term_id AND t.slug=%s";
|
||||
$args[] = $params['group'];
|
||||
}
|
||||
$query .= " WHERE p.post_title=%s";
|
||||
$args[] = $hook;
|
||||
$query .= " AND p.post_type=%s";
|
||||
$args[] = self::POST_TYPE;
|
||||
if ( !is_null($params['args']) ) {
|
||||
$query .= " AND p.post_content=%s";
|
||||
$args[] = json_encode($params['args']);
|
||||
}
|
||||
|
||||
if ( ! empty( $params['status'] ) ) {
|
||||
$query .= " AND p.post_status=%s";
|
||||
$args[] = $this->get_post_status_by_action_status( $params['status'] );
|
||||
}
|
||||
|
||||
switch ( $params['status'] ) {
|
||||
case self::STATUS_COMPLETE:
|
||||
case self::STATUS_RUNNING:
|
||||
case self::STATUS_FAILED:
|
||||
$order = 'DESC'; // Find the most recent action that matches
|
||||
break;
|
||||
case self::STATUS_PENDING:
|
||||
default:
|
||||
$order = 'ASC'; // Find the next action that matches
|
||||
break;
|
||||
}
|
||||
$query .= " ORDER BY post_date_gmt $order LIMIT 1";
|
||||
|
||||
$query = $wpdb->prepare( $query, $args );
|
||||
|
||||
$id = $wpdb->get_var($query);
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SQL statement to query (or count) actions.
|
||||
*
|
||||
* @param array $query Filtering options
|
||||
* @param string $select_or_count Whether the SQL should select and return the IDs or just the row count
|
||||
* @throws InvalidArgumentException if $select_or_count not count or select
|
||||
* @return string SQL statement. The returned SQL is already properly escaped.
|
||||
*/
|
||||
protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) {
|
||||
|
||||
if ( ! in_array( $select_or_count, array( 'select', 'count' ) ) ) {
|
||||
throw new InvalidArgumentException(__('Invalid schedule. Cannot save action.', 'woocommerce'));
|
||||
}
|
||||
|
||||
$query = wp_parse_args( $query, array(
|
||||
'hook' => '',
|
||||
'args' => NULL,
|
||||
'date' => NULL,
|
||||
'date_compare' => '<=',
|
||||
'modified' => NULL,
|
||||
'modified_compare' => '<=',
|
||||
'group' => '',
|
||||
'status' => '',
|
||||
'claimed' => NULL,
|
||||
'per_page' => 5,
|
||||
'offset' => 0,
|
||||
'orderby' => 'date',
|
||||
'order' => 'ASC',
|
||||
'search' => '',
|
||||
) );
|
||||
|
||||
/** @var wpdb $wpdb */
|
||||
global $wpdb;
|
||||
$sql = ( 'count' === $select_or_count ) ? 'SELECT count(p.ID)' : 'SELECT p.ID ';
|
||||
$sql .= "FROM {$wpdb->posts} p";
|
||||
$sql_params = array();
|
||||
if ( ! empty( $query['group'] ) || 'group' === $query['orderby'] ) {
|
||||
$sql .= " INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID";
|
||||
$sql .= " INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id";
|
||||
$sql .= " INNER JOIN {$wpdb->terms} t ON tt.term_id=t.term_id";
|
||||
|
||||
if ( ! empty( $query['group'] ) ) {
|
||||
$sql .= " AND t.slug=%s";
|
||||
$sql_params[] = $query['group'];
|
||||
}
|
||||
}
|
||||
$sql .= " WHERE post_type=%s";
|
||||
$sql_params[] = self::POST_TYPE;
|
||||
if ( $query['hook'] ) {
|
||||
$sql .= " AND p.post_title=%s";
|
||||
$sql_params[] = $query['hook'];
|
||||
}
|
||||
if ( !is_null($query['args']) ) {
|
||||
$sql .= " AND p.post_content=%s";
|
||||
$sql_params[] = json_encode($query['args']);
|
||||
}
|
||||
|
||||
if ( ! empty( $query['status'] ) ) {
|
||||
$sql .= " AND p.post_status=%s";
|
||||
$sql_params[] = $this->get_post_status_by_action_status( $query['status'] );
|
||||
}
|
||||
|
||||
if ( $query['date'] instanceof DateTime ) {
|
||||
$date = clone $query['date'];
|
||||
$date->setTimezone( new DateTimeZone('UTC') );
|
||||
$date_string = $date->format('Y-m-d H:i:s');
|
||||
$comparator = $this->validate_sql_comparator($query['date_compare']);
|
||||
$sql .= " AND p.post_date_gmt $comparator %s";
|
||||
$sql_params[] = $date_string;
|
||||
}
|
||||
|
||||
if ( $query['modified'] instanceof DateTime ) {
|
||||
$modified = clone $query['modified'];
|
||||
$modified->setTimezone( new DateTimeZone('UTC') );
|
||||
$date_string = $modified->format('Y-m-d H:i:s');
|
||||
$comparator = $this->validate_sql_comparator($query['modified_compare']);
|
||||
$sql .= " AND p.post_modified_gmt $comparator %s";
|
||||
$sql_params[] = $date_string;
|
||||
}
|
||||
|
||||
if ( $query['claimed'] === TRUE ) {
|
||||
$sql .= " AND p.post_password != ''";
|
||||
} elseif ( $query['claimed'] === FALSE ) {
|
||||
$sql .= " AND p.post_password = ''";
|
||||
} elseif ( !is_null($query['claimed']) ) {
|
||||
$sql .= " AND p.post_password = %s";
|
||||
$sql_params[] = $query['claimed'];
|
||||
}
|
||||
|
||||
if ( ! empty( $query['search'] ) ) {
|
||||
$sql .= " AND (p.post_title LIKE %s OR p.post_content LIKE %s OR p.post_password LIKE %s)";
|
||||
for( $i = 0; $i < 3; $i++ ) {
|
||||
$sql_params[] = sprintf( '%%%s%%', $query['search'] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( 'select' === $select_or_count ) {
|
||||
switch ( $query['orderby'] ) {
|
||||
case 'hook':
|
||||
$orderby = 'p.post_title';
|
||||
break;
|
||||
case 'group':
|
||||
$orderby = 't.name';
|
||||
break;
|
||||
case 'status':
|
||||
$orderby = 'p.post_status';
|
||||
break;
|
||||
case 'modified':
|
||||
$orderby = 'p.post_modified';
|
||||
break;
|
||||
case 'claim_id':
|
||||
$orderby = 'p.post_password';
|
||||
break;
|
||||
case 'schedule':
|
||||
case 'date':
|
||||
default:
|
||||
$orderby = 'p.post_date_gmt';
|
||||
break;
|
||||
}
|
||||
if ( 'ASC' === strtoupper( $query['order'] ) ) {
|
||||
$order = 'ASC';
|
||||
} else {
|
||||
$order = 'DESC';
|
||||
}
|
||||
$sql .= " ORDER BY $orderby $order";
|
||||
if ( $query['per_page'] > 0 ) {
|
||||
$sql .= " LIMIT %d, %d";
|
||||
$sql_params[] = $query['offset'];
|
||||
$sql_params[] = $query['per_page'];
|
||||
}
|
||||
}
|
||||
|
||||
return $wpdb->prepare( $sql, $sql_params );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $query
|
||||
* @param string $query_type Whether to select or count the results. Default, select.
|
||||
* @return string|array The IDs of actions matching the query
|
||||
*/
|
||||
public function query_actions( $query = array(), $query_type = 'select' ) {
|
||||
/** @var wpdb $wpdb */
|
||||
global $wpdb;
|
||||
|
||||
$sql = $this->get_query_actions_sql( $query, $query_type );
|
||||
|
||||
return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a count of all actions in the store, grouped by status
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function action_counts() {
|
||||
|
||||
$action_counts_by_status = array();
|
||||
$action_stati_and_labels = $this->get_status_labels();
|
||||
$posts_count_by_status = (array) wp_count_posts( self::POST_TYPE, 'readable' );
|
||||
|
||||
foreach ( $posts_count_by_status as $post_status_name => $count ) {
|
||||
|
||||
try {
|
||||
$action_status_name = $this->get_action_status_by_post_status( $post_status_name );
|
||||
} catch ( Exception $e ) {
|
||||
// Ignore any post statuses that aren't for actions
|
||||
continue;
|
||||
}
|
||||
if ( array_key_exists( $action_status_name, $action_stati_and_labels ) ) {
|
||||
$action_counts_by_status[ $action_status_name ] = $count;
|
||||
}
|
||||
}
|
||||
|
||||
return $action_counts_by_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function cancel_action( $action_id ) {
|
||||
$post = get_post($action_id);
|
||||
if ( empty($post) || ($post->post_type != self::POST_TYPE) ) {
|
||||
throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'woocommerce'), $action_id));
|
||||
}
|
||||
do_action( 'action_scheduler_canceled_action', $action_id );
|
||||
add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
|
||||
wp_trash_post($action_id);
|
||||
remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
|
||||
}
|
||||
|
||||
public function delete_action( $action_id ) {
|
||||
$post = get_post($action_id);
|
||||
if ( empty($post) || ($post->post_type != self::POST_TYPE) ) {
|
||||
throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'woocommerce'), $action_id));
|
||||
}
|
||||
do_action( 'action_scheduler_deleted_action', $action_id );
|
||||
wp_delete_post($action_id, TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran.
|
||||
*/
|
||||
public function get_date( $action_id ) {
|
||||
$next = $this->get_date_gmt( $action_id );
|
||||
return ActionScheduler_TimezoneHelper::set_local_timezone( $next );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran.
|
||||
*/
|
||||
public function get_date_gmt( $action_id ) {
|
||||
$post = get_post($action_id);
|
||||
if ( empty($post) || ($post->post_type != self::POST_TYPE) ) {
|
||||
throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'woocommerce'), $action_id));
|
||||
}
|
||||
if ( $post->post_status == 'publish' ) {
|
||||
return as_get_datetime_object($post->post_modified_gmt);
|
||||
} else {
|
||||
return as_get_datetime_object($post->post_date_gmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $max_actions
|
||||
* @param DateTime $before_date Jobs must be schedule before this 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
|
||||
* @throws RuntimeException When there is an error staking a claim.
|
||||
* @throws InvalidArgumentException When the given group is not valid.
|
||||
*/
|
||||
public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ) {
|
||||
$claim_id = $this->generate_claim_id();
|
||||
$this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group );
|
||||
$action_ids = $this->find_actions_by_claim_id( $claim_id );
|
||||
|
||||
return new ActionScheduler_ActionClaim( $claim_id, $action_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function get_claim_count(){
|
||||
global $wpdb;
|
||||
|
||||
$sql = "SELECT COUNT(DISTINCT post_password) FROM {$wpdb->posts} WHERE post_password != '' AND post_type = %s AND post_status IN ('in-progress','pending')";
|
||||
$sql = $wpdb->prepare( $sql, array( self::POST_TYPE ) );
|
||||
|
||||
return $wpdb->get_var( $sql );
|
||||
}
|
||||
|
||||
protected function generate_claim_id() {
|
||||
$claim_id = md5(microtime(true) . rand(0,1000));
|
||||
return substr($claim_id, 0, 20); // to fit in db field with 20 char limit
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $claim_id
|
||||
* @param int $limit
|
||||
* @param DateTime $before_date Should use UTC timezone.
|
||||
* @param array $hooks Claim only actions with a hook or hooks.
|
||||
* @param string $group Claim only actions in the given group.
|
||||
*
|
||||
* @return int The number of actions that were claimed
|
||||
* @throws RuntimeException When there is a database error.
|
||||
* @throws InvalidArgumentException When the group is invalid.
|
||||
*/
|
||||
protected function claim_actions( $claim_id, $limit, DateTime $before_date = null, $hooks = array(), $group = '' ) {
|
||||
// Set up initial variables.
|
||||
$date = null === $before_date ? as_get_datetime_object() : clone $before_date;
|
||||
$limit_ids = ! empty( $group );
|
||||
$ids = $limit_ids ? $this->get_actions_by_group( $group, $limit, $date ) : array();
|
||||
|
||||
// If limiting by IDs and no posts found, then return early since we have nothing to update.
|
||||
if ( $limit_ids && 0 === count( $ids ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @var wpdb $wpdb */
|
||||
global $wpdb;
|
||||
|
||||
/*
|
||||
* Build up custom query to update the affected posts. Parameters are built as a separate array
|
||||
* to make it easier to identify where they are in the query.
|
||||
*
|
||||
* We can't use $wpdb->update() here because of the "ID IN ..." clause.
|
||||
*/
|
||||
$update = "UPDATE {$wpdb->posts} SET post_password = %s, post_modified_gmt = %s, post_modified = %s";
|
||||
$params = array(
|
||||
$claim_id,
|
||||
current_time( 'mysql', true ),
|
||||
current_time( 'mysql' ),
|
||||
);
|
||||
|
||||
// Build initial WHERE clause.
|
||||
$where = "WHERE post_type = %s AND post_status = %s AND post_password = ''";
|
||||
$params[] = self::POST_TYPE;
|
||||
$params[] = ActionScheduler_Store::STATUS_PENDING;
|
||||
|
||||
if ( ! empty( $hooks ) ) {
|
||||
$placeholders = array_fill( 0, count( $hooks ), '%s' );
|
||||
$where .= ' AND post_title IN (' . join( ', ', $placeholders ) . ')';
|
||||
$params = array_merge( $params, array_values( $hooks ) );
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the IDs to the WHERE clause. IDs not escaped because they came directly from a prior DB query.
|
||||
*
|
||||
* If we're not limiting by IDs, then include the post_date_gmt clause.
|
||||
*/
|
||||
if ( $limit_ids ) {
|
||||
$where .= ' AND ID IN (' . join( ',', $ids ) . ')';
|
||||
} else {
|
||||
$where .= ' AND post_date_gmt <= %s';
|
||||
$params[] = $date->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
// Add the ORDER BY clause and,ms limit.
|
||||
$order = 'ORDER BY menu_order ASC, post_date_gmt ASC, ID ASC LIMIT %d';
|
||||
$params[] = $limit;
|
||||
|
||||
// Run the query and gather results.
|
||||
$rows_affected = $wpdb->query( $wpdb->prepare( "{$update} {$where} {$order}", $params ) );
|
||||
if ( $rows_affected === false ) {
|
||||
throw new RuntimeException( __( 'Unable to claim actions. Database error.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
return (int) $rows_affected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get IDs of actions within a certain group and up to a certain date/time.
|
||||
*
|
||||
* @param string $group The group to use in finding actions.
|
||||
* @param int $limit The number of actions to retrieve.
|
||||
* @param DateTime $date DateTime object representing cutoff time for actions. Actions retrieved will be
|
||||
* up to and including this DateTime.
|
||||
*
|
||||
* @return array IDs of actions in the appropriate group and before the appropriate time.
|
||||
* @throws InvalidArgumentException When the group does not exist.
|
||||
*/
|
||||
protected function get_actions_by_group( $group, $limit, DateTime $date ) {
|
||||
// Ensure the group exists before continuing.
|
||||
if ( ! term_exists( $group, self::GROUP_TAXONOMY )) {
|
||||
throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'woocommerce' ), $group ) );
|
||||
}
|
||||
|
||||
// Set up a query for post IDs to use later.
|
||||
$query = new WP_Query();
|
||||
$query_args = array(
|
||||
'fields' => 'ids',
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => ActionScheduler_Store::STATUS_PENDING,
|
||||
'has_password' => false,
|
||||
'posts_per_page' => $limit * 3,
|
||||
'suppress_filters' => true,
|
||||
'no_found_rows' => true,
|
||||
'orderby' => array(
|
||||
'menu_order' => 'ASC',
|
||||
'date' => 'ASC',
|
||||
'ID' => 'ASC',
|
||||
),
|
||||
'date_query' => array(
|
||||
'column' => 'post_date_gmt',
|
||||
'before' => $date->format( 'Y-m-d H:i' ),
|
||||
'inclusive' => true,
|
||||
),
|
||||
'tax_query' => array(
|
||||
array(
|
||||
'taxonomy' => self::GROUP_TAXONOMY,
|
||||
'field' => 'slug',
|
||||
'terms' => $group,
|
||||
'include_children' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $query->query( $query_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $claim_id
|
||||
* @return array
|
||||
*/
|
||||
public function find_actions_by_claim_id( $claim_id ) {
|
||||
/** @var wpdb $wpdb */
|
||||
global $wpdb;
|
||||
$sql = "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_password = %s";
|
||||
$sql = $wpdb->prepare( $sql, array( self::POST_TYPE, $claim_id ) );
|
||||
$action_ids = $wpdb->get_col( $sql );
|
||||
return $action_ids;
|
||||
}
|
||||
|
||||
public function release_claim( ActionScheduler_ActionClaim $claim ) {
|
||||
$action_ids = $this->find_actions_by_claim_id( $claim->get_id() );
|
||||
if ( empty($action_ids) ) {
|
||||
return; // nothing to do
|
||||
}
|
||||
$action_id_string = implode(',', array_map('intval', $action_ids));
|
||||
/** @var wpdb $wpdb */
|
||||
global $wpdb;
|
||||
$sql = "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID IN ($action_id_string) AND post_password = %s";
|
||||
$sql = $wpdb->prepare( $sql, array( $claim->get_id() ) );
|
||||
$result = $wpdb->query($sql);
|
||||
if ( $result === false ) {
|
||||
throw new RuntimeException( sprintf( __('Unable to unlock claim %s. Database error.', 'woocommerce'), $claim->get_id() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*/
|
||||
public function unclaim_action( $action_id ) {
|
||||
/** @var wpdb $wpdb */
|
||||
global $wpdb;
|
||||
$sql = "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID = %d AND post_type = %s";
|
||||
$sql = $wpdb->prepare( $sql, $action_id, self::POST_TYPE );
|
||||
$result = $wpdb->query($sql);
|
||||
if ( $result === false ) {
|
||||
throw new RuntimeException( sprintf( __('Unable to unlock claim on action %s. Database error.', 'woocommerce'), $action_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
public function mark_failure( $action_id ) {
|
||||
/** @var wpdb $wpdb */
|
||||
global $wpdb;
|
||||
$sql = "UPDATE {$wpdb->posts} SET post_status = %s WHERE ID = %d AND post_type = %s";
|
||||
$sql = $wpdb->prepare( $sql, self::STATUS_FAILED, $action_id, self::POST_TYPE );
|
||||
$result = $wpdb->query($sql);
|
||||
if ( $result === false ) {
|
||||
throw new RuntimeException( sprintf( __('Unable to mark failure on action %s. Database error.', 'woocommerce'), $action_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an action's claim ID, as stored in the post password column
|
||||
*
|
||||
* @param string $action_id
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_claim_id( $action_id ) {
|
||||
return $this->get_post_column( $action_id, 'post_password' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an action's status, as stored in the post status column
|
||||
*
|
||||
* @param string $action_id
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_status( $action_id ) {
|
||||
$status = $this->get_post_column( $action_id, 'post_status' );
|
||||
|
||||
if ( $status === null ) {
|
||||
throw new InvalidArgumentException( __( 'Invalid action ID. No status found.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
return $this->get_action_status_by_post_status( $status );
|
||||
}
|
||||
|
||||
private function get_post_column( $action_id, $column_name ) {
|
||||
/** @var \wpdb $wpdb */
|
||||
global $wpdb;
|
||||
return $wpdb->get_var( $wpdb->prepare( "SELECT {$column_name} FROM {$wpdb->posts} WHERE ID=%d AND post_type=%s", $action_id, self::POST_TYPE ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action_id
|
||||
*/
|
||||
public function log_execution( $action_id ) {
|
||||
/** @var wpdb $wpdb */
|
||||
global $wpdb;
|
||||
|
||||
$sql = "UPDATE {$wpdb->posts} SET menu_order = menu_order+1, post_status=%s, post_modified_gmt = %s, post_modified = %s WHERE ID = %d AND post_type = %s";
|
||||
$sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time('mysql', true), current_time('mysql'), $action_id, self::POST_TYPE );
|
||||
$wpdb->query($sql);
|
||||
}
|
||||
|
||||
|
||||
public function mark_complete( $action_id ) {
|
||||
$post = get_post($action_id);
|
||||
if ( empty($post) || ($post->post_type != self::POST_TYPE) ) {
|
||||
throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'woocommerce'), $action_id));
|
||||
}
|
||||
add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 );
|
||||
add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
|
||||
$result = wp_update_post(array(
|
||||
'ID' => $action_id,
|
||||
'post_status' => 'publish',
|
||||
), TRUE);
|
||||
remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 );
|
||||
remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
|
||||
if ( is_wp_error($result) ) {
|
||||
throw new RuntimeException($result->get_error_message());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
* as we prepare to move to custom tables, and can use an indexed VARCHAR column instead, we want to warn
|
||||
* developers of this impending requirement.
|
||||
*
|
||||
* @param ActionScheduler_Action $action
|
||||
*/
|
||||
protected function validate_action( ActionScheduler_Action $action ) {
|
||||
if ( strlen( json_encode( $action->get_args() ) ) > self::$max_index_length ) {
|
||||
_doing_it_wrong( 'ActionScheduler_Action::$args', sprintf( 'To ensure the action args column can be indexed, action args should not be more than %d characters when encoded as JSON. Support for strings longer than this will be removed in a future version.', self::$max_index_length ), '2.1.0' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function init() {
|
||||
$post_type_registrar = new ActionScheduler_wpPostStore_PostTypeRegistrar();
|
||||
$post_type_registrar->register();
|
||||
|
||||
$post_status_registrar = new ActionScheduler_wpPostStore_PostStatusRegistrar();
|
||||
$post_status_registrar->register();
|
||||
|
||||
$taxonomy_registrar = new ActionScheduler_wpPostStore_TaxonomyRegistrar();
|
||||
$taxonomy_registrar->register();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_wpPostStore_PostStatusRegistrar
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ActionScheduler_wpPostStore_PostStatusRegistrar {
|
||||
public function register() {
|
||||
register_post_status( ActionScheduler_Store::STATUS_RUNNING, array_merge( $this->post_status_args(), $this->post_status_running_labels() ) );
|
||||
register_post_status( ActionScheduler_Store::STATUS_FAILED, array_merge( $this->post_status_args(), $this->post_status_failed_labels() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the args array for the post type definition
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function post_status_args() {
|
||||
$args = array(
|
||||
'public' => false,
|
||||
'exclude_from_search' => false,
|
||||
'show_in_admin_all_list' => true,
|
||||
'show_in_admin_status_list' => true,
|
||||
);
|
||||
|
||||
return apply_filters( 'action_scheduler_post_status_args', $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the args array for the post type definition
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function post_status_failed_labels() {
|
||||
$labels = array(
|
||||
'label' => _x( 'Failed', 'post', 'woocommerce' ),
|
||||
'label_count' => _n_noop( 'Failed <span class="count">(%s)</span>', 'Failed <span class="count">(%s)</span>', 'woocommerce' ),
|
||||
);
|
||||
|
||||
return apply_filters( 'action_scheduler_post_status_failed_labels', $labels );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the args array for the post type definition
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function post_status_running_labels() {
|
||||
$labels = array(
|
||||
'label' => _x( 'In-Progress', 'post', 'woocommerce' ),
|
||||
'label_count' => _n_noop( 'In-Progress <span class="count">(%s)</span>', 'In-Progress <span class="count">(%s)</span>', 'woocommerce' ),
|
||||
);
|
||||
|
||||
return apply_filters( 'action_scheduler_post_status_running_labels', $labels );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_wpPostStore_PostTypeRegistrar
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ActionScheduler_wpPostStore_PostTypeRegistrar {
|
||||
public function register() {
|
||||
register_post_type( ActionScheduler_wpPostStore::POST_TYPE, $this->post_type_args() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the args array for the post type definition
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function post_type_args() {
|
||||
$args = array(
|
||||
'label' => __( 'Scheduled Actions', 'woocommerce' ),
|
||||
'description' => __( 'Scheduled actions are hooks triggered on a cetain date and time.', 'woocommerce' ),
|
||||
'public' => false,
|
||||
'map_meta_cap' => true,
|
||||
'hierarchical' => false,
|
||||
'supports' => array('title', 'editor','comments'),
|
||||
'rewrite' => false,
|
||||
'query_var' => false,
|
||||
'can_export' => true,
|
||||
'ep_mask' => EP_NONE,
|
||||
'labels' => array(
|
||||
'name' => __( 'Scheduled Actions', 'woocommerce' ),
|
||||
'singular_name' => __( 'Scheduled Action', 'woocommerce' ),
|
||||
'menu_name' => _x( 'Scheduled Actions', 'Admin menu name', 'woocommerce' ),
|
||||
'add_new' => __( 'Add', 'woocommerce' ),
|
||||
'add_new_item' => __( 'Add New Scheduled Action', 'woocommerce' ),
|
||||
'edit' => __( 'Edit', 'woocommerce' ),
|
||||
'edit_item' => __( 'Edit Scheduled Action', 'woocommerce' ),
|
||||
'new_item' => __( 'New Scheduled Action', 'woocommerce' ),
|
||||
'view' => __( 'View Action', 'woocommerce' ),
|
||||
'view_item' => __( 'View Action', 'woocommerce' ),
|
||||
'search_items' => __( 'Search Scheduled Actions', 'woocommerce' ),
|
||||
'not_found' => __( 'No actions found', 'woocommerce' ),
|
||||
'not_found_in_trash' => __( 'No actions found in trash', 'woocommerce' ),
|
||||
),
|
||||
);
|
||||
|
||||
$args = apply_filters('action_scheduler_post_type_args', $args);
|
||||
return $args;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_wpPostStore_TaxonomyRegistrar
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ActionScheduler_wpPostStore_TaxonomyRegistrar {
|
||||
public function register() {
|
||||
register_taxonomy( ActionScheduler_wpPostStore::GROUP_TAXONOMY, ActionScheduler_wpPostStore::POST_TYPE, $this->taxonomy_args() );
|
||||
}
|
||||
|
||||
protected function taxonomy_args() {
|
||||
$args = array(
|
||||
'label' => __('Action Group', 'woocommerce'),
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'show_admin_column' => true,
|
||||
'query_var' => false,
|
||||
'rewrite' => false,
|
||||
);
|
||||
|
||||
$args = apply_filters('action_scheduler_taxonomy_args', $args);
|
||||
return $args;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user