khaihihi
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/*
|
||||
* Plugin Name: Action Scheduler
|
||||
* Plugin URI: https://actionscheduler.org
|
||||
* Description: A robust scheduling library for use in WordPress plugins.
|
||||
* Author: Prospress
|
||||
* Author URI: https://prospress.com/
|
||||
* Version: 2.2.5
|
||||
* License: GPLv3
|
||||
*
|
||||
* Copyright 2019 Prospress, Inc. (email : freedoms@prospress.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
if ( ! function_exists( 'action_scheduler_register_2_dot_2_dot_5' ) ) {
|
||||
|
||||
if ( ! class_exists( 'ActionScheduler_Versions' ) ) {
|
||||
require_once( 'classes/ActionScheduler_Versions.php' );
|
||||
add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 );
|
||||
}
|
||||
|
||||
add_action( 'plugins_loaded', 'action_scheduler_register_2_dot_2_dot_5', 0, 0 );
|
||||
|
||||
function action_scheduler_register_2_dot_2_dot_5() {
|
||||
$versions = ActionScheduler_Versions::instance();
|
||||
$versions->register( '2.2.5', 'action_scheduler_initialize_2_dot_2_dot_5' );
|
||||
}
|
||||
|
||||
function action_scheduler_initialize_2_dot_2_dot_5() {
|
||||
require_once( 'classes/ActionScheduler.php' );
|
||||
ActionScheduler::init( __FILE__ );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Abstract class with common Queue Cleaner functionality.
|
||||
*/
|
||||
abstract class ActionScheduler_Abstract_QueueRunner_Deprecated {
|
||||
|
||||
/**
|
||||
* Get the maximum number of seconds a batch can run for.
|
||||
*
|
||||
* @deprecated 2.1.1
|
||||
* @return int The number of seconds.
|
||||
*/
|
||||
protected function get_maximum_execution_time() {
|
||||
_deprecated_function( __METHOD__, '2.1.1', 'ActionScheduler_Abstract_QueueRunner::get_time_limit()' );
|
||||
|
||||
$maximum_execution_time = 30;
|
||||
|
||||
// Apply deprecated filter
|
||||
if ( has_filter( 'action_scheduler_maximum_execution_time' ) ) {
|
||||
_deprecated_function( 'action_scheduler_maximum_execution_time', '2.1.1', 'action_scheduler_queue_runner_time_limit' );
|
||||
$maximum_execution_time = apply_filters( 'action_scheduler_maximum_execution_time', $maximum_execution_time );
|
||||
}
|
||||
|
||||
return absint( $maximum_execution_time );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ActionScheduler_AdminView_Deprecated
|
||||
*
|
||||
* Store deprecated public functions previously found in the ActionScheduler_AdminView class.
|
||||
* Keeps them out of the way of the main class.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ActionScheduler_AdminView_Deprecated {
|
||||
|
||||
public function action_scheduler_post_type_args( $args ) {
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customise the post status related views displayed on the Scheduled Actions administration screen.
|
||||
*
|
||||
* @param array $views An associative array of views and view labels which can be used to filter the 'scheduled-action' posts displayed on the Scheduled Actions administration screen.
|
||||
* @return array $views An associative array of views and view labels which can be used to filter the 'scheduled-action' posts displayed on the Scheduled Actions administration screen.
|
||||
*/
|
||||
public function list_table_views( $views ) {
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
return $views;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not include the "Edit" action for the Scheduled Actions administration screen.
|
||||
*
|
||||
* Hooked to the 'bulk_actions-edit-action-scheduler' filter.
|
||||
*
|
||||
* @param array $actions An associative array of actions which can be performed on the 'scheduled-action' post type.
|
||||
* @return array $actions An associative array of actions which can be performed on the 'scheduled-action' post type.
|
||||
*/
|
||||
public function bulk_actions( $actions ) {
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely customer the columns displayed on the Scheduled Actions administration screen.
|
||||
*
|
||||
* Because we can't filter the content of the default title and date columns, we need to recreate our own
|
||||
* custom columns for displaying those post fields. For the column content, @see self::list_table_column_content().
|
||||
*
|
||||
* @param array $columns An associative array of columns that are use for the table on the Scheduled Actions administration screen.
|
||||
* @return array $columns An associative array of columns that are use for the table on the Scheduled Actions administration screen.
|
||||
*/
|
||||
public function list_table_columns( $columns ) {
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make our custom title & date columns use defaulting title & date sorting.
|
||||
*
|
||||
* @param array $columns An associative array of columns that can be used to sort the table on the Scheduled Actions administration screen.
|
||||
* @return array $columns An associative array of columns that can be used to sort the table on the Scheduled Actions administration screen.
|
||||
*/
|
||||
public static function list_table_sortable_columns( $columns ) {
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the content for our custom columns.
|
||||
*
|
||||
* @param string $column_name The key for the column for which we should output our content.
|
||||
* @param int $post_id The ID of the 'scheduled-action' post for which this row relates.
|
||||
*/
|
||||
public static function list_table_column_content( $column_name, $post_id ) {
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the inline "Edit" action for all 'scheduled-action' posts.
|
||||
*
|
||||
* Hooked to the 'post_row_actions' filter.
|
||||
*
|
||||
* @param array $actions An associative array of actions which can be performed on the 'scheduled-action' post type.
|
||||
* @return array $actions An associative array of actions which can be performed on the 'scheduled-action' post type.
|
||||
*/
|
||||
public static function row_actions( $actions, $post ) {
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an action when triggered from the Action Scheduler administration screen.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function maybe_execute_action() {
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 funciton goes one step
|
||||
* further to display two degrees of accuracy.
|
||||
*
|
||||
* Based on Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/
|
||||
*
|
||||
* @param int $interval A interval in seconds.
|
||||
* @return string A human friendly string representation of the interval.
|
||||
*/
|
||||
public static function admin_notices() {
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter search queries to allow searching by Claim ID (i.e. post_password).
|
||||
*
|
||||
* @param string $orderby MySQL orderby string.
|
||||
* @param WP_Query $query Instance of a WP_Query object
|
||||
* @return string MySQL orderby string.
|
||||
*/
|
||||
public function custom_orderby( $orderby, $query ){
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter search queries to allow searching by Claim ID (i.e. post_password).
|
||||
*
|
||||
* @param string $search MySQL search string.
|
||||
* @param WP_Query $query Instance of a WP_Query object
|
||||
* @return string MySQL search string.
|
||||
*/
|
||||
public function search_post_password( $search, $query ) {
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Change messages when a scheduled action is updated.
|
||||
*
|
||||
* @param array $messages
|
||||
* @return array
|
||||
*/
|
||||
public function post_updated_messages( $messages ) {
|
||||
_deprecated_function( __METHOD__, '2.0.0' );
|
||||
return $messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Deprecated API functions for scheduling actions
|
||||
*
|
||||
* Functions with the wc prefix were deprecated to avoid confusion with
|
||||
* Action Scheduler being included in WooCommerce core, and it providing
|
||||
* a different set of APIs for working with the action queue.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Schedule an action to run one time
|
||||
*
|
||||
* @param int $timestamp When the job will run
|
||||
* @param string $hook The hook to trigger
|
||||
* @param array $args Arguments to pass when the hook triggers
|
||||
* @param string $group The group to assign this job to
|
||||
*
|
||||
* @return string The job ID
|
||||
*/
|
||||
function wc_schedule_single_action( $timestamp, $hook, $args = array(), $group = '' ) {
|
||||
_deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_single_action()' );
|
||||
return as_schedule_single_action( $timestamp, $hook, $args, $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a recurring action
|
||||
*
|
||||
* @param int $timestamp When the first instance of the job will run
|
||||
* @param int $interval_in_seconds How long to wait between runs
|
||||
* @param string $hook The hook to trigger
|
||||
* @param array $args Arguments to pass when the hook triggers
|
||||
* @param string $group The group to assign this job to
|
||||
*
|
||||
* @deprecated 2.1.0
|
||||
*
|
||||
* @return string The job ID
|
||||
*/
|
||||
function wc_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) {
|
||||
_deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_recurring_action()' );
|
||||
return as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action that recurs on a cron-like schedule.
|
||||
*
|
||||
* @param int $timestamp The schedule will start on or after this time
|
||||
* @param string $schedule A cron-link schedule string
|
||||
* @see http://en.wikipedia.org/wiki/Cron
|
||||
* * * * * * *
|
||||
* ┬ ┬ ┬ ┬ ┬ ┬
|
||||
* | | | | | |
|
||||
* | | | | | + year [optional]
|
||||
* | | | | +----- day of week (0 - 7) (Sunday=0 or 7)
|
||||
* | | | +---------- month (1 - 12)
|
||||
* | | +--------------- day of month (1 - 31)
|
||||
* | +-------------------- hour (0 - 23)
|
||||
* +------------------------- min (0 - 59)
|
||||
* @param string $hook The hook to trigger
|
||||
* @param array $args Arguments to pass when the hook triggers
|
||||
* @param string $group The group to assign this job to
|
||||
*
|
||||
* @deprecated 2.1.0
|
||||
*
|
||||
* @return string The job ID
|
||||
*/
|
||||
function wc_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '' ) {
|
||||
_deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_cron_action()' );
|
||||
return as_schedule_cron_action( $timestamp, $schedule, $hook, $args, $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the next occurrence of a job.
|
||||
*
|
||||
* @param string $hook The hook that the job will trigger
|
||||
* @param array $args Args that would have been passed to the job
|
||||
* @param string $group
|
||||
*
|
||||
* @deprecated 2.1.0
|
||||
*/
|
||||
function wc_unschedule_action( $hook, $args = array(), $group = '' ) {
|
||||
_deprecated_function( __FUNCTION__, '2.1.0', 'as_unschedule_action()' );
|
||||
as_unschedule_action( $hook, $args, $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hook
|
||||
* @param array $args
|
||||
* @param string $group
|
||||
*
|
||||
* @deprecated 2.1.0
|
||||
*
|
||||
* @return int|bool The timestamp for the next occurrence, or false if nothing was found
|
||||
*/
|
||||
function wc_next_scheduled_action( $hook, $args = NULL, $group = '' ) {
|
||||
_deprecated_function( __FUNCTION__, '2.1.0', 'as_next_scheduled_action()' );
|
||||
return as_next_scheduled_action( $hook, $args, $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find scheduled actions
|
||||
*
|
||||
* @param array $args Possible arguments, with their default values:
|
||||
* 'hook' => '' - the name of the action that will be triggered
|
||||
* 'args' => NULL - the args array that will be passed with the action
|
||||
* 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone.
|
||||
* 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='
|
||||
* 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone.
|
||||
* 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='
|
||||
* 'group' => '' - the group the action belongs to
|
||||
* 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING
|
||||
* 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID
|
||||
* 'per_page' => 5 - Number of results to return
|
||||
* 'offset' => 0
|
||||
* 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date'
|
||||
* 'order' => 'ASC'
|
||||
* @param string $return_format OBJECT, ARRAY_A, or ids
|
||||
*
|
||||
* @deprecated 2.1.0
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function wc_get_scheduled_actions( $args = array(), $return_format = OBJECT ) {
|
||||
_deprecated_function( __FUNCTION__, '2.1.0', 'as_get_scheduled_actions()' );
|
||||
return as_get_scheduled_actions( $args, $return_format );
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* General API functions for scheduling actions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Schedule an action to run one time
|
||||
*
|
||||
* @param int $timestamp When the job will run
|
||||
* @param string $hook The hook to trigger
|
||||
* @param array $args Arguments to pass when the hook triggers
|
||||
* @param string $group The group to assign this job to
|
||||
*
|
||||
* @return string The job ID
|
||||
*/
|
||||
function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '' ) {
|
||||
return ActionScheduler::factory()->single( $hook, $args, $timestamp, $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a recurring action
|
||||
*
|
||||
* @param int $timestamp When the first instance of the job will run
|
||||
* @param int $interval_in_seconds How long to wait between runs
|
||||
* @param string $hook The hook to trigger
|
||||
* @param array $args Arguments to pass when the hook triggers
|
||||
* @param string $group The group to assign this job to
|
||||
*
|
||||
* @return string The job ID
|
||||
*/
|
||||
function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) {
|
||||
return ActionScheduler::factory()->recurring( $hook, $args, $timestamp, $interval_in_seconds, $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action that recurs on a cron-like schedule.
|
||||
*
|
||||
* @param int $timestamp The schedule will start on or after this time
|
||||
* @param string $schedule A cron-link schedule string
|
||||
* @see http://en.wikipedia.org/wiki/Cron
|
||||
* * * * * * *
|
||||
* ┬ ┬ ┬ ┬ ┬ ┬
|
||||
* | | | | | |
|
||||
* | | | | | + year [optional]
|
||||
* | | | | +----- day of week (0 - 7) (Sunday=0 or 7)
|
||||
* | | | +---------- month (1 - 12)
|
||||
* | | +--------------- day of month (1 - 31)
|
||||
* | +-------------------- hour (0 - 23)
|
||||
* +------------------------- min (0 - 59)
|
||||
* @param string $hook The hook to trigger
|
||||
* @param array $args Arguments to pass when the hook triggers
|
||||
* @param string $group The group to assign this job to
|
||||
*
|
||||
* @return string The job ID
|
||||
*/
|
||||
function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '' ) {
|
||||
return ActionScheduler::factory()->cron( $hook, $args, $timestamp, $schedule, $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the next occurrence of a scheduled action.
|
||||
*
|
||||
* While only the next instance of a recurring or cron action is unscheduled by this method, that will also prevent
|
||||
* all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled in
|
||||
* a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled
|
||||
* only after the former action is run. If the next instance is never run, because it's unscheduled by this function,
|
||||
* then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled
|
||||
* by this method also.
|
||||
*
|
||||
* @param string $hook The hook that the job will trigger
|
||||
* @param array $args Args that would have been passed to the job
|
||||
* @param string $group
|
||||
*
|
||||
* @return string The scheduled action ID if a scheduled action was found, or empty string if no matching action found.
|
||||
*/
|
||||
function as_unschedule_action( $hook, $args = array(), $group = '' ) {
|
||||
$params = array();
|
||||
if ( is_array($args) ) {
|
||||
$params['args'] = $args;
|
||||
}
|
||||
if ( !empty($group) ) {
|
||||
$params['group'] = $group;
|
||||
}
|
||||
$job_id = ActionScheduler::store()->find_action( $hook, $params );
|
||||
|
||||
if ( ! empty( $job_id ) ) {
|
||||
ActionScheduler::store()->cancel_action( $job_id );
|
||||
}
|
||||
|
||||
return $job_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all occurrences of a scheduled action.
|
||||
*
|
||||
* @param string $hook The hook that the job will trigger
|
||||
* @param array $args Args that would have been passed to the job
|
||||
* @param string $group
|
||||
*/
|
||||
function as_unschedule_all_actions( $hook, $args = array(), $group = '' ) {
|
||||
do {
|
||||
$unscheduled_action = as_unschedule_action( $hook, $args, $group );
|
||||
} while ( ! empty( $unscheduled_action ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hook
|
||||
* @param array $args
|
||||
* @param string $group
|
||||
*
|
||||
* @return int|bool The timestamp for the next occurrence, or false if nothing was found
|
||||
*/
|
||||
function as_next_scheduled_action( $hook, $args = NULL, $group = '' ) {
|
||||
$params = array();
|
||||
if ( is_array($args) ) {
|
||||
$params['args'] = $args;
|
||||
}
|
||||
if ( !empty($group) ) {
|
||||
$params['group'] = $group;
|
||||
}
|
||||
$job_id = ActionScheduler::store()->find_action( $hook, $params );
|
||||
if ( empty($job_id) ) {
|
||||
return false;
|
||||
}
|
||||
$job = ActionScheduler::store()->fetch_action( $job_id );
|
||||
$next = $job->get_schedule()->next();
|
||||
if ( $next ) {
|
||||
return (int)($next->format('U'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find scheduled actions
|
||||
*
|
||||
* @param array $args Possible arguments, with their default values:
|
||||
* 'hook' => '' - the name of the action that will be triggered
|
||||
* 'args' => NULL - the args array that will be passed with the action
|
||||
* 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone.
|
||||
* 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='
|
||||
* 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone.
|
||||
* 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='
|
||||
* 'group' => '' - the group the action belongs to
|
||||
* 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING
|
||||
* 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID
|
||||
* 'per_page' => 5 - Number of results to return
|
||||
* 'offset' => 0
|
||||
* 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date'
|
||||
* 'order' => 'ASC'
|
||||
*
|
||||
* @param string $return_format OBJECT, ARRAY_A, or ids
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function as_get_scheduled_actions( $args = array(), $return_format = OBJECT ) {
|
||||
$store = ActionScheduler::store();
|
||||
foreach ( array('date', 'modified') as $key ) {
|
||||
if ( isset($args[$key]) ) {
|
||||
$args[$key] = as_get_datetime_object($args[$key]);
|
||||
}
|
||||
}
|
||||
$ids = $store->query_actions( $args );
|
||||
|
||||
if ( $return_format == 'ids' || $return_format == 'int' ) {
|
||||
return $ids;
|
||||
}
|
||||
|
||||
$actions = array();
|
||||
foreach ( $ids as $action_id ) {
|
||||
$actions[$action_id] = $store->fetch_action( $action_id );
|
||||
}
|
||||
|
||||
if ( $return_format == ARRAY_A ) {
|
||||
foreach ( $actions as $action_id => $action_object ) {
|
||||
$actions[$action_id] = get_object_vars($action_object);
|
||||
}
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create an instance of DateTime based on a given
|
||||
* string and timezone. By default, will return the current date/time
|
||||
* in the UTC timezone.
|
||||
*
|
||||
* Needed because new DateTime() called without an explicit timezone
|
||||
* will create a date/time in PHP's timezone, but we need to have
|
||||
* assurance that a date/time uses the right timezone (which we almost
|
||||
* always want to be UTC), which means we need to always include the
|
||||
* timezone when instantiating datetimes rather than leaving it up to
|
||||
* the PHP default.
|
||||
*
|
||||
* @param mixed $date_string A date/time string. Valid formats are explained in http://php.net/manual/en/datetime.formats.php
|
||||
* @param string $timezone A timezone identifier, like UTC or Europe/Lisbon. The list of valid identifiers is available http://php.net/manual/en/timezones.php
|
||||
*
|
||||
* @return ActionScheduler_DateTime
|
||||
*/
|
||||
function as_get_datetime_object( $date_string = null, $timezone = 'UTC' ) {
|
||||
if ( is_object( $date_string ) && $date_string instanceof DateTime ) {
|
||||
$date = new ActionScheduler_DateTime( $date_string->format( 'Y-m-d H:i:s' ), new DateTimeZone( $timezone ) );
|
||||
} elseif ( is_numeric( $date_string ) ) {
|
||||
$date = new ActionScheduler_DateTime( '@' . $date_string, new DateTimeZone( $timezone ) );
|
||||
} else {
|
||||
$date = new ActionScheduler_DateTime( $date_string, new DateTimeZone( $timezone ) );
|
||||
}
|
||||
return $date;
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* CRON expression parser that can determine whether or not a CRON expression is
|
||||
* due to run, the next run date and previous run date of a CRON expression.
|
||||
* The determinations made by this class are accurate if checked run once per
|
||||
* minute (seconds are dropped from date time comparisons).
|
||||
*
|
||||
* Schedule parts must map to:
|
||||
* minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week
|
||||
* [1-7|MON-SUN], and an optional year.
|
||||
*
|
||||
* @author Michael Dowling <mtdowling@gmail.com>
|
||||
* @link http://en.wikipedia.org/wiki/Cron
|
||||
*/
|
||||
class CronExpression
|
||||
{
|
||||
const MINUTE = 0;
|
||||
const HOUR = 1;
|
||||
const DAY = 2;
|
||||
const MONTH = 3;
|
||||
const WEEKDAY = 4;
|
||||
const YEAR = 5;
|
||||
|
||||
/**
|
||||
* @var array CRON expression parts
|
||||
*/
|
||||
private $cronParts;
|
||||
|
||||
/**
|
||||
* @var CronExpression_FieldFactory CRON field factory
|
||||
*/
|
||||
private $fieldFactory;
|
||||
|
||||
/**
|
||||
* @var array Order in which to test of cron parts
|
||||
*/
|
||||
private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE);
|
||||
|
||||
/**
|
||||
* Factory method to create a new CronExpression.
|
||||
*
|
||||
* @param string $expression The CRON expression to create. There are
|
||||
* several special predefined values which can be used to substitute the
|
||||
* CRON expression:
|
||||
*
|
||||
* @yearly, @annually) - Run once a year, midnight, Jan. 1 - 0 0 1 1 *
|
||||
* @monthly - Run once a month, midnight, first of month - 0 0 1 * *
|
||||
* @weekly - Run once a week, midnight on Sun - 0 0 * * 0
|
||||
* @daily - Run once a day, midnight - 0 0 * * *
|
||||
* @hourly - Run once an hour, first minute - 0 * * * *
|
||||
*
|
||||
*@param CronExpression_FieldFactory $fieldFactory (optional) Field factory to use
|
||||
*
|
||||
* @return CronExpression
|
||||
*/
|
||||
public static function factory($expression, CronExpression_FieldFactory $fieldFactory = null)
|
||||
{
|
||||
$mappings = array(
|
||||
'@yearly' => '0 0 1 1 *',
|
||||
'@annually' => '0 0 1 1 *',
|
||||
'@monthly' => '0 0 1 * *',
|
||||
'@weekly' => '0 0 * * 0',
|
||||
'@daily' => '0 0 * * *',
|
||||
'@hourly' => '0 * * * *'
|
||||
);
|
||||
|
||||
if (isset($mappings[$expression])) {
|
||||
$expression = $mappings[$expression];
|
||||
}
|
||||
|
||||
return new self($expression, $fieldFactory ? $fieldFactory : new CronExpression_FieldFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a CRON expression
|
||||
*
|
||||
* @param string $expression CRON expression (e.g. '8 * * * *')
|
||||
* @param CronExpression_FieldFactory $fieldFactory Factory to create cron fields
|
||||
*/
|
||||
public function __construct($expression, CronExpression_FieldFactory $fieldFactory)
|
||||
{
|
||||
$this->fieldFactory = $fieldFactory;
|
||||
$this->setExpression($expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or change the CRON expression
|
||||
*
|
||||
* @param string $value CRON expression (e.g. 8 * * * *)
|
||||
*
|
||||
* @return CronExpression
|
||||
* @throws InvalidArgumentException if not a valid CRON expression
|
||||
*/
|
||||
public function setExpression($value)
|
||||
{
|
||||
$this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
|
||||
if (count($this->cronParts) < 5) {
|
||||
throw new InvalidArgumentException(
|
||||
$value . ' is not a valid CRON expression'
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($this->cronParts as $position => $part) {
|
||||
$this->setPart($position, $part);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set part of the CRON expression
|
||||
*
|
||||
* @param int $position The position of the CRON expression to set
|
||||
* @param string $value The value to set
|
||||
*
|
||||
* @return CronExpression
|
||||
* @throws InvalidArgumentException if the value is not valid for the part
|
||||
*/
|
||||
public function setPart($position, $value)
|
||||
{
|
||||
if (!$this->fieldFactory->getField($position)->validate($value)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid CRON field value ' . $value . ' as position ' . $position
|
||||
);
|
||||
}
|
||||
|
||||
$this->cronParts[$position] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a next run date relative to the current date or a specific date
|
||||
*
|
||||
* @param string|DateTime $currentTime (optional) Relative calculation date
|
||||
* @param int $nth (optional) Number of matches to skip before returning a
|
||||
* matching next run date. 0, the default, will return the current
|
||||
* date and time if the next run date falls on the current date and
|
||||
* time. Setting this value to 1 will skip the first match and go to
|
||||
* the second match. Setting this value to 2 will skip the first 2
|
||||
* matches and so on.
|
||||
* @param bool $allowCurrentDate (optional) Set to TRUE to return the
|
||||
* current date if it matches the cron expression
|
||||
*
|
||||
* @return DateTime
|
||||
* @throws RuntimeException on too many iterations
|
||||
*/
|
||||
public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
|
||||
{
|
||||
return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a previous run date relative to the current date or a specific date
|
||||
*
|
||||
* @param string|DateTime $currentTime (optional) Relative calculation date
|
||||
* @param int $nth (optional) Number of matches to skip before returning
|
||||
* @param bool $allowCurrentDate (optional) Set to TRUE to return the
|
||||
* current date if it matches the cron expression
|
||||
*
|
||||
* @return DateTime
|
||||
* @throws RuntimeException on too many iterations
|
||||
* @see CronExpression::getNextRunDate
|
||||
*/
|
||||
public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
|
||||
{
|
||||
return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple run dates starting at the current date or a specific date
|
||||
*
|
||||
* @param int $total Set the total number of dates to calculate
|
||||
* @param string|DateTime $currentTime (optional) Relative calculation date
|
||||
* @param bool $invert (optional) Set to TRUE to retrieve previous dates
|
||||
* @param bool $allowCurrentDate (optional) Set to TRUE to return the
|
||||
* current date if it matches the cron expression
|
||||
*
|
||||
* @return array Returns an array of run dates
|
||||
*/
|
||||
public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false)
|
||||
{
|
||||
$matches = array();
|
||||
for ($i = 0; $i < max(0, $total); $i++) {
|
||||
$matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate);
|
||||
}
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all or part of the CRON expression
|
||||
*
|
||||
* @param string $part (optional) Specify the part to retrieve or NULL to
|
||||
* get the full cron schedule string.
|
||||
*
|
||||
* @return string|null Returns the CRON expression, a part of the
|
||||
* CRON expression, or NULL if the part was specified but not found
|
||||
*/
|
||||
public function getExpression($part = null)
|
||||
{
|
||||
if (null === $part) {
|
||||
return implode(' ', $this->cronParts);
|
||||
} elseif (array_key_exists($part, $this->cronParts)) {
|
||||
return $this->cronParts[$part];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to output the full expression.
|
||||
*
|
||||
* @return string Full CRON expression
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the cron is due to run based on the current date or a
|
||||
* specific date. This method assumes that the current number of
|
||||
* seconds are irrelevant, and should be called once per minute.
|
||||
*
|
||||
* @param string|DateTime $currentTime (optional) Relative calculation date
|
||||
*
|
||||
* @return bool Returns TRUE if the cron is due to run or FALSE if not
|
||||
*/
|
||||
public function isDue($currentTime = 'now')
|
||||
{
|
||||
if ('now' === $currentTime) {
|
||||
$currentDate = date('Y-m-d H:i');
|
||||
$currentTime = strtotime($currentDate);
|
||||
} elseif ($currentTime instanceof DateTime) {
|
||||
$currentDate = $currentTime->format('Y-m-d H:i');
|
||||
$currentTime = strtotime($currentDate);
|
||||
} else {
|
||||
$currentTime = new DateTime($currentTime);
|
||||
$currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0);
|
||||
$currentDate = $currentTime->format('Y-m-d H:i');
|
||||
$currentTime = (int)($currentTime->getTimestamp());
|
||||
}
|
||||
|
||||
return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next or previous run date of the expression relative to a date
|
||||
*
|
||||
* @param string|DateTime $currentTime (optional) Relative calculation date
|
||||
* @param int $nth (optional) Number of matches to skip before returning
|
||||
* @param bool $invert (optional) Set to TRUE to go backwards in time
|
||||
* @param bool $allowCurrentDate (optional) Set to TRUE to return the
|
||||
* current date if it matches the cron expression
|
||||
*
|
||||
* @return DateTime
|
||||
* @throws RuntimeException on too many iterations
|
||||
*/
|
||||
protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false)
|
||||
{
|
||||
if ($currentTime instanceof DateTime) {
|
||||
$currentDate = $currentTime;
|
||||
} else {
|
||||
$currentDate = new DateTime($currentTime ? $currentTime : 'now');
|
||||
$currentDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
|
||||
}
|
||||
|
||||
$currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0);
|
||||
$nextRun = clone $currentDate;
|
||||
$nth = (int) $nth;
|
||||
|
||||
// Set a hard limit to bail on an impossible date
|
||||
for ($i = 0; $i < 1000; $i++) {
|
||||
|
||||
foreach (self::$order as $position) {
|
||||
$part = $this->getExpression($position);
|
||||
if (null === $part) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$satisfied = false;
|
||||
// Get the field object used to validate this part
|
||||
$field = $this->fieldFactory->getField($position);
|
||||
// Check if this is singular or a list
|
||||
if (strpos($part, ',') === false) {
|
||||
$satisfied = $field->isSatisfiedBy($nextRun, $part);
|
||||
} else {
|
||||
foreach (array_map('trim', explode(',', $part)) as $listPart) {
|
||||
if ($field->isSatisfiedBy($nextRun, $listPart)) {
|
||||
$satisfied = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the field is not satisfied, then start over
|
||||
if (!$satisfied) {
|
||||
$field->increment($nextRun, $invert);
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip this match if needed
|
||||
if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) {
|
||||
$this->fieldFactory->getField(0)->increment($nextRun, $invert);
|
||||
continue;
|
||||
}
|
||||
|
||||
return $nextRun;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new RuntimeException('Impossible CRON expression');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Abstract CRON expression field
|
||||
*
|
||||
* @author Michael Dowling <mtdowling@gmail.com>
|
||||
*/
|
||||
abstract class CronExpression_AbstractField implements CronExpression_FieldInterface
|
||||
{
|
||||
/**
|
||||
* Check to see if a field is satisfied by a value
|
||||
*
|
||||
* @param string $dateValue Date value to check
|
||||
* @param string $value Value to test
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSatisfied($dateValue, $value)
|
||||
{
|
||||
if ($this->isIncrementsOfRanges($value)) {
|
||||
return $this->isInIncrementsOfRanges($dateValue, $value);
|
||||
} elseif ($this->isRange($value)) {
|
||||
return $this->isInRange($dateValue, $value);
|
||||
}
|
||||
|
||||
return $value == '*' || $dateValue == $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a range
|
||||
*
|
||||
* @param string $value Value to test
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isRange($value)
|
||||
{
|
||||
return strpos($value, '-') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is an increments of ranges
|
||||
*
|
||||
* @param string $value Value to test
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isIncrementsOfRanges($value)
|
||||
{
|
||||
return strpos($value, '/') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a value is within a range
|
||||
*
|
||||
* @param string $dateValue Set date value
|
||||
* @param string $value Value to test
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInRange($dateValue, $value)
|
||||
{
|
||||
$parts = array_map('trim', explode('-', $value, 2));
|
||||
|
||||
return $dateValue >= $parts[0] && $dateValue <= $parts[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a value is within an increments of ranges (offset[-to]/step size)
|
||||
*
|
||||
* @param string $dateValue Set date value
|
||||
* @param string $value Value to test
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInIncrementsOfRanges($dateValue, $value)
|
||||
{
|
||||
$parts = array_map('trim', explode('/', $value, 2));
|
||||
$stepSize = isset($parts[1]) ? $parts[1] : 0;
|
||||
if ($parts[0] == '*' || $parts[0] === '0') {
|
||||
return (int) $dateValue % $stepSize == 0;
|
||||
}
|
||||
|
||||
$range = explode('-', $parts[0], 2);
|
||||
$offset = $range[0];
|
||||
$to = isset($range[1]) ? $range[1] : $dateValue;
|
||||
// Ensure that the date value is within the range
|
||||
if ($dateValue < $offset || $dateValue > $to) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ($i = $offset; $i <= $to; $i+= $stepSize) {
|
||||
if ($i == $dateValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Day of month field. Allows: * , / - ? L W
|
||||
*
|
||||
* 'L' stands for "last" and specifies the last day of the month.
|
||||
*
|
||||
* The 'W' character is used to specify the weekday (Monday-Friday) nearest the
|
||||
* given day. As an example, if you were to specify "15W" as the value for the
|
||||
* day-of-month field, the meaning is: "the nearest weekday to the 15th of the
|
||||
* month". So if the 15th is a Saturday, the trigger will fire on Friday the
|
||||
* 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If
|
||||
* the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you
|
||||
* specify "1W" as the value for day-of-month, and the 1st is a Saturday, the
|
||||
* trigger will fire on Monday the 3rd, as it will not 'jump' over the boundary
|
||||
* of a month's days. The 'W' character can only be specified when the
|
||||
* day-of-month is a single day, not a range or list of days.
|
||||
*
|
||||
* @author Michael Dowling <mtdowling@gmail.com>
|
||||
*/
|
||||
class CronExpression_DayOfMonthField extends CronExpression_AbstractField
|
||||
{
|
||||
/**
|
||||
* Get the nearest day of the week for a given day in a month
|
||||
*
|
||||
* @param int $currentYear Current year
|
||||
* @param int $currentMonth Current month
|
||||
* @param int $targetDay Target day of the month
|
||||
*
|
||||
* @return DateTime Returns the nearest date
|
||||
*/
|
||||
private static function getNearestWeekday($currentYear, $currentMonth, $targetDay)
|
||||
{
|
||||
$tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT);
|
||||
$target = new DateTime("$currentYear-$currentMonth-$tday");
|
||||
$currentWeekday = (int) $target->format('N');
|
||||
|
||||
if ($currentWeekday < 6) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
$lastDayOfMonth = $target->format('t');
|
||||
|
||||
foreach (array(-1, 1, -2, 2) as $i) {
|
||||
$adjusted = $targetDay + $i;
|
||||
if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) {
|
||||
$target->setDate($currentYear, $currentMonth, $adjusted);
|
||||
if ($target->format('N') < 6 && $target->format('m') == $currentMonth) {
|
||||
return $target;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isSatisfiedBy(DateTime $date, $value)
|
||||
{
|
||||
// ? states that the field value is to be skipped
|
||||
if ($value == '?') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fieldValue = $date->format('d');
|
||||
|
||||
// Check to see if this is the last day of the month
|
||||
if ($value == 'L') {
|
||||
return $fieldValue == $date->format('t');
|
||||
}
|
||||
|
||||
// Check to see if this is the nearest weekday to a particular value
|
||||
if (strpos($value, 'W')) {
|
||||
// Parse the target day
|
||||
$targetDay = substr($value, 0, strpos($value, 'W'));
|
||||
// Find out if the current day is the nearest day of the week
|
||||
return $date->format('j') == self::getNearestWeekday(
|
||||
$date->format('Y'),
|
||||
$date->format('m'),
|
||||
$targetDay
|
||||
)->format('j');
|
||||
}
|
||||
|
||||
return $this->isSatisfied($date->format('d'), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function increment(DateTime $date, $invert = false)
|
||||
{
|
||||
if ($invert) {
|
||||
$date->modify('previous day');
|
||||
$date->setTime(23, 59);
|
||||
} else {
|
||||
$date->modify('next day');
|
||||
$date->setTime(0, 0);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value)
|
||||
{
|
||||
return (bool) preg_match('/[\*,\/\-\?LW0-9A-Za-z]+/', $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Day of week field. Allows: * / , - ? L #
|
||||
*
|
||||
* Days of the week can be represented as a number 0-7 (0|7 = Sunday)
|
||||
* or as a three letter string: SUN, MON, TUE, WED, THU, FRI, SAT.
|
||||
*
|
||||
* 'L' stands for "last". It allows you to specify constructs such as
|
||||
* "the last Friday" of a given month.
|
||||
*
|
||||
* '#' is allowed for the day-of-week field, and must be followed by a
|
||||
* number between one and five. It allows you to specify constructs such as
|
||||
* "the second Friday" of a given month.
|
||||
*
|
||||
* @author Michael Dowling <mtdowling@gmail.com>
|
||||
*/
|
||||
class CronExpression_DayOfWeekField extends CronExpression_AbstractField
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isSatisfiedBy(DateTime $date, $value)
|
||||
{
|
||||
if ($value == '?') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convert text day of the week values to integers
|
||||
$value = str_ireplace(
|
||||
array('SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'),
|
||||
range(0, 6),
|
||||
$value
|
||||
);
|
||||
|
||||
$currentYear = $date->format('Y');
|
||||
$currentMonth = $date->format('m');
|
||||
$lastDayOfMonth = $date->format('t');
|
||||
|
||||
// Find out if this is the last specific weekday of the month
|
||||
if (strpos($value, 'L')) {
|
||||
$weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L')));
|
||||
$tdate = clone $date;
|
||||
$tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth);
|
||||
while ($tdate->format('w') != $weekday) {
|
||||
$tdate->setDate($currentYear, $currentMonth, --$lastDayOfMonth);
|
||||
}
|
||||
|
||||
return $date->format('j') == $lastDayOfMonth;
|
||||
}
|
||||
|
||||
// Handle # hash tokens
|
||||
if (strpos($value, '#')) {
|
||||
list($weekday, $nth) = explode('#', $value);
|
||||
// Validate the hash fields
|
||||
if ($weekday < 1 || $weekday > 5) {
|
||||
throw new InvalidArgumentException("Weekday must be a value between 1 and 5. {$weekday} given");
|
||||
}
|
||||
if ($nth > 5) {
|
||||
throw new InvalidArgumentException('There are never more than 5 of a given weekday in a month');
|
||||
}
|
||||
// The current weekday must match the targeted weekday to proceed
|
||||
if ($date->format('N') != $weekday) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tdate = clone $date;
|
||||
$tdate->setDate($currentYear, $currentMonth, 1);
|
||||
$dayCount = 0;
|
||||
$currentDay = 1;
|
||||
while ($currentDay < $lastDayOfMonth + 1) {
|
||||
if ($tdate->format('N') == $weekday) {
|
||||
if (++$dayCount >= $nth) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$tdate->setDate($currentYear, $currentMonth, ++$currentDay);
|
||||
}
|
||||
|
||||
return $date->format('j') == $currentDay;
|
||||
}
|
||||
|
||||
// Handle day of the week values
|
||||
if (strpos($value, '-')) {
|
||||
$parts = explode('-', $value);
|
||||
if ($parts[0] == '7') {
|
||||
$parts[0] = '0';
|
||||
} elseif ($parts[1] == '0') {
|
||||
$parts[1] = '7';
|
||||
}
|
||||
$value = implode('-', $parts);
|
||||
}
|
||||
|
||||
// Test to see which Sunday to use -- 0 == 7 == Sunday
|
||||
$format = in_array(7, str_split($value)) ? 'N' : 'w';
|
||||
$fieldValue = $date->format($format);
|
||||
|
||||
return $this->isSatisfied($fieldValue, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function increment(DateTime $date, $invert = false)
|
||||
{
|
||||
if ($invert) {
|
||||
$date->modify('-1 day');
|
||||
$date->setTime(23, 59, 0);
|
||||
} else {
|
||||
$date->modify('+1 day');
|
||||
$date->setTime(0, 0, 0);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value)
|
||||
{
|
||||
return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* CRON field factory implementing a flyweight factory
|
||||
*
|
||||
* @author Michael Dowling <mtdowling@gmail.com>
|
||||
* @link http://en.wikipedia.org/wiki/Cron
|
||||
*/
|
||||
class CronExpression_FieldFactory
|
||||
{
|
||||
/**
|
||||
* @var array Cache of instantiated fields
|
||||
*/
|
||||
private $fields = array();
|
||||
|
||||
/**
|
||||
* Get an instance of a field object for a cron expression position
|
||||
*
|
||||
* @param int $position CRON expression position value to retrieve
|
||||
*
|
||||
* @return CronExpression_FieldInterface
|
||||
* @throws InvalidArgumentException if a position is not valid
|
||||
*/
|
||||
public function getField($position)
|
||||
{
|
||||
if (!isset($this->fields[$position])) {
|
||||
switch ($position) {
|
||||
case 0:
|
||||
$this->fields[$position] = new CronExpression_MinutesField();
|
||||
break;
|
||||
case 1:
|
||||
$this->fields[$position] = new CronExpression_HoursField();
|
||||
break;
|
||||
case 2:
|
||||
$this->fields[$position] = new CronExpression_DayOfMonthField();
|
||||
break;
|
||||
case 3:
|
||||
$this->fields[$position] = new CronExpression_MonthField();
|
||||
break;
|
||||
case 4:
|
||||
$this->fields[$position] = new CronExpression_DayOfWeekField();
|
||||
break;
|
||||
case 5:
|
||||
$this->fields[$position] = new CronExpression_YearField();
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException(
|
||||
$position . ' is not a valid position'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->fields[$position];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* CRON field interface
|
||||
*
|
||||
* @author Michael Dowling <mtdowling@gmail.com>
|
||||
*/
|
||||
interface CronExpression_FieldInterface
|
||||
{
|
||||
/**
|
||||
* Check if the respective value of a DateTime field satisfies a CRON exp
|
||||
*
|
||||
* @param DateTime $date DateTime object to check
|
||||
* @param string $value CRON expression to test against
|
||||
*
|
||||
* @return bool Returns TRUE if satisfied, FALSE otherwise
|
||||
*/
|
||||
public function isSatisfiedBy(DateTime $date, $value);
|
||||
|
||||
/**
|
||||
* When a CRON expression is not satisfied, this method is used to increment
|
||||
* or decrement a DateTime object by the unit of the cron field
|
||||
*
|
||||
* @param DateTime $date DateTime object to change
|
||||
* @param bool $invert (optional) Set to TRUE to decrement
|
||||
*
|
||||
* @return CronExpression_FieldInterface
|
||||
*/
|
||||
public function increment(DateTime $date, $invert = false);
|
||||
|
||||
/**
|
||||
* Validates a CRON expression for a given field
|
||||
*
|
||||
* @param string $value CRON expression value to validate
|
||||
*
|
||||
* @return bool Returns TRUE if valid, FALSE otherwise
|
||||
*/
|
||||
public function validate($value);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Hours field. Allows: * , / -
|
||||
*
|
||||
* @author Michael Dowling <mtdowling@gmail.com>
|
||||
*/
|
||||
class CronExpression_HoursField extends CronExpression_AbstractField
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isSatisfiedBy(DateTime $date, $value)
|
||||
{
|
||||
return $this->isSatisfied($date->format('H'), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function increment(DateTime $date, $invert = false)
|
||||
{
|
||||
// Change timezone to UTC temporarily. This will
|
||||
// allow us to go back or forwards and hour even
|
||||
// if DST will be changed between the hours.
|
||||
$timezone = $date->getTimezone();
|
||||
$date->setTimezone(new DateTimeZone('UTC'));
|
||||
if ($invert) {
|
||||
$date->modify('-1 hour');
|
||||
$date->setTime($date->format('H'), 59);
|
||||
} else {
|
||||
$date->modify('+1 hour');
|
||||
$date->setTime($date->format('H'), 0);
|
||||
}
|
||||
$date->setTimezone($timezone);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value)
|
||||
{
|
||||
return (bool) preg_match('/[\*,\/\-0-9]+/', $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Minutes field. Allows: * , / -
|
||||
*
|
||||
* @author Michael Dowling <mtdowling@gmail.com>
|
||||
*/
|
||||
class CronExpression_MinutesField extends CronExpression_AbstractField
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isSatisfiedBy(DateTime $date, $value)
|
||||
{
|
||||
return $this->isSatisfied($date->format('i'), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function increment(DateTime $date, $invert = false)
|
||||
{
|
||||
if ($invert) {
|
||||
$date->modify('-1 minute');
|
||||
} else {
|
||||
$date->modify('+1 minute');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value)
|
||||
{
|
||||
return (bool) preg_match('/[\*,\/\-0-9]+/', $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Month field. Allows: * , / -
|
||||
*
|
||||
* @author Michael Dowling <mtdowling@gmail.com>
|
||||
*/
|
||||
class CronExpression_MonthField extends CronExpression_AbstractField
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isSatisfiedBy(DateTime $date, $value)
|
||||
{
|
||||
// Convert text month values to integers
|
||||
$value = str_ireplace(
|
||||
array(
|
||||
'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
|
||||
'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'
|
||||
),
|
||||
range(1, 12),
|
||||
$value
|
||||
);
|
||||
|
||||
return $this->isSatisfied($date->format('m'), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function increment(DateTime $date, $invert = false)
|
||||
{
|
||||
if ($invert) {
|
||||
// $date->modify('last day of previous month'); // remove for php 5.2 compat
|
||||
$date->modify('previous month');
|
||||
$date->modify($date->format('Y-m-t'));
|
||||
$date->setTime(23, 59);
|
||||
} else {
|
||||
//$date->modify('first day of next month'); // remove for php 5.2 compat
|
||||
$date->modify('next month');
|
||||
$date->modify($date->format('Y-m-01'));
|
||||
$date->setTime(0, 0);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value)
|
||||
{
|
||||
return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Year field. Allows: * , / -
|
||||
*
|
||||
* @author Michael Dowling <mtdowling@gmail.com>
|
||||
*/
|
||||
class CronExpression_YearField extends CronExpression_AbstractField
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isSatisfiedBy(DateTime $date, $value)
|
||||
{
|
||||
return $this->isSatisfied($date->format('Y'), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function increment(DateTime $date, $invert = false)
|
||||
{
|
||||
if ($invert) {
|
||||
$date->modify('-1 year');
|
||||
$date->setDate($date->format('Y'), 12, 31);
|
||||
$date->setTime(23, 59, 0);
|
||||
} else {
|
||||
$date->modify('+1 year');
|
||||
$date->setDate($date->format('Y'), 1, 1);
|
||||
$date->setTime(0, 0, 0);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value)
|
||||
{
|
||||
return (bool) preg_match('/[\*,\/\-0-9]+/', $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2011 Michael Dowling <mtdowling@gmail.com> and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
Reference in New Issue
Block a user