From eeb44ca9a2ff271e01b95fb50f328035305846b7 Mon Sep 17 00:00:00 2001 From: Coen Jacobs Date: Wed, 11 Oct 2017 17:37:36 +0200 Subject: [PATCH 01/65] Suggest to install Mozart to prefix wrap the library --- composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/composer.json b/composer.json index e7341b1..29fa36e 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,9 @@ "require": { "php": ">=5.2" }, + "suggest": { + "coenjacobs/mozart": "Easily wrap this library with your own prefix, to prevent collisions when multiple plugins use this library" + }, "license": "GPLv2+", "authors": [ { From 565d159b0a8c0ad974899a5ed855993aa607b2b4 Mon Sep 17 00:00:00 2001 From: Coen Jacobs Date: Wed, 11 Oct 2017 17:43:24 +0200 Subject: [PATCH 02/65] Updated README to explain proper install through Composer --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 04d74cc..bc8bba3 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,18 @@ Inspired by [TechCrunch WP Asynchronous Tasks](https://github.com/techcrunch/wp- __Requires PHP 5.2+__ +## Install + +The recommended way to install this library in your project is by loading it through Composer: + +``` +composer require a5hleyrich/wp-background-processing +``` + +It is highly recommended to prefix wrap the library class files using [the Mozart package](https://packagist.org/packages/coenjacobs/mozart), to prevent collisions with other projects using this same library. + +## Usage + ### Async Request Async requests are useful for pushing slow one-off tasks such as sending emails to a background process. Once the request has been dispatched it will process in the background instantly. From 646c094dda8fbf2699edc51ad4f50b33190e9373 Mon Sep 17 00:00:00 2001 From: Mark Jaquith Date: Fri, 6 Jul 2018 12:33:12 -0400 Subject: [PATCH 03/65] Add a note about process instantiation Instantiation should happen unconditionally. If you only instantiate on-demand when you're pushing to the queue, it won't work. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 04d74cc..65f3bb7 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,8 @@ Instantiate your process: `$this->example_process = new WP_Example_Process();` +**Note:** You must instantiate your process unconditionally. All requests should do this, even if nothing is pushed to the queue. + Push items to the queue: ```php From 7729affd987535575eb05dc9cfe1a4f149b170c8 Mon Sep 17 00:00:00 2001 From: Boone B Gorges Date: Thu, 19 Jul 2018 21:54:44 -0500 Subject: [PATCH 04/65] Add filters for `wp_remote_post()` arguments. --- classes/wp-async-request.php | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/classes/wp-async-request.php b/classes/wp-async-request.php index f1bef83..c82b379 100644 --- a/classes/wp-async-request.php +++ b/classes/wp-async-request.php @@ -97,10 +97,17 @@ protected function get_query_args() { return $this->query_args; } - return array( + $args = array( 'action' => $this->identifier, 'nonce' => wp_create_nonce( $this->identifier ), ); + + /** + * Filters the post arguments used during an async request. + * + * @param array $url + */ + return apply_filters( $this->identifier . '_query_args', $args ); } /** @@ -113,7 +120,14 @@ protected function get_query_url() { return $this->query_url; } - return admin_url( 'admin-ajax.php' ); + $url = admin_url( 'admin-ajax.php' ); + + /** + * Filters the post arguments used during an async request. + * + * @param string $url + */ + return apply_filters( $this->identifier . '_query_url', $url ); } /** @@ -126,13 +140,20 @@ protected function get_post_args() { return $this->post_args; } - return array( + $args = array( 'timeout' => 0.01, 'blocking' => false, 'body' => $this->data, 'cookies' => $_COOKIE, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), ); + + /** + * Filters the post arguments used during an async request. + * + * @param array $args + */ + return apply_filters( $this->identifier . '_post_args', $args ); } /** From 36cea4c11fff53a774ced609b9ea6fafb830611c Mon Sep 17 00:00:00 2001 From: Iain Date: Thu, 13 Feb 2020 15:24:00 +0000 Subject: [PATCH 05/65] Ignore PhpStorm directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 57872d0..f45219c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /vendor/ +/.idea \ No newline at end of file From c8db1b9061904fb30e3d0d06430cb265b7ef829d Mon Sep 17 00:00:00 2001 From: Iain Date: Thu, 13 Feb 2020 15:24:30 +0000 Subject: [PATCH 06/65] Update composer file with deliciousbrains vendor and author details. --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 42956d6..15274c7 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "a5hleyrich/wp-background-processing", + "name": "deliciousbrains/wp-background-processing", "description": "WP Background Processing can be used to fire off non-blocking asynchronous requests or as a background processing tool, allowing you to queue tasks.", "type": "library", "require": { @@ -8,8 +8,8 @@ "license": "GPL-2.0-only", "authors": [ { - "name": "Ashley Rich", - "email": "hello@ashleyrich.com" + "name": "Delicious Brains", + "email": "nom@deliciousbrains.com" } ], "autoload": { From b082e78afc40b4d0c2128182b6282375bdad564a Mon Sep 17 00:00:00 2001 From: Iain Date: Thu, 30 Jul 2020 17:36:49 +0100 Subject: [PATCH 07/65] Fixed composer.json license to be GPL 2+, fixes https://github.com/deliciousbrains/wp-background-processing/issues/76 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 15274c7..4c3e4e7 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "require": { "php": ">=5.2" }, - "license": "GPL-2.0-only", + "license": "GPL-2.0-or-later", "authors": [ { "name": "Delicious Brains", From 00291be9eac4a5eb89bebe89c128f81783fc1b0d Mon Sep 17 00:00:00 2001 From: Iain Date: Thu, 30 Jul 2020 18:18:07 +0100 Subject: [PATCH 08/65] Update vendor name in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca32771..fc09783 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ __Requires PHP 5.2+__ The recommended way to install this library in your project is by loading it through Composer: ``` -composer require a5hleyrich/wp-background-processing +composer require deliciousbrains/wp-background-processing ``` It is highly recommended to prefix wrap the library class files using [the Mozart package](https://packagist.org/packages/coenjacobs/mozart), to prevent collisions with other projects using this same library. From 1ad6f269f6e2c760e6857f157391c0c6c1ef447d Mon Sep 17 00:00:00 2001 From: Iain Date: Thu, 30 Jul 2020 18:22:32 +0100 Subject: [PATCH 09/65] Move the checking if classes exists to the plugin bootstrap file, instead of in the class files themselves. --- classes/wp-async-request.php | 269 +++++----- classes/wp-background-process.php | 813 +++++++++++++++--------------- wp-background-processing.php | 8 +- 3 files changed, 545 insertions(+), 545 deletions(-) diff --git a/classes/wp-async-request.php b/classes/wp-async-request.php index f1bef83..dbfa129 100644 --- a/classes/wp-async-request.php +++ b/classes/wp-async-request.php @@ -5,159 +5,156 @@ * @package WP-Background-Processing */ -if ( ! class_exists( 'WP_Async_Request' ) ) { +/** + * Abstract WP_Async_Request class. + * + * @abstract + */ +abstract class WP_Async_Request { /** - * Abstract WP_Async_Request class. + * Prefix + * + * (default value: 'wp') * - * @abstract + * @var string + * @access protected */ - abstract class WP_Async_Request { - - /** - * Prefix - * - * (default value: 'wp') - * - * @var string - * @access protected - */ - protected $prefix = 'wp'; - - /** - * Action - * - * (default value: 'async_request') - * - * @var string - * @access protected - */ - protected $action = 'async_request'; - - /** - * Identifier - * - * @var mixed - * @access protected - */ - protected $identifier; - - /** - * Data - * - * (default value: array()) - * - * @var array - * @access protected - */ - protected $data = array(); - - /** - * Initiate new async request - */ - public function __construct() { - $this->identifier = $this->prefix . '_' . $this->action; - - add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); - add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); - } + protected $prefix = 'wp'; - /** - * Set data used during the request - * - * @param array $data Data. - * - * @return $this - */ - public function data( $data ) { - $this->data = $data; - - return $this; - } + /** + * Action + * + * (default value: 'async_request') + * + * @var string + * @access protected + */ + protected $action = 'async_request'; - /** - * Dispatch the async request - * - * @return array|WP_Error - */ - public function dispatch() { - $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); - $args = $this->get_post_args(); + /** + * Identifier + * + * @var mixed + * @access protected + */ + protected $identifier; - return wp_remote_post( esc_url_raw( $url ), $args ); - } + /** + * Data + * + * (default value: array()) + * + * @var array + * @access protected + */ + protected $data = array(); - /** - * Get query args - * - * @return array - */ - protected function get_query_args() { - if ( property_exists( $this, 'query_args' ) ) { - return $this->query_args; - } - - return array( - 'action' => $this->identifier, - 'nonce' => wp_create_nonce( $this->identifier ), - ); - } + /** + * Initiate new async request + */ + public function __construct() { + $this->identifier = $this->prefix . '_' . $this->action; - /** - * Get query URL - * - * @return string - */ - protected function get_query_url() { - if ( property_exists( $this, 'query_url' ) ) { - return $this->query_url; - } - - return admin_url( 'admin-ajax.php' ); - } + add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); + add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); + } + + /** + * Set data used during the request + * + * @param array $data Data. + * + * @return $this + */ + public function data( $data ) { + $this->data = $data; + + return $this; + } - /** - * Get post args - * - * @return array - */ - protected function get_post_args() { - if ( property_exists( $this, 'post_args' ) ) { - return $this->post_args; - } - - return array( - 'timeout' => 0.01, - 'blocking' => false, - 'body' => $this->data, - 'cookies' => $_COOKIE, - 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), - ); + /** + * Dispatch the async request + * + * @return array|WP_Error + */ + public function dispatch() { + $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); + $args = $this->get_post_args(); + + return wp_remote_post( esc_url_raw( $url ), $args ); + } + + /** + * Get query args + * + * @return array + */ + protected function get_query_args() { + if ( property_exists( $this, 'query_args' ) ) { + return $this->query_args; } - /** - * Maybe handle - * - * Check for correct nonce and pass to handler. - */ - public function maybe_handle() { - // Don't lock up other requests while processing - session_write_close(); + return array( + 'action' => $this->identifier, + 'nonce' => wp_create_nonce( $this->identifier ), + ); + } - check_ajax_referer( $this->identifier, 'nonce' ); + /** + * Get query URL + * + * @return string + */ + protected function get_query_url() { + if ( property_exists( $this, 'query_url' ) ) { + return $this->query_url; + } - $this->handle(); + return admin_url( 'admin-ajax.php' ); + } - wp_die(); + /** + * Get post args + * + * @return array + */ + protected function get_post_args() { + if ( property_exists( $this, 'post_args' ) ) { + return $this->post_args; } - /** - * Handle - * - * Override this method to perform any actions required - * during the async request. - */ - abstract protected function handle(); + return array( + 'timeout' => 0.01, + 'blocking' => false, + 'body' => $this->data, + 'cookies' => $_COOKIE, + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + ); + } + + /** + * Maybe handle + * + * Check for correct nonce and pass to handler. + */ + public function maybe_handle() { + // Don't lock up other requests while processing + session_write_close(); + check_ajax_referer( $this->identifier, 'nonce' ); + + $this->handle(); + + wp_die(); } + + /** + * Handle + * + * Override this method to perform any actions required + * during the async request. + */ + abstract protected function handle(); + } diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 03e3a3e..5f412c0 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -5,274 +5,272 @@ * @package WP-Background-Processing */ -if ( ! class_exists( 'WP_Background_Process' ) ) { +/** + * Abstract WP_Background_Process class. + * + * @abstract + * @extends WP_Async_Request + */ +abstract class WP_Background_Process extends WP_Async_Request { /** - * Abstract WP_Background_Process class. + * Action * - * @abstract - * @extends WP_Async_Request + * (default value: 'background_process') + * + * @var string + * @access protected */ - abstract class WP_Background_Process extends WP_Async_Request { - - /** - * Action - * - * (default value: 'background_process') - * - * @var string - * @access protected - */ - protected $action = 'background_process'; - - /** - * Start time of current process. - * - * (default value: 0) - * - * @var int - * @access protected - */ - protected $start_time = 0; - - /** - * Cron_hook_identifier - * - * @var mixed - * @access protected - */ - protected $cron_hook_identifier; - - /** - * Cron_interval_identifier - * - * @var mixed - * @access protected - */ - protected $cron_interval_identifier; - - /** - * Initiate new background process - */ - public function __construct() { - parent::__construct(); - - $this->cron_hook_identifier = $this->identifier . '_cron'; - $this->cron_interval_identifier = $this->identifier . '_cron_interval'; - - add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); - add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); - } + protected $action = 'background_process'; - /** - * Dispatch - * - * @access public - * @return void - */ - public function dispatch() { - // Schedule the cron healthcheck. - $this->schedule_event(); - - // Perform remote post. - return parent::dispatch(); - } + /** + * Start time of current process. + * + * (default value: 0) + * + * @var int + * @access protected + */ + protected $start_time = 0; - /** - * Push to queue - * - * @param mixed $data Data. - * - * @return $this - */ - public function push_to_queue( $data ) { - $this->data[] = $data; - - return $this; - } + /** + * Cron_hook_identifier + * + * @var mixed + * @access protected + */ + protected $cron_hook_identifier; - /** - * Save queue - * - * @return $this - */ - public function save() { - $key = $this->generate_key(); + /** + * Cron_interval_identifier + * + * @var mixed + * @access protected + */ + protected $cron_interval_identifier; - if ( ! empty( $this->data ) ) { - update_site_option( $key, $this->data ); - } + /** + * Initiate new background process + */ + public function __construct() { + parent::__construct(); - return $this; - } + $this->cron_hook_identifier = $this->identifier . '_cron'; + $this->cron_interval_identifier = $this->identifier . '_cron_interval'; - /** - * Update queue - * - * @param string $key Key. - * @param array $data Data. - * - * @return $this - */ - public function update( $key, $data ) { - if ( ! empty( $data ) ) { - update_site_option( $key, $data ); - } + add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); + add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); + } - return $this; - } + /** + * Dispatch + * + * @access public + * @return void + */ + public function dispatch() { + // Schedule the cron healthcheck. + $this->schedule_event(); + + // Perform remote post. + return parent::dispatch(); + } - /** - * Delete queue - * - * @param string $key Key. - * - * @return $this - */ - public function delete( $key ) { - delete_site_option( $key ); - - return $this; + /** + * Push to queue + * + * @param mixed $data Data. + * + * @return $this + */ + public function push_to_queue( $data ) { + $this->data[] = $data; + + return $this; + } + + /** + * Save queue + * + * @return $this + */ + public function save() { + $key = $this->generate_key(); + + if ( ! empty( $this->data ) ) { + update_site_option( $key, $this->data ); } - /** - * Generate key - * - * Generates a unique key based on microtime. Queue items are - * given a unique key so that they can be merged upon save. - * - * @param int $length Length. - * - * @return string - */ - protected function generate_key( $length = 64 ) { - $unique = md5( microtime() . rand() ); - $prepend = $this->identifier . '_batch_'; - - return substr( $prepend . $unique, 0, $length ); + return $this; + } + + /** + * Update queue + * + * @param string $key Key. + * @param array $data Data. + * + * @return $this + */ + public function update( $key, $data ) { + if ( ! empty( $data ) ) { + update_site_option( $key, $data ); } - /** - * Maybe process queue - * - * Checks whether data exists within the queue and that - * the process is not already running. - */ - public function maybe_handle() { - // Don't lock up other requests while processing - session_write_close(); - - if ( $this->is_process_running() ) { - // Background process already running. - wp_die(); - } + return $this; + } - if ( $this->is_queue_empty() ) { - // No data to process. - wp_die(); - } + /** + * Delete queue + * + * @param string $key Key. + * + * @return $this + */ + public function delete( $key ) { + delete_site_option( $key ); - check_ajax_referer( $this->identifier, 'nonce' ); + return $this; + } - $this->handle(); + /** + * Generate key + * + * Generates a unique key based on microtime. Queue items are + * given a unique key so that they can be merged upon save. + * + * @param int $length Length. + * + * @return string + */ + protected function generate_key( $length = 64 ) { + $unique = md5( microtime() . rand() ); + $prepend = $this->identifier . '_batch_'; + + return substr( $prepend . $unique, 0, $length ); + } + /** + * Maybe process queue + * + * Checks whether data exists within the queue and that + * the process is not already running. + */ + public function maybe_handle() { + // Don't lock up other requests while processing + session_write_close(); + + if ( $this->is_process_running() ) { + // Background process already running. wp_die(); } - /** - * Is queue empty - * - * @return bool - */ - protected function is_queue_empty() { - global $wpdb; + if ( $this->is_queue_empty() ) { + // No data to process. + wp_die(); + } - $table = $wpdb->options; - $column = 'option_name'; + check_ajax_referer( $this->identifier, 'nonce' ); - if ( is_multisite() ) { - $table = $wpdb->sitemeta; - $column = 'meta_key'; - } + $this->handle(); + + wp_die(); + } + + /** + * Is queue empty + * + * @return bool + */ + protected function is_queue_empty() { + global $wpdb; + + $table = $wpdb->options; + $column = 'option_name'; - $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; + if ( is_multisite() ) { + $table = $wpdb->sitemeta; + $column = 'meta_key'; + } + + $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; - $count = $wpdb->get_var( $wpdb->prepare( " + $count = $wpdb->get_var( $wpdb->prepare( " SELECT COUNT(*) FROM {$table} WHERE {$column} LIKE %s ", $key ) ); - return ( $count > 0 ) ? false : true; + return ( $count > 0 ) ? false : true; + } + + /** + * Is process running + * + * Check whether the current process is already running + * in a background process. + */ + protected function is_process_running() { + if ( get_site_transient( $this->identifier . '_process_lock' ) ) { + // Process already running. + return true; } - /** - * Is process running - * - * Check whether the current process is already running - * in a background process. - */ - protected function is_process_running() { - if ( get_site_transient( $this->identifier . '_process_lock' ) ) { - // Process already running. - return true; - } + return false; + } - return false; - } + /** + * Lock process + * + * Lock the process so that multiple instances can't run simultaneously. + * Override if applicable, but the duration should be greater than that + * defined in the time_exceeded() method. + */ + protected function lock_process() { + $this->start_time = time(); // Set start time of current process. - /** - * Lock process - * - * Lock the process so that multiple instances can't run simultaneously. - * Override if applicable, but the duration should be greater than that - * defined in the time_exceeded() method. - */ - protected function lock_process() { - $this->start_time = time(); // Set start time of current process. + $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute + $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); - $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute - $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); + set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); + } - set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); - } + /** + * Unlock process + * + * Unlock the process so that other instances can spawn. + * + * @return $this + */ + protected function unlock_process() { + delete_site_transient( $this->identifier . '_process_lock' ); - /** - * Unlock process - * - * Unlock the process so that other instances can spawn. - * - * @return $this - */ - protected function unlock_process() { - delete_site_transient( $this->identifier . '_process_lock' ); - - return $this; - } + return $this; + } - /** - * Get batch - * - * @return stdClass Return the first batch from the queue - */ - protected function get_batch() { - global $wpdb; - - $table = $wpdb->options; - $column = 'option_name'; - $key_column = 'option_id'; - $value_column = 'option_value'; - - if ( is_multisite() ) { - $table = $wpdb->sitemeta; - $column = 'meta_key'; - $key_column = 'meta_id'; - $value_column = 'meta_value'; - } + /** + * Get batch + * + * @return stdClass Return the first batch from the queue + */ + protected function get_batch() { + global $wpdb; + + $table = $wpdb->options; + $column = 'option_name'; + $key_column = 'option_id'; + $value_column = 'option_value'; + + if ( is_multisite() ) { + $table = $wpdb->sitemeta; + $column = 'meta_key'; + $key_column = 'meta_id'; + $value_column = 'meta_value'; + } - $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; + $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; - $query = $wpdb->get_row( $wpdb->prepare( " + $query = $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$table} WHERE {$column} LIKE %s @@ -280,227 +278,228 @@ protected function get_batch() { LIMIT 1 ", $key ) ); - $batch = new stdClass(); - $batch->key = $query->$column; - $batch->data = maybe_unserialize( $query->$value_column ); + $batch = new stdClass(); + $batch->key = $query->$column; + $batch->data = maybe_unserialize( $query->$value_column ); - return $batch; - } + return $batch; + } - /** - * Handle - * - * Pass each queue item to the task handler, while remaining - * within server memory and time limit constraints. - */ - protected function handle() { - $this->lock_process(); - - do { - $batch = $this->get_batch(); - - foreach ( $batch->data as $key => $value ) { - $task = $this->task( $value ); - - if ( false !== $task ) { - $batch->data[ $key ] = $task; - } else { - unset( $batch->data[ $key ] ); - } - - if ( $this->time_exceeded() || $this->memory_exceeded() ) { - // Batch limits reached. - break; - } - } + /** + * Handle + * + * Pass each queue item to the task handler, while remaining + * within server memory and time limit constraints. + */ + protected function handle() { + $this->lock_process(); - // Update or delete current batch. - if ( ! empty( $batch->data ) ) { - $this->update( $batch->key, $batch->data ); + do { + $batch = $this->get_batch(); + + foreach ( $batch->data as $key => $value ) { + $task = $this->task( $value ); + + if ( false !== $task ) { + $batch->data[ $key ] = $task; } else { - $this->delete( $batch->key ); + unset( $batch->data[ $key ] ); } - } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); - $this->unlock_process(); + if ( $this->time_exceeded() || $this->memory_exceeded() ) { + // Batch limits reached. + break; + } + } - // Start next batch or complete process. - if ( ! $this->is_queue_empty() ) { - $this->dispatch(); + // Update or delete current batch. + if ( ! empty( $batch->data ) ) { + $this->update( $batch->key, $batch->data ); } else { - $this->complete(); + $this->delete( $batch->key ); } + } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); - wp_die(); - } - - /** - * Memory exceeded - * - * Ensures the batch process never exceeds 90% - * of the maximum WordPress memory. - * - * @return bool - */ - protected function memory_exceeded() { - $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory - $current_memory = memory_get_usage( true ); - $return = false; - - if ( $current_memory >= $memory_limit ) { - $return = true; - } + $this->unlock_process(); - return apply_filters( $this->identifier . '_memory_exceeded', $return ); + // Start next batch or complete process. + if ( ! $this->is_queue_empty() ) { + $this->dispatch(); + } else { + $this->complete(); } - /** - * Get memory limit - * - * @return int - */ - protected function get_memory_limit() { - if ( function_exists( 'ini_get' ) ) { - $memory_limit = ini_get( 'memory_limit' ); - } else { - // Sensible default. - $memory_limit = '128M'; - } + wp_die(); + } - if ( ! $memory_limit || -1 === intval( $memory_limit ) ) { - // Unlimited, set to 32GB. - $memory_limit = '32000M'; - } + /** + * Memory exceeded + * + * Ensures the batch process never exceeds 90% + * of the maximum WordPress memory. + * + * @return bool + */ + protected function memory_exceeded() { + $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory + $current_memory = memory_get_usage( true ); + $return = false; - return intval( $memory_limit ) * 1024 * 1024; + if ( $current_memory >= $memory_limit ) { + $return = true; } - /** - * Time exceeded. - * - * Ensures the batch never exceeds a sensible time limit. - * A timeout limit of 30s is common on shared hosting. - * - * @return bool - */ - protected function time_exceeded() { - $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds - $return = false; - - if ( time() >= $finish ) { - $return = true; - } + return apply_filters( $this->identifier . '_memory_exceeded', $return ); + } - return apply_filters( $this->identifier . '_time_exceeded', $return ); + /** + * Get memory limit + * + * @return int + */ + protected function get_memory_limit() { + if ( function_exists( 'ini_get' ) ) { + $memory_limit = ini_get( 'memory_limit' ); + } else { + // Sensible default. + $memory_limit = '128M'; } - /** - * Complete. - * - * Override if applicable, but ensure that the below actions are - * performed, or, call parent::complete(). - */ - protected function complete() { - // Unschedule the cron healthcheck. - $this->clear_scheduled_event(); + if ( ! $memory_limit || - 1 === intval( $memory_limit ) ) { + // Unlimited, set to 32GB. + $memory_limit = '32000M'; } - /** - * Schedule cron healthcheck - * - * @access public - * @param mixed $schedules Schedules. - * @return mixed - */ - public function schedule_cron_healthcheck( $schedules ) { - $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); - - if ( property_exists( $this, 'cron_interval' ) ) { - $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); - } + return intval( $memory_limit ) * 1024 * 1024; + } - // Adds every 5 minutes to the existing schedules. - $schedules[ $this->identifier . '_cron_interval' ] = array( - 'interval' => MINUTE_IN_SECONDS * $interval, - 'display' => sprintf( __( 'Every %d Minutes' ), $interval ), - ); + /** + * Time exceeded. + * + * Ensures the batch never exceeds a sensible time limit. + * A timeout limit of 30s is common on shared hosting. + * + * @return bool + */ + protected function time_exceeded() { + $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds + $return = false; - return $schedules; + if ( time() >= $finish ) { + $return = true; } - /** - * Handle cron healthcheck - * - * Restart the background process if not already running - * and data exists in the queue. - */ - public function handle_cron_healthcheck() { - if ( $this->is_process_running() ) { - // Background process already running. - exit; - } + return apply_filters( $this->identifier . '_time_exceeded', $return ); + } - if ( $this->is_queue_empty() ) { - // No data to process. - $this->clear_scheduled_event(); - exit; - } + /** + * Complete. + * + * Override if applicable, but ensure that the below actions are + * performed, or, call parent::complete(). + */ + protected function complete() { + // Unschedule the cron healthcheck. + $this->clear_scheduled_event(); + } + + /** + * Schedule cron healthcheck + * + * @access public + * + * @param mixed $schedules Schedules. + * + * @return mixed + */ + public function schedule_cron_healthcheck( $schedules ) { + $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); - $this->handle(); + if ( property_exists( $this, 'cron_interval' ) ) { + $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); + } + // Adds every 5 minutes to the existing schedules. + $schedules[ $this->identifier . '_cron_interval' ] = array( + 'interval' => MINUTE_IN_SECONDS * $interval, + 'display' => sprintf( __( 'Every %d Minutes' ), $interval ), + ); + + return $schedules; + } + + /** + * Handle cron healthcheck + * + * Restart the background process if not already running + * and data exists in the queue. + */ + public function handle_cron_healthcheck() { + if ( $this->is_process_running() ) { + // Background process already running. exit; } - /** - * Schedule event - */ - protected function schedule_event() { - if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { - wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier ); - } + if ( $this->is_queue_empty() ) { + // No data to process. + $this->clear_scheduled_event(); + exit; } - /** - * Clear scheduled event - */ - protected function clear_scheduled_event() { - $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); + $this->handle(); - if ( $timestamp ) { - wp_unschedule_event( $timestamp, $this->cron_hook_identifier ); - } + exit; + } + + /** + * Schedule event + */ + protected function schedule_event() { + if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { + wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier ); } + } - /** - * Cancel Process - * - * Stop processing queue items, clear cronjob and delete batch. - * - */ - public function cancel_process() { - if ( ! $this->is_queue_empty() ) { - $batch = $this->get_batch(); + /** + * Clear scheduled event + */ + protected function clear_scheduled_event() { + $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); - $this->delete( $batch->key ); + if ( $timestamp ) { + wp_unschedule_event( $timestamp, $this->cron_hook_identifier ); + } + } - wp_clear_scheduled_hook( $this->cron_hook_identifier ); - } + /** + * Cancel Process + * + * Stop processing queue items, clear cronjob and delete batch. + * + */ + public function cancel_process() { + if ( ! $this->is_queue_empty() ) { + $batch = $this->get_batch(); - } + $this->delete( $batch->key ); - /** - * Task - * - * Override this method to perform any actions required on each - * queue item. Return the modified item for further processing - * in the next pass through. Or, return false to remove the - * item from the queue. - * - * @param mixed $item Queue item to iterate over. - * - * @return mixed - */ - abstract protected function task( $item ); + wp_clear_scheduled_hook( $this->cron_hook_identifier ); + } } -} + + /** + * Task + * + * Override this method to perform any actions required on each + * queue item. Return the modified item for further processing + * in the next pass through. Or, return false to remove the + * item from the queue. + * + * @param mixed $item Queue item to iterate over. + * + * @return mixed + */ + abstract protected function task( $item ); + +} \ No newline at end of file diff --git a/wp-background-processing.php b/wp-background-processing.php index 2205562..c2fc252 100644 --- a/wp-background-processing.php +++ b/wp-background-processing.php @@ -16,5 +16,9 @@ GitHub Branch: master */ -require_once plugin_dir_path( __FILE__ ) . 'classes/wp-async-request.php'; -require_once plugin_dir_path( __FILE__ ) . 'classes/wp-background-process.php'; +if ( ! class_exists( 'WP_Async_Request' ) ) { + require_once plugin_dir_path( __FILE__ ) . 'classes/wp-async-request.php'; +} +if ( ! class_exists( 'WP_Background_Process' ) ) { + require_once plugin_dir_path( __FILE__ ) . 'classes/wp-background-process.php'; +} From ffc4e9b6235c5ff4b3bdb7e1dce5348b0a26720d Mon Sep 17 00:00:00 2001 From: Iain Date: Fri, 31 Jul 2020 07:54:47 +0100 Subject: [PATCH 10/65] Fix converting byte notation not converted to bytes correctly if not in M. Fxies https://github.com/deliciousbrains/wp-background-processing/issues/77 --- classes/wp-background-process.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 5f412c0..ce7904a 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -370,7 +370,7 @@ protected function get_memory_limit() { $memory_limit = '32000M'; } - return intval( $memory_limit ) * 1024 * 1024; + return wp_convert_hr_to_bytes( $memory_limit ); } /** From a86825bf78b8a60680b62add68f66f88eae28036 Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 22 Dec 2021 13:33:34 +0100 Subject: [PATCH 11/65] Added check for is_process_running() in dispatch() --- classes/wp-background-process.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index ce7904a..6e0a464 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -69,6 +69,12 @@ public function __construct() { * @return void */ public function dispatch() { + + if ( $this->is_process_running() ) { + // Process already running. + return true; + } + // Schedule the cron healthcheck. $this->schedule_event(); From b6d2805fcbc46ea80fced675523cd29f405fa8d7 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Tue, 11 Apr 2023 13:38:27 +0100 Subject: [PATCH 12/65] Apply minor formatting and documentation updates --- README.md | 16 +++--- classes/wp-async-request.php | 19 ++++--- classes/wp-background-process.php | 84 +++++++++++++++---------------- composer.json | 6 ++- 4 files changed, 63 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index fc09783..9317f82 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,13 @@ class WP_Example_Request extends WP_Async_Request { protected $action = 'example_request'; /** - * Handle + * Handle a dispatched request. * * Override this method to perform any actions required * during the async request. */ protected function handle() { - // Actions to perform + // Actions to perform. } } @@ -73,7 +73,7 @@ Chaining is also supported: ### Background Process -Background processes work in a similar fashion to async requests but they allow you to queue tasks. Items pushed onto the queue will be processed in the background once the queue has been dispatched. Queues will also scale based on available server resources, so higher end servers will process more items per batch. Once a batch has completed the next batch will start instantly. +Background processes work in a similar fashion to async requests, but they allow you to queue tasks. Items pushed onto the queue will be processed in the background once the queue has been dispatched. Queues will also scale based on available server resources, so higher end servers will process more items per batch. Once a batch has completed, the next batch will start instantly. Health checks run by default every 5 minutes to ensure the queue is running when queued items exist. If the queue has failed it will be restarted. @@ -90,25 +90,25 @@ class WP_Example_Process extends WP_Background_Process { protected $action = 'example_process'; /** - * Task + * Perform task with queued item. * * Override this method to perform any actions required on each * queue item. Return the modified item for further processing * in the next pass through. Or, return false to remove the * item from the queue. * - * @param mixed $item Queue item to iterate over + * @param mixed $item Queue item to iterate over. * * @return mixed */ protected function task( $item ) { - // Actions to perform + // Actions to perform. return false; } /** - * Complete + * Complete processing. * * Override if applicable, but ensure that the below actions are * performed, or, call parent::complete(). @@ -156,7 +156,7 @@ Save and dispatch the queue: ### BasicAuth -If your site is behind BasicAuth, both async requests and background processes will fail to complete. This is because WP Background Processing relies on the [WordPress HTTP API](http://codex.wordpress.org/HTTP_API), which requires you to attach your BasicAuth credentials to requests. The easiest way to do this is using the following filter: +If your site is behind BasicAuth, both async requests and background processes will fail to complete. This is because WP Background Processing relies on the [WordPress HTTP API](https://developer.wordpress.org/plugins/http-api/), which requires you to attach your BasicAuth credentials to requests. The easiest way to do this is using the following filter: ```php function wpbp_http_request_args( $r, $url ) { diff --git a/classes/wp-async-request.php b/classes/wp-async-request.php index 7a37dd6..4336f66 100644 --- a/classes/wp-async-request.php +++ b/classes/wp-async-request.php @@ -51,7 +51,7 @@ abstract class WP_Async_Request { protected $data = array(); /** - * Initiate new async request + * Initiate new async request. */ public function __construct() { $this->identifier = $this->prefix . '_' . $this->action; @@ -61,7 +61,7 @@ public function __construct() { } /** - * Set data used during the request + * Set data used during the request. * * @param array $data Data. * @@ -74,7 +74,7 @@ public function data( $data ) { } /** - * Dispatch the async request + * Dispatch the async request. * * @return array|WP_Error */ @@ -86,7 +86,7 @@ public function dispatch() { } /** - * Get query args + * Get query args. * * @return array */ @@ -109,7 +109,7 @@ protected function get_query_args() { } /** - * Get query URL + * Get query URL. * * @return string */ @@ -129,7 +129,7 @@ protected function get_query_url() { } /** - * Get post args + * Get post args. * * @return array */ @@ -155,12 +155,12 @@ protected function get_post_args() { } /** - * Maybe handle + * Maybe handle a dispatched request. * * Check for correct nonce and pass to handler. */ public function maybe_handle() { - // Don't lock up other requests while processing + // Don't lock up other requests while processing. session_write_close(); check_ajax_referer( $this->identifier, 'nonce' ); @@ -171,11 +171,10 @@ public function maybe_handle() { } /** - * Handle + * Handle a dispatched request. * * Override this method to perform any actions required * during the async request. */ abstract protected function handle(); - } diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index ce7904a..20a96a7 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -50,7 +50,7 @@ abstract class WP_Background_Process extends WP_Async_Request { protected $cron_interval_identifier; /** - * Initiate new background process + * Initiate new background process. */ public function __construct() { parent::__construct(); @@ -63,10 +63,10 @@ public function __construct() { } /** - * Dispatch + * Schedule the cron healthcheck and dispatch an async request to start processing the queue. * * @access public - * @return void + * @return array|WP_Error */ public function dispatch() { // Schedule the cron healthcheck. @@ -77,7 +77,9 @@ public function dispatch() { } /** - * Push to queue + * Push to the queue. + * + * Note, save must be called in order to persist queued items to a batch for processing. * * @param mixed $data Data. * @@ -90,7 +92,7 @@ public function push_to_queue( $data ) { } /** - * Save queue + * Save the queued items for future processing. * * @return $this */ @@ -105,7 +107,7 @@ public function save() { } /** - * Update queue + * Update a batch's queued items. * * @param string $key Key. * @param array $data Data. @@ -121,7 +123,7 @@ public function update( $key, $data ) { } /** - * Delete queue + * Delete a batch of queued items. * * @param string $key Key. * @@ -134,7 +136,7 @@ public function delete( $key ) { } /** - * Generate key + * Generate key for a batch. * * Generates a unique key based on microtime. Queue items are * given a unique key so that they can be merged upon save. @@ -151,13 +153,13 @@ protected function generate_key( $length = 64 ) { } /** - * Maybe process queue + * Maybe process a batch of queued items. * * Checks whether data exists within the queue and that * the process is not already running. */ public function maybe_handle() { - // Don't lock up other requests while processing + // Don't lock up other requests while processing. session_write_close(); if ( $this->is_process_running() ) { @@ -178,7 +180,7 @@ public function maybe_handle() { } /** - * Is queue empty + * Is queue empty? * * @return bool */ @@ -197,15 +199,15 @@ protected function is_queue_empty() { $count = $wpdb->get_var( $wpdb->prepare( " SELECT COUNT(*) - FROM {$table} - WHERE {$column} LIKE %s + FROM $table + WHERE $column LIKE %s ", $key ) ); - return ( $count > 0 ) ? false : true; + return ! ( $count > 0 ); } /** - * Is process running + * Is process running? * * Check whether the current process is already running * in a background process. @@ -220,7 +222,7 @@ protected function is_process_running() { } /** - * Lock process + * Lock process. * * Lock the process so that multiple instances can't run simultaneously. * Override if applicable, but the duration should be greater than that @@ -236,7 +238,7 @@ protected function lock_process() { } /** - * Unlock process + * Unlock process. * * Unlock the process so that other instances can spawn. * @@ -249,9 +251,9 @@ protected function unlock_process() { } /** - * Get batch + * Get batch. * - * @return stdClass Return the first batch from the queue + * @return stdClass Return the first batch of queued items. */ protected function get_batch() { global $wpdb; @@ -272,21 +274,21 @@ protected function get_batch() { $query = $wpdb->get_row( $wpdb->prepare( " SELECT * - FROM {$table} - WHERE {$column} LIKE %s - ORDER BY {$key_column} ASC + FROM $table + WHERE $column LIKE %s + ORDER BY $key_column ASC LIMIT 1 ", $key ) ); $batch = new stdClass(); - $batch->key = $query->$column; - $batch->data = maybe_unserialize( $query->$value_column ); + $batch->key = $query->{$column}; + $batch->data = maybe_unserialize( $query->{$value_column} ); return $batch; } /** - * Handle + * Handle a dispatched request. * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. @@ -333,7 +335,7 @@ protected function handle() { } /** - * Memory exceeded + * Memory exceeded? * * Ensures the batch process never exceeds 90% * of the maximum WordPress memory. @@ -353,7 +355,7 @@ protected function memory_exceeded() { } /** - * Get memory limit + * Get memory limit in bytes. * * @return int */ @@ -365,7 +367,7 @@ protected function get_memory_limit() { $memory_limit = '128M'; } - if ( ! $memory_limit || - 1 === intval( $memory_limit ) ) { + if ( ! $memory_limit || -1 === intval( $memory_limit ) ) { // Unlimited, set to 32GB. $memory_limit = '32000M'; } @@ -374,7 +376,7 @@ protected function get_memory_limit() { } /** - * Time exceeded. + * Time limit exceeded? * * Ensures the batch never exceeds a sensible time limit. * A timeout limit of 30s is common on shared hosting. @@ -393,18 +395,18 @@ protected function time_exceeded() { } /** - * Complete. + * Complete processing. * * Override if applicable, but ensure that the below actions are * performed, or, call parent::complete(). */ protected function complete() { - // Unschedule the cron healthcheck. + // Remove the cron healthcheck job from the cron schedule. $this->clear_scheduled_event(); } /** - * Schedule cron healthcheck + * Schedule the cron healthcheck job. * * @access public * @@ -419,7 +421,7 @@ public function schedule_cron_healthcheck( $schedules ) { $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); } - // Adds every 5 minutes to the existing schedules. + // Adds an "Every NNN Minutes" schedule to the existing cron schedules. $schedules[ $this->identifier . '_cron_interval' ] = array( 'interval' => MINUTE_IN_SECONDS * $interval, 'display' => sprintf( __( 'Every %d Minutes' ), $interval ), @@ -429,7 +431,7 @@ public function schedule_cron_healthcheck( $schedules ) { } /** - * Handle cron healthcheck + * Handle cron healthcheck event. * * Restart the background process if not already running * and data exists in the queue. @@ -452,7 +454,7 @@ public function handle_cron_healthcheck() { } /** - * Schedule event + * Schedule the cron healthcheck event. */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { @@ -461,7 +463,7 @@ protected function schedule_event() { } /** - * Clear scheduled event + * Clear scheduled cron healthcheck event. */ protected function clear_scheduled_event() { $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); @@ -472,9 +474,9 @@ protected function clear_scheduled_event() { } /** - * Cancel Process + * Cancel the background process. * - * Stop processing queue items, clear cronjob and delete batch. + * Stop processing queue items, clear cron job and delete batch. * */ public function cancel_process() { @@ -485,11 +487,10 @@ public function cancel_process() { wp_clear_scheduled_hook( $this->cron_hook_identifier ); } - } /** - * Task + * Perform task with queued item. * * Override this method to perform any actions required on each * queue item. Return the modified item for further processing @@ -501,5 +502,4 @@ public function cancel_process() { * @return mixed */ abstract protected function task( $item ); - -} \ No newline at end of file +} diff --git a/composer.json b/composer.json index 9453fe8..267c6f3 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,8 @@ } ], "autoload": { - "classmap": [ "classes/" ] + "classmap": [ + "classes/" + ] } -} \ No newline at end of file +} From cc7b24c5e967a76b57f42d6ec642c26d9b627a66 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Tue, 11 Apr 2023 15:34:34 +0100 Subject: [PATCH 13/65] Dispatch returns HTTP Response array, WP_Error on failure, or false if not attempted --- classes/wp-async-request.php | 2 +- classes/wp-background-process.php | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/classes/wp-async-request.php b/classes/wp-async-request.php index 4336f66..595788b 100644 --- a/classes/wp-async-request.php +++ b/classes/wp-async-request.php @@ -76,7 +76,7 @@ public function data( $data ) { /** * Dispatch the async request. * - * @return array|WP_Error + * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted. */ public function dispatch() { $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index b09d6ab..9af3a40 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -66,13 +66,12 @@ public function __construct() { * Schedule the cron healthcheck and dispatch an async request to start processing the queue. * * @access public - * @return array|WP_Error + * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted. */ public function dispatch() { - if ( $this->is_process_running() ) { // Process already running. - return true; + return false; } // Schedule the cron healthcheck. From bf3fbc73396cb03ba4b92b414f1d53471a443be0 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Wed, 12 Apr 2023 16:49:14 +0100 Subject: [PATCH 14/65] Add skeleton unit tests setup --- .circleci/config.yml | 109 +++ .gitignore | 3 +- .phpcs.xml.dist | 47 ++ bin/install-wp-tests.sh | 181 +++++ composer.json | 6 +- composer.lock | 1494 +++++++++++++++++++++++++++++++++++++++ phpunit.xml.dist | 15 + tests/bootstrap.php | 38 + tests/test-it-works.php | 19 + 9 files changed, 1910 insertions(+), 2 deletions(-) create mode 100644 .circleci/config.yml create mode 100644 .phpcs.xml.dist create mode 100755 bin/install-wp-tests.sh create mode 100644 composer.lock create mode 100644 phpunit.xml.dist create mode 100644 tests/bootstrap.php create mode 100644 tests/test-it-works.php diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..bdb5be1 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,109 @@ +workflows: + version: 2 + main: + jobs: + - php56-build + - php70-build + - php71-build + - php72-build + - php73-build + - php74-build + +version: 2 + +job-references: + mysql_image: &mysql_image + circleci/mysql:5.6 + + setup_environment: &setup_environment + name: "Setup Environment Variables" + command: | + echo "export PATH=$HOME/.composer/vendor/bin:$PATH" >> $BASH_ENV + source /home/circleci/.bashrc + + install_dependencies: &install_dependencies + name: "Install Dependencies" + command: | + sudo apt-get update && sudo apt-get install subversion + sudo -E docker-php-ext-install mysqli + sudo sh -c "printf '\ndeb http://ftp.us.debian.org/debian sid main\n' >> /etc/apt/sources.list" + sudo apt-get update && sudo apt-get install mysql-client-5.7 + + php_job: &php_job + environment: + - WP_TESTS_DIR: "/tmp/wordpress-tests-lib" + - WP_CORE_DIR: "/tmp/wordpress/" + steps: + - checkout + - run: *setup_environment + - run: *install_dependencies + - run: + name: "Run Tests" + command: | + composer global require "phpunit/phpunit=5.7.*" + composer global require wp-coding-standards/wpcs + phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs + phpcs + rm -rf $WP_TESTS_DIR $WP_CORE_DIR + bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 latest + phpunit + WP_MULTISITE=1 phpunit + +jobs: + php56-build: + <<: *php_job + docker: + - image: circleci/php:5.6 + - image: *mysql_image + steps: + - checkout + - run: *setup_environment + - run: *install_dependencies + - run: + name: "Run Tests" + command: | + composer global require "phpunit/phpunit=5.7.*" + composer global require wp-coding-standards/wpcs + phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs + phpcs + SKIP_DB_CREATE=false + rm -rf $WP_TESTS_DIR $WP_CORE_DIR + bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 latest $SKIP_DB_CREATE + phpunit + WP_MULTISITE=1 phpunit + SKIP_DB_CREATE=true + rm -rf $WP_TESTS_DIR $WP_CORE_DIR + bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 trunk $SKIP_DB_CREATE + phpunit + WP_MULTISITE=1 phpunit + SKIP_DB_CREATE=true + + php70-build: + <<: *php_job + docker: + - image: circleci/php:7.0 + - image: *mysql_image + + php71-build: + <<: *php_job + docker: + - image: circleci/php:7.1 + - image: *mysql_image + + php72-build: + <<: *php_job + docker: + - image: circleci/php:7.2 + - image: *mysql_image + + php73-build: + <<: *php_job + docker: + - image: circleci/php:7.3 + - image: *mysql_image + + php74-build: + <<: *php_job + docker: + - image: circleci/php:7.4 + - image: *mysql_image diff --git a/.gitignore b/.gitignore index f45219c..23505a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor/ -/.idea \ No newline at end of file +/.idea +*.cache diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..df58068 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,47 @@ + + + Generally-applicable sniffs for WordPress plugins. + + + . + /vendor/ + /node_modules/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000..ee05775 --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +TMPDIR=${TMPDIR-/tmp} +TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") +WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then + WP_BRANCH=${WP_VERSION%\-*} + WP_TESTS_TAG="branches/$WP_BRANCH" + +elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then + WP_TESTS_TAG="branches/$WP_VERSION" +elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p $TMPDIR/wordpress-trunk + rm -rf $TMPDIR/wordpress-trunk/* + svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress + mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i.bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + rm -rf $WP_TESTS_DIR/{includes,data} + svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + +} + +recreate_db() { + shopt -s nocasematch + if [[ $1 =~ ^(y|yes)$ ]] + then + mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA + create_db + echo "Recreated the database ($DB_NAME)." + else + echo "Leaving the existing database ($DB_NAME) in place." + fi + shopt -u nocasematch +} + +create_db() { + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] + then + echo "Reinstalling will delete the existing test database ($DB_NAME)" + read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB + recreate_db $DELETE_EXISTING_DB + else + create_db + fi +} + +install_wp +install_test_suite +install_db diff --git a/composer.json b/composer.json index 267c6f3..787e10b 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "WP Background Processing can be used to fire off non-blocking asynchronous requests or as a background processing tool, allowing you to queue tasks.", "type": "library", "require": { - "php": ">=5.2" + "php": ">=5.6" }, "suggest": { "coenjacobs/mozart": "Easily wrap this library with your own prefix, to prevent collisions when multiple plugins use this library" @@ -19,5 +19,9 @@ "classmap": [ "classes/" ] + }, + "require-dev": { + "phpunit/phpunit": "^8.0", + "yoast/phpunit-polyfills": "^1.0" } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..57a192b --- /dev/null +++ b/composer.lock @@ -0,0 +1,1494 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "080e96d15aa84fb1cbedb02b9957af71", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "7.0.15", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "819f92bba8b001d4363065928088de22f25a3a48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/819f92bba8b001d4363065928088de22f25a3a48", + "reference": "819f92bba8b001d4363065928088de22f25a3a48", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": ">=7.2", + "phpunit/php-file-iterator": "^2.0.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.1.3 || ^4.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^4.2.2", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.2.2" + }, + "suggest": { + "ext-xdebug": "^2.7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.15" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-07-26T12:20:09+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5", + "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:42:26+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662", + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T08:20:02+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/a853a0e183b9db7eed023d7933a858fa1c8d25a3", + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "abandoned": true, + "time": "2020-08-04T08:28:15+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "8.5.33", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e", + "reference": "7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.0", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.2", + "phpunit/php-code-coverage": "^7.0.12", + "phpunit/php-file-iterator": "^2.0.4", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1.2", + "sebastian/comparator": "^3.0.5", + "sebastian/diff": "^3.0.2", + "sebastian/environment": "^4.2.3", + "sebastian/exporter": "^3.1.5", + "sebastian/global-state": "^3.0.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0.1", + "sebastian/type": "^1.1.3", + "sebastian/version": "^2.0.1" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0.0" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.33" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2023-02-27T13:04:50+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T08:15:22+00:00" + }, + { + "name": "sebastian/comparator", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1dc7ceb4a24aede938c7af2a9ed1de09609ca770", + "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:31:48+00:00" + }, + { + "name": "sebastian/diff", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:59:04+00:00" + }, + { + "name": "sebastian/environment", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:53:42+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/73a9676f2833b9a7c36968f9d882589cd75511e6", + "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:00:17+00:00" + }, + { + "name": "sebastian/global-state", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "de036ec91d55d2a9e0db2ba975b512cdb1c23921" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/de036ec91d55d2a9e0db2ba975b512cdb1c23921", + "reference": "de036ec91d55d2a9e0db2ba975b512cdb1c23921", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^8.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-10T06:55:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:40:27+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:37:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:34:24+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3", + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:30:19+00:00" + }, + { + "name": "sebastian/type", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4", + "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/1.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:25:11+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/master" + }, + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + }, + { + "name": "yoast/phpunit-polyfills", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", + "reference": "3b59adeef77fb1c03ff5381dbb9d68b0aaff3171" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/3b59adeef77fb1c03ff5381dbb9d68b0aaff3171", + "reference": "3b59adeef77fb1c03ff5381dbb9d68b0aaff3171", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "require-dev": { + "yoast/yoastcs": "^2.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "files": [ + "phpunitpolyfills-autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Team Yoast", + "email": "support@yoast.com", + "homepage": "https://yoast.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills/graphs/contributors" + } + ], + "description": "Set of polyfills for changed PHPUnit functionality to allow for creating PHPUnit cross-version compatible tests", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills", + "keywords": [ + "phpunit", + "polyfill", + "testing" + ], + "support": { + "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", + "source": "https://github.com/Yoast/PHPUnit-Polyfills" + }, + "time": "2023-03-30T23:39:05+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.6" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..557663e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,15 @@ + + + + + ./tests/ + + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..94036dc --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,38 @@ +assertTrue( true ); + } +} From 352519ca9442a3ab6cff9f172241f054ed9477da Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Wed, 12 Apr 2023 17:13:09 +0100 Subject: [PATCH 15/65] Add minimal Makefile for running unit tests --- Makefile | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d68f50d --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +.PHONY: test +test: vendor + vendor/bin/phpunit + +vendor: composer.json + composer install --ignore-platform-reqs + +.PHONY: clean +clean: + rm -rf vendor + rm -f tests/.phpunit.result.cache From f2558bcc4531d2eb0df265f0682918c9d7771f3e Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Thu, 13 Apr 2023 09:36:32 +0100 Subject: [PATCH 16/65] Add test-style Makefile target for running phpcs --- .phpcs.xml.dist => .phpcs.xml | 32 +- Makefile | 9 +- composer.json | 10 +- composer.lock | 530 +++++++++++++++++++++++++++++++++- wp-background-processing.php | 4 +- 5 files changed, 577 insertions(+), 8 deletions(-) rename .phpcs.xml.dist => .phpcs.xml (57%) diff --git a/.phpcs.xml.dist b/.phpcs.xml similarity index 57% rename from .phpcs.xml.dist rename to .phpcs.xml index df58068..32ca255 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml @@ -6,6 +6,7 @@ . /vendor/ /node_modules/ + /tests/bootstrap.php @@ -26,18 +27,43 @@ - + + + + + + + + + + + + + + + + + + + - + + + + - + + + + + diff --git a/Makefile b/Makefile index d68f50d..7ad63b8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,14 @@ .PHONY: test -test: vendor +test: test-unit test-style + +.PHONY: test-unit +test-unit: vendor vendor/bin/phpunit +.PHONY: test-style +test-style: vendor + vendor/bin/phpcs + vendor: composer.json composer install --ignore-platform-reqs diff --git a/composer.json b/composer.json index 787e10b..5f65733 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,14 @@ }, "require-dev": { "phpunit/phpunit": "^8.0", - "yoast/phpunit-polyfills": "^1.0" + "yoast/phpunit-polyfills": "^1.0", + "spryker/code-sniffer": "^0.17.18", + "phpcompatibility/phpcompatibility-wp": "*", + "wp-coding-standards/wpcs": "^2.3" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/composer.lock b/composer.lock index 57a192b..3674cd5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,87 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "080e96d15aa84fb1cbedb02b9957af71", + "content-hash": "a9e05db155afa8fde60a18a19cc4f591", "packages": [], "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "time": "2023-01-05T11:28:13+00:00" + }, { "name": "doctrine/instantiator", "version": "1.5.0", @@ -247,6 +325,225 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "1.3.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "time": "2022-10-25T01:46:02+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "2.1.4", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0", + "phpcompatibility/phpcompatibility-paragonie": "^1.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, + "time": "2022-10-24T09:00:36+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.18.1", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1" + }, + "time": "2023-04-07T11:51:11+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "7.0.15", @@ -1370,6 +1667,186 @@ }, "time": "2016-10-03T07:35:21+00:00" }, + { + "name": "slevomat/coding-standard", + "version": "8.10.0", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "c4e213e6e57f741451a08e68ef838802eec92287" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/c4e213e6e57f741451a08e68ef838802eec92287", + "reference": "c4e213e6e57f741451a08e68ef838802eec92287", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", + "php": "^7.2 || ^8.0", + "phpstan/phpdoc-parser": ">=1.18.0 <1.19.0", + "squizlabs/php_codesniffer": "^3.7.1" + }, + "require-dev": { + "phing/phing": "2.17.4", + "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpstan/phpstan": "1.4.10|1.10.11", + "phpstan/phpstan-deprecation-rules": "1.1.3", + "phpstan/phpstan-phpunit": "1.0.0|1.3.11", + "phpstan/phpstan-strict-rules": "1.5.1", + "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.0.19" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.10.0" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2023-04-10T07:39:29+00:00" + }, + { + "name": "spryker/code-sniffer", + "version": "0.17.18", + "source": { + "type": "git", + "url": "https://github.com/spryker/code-sniffer.git", + "reference": "5fb8b573abc4a906d0d364a4a03abd38e565ba29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spryker/code-sniffer/zipball/5fb8b573abc4a906d0d364a4a03abd38e565ba29", + "reference": "5fb8b573abc4a906d0d364a4a03abd38e565ba29", + "shasum": "" + }, + "require": { + "php": ">=7.4", + "slevomat/coding-standard": "^7.2.0 || ^8.0.1", + "squizlabs/php_codesniffer": "^3.6.2" + }, + "require-dev": { + "phpstan/phpstan": "^1.0.0", + "phpunit/phpunit": "^9.5" + }, + "bin": [ + "bin/tokenize" + ], + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "Spryker\\": "Spryker/", + "SprykerStrict\\": "SprykerStrict/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Spryker", + "homepage": "https://spryker.com" + } + ], + "description": "Spryker Code Sniffer Standards", + "homepage": "https://spryker.com", + "keywords": [ + "codesniffer", + "framework", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/spryker/code-sniffer/issues", + "source": "https://github.com/spryker/code-sniffer" + }, + "time": "2023-01-03T16:08:22+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.7.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2023-02-22T23:07:41+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.1", @@ -1420,6 +1897,57 @@ ], "time": "2021-07-28T10:34:58+00:00" }, + { + "name": "wp-coding-standards/wpcs", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "7da1894633f168fe244afc6de00d141f27517b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62", + "reference": "7da1894633f168fe244afc6de00d141f27517b62", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.3.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", + "phpcompatibility/php-compatibility": "^9.0", + "phpcsstandards/phpcsdevtools": "^1.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "time": "2020-05-13T23:57:56+00:00" + }, { "name": "yoast/phpunit-polyfills", "version": "1.0.5", diff --git a/wp-background-processing.php b/wp-background-processing.php index c2fc252..6881bcd 100644 --- a/wp-background-processing.php +++ b/wp-background-processing.php @@ -7,12 +7,12 @@ /* Plugin Name: WP Background Processing -Plugin URI: https://github.com/A5hleyRich/wp-background-processing +Plugin URI: https://github.com/deliciousbrains/wp-background-processing Description: Asynchronous requests and background processing in WordPress. Author: Delicious Brains Inc. Version: 1.0 Author URI: https://deliciousbrains.com/ -GitHub Plugin URI: https://github.com/A5hleyRich/wp-background-processing +GitHub Plugin URI: https://github.com/deliciousbrains/wp-background-processing GitHub Branch: master */ From 2e66054ad751476d20187d63433d04160a0070d5 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Thu, 13 Apr 2023 09:41:15 +0100 Subject: [PATCH 17/65] Use mariadb-client instead of mysql-client in CircleCI --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bdb5be1..45a61ab 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,7 +27,7 @@ job-references: sudo apt-get update && sudo apt-get install subversion sudo -E docker-php-ext-install mysqli sudo sh -c "printf '\ndeb http://ftp.us.debian.org/debian sid main\n' >> /etc/apt/sources.list" - sudo apt-get update && sudo apt-get install mysql-client-5.7 + sudo apt-get update && sudo apt-get install mariadb-client php_job: &php_job environment: From ac23ebb266eb135ef58878868a3a8fb5fbaf83ba Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Thu, 13 Apr 2023 10:13:58 +0100 Subject: [PATCH 18/65] Simplify CircleCI workflow and use recent images --- .circleci/config.yml | 49 ++++++++++---------------------------------- 1 file changed, 11 insertions(+), 38 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 45a61ab..4b20fce 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,7 +13,7 @@ version: 2 job-references: mysql_image: &mysql_image - circleci/mysql:5.6 + cimg/mysql:5.6 setup_environment: &setup_environment name: "Setup Environment Variables" @@ -25,9 +25,6 @@ job-references: name: "Install Dependencies" command: | sudo apt-get update && sudo apt-get install subversion - sudo -E docker-php-ext-install mysqli - sudo sh -c "printf '\ndeb http://ftp.us.debian.org/debian sid main\n' >> /etc/apt/sources.list" - sudo apt-get update && sudo apt-get install mariadb-client php_job: &php_job environment: @@ -35,75 +32,51 @@ job-references: - WP_CORE_DIR: "/tmp/wordpress/" steps: - checkout + - php/install-composer - run: *setup_environment - run: *install_dependencies - run: name: "Run Tests" command: | - composer global require "phpunit/phpunit=5.7.*" - composer global require wp-coding-standards/wpcs - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs - phpcs rm -rf $WP_TESTS_DIR $WP_CORE_DIR bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 latest - phpunit - WP_MULTISITE=1 phpunit + make test-unit + WP_MULTISITE=1 make test-unit + make test-style jobs: php56-build: <<: *php_job docker: - - image: circleci/php:5.6 + - image: cimg/php:5.6 - image: *mysql_image - steps: - - checkout - - run: *setup_environment - - run: *install_dependencies - - run: - name: "Run Tests" - command: | - composer global require "phpunit/phpunit=5.7.*" - composer global require wp-coding-standards/wpcs - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs - phpcs - SKIP_DB_CREATE=false - rm -rf $WP_TESTS_DIR $WP_CORE_DIR - bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 latest $SKIP_DB_CREATE - phpunit - WP_MULTISITE=1 phpunit - SKIP_DB_CREATE=true - rm -rf $WP_TESTS_DIR $WP_CORE_DIR - bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 trunk $SKIP_DB_CREATE - phpunit - WP_MULTISITE=1 phpunit - SKIP_DB_CREATE=true php70-build: <<: *php_job docker: - - image: circleci/php:7.0 + - image: cimg/php:7.0 - image: *mysql_image php71-build: <<: *php_job docker: - - image: circleci/php:7.1 + - image: cimg/php:7.1 - image: *mysql_image php72-build: <<: *php_job docker: - - image: circleci/php:7.2 + - image: cimg/php:7.2 - image: *mysql_image php73-build: <<: *php_job docker: - - image: circleci/php:7.3 + - image: cimg/php:7.3 - image: *mysql_image php74-build: <<: *php_job docker: - - image: circleci/php:7.4 + - image: cimg/php:7.4 - image: *mysql_image From ab8a46077ef0b9ca2a7baa5e38fb17c5b3208c01 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Thu, 13 Apr 2023 10:26:13 +0100 Subject: [PATCH 19/65] Display PHP and Composer versions at start of CircleCI jobs --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b20fce..073719d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,7 +32,8 @@ job-references: - WP_CORE_DIR: "/tmp/wordpress/" steps: - checkout - - php/install-composer + - run: php --version + - run: composer --version - run: *setup_environment - run: *install_dependencies - run: From e4880d7c7a3b2d8dd960705a27ac948cf0d6228b Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Thu, 13 Apr 2023 10:29:58 +0100 Subject: [PATCH 20/65] Use MySQL 5.7 in CircleCI workflow --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 073719d..703351c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,7 +13,7 @@ version: 2 job-references: mysql_image: &mysql_image - cimg/mysql:5.6 + cimg/mysql:5.7 setup_environment: &setup_environment name: "Setup Environment Variables" From 9b0cccb4bd990f2c6e1fec45e6b9626b3bfc4394 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Thu, 13 Apr 2023 10:35:57 +0100 Subject: [PATCH 21/65] Add mysql-client to CircleCI workflow job --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 703351c..23f1de2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,7 @@ job-references: install_dependencies: &install_dependencies name: "Install Dependencies" command: | - sudo apt-get update && sudo apt-get install subversion + sudo apt-get update && sudo apt-get install mysql-client subversion php_job: &php_job environment: From 6cde98524ec2dbf60389cbfc962cd98a3155de87 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Thu, 13 Apr 2023 10:44:04 +0100 Subject: [PATCH 22/65] Update versions of PHP used for CircleCI tests --- .circleci/config.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 23f1de2..c894164 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,12 +2,11 @@ workflows: version: 2 main: jobs: - - php56-build - - php70-build - - php71-build - php72-build - php73-build - php74-build + - php80-build + - php81-build version: 2 @@ -81,3 +80,15 @@ jobs: docker: - image: cimg/php:7.4 - image: *mysql_image + + php80-build: + <<: *php_job + docker: + - image: cimg/php:8.0 + - image: *mysql_image + + php81-build: + <<: *php_job + docker: + - image: cimg/php:8.1 + - image: *mysql_image From b1c7ec53674653ead1d0ceec445d38e75ea57f81 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Thu, 13 Apr 2023 10:50:54 +0100 Subject: [PATCH 23/65] Remove PHP 8.1 from CircleCI tests --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c894164..212e51c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,6 @@ workflows: - php73-build - php74-build - php80-build - - php81-build version: 2 From 6093f2eea0c6a41fb70a45d87d07ba5165bd6835 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Thu, 13 Apr 2023 13:55:07 +0100 Subject: [PATCH 24/65] Add unit test for WP_Background_Process::push_to_queue --- .phpcs.xml | 4 +- phpunit.xml.dist => phpunit.xml | 2 +- tests/{test-it-works.php => Test_Setup.php} | 4 +- tests/Test_WP_Background_Process.php | 70 +++++++++++++++++++++ 4 files changed, 75 insertions(+), 5 deletions(-) rename phpunit.xml.dist => phpunit.xml (83%) rename tests/{test-it-works.php => Test_Setup.php} (75%) create mode 100644 tests/Test_WP_Background_Process.php diff --git a/.phpcs.xml b/.phpcs.xml index 32ca255..d74d785 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -6,7 +6,7 @@ . /vendor/ /node_modules/ - /tests/bootstrap.php + /tests/* @@ -19,7 +19,7 @@ - + diff --git a/phpunit.xml.dist b/phpunit.xml similarity index 83% rename from phpunit.xml.dist rename to phpunit.xml index 557663e..d621876 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml @@ -9,7 +9,7 @@ > - ./tests/ + ./tests/ diff --git a/tests/test-it-works.php b/tests/Test_Setup.php similarity index 75% rename from tests/test-it-works.php rename to tests/Test_Setup.php index 25e9271..cfbaa8e 100644 --- a/tests/test-it-works.php +++ b/tests/Test_Setup.php @@ -1,6 +1,6 @@ wpbp = $this->getMockForAbstractClass( WP_Background_Process::class ); + + $this->wpbp->expects( $this->any() ) + ->method( 'task' ) + ->will( $this->returnValue( false ) ); + } + + /** + * Get a property value from WPBP regardless of accessibility. + * + * @param string $name + * + * @return mixed + */ + private function getWPBPProperty( string $name ) { + try { + $property = new ReflectionProperty( 'WP_Background_Process', $name ); + } catch ( Exception $e ) { + return new WP_Error( $e->getCode(), $e->getMessage() ); + } + $property->setAccessible( true ); + + return $property->getValue( $this->wpbp ); + } + + /** + * Test push_to_queue. + * + * @return void + */ + public function test_push_to_queue() { + $this->assertClassHasAttribute( 'data', 'WP_Background_Process', 'class has data property' ); + $this->assertEmpty( $this->getWPBPProperty( 'data' ) ); + + $this->wpbp->push_to_queue( 'wibble' ); + $this->assertNotEmpty( $this->getWPBPProperty( 'data' ) ); + $this->assertEquals( array( 'wibble' ), $this->getWPBPProperty( 'data' ) ); + + $this->wpbp->push_to_queue( 'wobble' ); + $this->assertEquals( array( 'wibble', 'wobble' ), $this->getWPBPProperty( 'data' ) ); + } +} From 4447d1d980c30dd8af216ae5eb3e488d24bd76a2 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Fri, 14 Apr 2023 10:25:39 +0100 Subject: [PATCH 25/65] Add information to README regarding running tests --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/README.md b/README.md index 9317f82..f929c2e 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,76 @@ function wpbp_http_request_args( $r, $url ) { add_filter( 'http_request_args', 'wpbp_http_request_args', 10, 2); ``` +## Contributing + +Contributions are welcome via Pull Requests, but please do raise an issue before +working on anything to discuss the change if there isn't already an issue. If there +is an approved issue you'd like to tackle, please post a comment on it to let people know +you're going to have a go at it so that effort isn't wasted through duplicated work. + +### Unit & Style Tests + +When working on the library, please add unit tests to the appropriate file in the +`tests` directory that cover your changes. + +#### Setting Up + +We use the standard WordPress test libraries for running unit tests. + +Please run the following command to set up the libraries: + +```shell +bin/install-wp-tests.sh db_name db_user db_pass +``` + +Substitute `db_name`, `db_user` and `db_pass` as appropriate. + +Please be aware that running the unit tests is a **destructive operation**, *database +tables will be cleared*, so please use a database name dedicated to running unit tests. +The standard database name usually used by the WordPress community is `wordpress_test`, e.g. + +```shell +bin/install-wp-tests.sh wordpress_test root root +``` + +Please refer to the [Initialize the testing environment locally](https://make.wordpress.org/cli/handbook/misc/plugin-unit-tests/#3-initialize-the-testing-environment-locally) +section of the WordPress Handbook's [Plugin Integration Tests](https://make.wordpress.org/cli/handbook/misc/plugin-unit-tests/) +entry should you run into any issues. + +#### Running Unit Tests + +To run the unit tests, simply run: + +```shell +make test-unit +``` + +If the `composer` dependencies aren't in place, they'll be automatically installed first. + +#### Running Style Tests + +It's important that the code in the library use a consistent style to aid in quickly +understanding it, and to avoid some common issues. `PHP_Code_Sniffer` is used with +mostly standard WordPress rules to help check for consistency. + +To run the style tests, simply run: + +```shell +make test-style +``` + +If the `composer` dependencies aren't in place, they'll be automatically installed first. + +#### Running All Tests + +To make things super simple, just run the following to run all tests: + +```shell +make +``` + +If the `composer` dependencies aren't in place, they'll be automatically installed first. + ## License [GPLv2+](http://www.gnu.org/licenses/gpl-2.0.html) From 3df7ecdd11857dd30f1018a6a168ea777c345d5c Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Fri, 14 Apr 2023 10:31:48 +0100 Subject: [PATCH 26/65] Update README to note that PHP 5.6+ is required --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f929c2e..73535d2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ WP Background Processing can be used to fire off non-blocking asynchronous reque Inspired by [TechCrunch WP Asynchronous Tasks](https://github.com/techcrunch/wp-async-task). -__Requires PHP 5.2+__ +__Requires PHP 5.6+__ ## Install From 8feb3706bab0846e28029ba2eabd05b7073705c1 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Fri, 14 Apr 2023 14:58:11 +0100 Subject: [PATCH 27/65] Clean out data on save so new data isn't prepended to next in-process save Resolves #7 --- classes/wp-background-process.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 9af3a40..64ca7ad 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -108,6 +108,9 @@ public function save() { update_site_option( $key, $this->data ); } + // Clean out data so that new data isn't prepended with closed session's data. + $this->data = array(); + return $this; } From 949b4ab04ce7ad465b6a21c6cbd3708350fd5f57 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Fri, 14 Apr 2023 14:59:51 +0100 Subject: [PATCH 28/65] Add get_batches function to WP_Background_Process class --- classes/wp-background-process.php | 68 ++++++++++++++++---- tests/Test_WP_Background_Process.php | 94 ++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 13 deletions(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 64ca7ad..3aa7227 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -149,13 +149,14 @@ public function delete( $key ) { * Generates a unique key based on microtime. Queue items are * given a unique key so that they can be merged upon save. * - * @param int $length Length. + * @param int $length Optional max length to trim key to, defaults to 64 characters. + * @param string $key Optional string to append to identifier before hash, defaults to "batch". * * @return string */ - protected function generate_key( $length = 64 ) { + protected function generate_key( $length = 64, $key = 'batch' ) { $unique = md5( microtime() . rand() ); - $prepend = $this->identifier . '_batch_'; + $prepend = $this->identifier . '_' . $key . '_'; return substr( $prepend . $unique, 0, $length ); } @@ -264,8 +265,29 @@ protected function unlock_process() { * @return stdClass Return the first batch of queued items. */ protected function get_batch() { + return array_reduce( + $this->get_batches( 1 ), + function ( $carry, $batch ) { + return $batch; + }, + array() + ); + } + + /** + * Get batches. + * + * @param int $limit Number of batches to return, defaults to all. + * + * @return array of stdClass + */ + public function get_batches( $limit = 0 ) { global $wpdb; + if ( empty( $limit ) || ! is_int( $limit ) ) { + $limit = 0; + } + $table = $wpdb->options; $column = 'option_name'; $key_column = 'option_id'; @@ -280,19 +302,39 @@ protected function get_batch() { $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; - $query = $wpdb->get_row( $wpdb->prepare( " + $sql = ' SELECT * - FROM $table - WHERE $column LIKE %s - ORDER BY $key_column ASC - LIMIT 1 - ", $key ) ); + FROM ' . $table . ' + WHERE ' . $column . ' LIKE %s + ORDER BY ' . $key_column . ' ASC + '; + + $args = array( $key ); + + if ( ! empty( $limit ) ) { + $sql .= ' LIMIT %d'; + + $args[] = $limit; + } + + $items = $wpdb->get_results( $wpdb->prepare( $sql, $args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + $batches = array(); - $batch = new stdClass(); - $batch->key = $query->{$column}; - $batch->data = maybe_unserialize( $query->{$value_column} ); + if ( ! empty( $items ) ) { + $batches = array_map( + function ( $item ) use ( $column, $value_column ) { + $batch = new stdClass(); + $batch->key = $item->{$column}; + $batch->data = maybe_unserialize( $item->{$value_column} ); + + return $batch; + }, + $items + ); + } - return $batch; + return $batches; } /** diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index f499abe..f86df60 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -51,6 +51,25 @@ private function getWPBPProperty( string $name ) { return $property->getValue( $this->wpbp ); } + /** + * Execute a method of WPBP regardless of accessibility. + * + * @param string $name Method name. + * @param mixed $args None, one or more args to pass to method. + * + * @return mixed + */ + private function executeWPBPMethod( string $name, ...$args ) { + try { + $method = new ReflectionMethod( 'WP_Background_Process', $name ); + $method->setAccessible( true ); + + return $method->invoke( $this->wpbp, ...$args ); + } catch ( Exception $e ) { + return new WP_Error( $e->getCode(), $e->getMessage() ); + } + } + /** * Test push_to_queue. * @@ -67,4 +86,79 @@ public function test_push_to_queue() { $this->wpbp->push_to_queue( 'wobble' ); $this->assertEquals( array( 'wibble', 'wobble' ), $this->getWPBPProperty( 'data' ) ); } + + /** + * Test get_batches. + * + * @return void + */ + public function test_get_batches() { + $this->assertEmpty( $this->wpbp->get_batches(), 'no batches until save' ); + + $this->wpbp->push_to_queue( 'wibble' ); + $this->assertNotEmpty( $this->getWPBPProperty( 'data' ) ); + $this->assertEquals( array( 'wibble' ), $this->getWPBPProperty( 'data' ) ); + $this->assertEmpty( $this->wpbp->get_batches(), 'no batches until save' ); + + $this->wpbp->push_to_queue( 'wobble' ); + $this->assertEquals( array( 'wibble', 'wobble' ), $this->getWPBPProperty( 'data' ) ); + $this->assertEmpty( $this->wpbp->get_batches(), 'no batches until save' ); + + $this->wpbp->save(); + $first_batch = $this->wpbp->get_batches(); + $this->assertNotEmpty( $first_batch ); + $this->assertCount( 1, $first_batch ); + + $this->wpbp->push_to_queue( 'more wibble' ); + $this->wpbp->save(); + $this->assertCount( 2, $this->wpbp->get_batches() ); + + $this->wpbp->push_to_queue( 'Wibble wobble all day long.' ); + $this->wpbp->save(); + $this->assertCount( 3, $this->wpbp->get_batches() ); + + $this->assertEquals( $first_batch, $this->wpbp->get_batches( 1 ) ); + $this->assertNotEquals( $first_batch, $this->wpbp->get_batches( 2 ) ); + $this->assertCount( 2, $this->wpbp->get_batches( 2 ) ); + $this->assertCount( 3, $this->wpbp->get_batches( 3 ) ); + $this->assertCount( 3, $this->wpbp->get_batches( 5 ) ); + } + + /** + * Test get_batch. + * + * @return void + */ + public function test_get_batch() { + $this->assertEmpty( $this->executeWPBPMethod( 'get_batch' ), 'no batches until save' ); + + $this->wpbp->push_to_queue( 'wibble' ); + $this->assertNotEmpty( $this->getWPBPProperty( 'data' ) ); + $this->assertEquals( array( 'wibble' ), $this->getWPBPProperty( 'data' ) ); + $this->assertEmpty( $this->executeWPBPMethod( 'get_batch' ), 'no batches until save' ); + + $this->wpbp->push_to_queue( 'wobble' ); + $this->assertEquals( array( 'wibble', 'wobble' ), $this->getWPBPProperty( 'data' ) ); + $this->assertEmpty( $this->executeWPBPMethod( 'get_batch' ), 'no batches until save' ); + + $this->wpbp->save(); + $first_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->assertNotEmpty( $first_batch ); + $this->assertInstanceOf( 'stdClass', $first_batch ); + $this->assertEquals( array( 'wibble', 'wobble' ), $first_batch->data ); + + $this->wpbp->push_to_queue( 'more wibble' ); + $this->wpbp->save(); + $second_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->assertNotEmpty( $second_batch ); + $this->assertInstanceOf( 'stdClass', $second_batch ); + $this->assertEquals( $first_batch, $second_batch, 'same 1st batch returned until deleted' ); + + $this->executeWPBPMethod( 'delete', $first_batch->key ); + $second_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->assertNotEmpty( $second_batch ); + $this->assertInstanceOf( 'stdClass', $second_batch ); + $this->assertNotEquals( $first_batch, $second_batch, '2nd batch returned as 1st deleted' ); + $this->assertEquals( array( 'more wibble' ), $second_batch->data ); + } } From 1b263749516feab77436dbcb046976886c6970da Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Fri, 14 Apr 2023 17:48:08 +0100 Subject: [PATCH 29/65] Port forward new features and improvements from WP Offload Media --- .phpcs.xml | 8 - classes/wp-async-request.php | 4 +- classes/wp-background-process.php | 216 +++++++++++++++++++++++---- tests/Test_WP_Background_Process.php | 2 +- 4 files changed, 193 insertions(+), 37 deletions(-) diff --git a/.phpcs.xml b/.phpcs.xml index d74d785..c082481 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -36,15 +36,7 @@ - - - - - - - - diff --git a/classes/wp-async-request.php b/classes/wp-async-request.php index 595788b..7c896c3 100644 --- a/classes/wp-async-request.php +++ b/classes/wp-async-request.php @@ -142,8 +142,8 @@ protected function get_post_args() { 'timeout' => 0.01, 'blocking' => false, 'body' => $this->data, - 'cookies' => $_COOKIE, - 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + 'cookies' => $_COOKIE, // Passing cookies ensures request is performed as initiating user. + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // Local requests, fine to pass false. ); /** diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 3aa7227..6ce37a3 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -36,7 +36,7 @@ abstract class WP_Background_Process extends WP_Async_Request { /** * Cron_hook_identifier * - * @var mixed + * @var string * @access protected */ protected $cron_hook_identifier; @@ -44,11 +44,25 @@ abstract class WP_Background_Process extends WP_Async_Request { /** * Cron_interval_identifier * - * @var mixed + * @var string * @access protected */ protected $cron_interval_identifier; + /** + * The status set when process is cancelling. + * + * @var int + */ + const STATUS_CANCELLED = 1; + + /** + * The status set when process is paused or pausing. + * + * @var int; + */ + const STATUS_PAUSED = 2; + /** * Initiate new background process. */ @@ -143,6 +157,100 @@ public function delete( $key ) { return $this; } + /** + * Delete entire job queue. + */ + public function delete_all() { + $batches = $this->get_batches(); + + foreach ( $batches as $batch ) { + $this->delete( $batch->key ); + } + + delete_site_option( $this->get_status_key() ); + + $this->cancelled(); + } + + /** + * Cancel job on next batch. + */ + public function cancel() { + update_site_option( $this->get_status_key(), self::STATUS_CANCELLED ); + + // Just in case the job was paused at the time. + $this->dispatch(); + } + + /** + * Has the process been cancelled? + * + * @return bool + */ + public function is_cancelled() { + $status = get_site_option( $this->get_status_key(), 0 ); + + if ( absint( $status ) === self::STATUS_CANCELLED ) { + return true; + } + + return false; + } + + /** + * Called when background process has been cancelled. + */ + protected function cancelled() { + do_action( $this->identifier . '_cancelled' ); + } + + /** + * Pause job on next batch. + */ + public function pause() { + update_site_option( $this->get_status_key(), self::STATUS_PAUSED ); + } + + /** + * Is the job paused? + * + * @return bool + */ + public function is_paused() { + $status = get_site_option( $this->get_status_key(), 0 ); + + if ( absint( $status ) === self::STATUS_PAUSED ) { + return true; + } + + return false; + } + + /** + * Called when background process has been paused. + */ + protected function paused() { + do_action( $this->identifier . '_paused' ); + } + + /** + * Resume job. + */ + public function resume() { + delete_site_option( $this->get_status_key() ); + + $this->schedule_event(); + $this->dispatch(); + $this->resumed(); + } + + /** + * Called when background process has been resumed. + */ + protected function resumed() { + do_action( $this->identifier . '_resumed' ); + } + /** * Generate key for a batch. * @@ -155,12 +263,21 @@ public function delete( $key ) { * @return string */ protected function generate_key( $length = 64, $key = 'batch' ) { - $unique = md5( microtime() . rand() ); + $unique = md5( microtime() . wp_rand() ); $prepend = $this->identifier . '_' . $key . '_'; return substr( $prepend . $unique, 0, $length ); } + /** + * Get the status key. + * + * @return string + */ + protected function get_status_key() { + return $this->identifier . '_status'; + } + /** * Maybe process a batch of queued items. * @@ -176,6 +293,18 @@ public function maybe_handle() { wp_die(); } + if ( $this->is_cancelled() ) { + $this->clear_scheduled_event(); + $this->delete_all(); + wp_die(); + } + + if ( $this->is_paused() ) { + $this->clear_scheduled_event(); + $this->paused(); + wp_die(); + } + if ( $this->is_queue_empty() ) { // No data to process. wp_die(); @@ -206,11 +335,13 @@ protected function is_queue_empty() { $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; - $count = $wpdb->get_var( $wpdb->prepare( " + $sql = ' SELECT COUNT(*) - FROM $table - WHERE $column LIKE %s - ", $key ) ); + FROM ' . $table . ' + WHERE ' . $column . ' LIKE %s + '; + + $count = $wpdb->get_var( $wpdb->prepare( $sql, $key ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared return ! ( $count > 0 ); } @@ -346,6 +477,22 @@ function ( $item ) use ( $column, $value_column ) { protected function handle() { $this->lock_process(); + /** + * Number of seconds to sleep between batches. Defaults to 0 seconds, minimum 0. + * + * @param int $seconds + */ + $throttle_seconds = max( + 0, + apply_filters( + $this->identifier . '_seconds_between_batches', + apply_filters( + $this->prefix . '_seconds_between_batches', + 0 + ) + ) + ); + do { $batch = $this->get_batch(); @@ -358,16 +505,22 @@ protected function handle() { unset( $batch->data[ $key ] ); } + // Keep the batch up to date while processing it. + if ( ! empty( $batch->data ) ) { + $this->update( $batch->key, $batch->data ); + } + + // Let the server breathe a little. + sleep( $throttle_seconds ); + if ( $this->time_exceeded() || $this->memory_exceeded() ) { // Batch limits reached. break; } } - // Update or delete current batch. - if ( ! empty( $batch->data ) ) { - $this->update( $batch->key, $batch->data ); - } else { + // Delete current batch if fully processed. + if ( empty( $batch->data ) ) { $this->delete( $batch->key ); } } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); @@ -451,8 +604,19 @@ protected function time_exceeded() { * performed, or, call parent::complete(). */ protected function complete() { + delete_site_option( $this->get_status_key() ); + // Remove the cron healthcheck job from the cron schedule. $this->clear_scheduled_event(); + + $this->completed(); + } + + /** + * Called when background process has completed. + */ + protected function completed() { + do_action( $this->identifier . '_completed' ); } /** @@ -465,16 +629,22 @@ protected function complete() { * @return mixed */ public function schedule_cron_healthcheck( $schedules ) { - $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); + $interval = apply_filters( $this->cron_interval_identifier, 5 ); if ( property_exists( $this, 'cron_interval' ) ) { - $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); + $interval = apply_filters( $this->cron_interval_identifier, $this->cron_interval ); } - // Adds an "Every NNN Minutes" schedule to the existing cron schedules. - $schedules[ $this->identifier . '_cron_interval' ] = array( + if ( 1 === $interval ) { + $display = __( 'Every Minute' ); + } else { + $display = sprintf( __( 'Every %d Minutes' ), $interval ); + } + + // Adds an "Every NNN Minute(s)" schedule to the existing cron schedules. + $schedules[ $this->cron_interval_identifier ] = array( 'interval' => MINUTE_IN_SECONDS * $interval, - 'display' => sprintf( __( 'Every %d Minutes' ), $interval ), + 'display' => $display, ); return $schedules; @@ -498,9 +668,7 @@ public function handle_cron_healthcheck() { exit; } - $this->handle(); - - exit; + $this->dispatch(); } /** @@ -528,15 +696,11 @@ protected function clear_scheduled_event() { * * Stop processing queue items, clear cron job and delete batch. * + * @deprecated 1.1.0 Superseded. + * @see cancel() */ public function cancel_process() { - if ( ! $this->is_queue_empty() ) { - $batch = $this->get_batch(); - - $this->delete( $batch->key ); - - wp_clear_scheduled_hook( $this->cron_hook_identifier ); - } + $this->cancel(); } /** diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index f86df60..863e1c2 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -154,7 +154,7 @@ public function test_get_batch() { $this->assertInstanceOf( 'stdClass', $second_batch ); $this->assertEquals( $first_batch, $second_batch, 'same 1st batch returned until deleted' ); - $this->executeWPBPMethod( 'delete', $first_batch->key ); + $this->wpbp->delete( $first_batch->key ); $second_batch = $this->executeWPBPMethod( 'get_batch' ); $this->assertNotEmpty( $second_batch ); $this->assertInstanceOf( 'stdClass', $second_batch ); From 64a6139d857de7ef42e380c6ca4884de2bdd5851 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 10:54:59 +0100 Subject: [PATCH 30/65] Add unit test for WP_Background_Process::save() --- tests/Test_WP_Background_Process.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index 863e1c2..9a27ca7 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -87,6 +87,24 @@ public function test_push_to_queue() { $this->assertEquals( array( 'wibble', 'wobble' ), $this->getWPBPProperty( 'data' ) ); } + /** + * Test save. + * + * @return void + */ + public function test_save() { + $this->assertClassHasAttribute( 'data', 'WP_Background_Process', 'class has data property' ); + $this->assertEmpty( $this->getWPBPProperty( 'data' ) ); + $this->assertEmpty( $this->wpbp->get_batches(), 'no batches until save' ); + + $this->wpbp->push_to_queue( 'wibble' ); + $this->assertNotEmpty( $this->getWPBPProperty( 'data' ) ); + $this->assertEquals( array( 'wibble' ), $this->getWPBPProperty( 'data' ) ); + $this->wpbp->save(); + $this->assertEmpty( $this->getWPBPProperty( 'data' ), 'data emptied after save' ); + $this->assertNotEmpty( $this->wpbp->get_batches(), 'batches exist after save' ); + } + /** * Test get_batches. * From 203e6492da67629096dabc3ac82c229056556f91 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 11:08:54 +0100 Subject: [PATCH 31/65] Add unit test for WP_Background_Process::cancel() --- tests/Test_WP_Background_Process.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index 9a27ca7..f1f760c 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -179,4 +179,17 @@ public function test_get_batch() { $this->assertNotEquals( $first_batch, $second_batch, '2nd batch returned as 1st deleted' ); $this->assertEquals( array( 'more wibble' ), $second_batch->data ); } + + /** + * Test cancel. + * + * @return void + */ + public function test_cancel() { + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertFalse( $this->wpbp->is_cancelled() ); + $this->wpbp->cancel(); + $this->assertTrue( $this->wpbp->is_cancelled() ); + } } From 550c202d69d284145ca29a511693500a93da0b7b Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 11:10:12 +0100 Subject: [PATCH 32/65] Add unit test for WP_Background_Process::pause() --- tests/Test_WP_Background_Process.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index f1f760c..aef498a 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -192,4 +192,17 @@ public function test_cancel() { $this->wpbp->cancel(); $this->assertTrue( $this->wpbp->is_cancelled() ); } + + /** + * Test pause. + * + * @return void + */ + public function test_pause() { + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertFalse( $this->wpbp->is_paused() ); + $this->wpbp->pause(); + $this->assertTrue( $this->wpbp->is_paused() ); + } } From 77f5029b899f282844a6bca20983b8411f297ede Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 11:21:12 +0100 Subject: [PATCH 33/65] Add unit test for WP_Background_Process::delete() --- tests/Test_WP_Background_Process.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index aef498a..0acf1bd 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -205,4 +205,23 @@ public function test_pause() { $this->wpbp->pause(); $this->assertTrue( $this->wpbp->is_paused() ); } + + /** + * Test delete. + * + * @return void + */ + public function test_delete() { + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertCount( 1, $this->wpbp->get_batches() ); + $this->wpbp->push_to_queue( 'wobble' ); + $this->wpbp->save(); + $this->assertCount( 2, $this->wpbp->get_batches() ); + $first_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->wpbp->delete( $first_batch->key ); + $this->assertCount( 1, $this->wpbp->get_batches() ); + $second_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->assertNotEquals( $first_batch, $second_batch, '2nd batch returned as 1st deleted' ); + } } From 76010289905a0262f71355cbc30d020f827c6ddc Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 11:23:01 +0100 Subject: [PATCH 34/65] Add unit test for WP_Background_Process::delete_all() --- tests/Test_WP_Background_Process.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index 0acf1bd..3781066 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -224,4 +224,20 @@ public function test_delete() { $second_batch = $this->executeWPBPMethod( 'get_batch' ); $this->assertNotEquals( $first_batch, $second_batch, '2nd batch returned as 1st deleted' ); } + + /** + * Test delete_all. + * + * @return void + */ + public function test_delete_all() { + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertCount( 1, $this->wpbp->get_batches() ); + $this->wpbp->push_to_queue( 'wobble' ); + $this->wpbp->save(); + $this->assertCount( 2, $this->wpbp->get_batches() ); + $this->wpbp->delete_all(); + $this->assertCount( 0, $this->wpbp->get_batches() ); + } } From 32bb7d8dc1f1457ec3e5f59a125bab5c3f67a1b3 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 11:26:57 +0100 Subject: [PATCH 35/65] Add unit test for WP_Background_Process::resume() --- tests/Test_WP_Background_Process.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index 3781066..e6a887d 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -206,6 +206,21 @@ public function test_pause() { $this->assertTrue( $this->wpbp->is_paused() ); } + /** + * Test resume. + * + * @return void + */ + public function test_resume() { + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertFalse( $this->wpbp->is_paused() ); + $this->wpbp->pause(); + $this->assertTrue( $this->wpbp->is_paused() ); + $this->wpbp->resume(); + $this->assertFalse( $this->wpbp->is_paused() ); + } + /** * Test delete. * From 57a7a17683666c0ec05ced695203e84c5dc74a18 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 11:37:44 +0100 Subject: [PATCH 36/65] Add unit test for WP_Background_Process::update() --- tests/Test_WP_Background_Process.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index e6a887d..6488dbd 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -255,4 +255,24 @@ public function test_delete_all() { $this->wpbp->delete_all(); $this->assertCount( 0, $this->wpbp->get_batches() ); } + + /** + * Test update. + * + * @return void + */ + public function test_update() { + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertCount( 1, $this->wpbp->get_batches() ); + $this->wpbp->push_to_queue( 'wobble' ); + $this->wpbp->save(); + $this->assertCount( 2, $this->wpbp->get_batches() ); + $first_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->wpbp->update( $first_batch->key, array( 'Wibble wobble all day long!' ) ); + $this->assertCount( 2, $this->wpbp->get_batches() ); + $updated_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->assertNotEquals( $first_batch, $updated_batch, 'fetched updated batch different to 1st fetch' ); + $this->assertEquals( array( 'Wibble wobble all day long!' ), $updated_batch->data, 'fetched updated batch has expected data' ); + } } From 3dc2eb57ae90e3dd957d13c2827a686aaf33ef51 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 14:35:47 +0100 Subject: [PATCH 37/65] Add unit test for WP_Background_Process::maybe_handle() when cancelling --- classes/wp-async-request.php | 24 +++++++++++++++++++- classes/wp-background-process.php | 14 +++++++----- tests/Test_WP_Background_Process.php | 33 ++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/classes/wp-async-request.php b/classes/wp-async-request.php index 7c896c3..78471a4 100644 --- a/classes/wp-async-request.php +++ b/classes/wp-async-request.php @@ -158,6 +158,8 @@ protected function get_post_args() { * Maybe handle a dispatched request. * * Check for correct nonce and pass to handler. + * + * @return void|mixed */ public function maybe_handle() { // Don't lock up other requests while processing. @@ -167,7 +169,27 @@ public function maybe_handle() { $this->handle(); - wp_die(); + return $this->maybe_wp_die(); + } + + /** + * Should the process exit with wp_die? + * + * @param mixed $return What to return if filter says don't die, default is null. + * + * @return void|mixed + */ + protected function maybe_wp_die( $return = null ) { + /** + * Should wp_die be used? + * + * @returns bool + */ + if ( apply_filters( $this->identifier . '_wp_die', true ) ) { + wp_die(); + } + + return $return; } /** diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 6ce37a3..9a05c7c 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -290,31 +290,33 @@ public function maybe_handle() { if ( $this->is_process_running() ) { // Background process already running. - wp_die(); + return $this->maybe_wp_die(); } if ( $this->is_cancelled() ) { $this->clear_scheduled_event(); $this->delete_all(); - wp_die(); + + return $this->maybe_wp_die(); } if ( $this->is_paused() ) { $this->clear_scheduled_event(); $this->paused(); - wp_die(); + + return $this->maybe_wp_die(); } if ( $this->is_queue_empty() ) { // No data to process. - wp_die(); + return $this->maybe_wp_die(); } check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); - wp_die(); + return $this->maybe_wp_die(); } /** @@ -534,7 +536,7 @@ protected function handle() { $this->complete(); } - wp_die(); + return $this->maybe_wp_die(); } /** diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index 6488dbd..b9343c4 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -275,4 +275,37 @@ public function test_update() { $this->assertNotEquals( $first_batch, $updated_batch, 'fetched updated batch different to 1st fetch' ); $this->assertEquals( array( 'Wibble wobble all day long!' ), $updated_batch->data, 'fetched updated batch has expected data' ); } + + /** + * Test maybe_handle when cancelling. + * + * @return void + */ + public function test_maybe_handle_cancelled() { + // Cancelled status results in cleared batches and action fired. + $cancelled_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_cancelled', function () use ( &$cancelled_fired ) { + $cancelled_fired = true; + } ); + // Paused action should not be fired though. + $paused_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_paused', function () use ( &$paused_fired ) { + $paused_fired = true; + } ); + add_filter( $this->getWPBPProperty( 'identifier' ) . '_wp_die', '__return_false' ); + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertCount( 1, $this->wpbp->get_batches() ); + $this->wpbp->push_to_queue( 'wobble' ); + $this->wpbp->save(); + $this->assertCount( 2, $this->wpbp->get_batches() ); + update_site_option( $this->executeWPBPMethod( 'get_status_key' ), WP_Background_Process::STATUS_CANCELLED ); + $this->assertCount( 2, $this->wpbp->get_batches() ); + $this->assertFalse( $cancelled_fired, 'cancelled action not fired yet' ); + $this->assertFalse( $paused_fired, 'paused action not fired yet' ); + $this->wpbp->maybe_handle(); + $this->assertCount( 0, $this->wpbp->get_batches() ); + $this->assertTrue( $cancelled_fired, 'cancelled action fired' ); + $this->assertFalse( $paused_fired, 'paused action still not fired yet' ); + } } From a673aea43d0b4aaec22f60f826222917b742c6bd Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 15:56:08 +0100 Subject: [PATCH 38/65] Add unit test for WP_Background_Process::maybe_handle() when pausing and resuming --- tests/Test_WP_Background_Process.php | 78 ++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index b9343c4..366302c 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -292,6 +292,11 @@ public function test_maybe_handle_cancelled() { add_action( $this->getWPBPProperty( 'identifier' ) . '_paused', function () use ( &$paused_fired ) { $paused_fired = true; } ); + // Completed action should not be fired though. + $completed_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_completed', function () use ( &$completed_fired ) { + $completed_fired = true; + } ); add_filter( $this->getWPBPProperty( 'identifier' ) . '_wp_die', '__return_false' ); $this->wpbp->push_to_queue( 'wibble' ); $this->wpbp->save(); @@ -300,12 +305,85 @@ public function test_maybe_handle_cancelled() { $this->wpbp->save(); $this->assertCount( 2, $this->wpbp->get_batches() ); update_site_option( $this->executeWPBPMethod( 'get_status_key' ), WP_Background_Process::STATUS_CANCELLED ); + $this->assertTrue( $this->wpbp->is_cancelled(), 'is_cancelled' ); $this->assertCount( 2, $this->wpbp->get_batches() ); $this->assertFalse( $cancelled_fired, 'cancelled action not fired yet' ); $this->assertFalse( $paused_fired, 'paused action not fired yet' ); + $this->assertFalse( $completed_fired, 'completed action not fired yet' ); $this->wpbp->maybe_handle(); $this->assertCount( 0, $this->wpbp->get_batches() ); $this->assertTrue( $cancelled_fired, 'cancelled action fired' ); $this->assertFalse( $paused_fired, 'paused action still not fired yet' ); + $this->assertFalse( $completed_fired, 'completed action not fired yet' ); + } + + /** + * Test maybe_handle when pausing and resuming. + * + * @return void + */ + public function test_maybe_handle_paused_resumed() { + // Cancelled action should not be fired. + $cancelled_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_cancelled', function () use ( &$cancelled_fired ) { + $cancelled_fired = true; + } ); + // Paused action should fire and batches remain intact. + $paused_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_paused', function () use ( &$paused_fired ) { + $paused_fired = true; + } ); + // Resumed action should fire on resume before batches handled. + $resumed_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_resumed', function () use ( &$resumed_fired ) { + $resumed_fired = true; + } ); + // Completed action should fire after batches handled. + $completed_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_completed', function () use ( &$completed_fired ) { + $completed_fired = true; + } ); + add_filter( $this->getWPBPProperty( 'identifier' ) . '_wp_die', '__return_false' ); + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertCount( 1, $this->wpbp->get_batches() ); + $this->wpbp->push_to_queue( 'wobble' ); + $this->wpbp->save(); + $this->assertCount( 2, $this->wpbp->get_batches() ); + $this->wpbp->pause(); + $this->assertTrue( $this->wpbp->is_paused(), 'is_paused' ); + $this->assertCount( 2, $this->wpbp->get_batches() ); + $this->assertFalse( $cancelled_fired, 'cancelled action not fired yet' ); + $this->assertFalse( $paused_fired, 'paused action not fired yet' ); + $this->assertFalse( $resumed_fired, 'resumed action not fired yet' ); + $this->assertFalse( $completed_fired, 'completed action not fired yet' ); + $this->wpbp->maybe_handle(); + $this->assertCount( 2, $this->wpbp->get_batches() ); + $this->assertFalse( $cancelled_fired, 'cancelled action still not fired yet' ); + $this->assertTrue( $paused_fired, 'paused action fired' ); + $this->assertFalse( $resumed_fired, 'resumed action still not fired yet' ); + $this->assertFalse( $completed_fired, 'completed action not fired yet' ); + + // Reset for resume and ensure dispatch does nothing to that maybe_handle can be monitored. + $paused_fired = false; + add_filter( 'pre_http_request', '__return_true' ); + $this->wpbp->resume(); + remove_filter( 'pre_http_request', '__return_true' ); + $this->assertFalse( $this->wpbp->is_paused(), 'not is_paused after resume' ); + $this->assertCount( 2, $this->wpbp->get_batches() ); + $this->assertFalse( $cancelled_fired, 'cancelled action not fired yet' ); + $this->assertFalse( $paused_fired, 'paused action not fired yet' ); + $this->assertTrue( $resumed_fired, 'resumed action fired' ); + $this->assertFalse( $completed_fired, 'completed action not fired yet' ); + + // Don't expect resumed to be fired again, and batches to be handled with valid security. + $resumed_fired = false; + $_REQUEST['nonce'] = wp_create_nonce( $this->getWPBPProperty( 'identifier' ) ); + $this->wpbp->maybe_handle(); + $this->assertCount( 0, $this->wpbp->get_batches(), 'after resume all batches processed with maybe_handle' ); + $this->assertFalse( $cancelled_fired, 'cancelled action still not fired yet' ); + $this->assertFalse( $paused_fired, 'paused action not fired yet' ); + $this->assertFalse( $resumed_fired, 'resumed action still not fired yet' ); + $this->assertTrue( $completed_fired, 'completed action fired' ); } } From 358cedfac2da213c46432681a4f872a813ca5599 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 16:13:46 +0100 Subject: [PATCH 39/65] Add unit tests for WP_Background_Process::maybe_handle() simple scenarios --- tests/Test_WP_Background_Process.php | 85 ++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index 366302c..6532801 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -386,4 +386,89 @@ public function test_maybe_handle_paused_resumed() { $this->assertFalse( $resumed_fired, 'resumed action still not fired yet' ); $this->assertTrue( $completed_fired, 'completed action fired' ); } + + /** + * Test maybe_handle when handling a single batch. + * + * @return void + */ + public function test_maybe_handle_single_batch() { + // Cancelled action should not be fired. + $cancelled_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_cancelled', function () use ( &$cancelled_fired ) { + $cancelled_fired = true; + } ); + // Paused action should not be fired. + $paused_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_paused', function () use ( &$paused_fired ) { + $paused_fired = true; + } ); + // Resumed action should not be fired. + $resumed_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_resumed', function () use ( &$resumed_fired ) { + $resumed_fired = true; + } ); + // Completed action should fire after batches handled. + $completed_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_completed', function () use ( &$completed_fired ) { + $completed_fired = true; + } ); + add_filter( $this->getWPBPProperty( 'identifier' ) . '_wp_die', '__return_false' ); + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertCount( 1, $this->wpbp->get_batches() ); + $this->assertFalse( $cancelled_fired, 'cancelled action not fired yet' ); + $this->assertFalse( $paused_fired, 'paused action not fired yet' ); + $this->assertFalse( $resumed_fired, 'resumed action not fired yet' ); + $this->assertFalse( $completed_fired, 'completed action not fired yet' ); + + $_REQUEST['nonce'] = wp_create_nonce( $this->getWPBPProperty( 'identifier' ) ); + $this->wpbp->maybe_handle(); + $this->assertCount( 0, $this->wpbp->get_batches(), 'after resume all batches processed with maybe_handle' ); + $this->assertFalse( $cancelled_fired, 'cancelled action still not fired yet' ); + $this->assertFalse( $paused_fired, 'paused action not fired yet' ); + $this->assertFalse( $resumed_fired, 'resumed action still not fired yet' ); + $this->assertTrue( $completed_fired, 'completed action fired' ); + } + + /** + * Test maybe_handle when handling nothing. + * + * @return void + */ + public function test_maybe_handle_nothing() { + // Cancelled action should not be fired. + $cancelled_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_cancelled', function () use ( &$cancelled_fired ) { + $cancelled_fired = true; + } ); + // Paused action should not be fired. + $paused_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_paused', function () use ( &$paused_fired ) { + $paused_fired = true; + } ); + // Resumed action should not be fired. + $resumed_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_resumed', function () use ( &$resumed_fired ) { + $resumed_fired = true; + } ); + // Completed action should not be fired. + $completed_fired = false; + add_action( $this->getWPBPProperty( 'identifier' ) . '_completed', function () use ( &$completed_fired ) { + $completed_fired = true; + } ); + add_filter( $this->getWPBPProperty( 'identifier' ) . '_wp_die', '__return_false' ); + $this->assertCount( 0, $this->wpbp->get_batches() ); + $this->assertFalse( $cancelled_fired, 'cancelled action not fired yet' ); + $this->assertFalse( $paused_fired, 'paused action not fired yet' ); + $this->assertFalse( $resumed_fired, 'resumed action not fired yet' ); + $this->assertFalse( $completed_fired, 'completed action not fired yet' ); + + $this->wpbp->maybe_handle(); + $this->assertCount( 0, $this->wpbp->get_batches(), 'after resume all batches processed with maybe_handle' ); + $this->assertFalse( $cancelled_fired, 'cancelled action still not fired yet' ); + $this->assertFalse( $paused_fired, 'paused action not fired yet' ); + $this->assertFalse( $resumed_fired, 'resumed action still not fired yet' ); + $this->assertFalse( $completed_fired, 'completed action not fired yet' ); + } } From ebebef059dc43a3d4be421277a294dcf220dbea8 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 16:37:51 +0100 Subject: [PATCH 40/65] Add unit tests for WP_Background_Process::is_processing() --- classes/wp-async-request.php | 2 +- classes/wp-background-process.php | 20 +++++++++++++++++--- tests/Test_WP_Background_Process.php | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/classes/wp-async-request.php b/classes/wp-async-request.php index 78471a4..9759ab0 100644 --- a/classes/wp-async-request.php +++ b/classes/wp-async-request.php @@ -183,7 +183,7 @@ protected function maybe_wp_die( $return = null ) { /** * Should wp_die be used? * - * @returns bool + * @return bool */ if ( apply_filters( $this->identifier . '_wp_die', true ) ) { wp_die(); diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 9a05c7c..54903c2 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -83,7 +83,7 @@ public function __construct() { * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted. */ public function dispatch() { - if ( $this->is_process_running() ) { + if ( $this->is_processing() ) { // Process already running. return false; } @@ -288,7 +288,7 @@ public function maybe_handle() { // Don't lock up other requests while processing. session_write_close(); - if ( $this->is_process_running() ) { + if ( $this->is_processing() ) { // Background process already running. return $this->maybe_wp_die(); } @@ -353,8 +353,22 @@ protected function is_queue_empty() { * * Check whether the current process is already running * in a background process. + * + * @return bool + * + * @deprecated 1.1.0 Superseded. + * @see is_processing() */ protected function is_process_running() { + return $this->is_processing(); + } + + /** + * Is the background process currently running? + * + * @return bool + */ + public function is_processing() { if ( get_site_transient( $this->identifier . '_process_lock' ) ) { // Process already running. return true; @@ -659,7 +673,7 @@ public function schedule_cron_healthcheck( $schedules ) { * and data exists in the queue. */ public function handle_cron_healthcheck() { - if ( $this->is_process_running() ) { + if ( $this->is_processing() ) { // Background process already running. exit; } diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index 6532801..16b265d 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -471,4 +471,32 @@ public function test_maybe_handle_nothing() { $this->assertFalse( $resumed_fired, 'resumed action still not fired yet' ); $this->assertFalse( $completed_fired, 'completed action not fired yet' ); } + + /** + * Test is_processing. + * + * @return void + */ + public function test_is_processing() { + $this->assertFalse( $this->wpbp->is_processing(), 'not processing yet' ); + $this->executeWPBPMethod( 'lock_process' ); + $this->assertTrue( $this->wpbp->is_processing(), 'processing' ); + + // With batches to be processed, maybe_handle does nothing as "another instance is processing". + add_filter( $this->getWPBPProperty( 'identifier' ) . '_wp_die', '__return_false' ); + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertCount( 1, $this->wpbp->get_batches() ); + $this->wpbp->maybe_handle(); + $this->assertCount( 1, $this->wpbp->get_batches() ); + + // Unlock and maybe_handle can process the batch. + $this->executeWPBPMethod( 'unlock_process' ); + $this->assertFalse( $this->wpbp->is_processing(), 'not processing yet' ); + $this->assertCount( 1, $this->wpbp->get_batches() ); + $_REQUEST['nonce'] = wp_create_nonce( $this->getWPBPProperty( 'identifier' ) ); + $this->wpbp->maybe_handle(); + $this->assertCount( 0, $this->wpbp->get_batches() ); + $this->assertFalse( $this->wpbp->is_processing(), 'not left processing on complete' ); + } } From 3608207394c7d1a3bb7f108edbfe6406229e2032 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 17 Apr 2023 18:06:28 +0100 Subject: [PATCH 41/65] Update README with information about background process statuses --- README.md | 144 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 128 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 73535d2..7aea477 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ __Requires PHP 5.6+__ The recommended way to install this library in your project is by loading it through Composer: -``` +```shell composer require deliciousbrains/wp-background-processing ``` @@ -27,6 +27,11 @@ Extend the `WP_Async_Request` class: ```php class WP_Example_Request extends WP_Async_Request { + /** + * @var string + */ + protected $prefix = 'my_plugin'; + /** * @var string */ @@ -45,45 +50,62 @@ class WP_Example_Request extends WP_Async_Request { } ``` -##### `protected $action` +#### `protected $prefix` + +Should be set to a unique prefix associated with your plugin, theme, or site's custom function prefix. + +#### `protected $action` Should be set to a unique name. -##### `protected function handle()` +#### `protected function handle()` Should contain any logic to perform during the non-blocking request. The data passed to the request will be accessible via `$_POST`. -##### Dispatching Requests +#### Dispatching Requests Instantiate your request: -`$this->example_request = new WP_Example_Request();` +```php +$this->example_request = new WP_Example_Request(); +``` Add data to the request if required: -`$this->example_request->data( array( 'value1' => $value1, 'value2' => $value2 ) );` +```php +$this->example_request->data( array( 'value1' => $value1, 'value2' => $value2 ) ); +``` Fire off the request: -`$this->example_request->dispatch();` +```php +$this->example_request->dispatch(); +``` Chaining is also supported: -`$this->example_request->data( array( 'data' => $data ) )->dispatch();` +```php +$this->example_request->data( array( 'data' => $data ) )->dispatch(); +``` ### Background Process -Background processes work in a similar fashion to async requests, but they allow you to queue tasks. Items pushed onto the queue will be processed in the background once the queue has been dispatched. Queues will also scale based on available server resources, so higher end servers will process more items per batch. Once a batch has completed, the next batch will start instantly. +Background processes work in a similar fashion to async requests, but they allow you to queue tasks. Items pushed onto the queue will be processed in the background once the queue has been saved and dispatched. Queues will also scale based on available server resources, so higher end servers will process more items per batch. Once a batch has completed, the next batch will start instantly. Health checks run by default every 5 minutes to ensure the queue is running when queued items exist. If the queue has failed it will be restarted. -Queues work on a first in first out basis, which allows additional items to be pushed to the queue even if it’s already processing. +Queues work on a first in first out basis, which allows additional items to be pushed to the queue even if it’s already processing. Saving a new batch of queued items and dispatching while another background processing instance is already running will result in the dispatch shortcutting out and the existing instance eventually picking up the new items and processing them when it is their turn. Extend the `WP_Background_Process` class: ```php class WP_Example_Process extends WP_Background_Process { + /** + * @var string + */ + protected $prefix = 'my_plugin'; + /** * @var string */ @@ -122,23 +144,29 @@ class WP_Example_Process extends WP_Background_Process { } ``` -##### `protected $action` +#### `protected $prefix` + +Should be set to a unique prefix associated with your plugin, theme, or site's custom function prefix. + +#### `protected $action` Should be set to a unique name. -##### `protected function task( $item )` +#### `protected function task( $item )` Should contain any logic to perform on the queued item. Return `false` to remove the item from the queue or return `$item` to push it back onto the queue for further processing. If the item has been modified and is pushed back onto the queue the current state will be saved before the batch is exited. -##### `protected function complete()` +#### `protected function complete()` Optionally contain any logic to perform once the queue has completed. -##### Dispatching Processes +#### Dispatching Processes Instantiate your process: -`$this->example_process = new WP_Example_Process();` +```php +$this->example_process = new WP_Example_Process(); +``` **Note:** You must instantiate your process unconditionally. All requests should do this, even if nothing is pushed to the queue. @@ -152,7 +180,91 @@ foreach ( $items as $item ) { Save and dispatch the queue: -`$this->example_process->save()->dispatch();` +```php +$this->example_process->save()->dispatch(); +``` + +#### Background Process Status + +A background process can be processing, paused, cancelled, or none of the above (not started or has completed). + +##### Processing + +To check whether a background process is currently handling a queue of items use `is_processing()`. + +```php +if ( $this->example_process->is_processing() ) { + // Do something because background process is running, e.g. add notice in admin UI. +} +``` + +##### Paused + +You can pause a background process with `pause()`. + +```php +$this->example_process->pause(); +``` + +The currently processing batch will continue until it either completes or reaches the time or memory limit. At that point it'll unlock the process and either complete the batch if the queue is empty, or perform a dispatch that will result in the handler removing the healthcheck cron and firing a "paused" action. + +To check whether a background process is currently paused use `is_paused()`. + +```php +if ( $this->example_process->is_paused() ) { + // Do something because background process is paused, e.g. add notice in admin UI. +} +``` + +You can perform an action in response to background processing being paused by handling the "paused" action for the background process's identifier ($prefix + $action). + +```php +add_action( 'my_plugin_example_process_paused', function() { + // Do something because background process is paused, e.g. add notice in admin UI. +}); +``` + +You can resume a background process with `resume()`. + +```php +$this->example_process->resume(); +``` + +You can perform an action in response to background processing being resumed by handling the "resumed" action for the background process's identifier ($prefix + $action). + +```php +add_action( 'my_plugin_example_process_resumed', function() { + // Do something because background process is resumed, e.g. add notice in admin UI. +}); +``` + +##### Cancelled + +You can cancel a background process with `cancel()`. + +```php +$this->example_process->cancel(); +``` + +The currently processing batch will continue until it either completes or reaches the time or memory limit. At that point it'll unlock the process and either complete the batch if the queue is empty, or perform a dispatch that will result in the handler removing the healthcheck cron, deleting all batches of queued items and firing a "cancelled" action. + +To check whether a background process is currently cancelled use `is_cancelled()`. + +```php +if ( $this->example_process->is_cancelled() ) { + // Do something because background process is cancelled, e.g. add notice in admin UI. +} +``` + +You can perform an action in response to background processing being cancelled by handling the "cancelled" action for the background process's identifier ($prefix + $action). + +```php +add_action( 'my_plugin_example_process_cancelled', function() { + // Do something because background process is paused, e.g. add notice in admin UI. +}); +``` + +The "cancelled" action fires once the queue has been cleared down and cancelled status removed. After which `is_cancelled()` will no longer be true as the background process is now dormant. ### BasicAuth From cb34ba126d9a0a72d98fd5edf97ba5255765cb4c Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Tue, 18 Apr 2023 10:44:13 +0100 Subject: [PATCH 42/65] Add WP_Background_Process::is_queued() --- README.md | 12 +++++++++++- classes/wp-background-process.php | 15 +++++++++++++++ tests/Test_WP_Background_Process.php | 22 ++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7aea477..6f5c1b1 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,17 @@ $this->example_process->save()->dispatch(); #### Background Process Status -A background process can be processing, paused, cancelled, or none of the above (not started or has completed). +A background process can be queued, processing, paused, cancelled, or none of the above (not started or has completed). + +##### Queued + +To check whether a background process has queued items use `is_queued()`. + +```php +if ( $this->example_process->is_queued() ) { + // Do something because background process has queued items, e.g. add notice in admin UI. +} +``` ##### Processing diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 54903c2..0555edd 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -251,6 +251,21 @@ protected function resumed() { do_action( $this->identifier . '_resumed' ); } + /** + * Is queued? + * + * @return bool + */ + public function is_queued() { + $batch = $this->get_batch(); + + if ( empty( $batch ) ) { + return false; + } + + return true; + } + /** * Generate key for a batch. * diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index 16b265d..1e6641c 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -499,4 +499,26 @@ public function test_is_processing() { $this->assertCount( 0, $this->wpbp->get_batches() ); $this->assertFalse( $this->wpbp->is_processing(), 'not left processing on complete' ); } + + /** + * Test is_queued. + * + * @return void + */ + public function test_is_queued() { + $this->assertFalse( $this->wpbp->is_queued(), 'nothing queued until save' ); + + $this->wpbp->push_to_queue( 'wibble' ); + $this->assertFalse( $this->wpbp->is_queued(), 'nothing queued until save' ); + + $this->wpbp->save(); + $this->assertTrue( $this->wpbp->is_queued(), 'queued items exist' ); + + $this->wpbp->push_to_queue( 'wobble' ); + $this->wpbp->save(); + $this->assertTrue( $this->wpbp->is_queued(), 'queued items exist' ); + + $this->wpbp->delete_all(); + $this->assertFalse( $this->wpbp->is_queued(), 'queue emptied' ); + } } From f172c837c66c983825ae1ca1e2b5dfbebbc5f733 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Tue, 18 Apr 2023 11:24:14 +0100 Subject: [PATCH 43/65] Add WP_Background_Process::is_active() --- README.md | 12 +++++ classes/wp-background-process.php | 9 ++++ tests/Test_WP_Background_Process.php | 65 ++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/README.md b/README.md index 6f5c1b1..923e129 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,18 @@ add_action( 'my_plugin_example_process_cancelled', function() { The "cancelled" action fires once the queue has been cleared down and cancelled status removed. After which `is_cancelled()` will no longer be true as the background process is now dormant. +##### Active + +To check whether a background process has queued items, is processing, is paused, or is cancelling, use `is_active()`. + +```php +if ( $this->example_process->is_active() ) { + // Do something because background process is active, e.g. add notice in admin UI. +} +``` + +If a background process is not active, then it either has not had anything queued yet and not started, or has finished processing all queued items. + ### BasicAuth If your site is behind BasicAuth, both async requests and background processes will fail to complete. This is because WP Background Processing relies on the [WordPress HTTP API](https://developer.wordpress.org/plugins/http-api/), which requires you to attach your BasicAuth credentials to requests. The easiest way to do this is using the following filter: diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 0555edd..ab3b7c5 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -266,6 +266,15 @@ public function is_queued() { return true; } + /** + * Is the tool currently active, e.g. starting, working, paused or cleaning up? + * + * @return bool + */ + public function is_active() { + return $this->is_queued() || $this->is_processing() || $this->is_paused() || $this->is_cancelled(); + } + /** * Generate key for a batch. * diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index 1e6641c..19926e9 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -521,4 +521,69 @@ public function test_is_queued() { $this->wpbp->delete_all(); $this->assertFalse( $this->wpbp->is_queued(), 'queue emptied' ); } + + /** + * Test is_active. + * + * @return void + */ + public function test_is_active() { + $this->assertFalse( $this->wpbp->is_active(), 'not queued, processing, paused or cancelling' ); + + // Queued. + $this->wpbp->push_to_queue( 'wibble' ); + $this->assertFalse( $this->wpbp->is_active(), 'nothing queued until save' ); + + $this->wpbp->save(); + $this->assertTrue( $this->wpbp->is_active(), 'queued items exist, so now active' ); + + $this->wpbp->delete_all(); + $this->assertFalse( $this->wpbp->is_active(), 'queue emptied, so no longer active' ); + + // Processing. + $this->executeWPBPMethod( 'lock_process' ); + $this->assertTrue( $this->wpbp->is_active(), 'processing, so now active' ); + + $this->executeWPBPMethod( 'unlock_process' ); + $this->assertFalse( $this->wpbp->is_active(), 'not processing, so no longer active' ); + + // Paused. + $this->wpbp->pause(); + $this->assertTrue( $this->wpbp->is_active(), 'paused, so now active' ); + + $this->wpbp->resume(); + $this->assertFalse( $this->wpbp->is_active(), 'not paused, nothing queued, so no longer active' ); + + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertTrue( $this->wpbp->is_active(), 'queued items exist, so now active' ); + $this->wpbp->pause(); + $this->assertTrue( $this->wpbp->is_active(), 'paused, so still active' ); + add_filter( 'pre_http_request', '__return_true' ); + $this->wpbp->resume(); + remove_filter( 'pre_http_request', '__return_true' ); + $this->assertTrue( $this->wpbp->is_active(), 'resumed but with queued items, so still active' ); + $this->wpbp->delete_all(); + $this->assertFalse( $this->wpbp->is_active(), 'queue emptied, so no longer active' ); + + // Cancelled. + add_filter( 'pre_http_request', '__return_true' ); + $this->wpbp->cancel(); + remove_filter( 'pre_http_request', '__return_true' ); + $this->assertTrue( $this->wpbp->is_active(), 'cancelling, so now active' ); + + add_filter( $this->getWPBPProperty( 'identifier' ) . '_wp_die', '__return_false' ); + $this->wpbp->maybe_handle(); + $this->assertFalse( $this->wpbp->is_active(), 'cancel handled, so no longer active' ); + + $this->wpbp->push_to_queue( 'wibble' ); + $this->wpbp->save(); + $this->assertTrue( $this->wpbp->is_active(), 'queued items exist, so now active' ); + add_filter( 'pre_http_request', '__return_true' ); + $this->wpbp->cancel(); + remove_filter( 'pre_http_request', '__return_true' ); + $this->assertTrue( $this->wpbp->is_active(), 'cancelling, so still active' ); + $this->wpbp->maybe_handle(); + $this->assertFalse( $this->wpbp->is_active(), 'cancel handled, queue emptied, so no longer active' ); + } } From b6dd6b7b375f9213802bc4521ae2d866e15258d1 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Tue, 18 Apr 2023 13:20:36 +0100 Subject: [PATCH 44/65] Streamline WP_Background_Process::is_queued() and WP_Background_Process::is_queue_empty() --- classes/wp-background-process.php | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index ab3b7c5..82a8e63 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -257,13 +257,7 @@ protected function resumed() { * @return bool */ public function is_queued() { - $batch = $this->get_batch(); - - if ( empty( $batch ) ) { - return false; - } - - return true; + return ! $this->is_queue_empty(); } /** @@ -349,27 +343,7 @@ public function maybe_handle() { * @return bool */ protected function is_queue_empty() { - global $wpdb; - - $table = $wpdb->options; - $column = 'option_name'; - - if ( is_multisite() ) { - $table = $wpdb->sitemeta; - $column = 'meta_key'; - } - - $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; - - $sql = ' - SELECT COUNT(*) - FROM ' . $table . ' - WHERE ' . $column . ' LIKE %s - '; - - $count = $wpdb->get_var( $wpdb->prepare( $sql, $key ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - - return ! ( $count > 0 ); + return empty( $this->get_batch() ); } /** From c36c9d23916859255b8ce3417d21d4899129f7d3 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Tue, 18 Jul 2023 10:25:40 +0100 Subject: [PATCH 45/65] Ensure task processing cleanly pauses or cancels as soon as possible --- classes/wp-background-process.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 82a8e63..d10351a 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -527,8 +527,8 @@ protected function handle() { // Let the server breathe a little. sleep( $throttle_seconds ); - if ( $this->time_exceeded() || $this->memory_exceeded() ) { - // Batch limits reached. + // Batch limits reached, or pause or cancel request. + if ( $this->time_exceeded() || $this->memory_exceeded() || $this->is_paused() || $this->is_cancelled() ) { break; } } @@ -537,7 +537,7 @@ protected function handle() { if ( empty( $batch->data ) ) { $this->delete( $batch->key ); } - } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); + } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() && ! $this->is_paused() && ! $this->is_cancelled() ); $this->unlock_process(); From bc8e3cd683a0362b01ad95d2b7a73ed3e73286dc Mon Sep 17 00:00:00 2001 From: kagg-design Date: Thu, 20 Jul 2023 16:43:36 +0300 Subject: [PATCH 46/65] WPCS and EA Extended fixes and minor code refactoring. --- .phpcs.xml | 2 +- classes/wp-async-request.php | 6 ++++++ classes/wp-background-process.php | 31 +++++++++++++++++-------------- wp-background-processing.php | 18 +++++++++--------- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/.phpcs.xml b/.phpcs.xml index c082481..9032383 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -19,7 +19,7 @@ - + diff --git a/classes/wp-async-request.php b/classes/wp-async-request.php index 9759ab0..49e27f3 100644 --- a/classes/wp-async-request.php +++ b/classes/wp-async-request.php @@ -5,6 +5,11 @@ * @package WP-Background-Processing */ +// phpcs:disable Generic.Commenting.DocComment.MissingShort +/** @noinspection PhpIllegalPsrClassPathInspection */ +/** @noinspection AutoloadingIssuesInspection */ +// phpcs:disable Generic.Commenting.DocComment.MissingShort + /** * Abstract WP_Async_Request class. * @@ -178,6 +183,7 @@ public function maybe_handle() { * @param mixed $return What to return if filter says don't die, default is null. * * @return void|mixed + * @noinspection ForgottenDebugOutputInspection */ protected function maybe_wp_die( $return = null ) { /** diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index d10351a..5e65e34 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -5,6 +5,11 @@ * @package WP-Background-Processing */ +// phpcs:disable Generic.Commenting.DocComment.MissingShort +/** @noinspection PhpIllegalPsrClassPathInspection */ +/** @noinspection AutoloadingIssuesInspection */ +// phpcs:disable Generic.Commenting.DocComment.MissingShort + /** * Abstract WP_Background_Process class. * @@ -73,6 +78,7 @@ public function __construct() { $this->cron_interval_identifier = $this->identifier . '_cron_interval'; add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); + // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); } @@ -190,11 +196,7 @@ public function cancel() { public function is_cancelled() { $status = get_site_option( $this->get_status_key(), 0 ); - if ( absint( $status ) === self::STATUS_CANCELLED ) { - return true; - } - - return false; + return absint( $status ) === self::STATUS_CANCELLED; } /** @@ -219,11 +221,7 @@ public function pause() { public function is_paused() { $status = get_site_option( $this->get_status_key(), 0 ); - if ( absint( $status ) === self::STATUS_PAUSED ) { - return true; - } - - return false; + return absint( $status ) === self::STATUS_PAUSED; } /** @@ -341,6 +339,7 @@ public function maybe_handle() { * Is queue empty? * * @return bool + * @noinspection IsEmptyFunctionUsageInspection */ protected function is_queue_empty() { return empty( $this->get_batch() ); @@ -356,6 +355,7 @@ protected function is_queue_empty() { * * @deprecated 1.1.0 Superseded. * @see is_processing() + * @noinspection PhpUnused */ protected function is_process_running() { return $this->is_processing(); @@ -412,7 +412,7 @@ protected function unlock_process() { protected function get_batch() { return array_reduce( $this->get_batches( 1 ), - function ( $carry, $batch ) { + static function ( $carry, $batch ) { return $batch; }, array() @@ -451,7 +451,7 @@ public function get_batches( $limit = 0 ) { SELECT * FROM ' . $table . ' WHERE ' . $column . ' LIKE %s - ORDER BY ' . $key_column . ' ASC + ORDER BY ' . $key_column . ' '; $args = array( $key ); @@ -468,7 +468,7 @@ public function get_batches( $limit = 0 ) { if ( ! empty( $items ) ) { $batches = array_map( - function ( $item ) use ( $column, $value_column ) { + static function ( $item ) use ( $column, $value_column ) { $batch = new stdClass(); $batch->key = $item->{$column}; $batch->data = maybe_unserialize( $item->{$value_column} ); @@ -487,6 +487,8 @@ function ( $item ) use ( $column, $value_column ) { * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. + * + * @noinspection DisconnectedForeachInstructionInspection */ protected function handle() { $this->lock_process(); @@ -584,7 +586,7 @@ protected function get_memory_limit() { $memory_limit = '128M'; } - if ( ! $memory_limit || -1 === intval( $memory_limit ) ) { + if ( ! $memory_limit || -1 === (int) $memory_limit ) { // Unlimited, set to 32GB. $memory_limit = '32000M'; } @@ -712,6 +714,7 @@ protected function clear_scheduled_event() { * * @deprecated 1.1.0 Superseded. * @see cancel() + * @noinspection PhpUnused */ public function cancel_process() { $this->cancel(); diff --git a/wp-background-processing.php b/wp-background-processing.php index 6881bcd..ef66d64 100644 --- a/wp-background-processing.php +++ b/wp-background-processing.php @@ -5,15 +5,15 @@ * @package WP-Background-Processing */ -/* -Plugin Name: WP Background Processing -Plugin URI: https://github.com/deliciousbrains/wp-background-processing -Description: Asynchronous requests and background processing in WordPress. -Author: Delicious Brains Inc. -Version: 1.0 -Author URI: https://deliciousbrains.com/ -GitHub Plugin URI: https://github.com/deliciousbrains/wp-background-processing -GitHub Branch: master +/** + * Plugin Name: WP Background Processing + * Plugin URI: https://github.com/deliciousbrains/wp-background-processing + * Description: Asynchronous requests and background processing in WordPress. + * Author: Delicious Brains Inc. + * Version: 1.0 + * Author URI: https://deliciousbrains.com/ + * GitHub Plugin URI: https://github.com/deliciousbrains/wp-background-processing + * GitHub Branch: master */ if ( ! class_exists( 'WP_Async_Request' ) ) { From 54983f64036f6853ee961845f36144e481503bac Mon Sep 17 00:00:00 2001 From: kagg-design Date: Thu, 20 Jul 2023 16:48:11 +0300 Subject: [PATCH 47/65] Lst space :) --- wp-background-processing.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wp-background-processing.php b/wp-background-processing.php index ef66d64..b6e92aa 100644 --- a/wp-background-processing.php +++ b/wp-background-processing.php @@ -14,7 +14,7 @@ * Author URI: https://deliciousbrains.com/ * GitHub Plugin URI: https://github.com/deliciousbrains/wp-background-processing * GitHub Branch: master -*/ + */ if ( ! class_exists( 'WP_Async_Request' ) ) { require_once plugin_dir_path( __FILE__ ) . 'classes/wp-async-request.php'; From 838945b5346b060b91d7f39fdeadf687906a11ed Mon Sep 17 00:00:00 2001 From: kagg-design Date: Thu, 20 Jul 2023 17:54:25 +0300 Subject: [PATCH 48/65] Increase timeout for non-blocking request. When timeout is too small, request could not be sent at all. Actual value of timeout is not significant, as wp_remote_post() returns control immediately after sending of non-blocking request. --- classes/wp-async-request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/wp-async-request.php b/classes/wp-async-request.php index 9759ab0..0503a60 100644 --- a/classes/wp-async-request.php +++ b/classes/wp-async-request.php @@ -139,7 +139,7 @@ protected function get_post_args() { } $args = array( - 'timeout' => 0.01, + 'timeout' => 5, 'blocking' => false, 'body' => $this->data, 'cookies' => $_COOKIE, // Passing cookies ensures request is performed as initiating user. From 804a252d152831a6485302c9f66b28e5ad320aa4 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Thu, 21 Sep 2023 12:19:53 +0100 Subject: [PATCH 49/65] Add note to README regarding PRs --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 923e129..df27f02 100644 --- a/README.md +++ b/README.md @@ -371,6 +371,16 @@ make If the `composer` dependencies aren't in place, they'll be automatically installed first. +#### Creating a PR + +When creating a PR, please make sure to mention which GitHub issue is being resolved +at the top of the description, e.g.: + +`Resolves #123` + +The unit and style tests will be run automatically, the PR will not be eligible for +merge unless they pass, and the branch is up-to-date with `master`. + ## License [GPLv2+](http://www.gnu.org/licenses/gpl-2.0.html) From 67138814b16c7ef9e6739fcdeebfe0849e3b8494 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Tue, 16 Jan 2024 15:11:49 +0000 Subject: [PATCH 50/65] Update composer dependencies for tests --- composer.lock | 166 +++++++++++++++++++++++++++++--------------------- 1 file changed, 96 insertions(+), 70 deletions(-) diff --git a/composer.lock b/composer.lock index 3674cd5..00972a5 100644 --- a/composer.lock +++ b/composer.lock @@ -501,22 +501,24 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.18.1", + "version": "1.25.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f" + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f", - "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.5", @@ -540,9 +542,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" }, - "time": "2023-04-07T11:51:11+00:00" + "time": "2024-01-04T17:06:16+00:00" }, { "name": "phpunit/php-code-coverage", @@ -843,16 +845,16 @@ }, { "name": "phpunit/phpunit", - "version": "8.5.33", + "version": "8.5.36", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e" + "reference": "9652df58e06a681429d8cfdaec3c43d6de581d5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e", - "reference": "7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9652df58e06a681429d8cfdaec3c43d6de581d5a", + "reference": "9652df58e06a681429d8cfdaec3c43d6de581d5a", "shasum": "" }, "require": { @@ -882,9 +884,9 @@ "sebastian/version": "^2.0.1" }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*", - "phpunit/php-invoker": "^2.0.0" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage", + "phpunit/php-invoker": "To allow enforcing time limits" }, "bin": [ "phpunit" @@ -920,7 +922,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.33" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.36" }, "funding": [ { @@ -936,7 +939,7 @@ "type": "tidelift" } ], - "time": "2023-02-27T13:04:50+00:00" + "time": "2023-12-01T16:52:15+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1069,16 +1072,16 @@ }, { "name": "sebastian/diff", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" + "reference": "6296a0c086dd0117c1b78b059374d7fcbe7545ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", - "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/6296a0c086dd0117c1b78b059374d7fcbe7545ae", + "reference": "6296a0c086dd0117c1b78b059374d7fcbe7545ae", "shasum": "" }, "require": { @@ -1123,7 +1126,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/diff/tree/3.0.4" }, "funding": [ { @@ -1131,7 +1134,7 @@ "type": "github" } ], - "time": "2020-11-30T07:59:04+00:00" + "time": "2023-05-07T05:30:20+00:00" }, { "name": "sebastian/environment", @@ -1275,16 +1278,16 @@ }, { "name": "sebastian/global-state", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "de036ec91d55d2a9e0db2ba975b512cdb1c23921" + "reference": "66783ce213de415b451b904bfef9dda0cf9aeae0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/de036ec91d55d2a9e0db2ba975b512cdb1c23921", - "reference": "de036ec91d55d2a9e0db2ba975b512cdb1c23921", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/66783ce213de415b451b904bfef9dda0cf9aeae0", + "reference": "66783ce213de415b451b904bfef9dda0cf9aeae0", "shasum": "" }, "require": { @@ -1327,7 +1330,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.3" }, "funding": [ { @@ -1335,7 +1338,7 @@ "type": "github" } ], - "time": "2022-02-10T06:55:38+00:00" + "time": "2023-08-02T09:23:32+00:00" }, { "name": "sebastian/object-enumerator", @@ -1669,32 +1672,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.10.0", + "version": "8.14.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "c4e213e6e57f741451a08e68ef838802eec92287" + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/c4e213e6e57f741451a08e68ef838802eec92287", - "reference": "c4e213e6e57f741451a08e68ef838802eec92287", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.18.0 <1.19.0", + "phpstan/phpdoc-parser": "^1.23.1", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.10.11", - "phpstan/phpstan-deprecation-rules": "1.1.3", - "phpstan/phpstan-phpunit": "1.0.0|1.3.11", + "phpstan/phpstan": "1.10.37", + "phpstan/phpstan-deprecation-rules": "1.1.4", + "phpstan/phpstan-phpunit": "1.3.14", "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.0.19" + "phpunit/phpunit": "8.5.21|9.6.8|10.3.5" }, "type": "phpcodesniffer-standard", "extra": { @@ -1718,7 +1721,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.10.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.14.1" }, "funding": [ { @@ -1730,24 +1733,24 @@ "type": "tidelift" } ], - "time": "2023-04-10T07:39:29+00:00" + "time": "2023-10-08T07:28:08+00:00" }, { "name": "spryker/code-sniffer", - "version": "0.17.18", + "version": "0.17.20", "source": { "type": "git", "url": "https://github.com/spryker/code-sniffer.git", - "reference": "5fb8b573abc4a906d0d364a4a03abd38e565ba29" + "reference": "5374a2c2ad29c361a6aeec0c81730aed438d08e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spryker/code-sniffer/zipball/5fb8b573abc4a906d0d364a4a03abd38e565ba29", - "reference": "5fb8b573abc4a906d0d364a4a03abd38e565ba29", + "url": "https://api.github.com/repos/spryker/code-sniffer/zipball/5374a2c2ad29c361a6aeec0c81730aed438d08e4", + "reference": "5374a2c2ad29c361a6aeec0c81730aed438d08e4", "shasum": "" }, "require": { - "php": ">=7.4", + "php": ">=8.0", "slevomat/coding-standard": "^7.2.0 || ^8.0.1", "squizlabs/php_codesniffer": "^3.6.2" }, @@ -1788,20 +1791,20 @@ "issues": "https://github.com/spryker/code-sniffer/issues", "source": "https://github.com/spryker/code-sniffer" }, - "time": "2023-01-03T16:08:22+00:00" + "time": "2024-01-04T17:11:52+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.2", + "version": "3.8.1", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "14f5fff1e64118595db5408e946f3a22c75807f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/14f5fff1e64118595db5408e946f3a22c75807f7", + "reference": "14f5fff1e64118595db5408e946f3a22c75807f7", "shasum": "" }, "require": { @@ -1811,11 +1814,11 @@ "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "bin": [ - "bin/phpcs", - "bin/phpcbf" + "bin/phpcbf", + "bin/phpcs" ], "type": "library", "extra": { @@ -1830,35 +1833,58 @@ "authors": [ { "name": "Greg Sherwood", - "role": "lead" + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", "standards", "static analysis" ], "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, - "time": "2023-02-22T23:07:41+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-01-11T20:47:48+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "shasum": "" }, "require": { @@ -1887,7 +1913,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" }, "funding": [ { @@ -1895,7 +1921,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2023-11-20T00:12:19+00:00" }, { "name": "wp-coding-standards/wpcs", @@ -1950,16 +1976,16 @@ }, { "name": "yoast/phpunit-polyfills", - "version": "1.0.5", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "3b59adeef77fb1c03ff5381dbb9d68b0aaff3171" + "reference": "224e4a1329c03d8bad520e3fc4ec980034a4b212" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/3b59adeef77fb1c03ff5381dbb9d68b0aaff3171", - "reference": "3b59adeef77fb1c03ff5381dbb9d68b0aaff3171", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/224e4a1329c03d8bad520e3fc4ec980034a4b212", + "reference": "224e4a1329c03d8bad520e3fc4ec980034a4b212", "shasum": "" }, "require": { @@ -2006,7 +2032,7 @@ "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2023-03-30T23:39:05+00:00" + "time": "2023-08-19T14:25:08+00:00" } ], "aliases": [], @@ -2018,5 +2044,5 @@ "php": ">=5.6" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } From 7f347beb2a1c55bfa922657b380b26b8c9677b19 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Tue, 16 Jan 2024 15:14:10 +0000 Subject: [PATCH 51/65] Remove inspection disablement --- classes/wp-async-request.php | 6 ------ classes/wp-background-process.php | 11 ----------- 2 files changed, 17 deletions(-) diff --git a/classes/wp-async-request.php b/classes/wp-async-request.php index 49e27f3..9759ab0 100644 --- a/classes/wp-async-request.php +++ b/classes/wp-async-request.php @@ -5,11 +5,6 @@ * @package WP-Background-Processing */ -// phpcs:disable Generic.Commenting.DocComment.MissingShort -/** @noinspection PhpIllegalPsrClassPathInspection */ -/** @noinspection AutoloadingIssuesInspection */ -// phpcs:disable Generic.Commenting.DocComment.MissingShort - /** * Abstract WP_Async_Request class. * @@ -183,7 +178,6 @@ public function maybe_handle() { * @param mixed $return What to return if filter says don't die, default is null. * * @return void|mixed - * @noinspection ForgottenDebugOutputInspection */ protected function maybe_wp_die( $return = null ) { /** diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 5e65e34..b08d6c6 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -5,11 +5,6 @@ * @package WP-Background-Processing */ -// phpcs:disable Generic.Commenting.DocComment.MissingShort -/** @noinspection PhpIllegalPsrClassPathInspection */ -/** @noinspection AutoloadingIssuesInspection */ -// phpcs:disable Generic.Commenting.DocComment.MissingShort - /** * Abstract WP_Background_Process class. * @@ -78,7 +73,6 @@ public function __construct() { $this->cron_interval_identifier = $this->identifier . '_cron_interval'; add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); - // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); } @@ -339,7 +333,6 @@ public function maybe_handle() { * Is queue empty? * * @return bool - * @noinspection IsEmptyFunctionUsageInspection */ protected function is_queue_empty() { return empty( $this->get_batch() ); @@ -355,7 +348,6 @@ protected function is_queue_empty() { * * @deprecated 1.1.0 Superseded. * @see is_processing() - * @noinspection PhpUnused */ protected function is_process_running() { return $this->is_processing(); @@ -487,8 +479,6 @@ static function ( $item ) use ( $column, $value_column ) { * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. - * - * @noinspection DisconnectedForeachInstructionInspection */ protected function handle() { $this->lock_process(); @@ -714,7 +704,6 @@ protected function clear_scheduled_event() { * * @deprecated 1.1.0 Superseded. * @see cancel() - * @noinspection PhpUnused */ public function cancel_process() { $this->cancel(); From 710d404634bc798fb788920c984f19497957c71f Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Tue, 16 Jan 2024 15:15:18 +0000 Subject: [PATCH 52/65] Keep explicit SQL sort direction --- classes/wp-background-process.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index b08d6c6..60ca0cf 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -443,7 +443,7 @@ public function get_batches( $limit = 0 ) { SELECT * FROM ' . $table . ' WHERE ' . $column . ' LIKE %s - ORDER BY ' . $key_column . ' + ORDER BY ' . $key_column . ' ASC '; $args = array( $key ); From e5ce47938199c339262d76281a577d245f138c73 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Tue, 16 Jan 2024 15:16:46 +0000 Subject: [PATCH 53/65] Keep use of intval function --- classes/wp-background-process.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 60ca0cf..8a053d8 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -576,7 +576,7 @@ protected function get_memory_limit() { $memory_limit = '128M'; } - if ( ! $memory_limit || -1 === (int) $memory_limit ) { + if ( ! $memory_limit || -1 === intval( $memory_limit ) ) { // Unlimited, set to 32GB. $memory_limit = '32000M'; } From 7ba50e947cd83edb7c12450dbf8a8af995c3852c Mon Sep 17 00:00:00 2001 From: Erik Torsner Date: Thu, 1 Feb 2024 18:13:01 +0100 Subject: [PATCH 54/65] Add $allowed_classes param to the constructor --- README.md | 52 ++++++++++++++++++++++++++ classes/wp-background-process.php | 48 ++++++++++++++++++++++-- composer.json | 2 +- tests/Test_WP_Background_Process.php | 56 ++++++++++++++++++++++++++++ tests/fixtures/Test_Batch_Data.php | 5 +++ 5 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/Test_Batch_Data.php diff --git a/README.md b/README.md index df27f02..a6e534b 100644 --- a/README.md +++ b/README.md @@ -178,12 +178,64 @@ foreach ( $items as $item ) { } ``` +An item can be any valid PHP value, string, integer, array or object. If needed, the $item is serialized when written to the database. + Save and dispatch the queue: ```php $this->example_process->save()->dispatch(); ``` +#### Handling serialized objects in queue items + +Queue items that contain non-scalar values are serialized when stored in the database. To avoid potential security issues during unserialize, this library provides the option to set the `allowed_classes` option when calling `unserialize()` which limits which classes can be instantiated. It's kept internally as the protected `$allowed_batch_data_classes` property. + +To maintain backward compatibility the default value is `true`, meaning that any serialized object will be instantiated. Please note that this default behavior may change in a future major release. + +We encourage all users of this library to take advantage of setting a strict value for `$allowed_batch_data_classes`. If possible, set the value to `false` to disallow any objects from being instantiated, or a very limited list of class names, see examples below. + +Objects in the serialized string that are not allowed to be instantiated will instead get the class type `__PHP_Incomplete_Class`. + +##### Overriding the default `$allowed_batch_data_classes` + +The default behavior can be overridden by passing an array of allowed classes to the constructor: + +``` php +$allowed_batch_data_classes = array( MyCustomItem::class, MyItemHelper::class ); +$this->example_process = new WP_Example_Process( $allowed_batch_data_classes ); +``` + +Or, set the value to `false`: + +``` php +$this->example_process = new WP_Example_Process( false ); +``` + + +Another way to change the default is to override the `$allowed_batch_data_classes` property in your process class: + +``` php +class WP_Example_Process extends WP_Background_Process { + + /** + * @var string + */ + protected $prefix = 'my_plugin'; + + /** + * @var string + */ + protected $action = 'example_process'; + + /** + * + * @var bool|array + */ + protected $allowed_batch_data_classes = array( MyCustomItem::class, MyItemHelper::class ); + ... + +``` + #### Background Process Status A background process can be queued, processing, paused, cancelled, or none of the above (not started or has completed). diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 8a053d8..0ce9b92 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -49,6 +49,13 @@ abstract class WP_Background_Process extends WP_Async_Request { */ protected $cron_interval_identifier; + /** + * Restrict object instantiation when using unserialize. + * + * @var bool|array + */ + protected $allowed_batch_data_classes = true; + /** * The status set when process is cancelling. * @@ -65,10 +72,22 @@ abstract class WP_Background_Process extends WP_Async_Request { /** * Initiate new background process. + * + * @param bool|array $allowed_batch_data_classes Optional. Array of class names that can be unserialized. Default true (any class). */ - public function __construct() { + public function __construct( $allowed_batch_data_classes = true ) { parent::__construct(); + if ( empty( $allowed_batch_data_classes ) && false !== $allowed_batch_data_classes ) { + $allowed_batch_data_classes = true; + } + + if ( ! is_bool( $allowed_batch_data_classes ) && ! is_array( $allowed_batch_data_classes ) ) { + $allowed_batch_data_classes = true; + } + + $this->allowed_batch_data_classes = $allowed_batch_data_classes; + $this->cron_hook_identifier = $this->identifier . '_cron'; $this->cron_interval_identifier = $this->identifier . '_cron_interval'; @@ -459,11 +478,13 @@ public function get_batches( $limit = 0 ) { $batches = array(); if ( ! empty( $items ) ) { + $allowed_classes = $this->allowed_batch_data_classes; + $batches = array_map( - static function ( $item ) use ( $column, $value_column ) { + static function ( $item ) use ( $column, $value_column, $allowed_classes ) { $batch = new stdClass(); $batch->key = $item->{$column}; - $batch->data = maybe_unserialize( $item->{$value_column} ); + $batch->data = static::maybe_unserialize( $item->{$value_column}, $allowed_classes ); return $batch; }, @@ -722,4 +743,25 @@ public function cancel_process() { * @return mixed */ abstract protected function task( $item ); + + /** + * Maybe unserialize data, but not if an object. + * + * @param mixed $data Data to be unserialized. + * @param bool|array $allowed_classes Array of class names that can be unserialized. + * + * @return mixed + */ + protected static function maybe_unserialize( $data, $allowed_classes ) { + if ( is_serialized( $data ) ) { + $options = array(); + if ( is_bool( $allowed_classes ) || is_array( $allowed_classes ) ) { + $options['allowed_classes'] = $allowed_classes; + } + + return @unserialize( $data, $options ); // @phpcs:ignore + } + + return $data; + } } diff --git a/composer.json b/composer.json index 5f65733..7f65bbf 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "WP Background Processing can be used to fire off non-blocking asynchronous requests or as a background processing tool, allowing you to queue tasks.", "type": "library", "require": { - "php": ">=5.6" + "php": ">=7.0" }, "suggest": { "coenjacobs/mozart": "Easily wrap this library with your own prefix, to prevent collisions when multiple plugins use this library" diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index 19926e9..fe9ff9a 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -5,6 +5,8 @@ * @package WP-Background-Processing */ + require_once __DIR__ . '/fixtures/Test_Batch_Data.php'; + use PHPUnit\Framework\MockObject\MockObject; /** @@ -51,6 +53,25 @@ private function getWPBPProperty( string $name ) { return $property->getValue( $this->wpbp ); } + /** + * Set a property value on WPBP regardless of accessibility. + * + * @param string $name + * @param mixed $value + * + * @return mixed + */ + private function setWPBPProperty( string $name, $value ) { + try { + $property = new ReflectionProperty( 'WP_Background_Process', $name ); + } catch ( Exception $e ) { + return new WP_Error( $e->getCode(), $e->getMessage() ); + } + $property->setAccessible( true ); + + return $property->setValue( $this->wpbp, $value ); + } + /** * Execute a method of WPBP regardless of accessibility. * @@ -178,6 +199,41 @@ public function test_get_batch() { $this->assertInstanceOf( 'stdClass', $second_batch ); $this->assertNotEquals( $first_batch, $second_batch, '2nd batch returned as 1st deleted' ); $this->assertEquals( array( 'more wibble' ), $second_batch->data ); + + // Tests using a custom class for the $item. + $this->wpbp->delete( $second_batch->key ); + $batch_data_object = new Test_Batch_Data(); + $this->wpbp->push_to_queue( $batch_data_object ); + $this->assertNotEmpty( $this->getWPBPProperty( 'data' ) ); + $this->assertEquals( array( $batch_data_object ), $this->getWPBPProperty( 'data' ) ); + $this->wpbp->save(); + $third_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->assertCount( 1, $third_batch->data ); + $this->assertInstanceOf( Test_Batch_Data::class, $third_batch->data[0] ); + + // Explicitly set allowed classes to Test_Batch_Data. + $this->setWPBPProperty( 'allowed_batch_data_classes', array( Test_Batch_Data::class ) ); + $third_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->assertCount( 1, $third_batch->data ); + $this->assertInstanceOf( Test_Batch_Data::class, $third_batch->data[0] ); + + // Allow a different class. + $this->setWPBPProperty( 'allowed_batch_data_classes', array( stdClass::class ) ); + $third_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->assertCount( 1, $third_batch->data ); + $this->assertInstanceOf( __PHP_Incomplete_Class::class, $third_batch->data[0] ); + + // Disallow all classes. + $this->setWPBPProperty( 'allowed_batch_data_classes', false ); + $third_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->assertCount( 1, $third_batch->data ); + $this->assertInstanceOf( __PHP_Incomplete_Class::class, $third_batch->data[0] ); + + // Allow everything. + $this->setWPBPProperty( 'allowed_batch_data_classes', true ); + $third_batch = $this->executeWPBPMethod( 'get_batch' ); + $this->assertCount( 1, $third_batch->data ); + $this->assertInstanceOf( Test_Batch_Data::class, $third_batch->data[0] ); } /** diff --git a/tests/fixtures/Test_Batch_Data.php b/tests/fixtures/Test_Batch_Data.php new file mode 100644 index 0000000..b19c72d --- /dev/null +++ b/tests/fixtures/Test_Batch_Data.php @@ -0,0 +1,5 @@ + Date: Thu, 8 Feb 2024 11:10:36 +0000 Subject: [PATCH 55/65] Conditionally retain allowed_batch_data_classes property in subclass if set --- classes/wp-background-process.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 0ce9b92..02baa91 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -86,7 +86,11 @@ public function __construct( $allowed_batch_data_classes = true ) { $allowed_batch_data_classes = true; } - $this->allowed_batch_data_classes = $allowed_batch_data_classes; + // If allowed_batch_data_classes property set in subclass, + // only apply override if not allowing any class. + if ( true === $this->allowed_batch_data_classes || true !== $allowed_batch_data_classes ) { + $this->allowed_batch_data_classes = $allowed_batch_data_classes; + } $this->cron_hook_identifier = $this->identifier . '_cron'; $this->cron_interval_identifier = $this->identifier . '_cron_interval'; From 3e2a82f218160db3a282dd17826dbc4a94b974ff Mon Sep 17 00:00:00 2001 From: Brian Hardie <777730+bhardie@users.noreply.github.com> Date: Thu, 15 Feb 2024 14:50:45 -0800 Subject: [PATCH 56/65] Added CODEOWNERS file --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f3381f7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +* @deliciousbrains/deli-eng + +#jira:[18802] is where issues related to this repository should be ticketed] \ No newline at end of file From dccdbdfbbbc45f2cf467c96ff40aeb5515fe84b1 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Tue, 27 Feb 2024 13:31:09 +0000 Subject: [PATCH 57/65] Ensure 1st healthcheck runs in the future, not immediately before dispatch --- classes/wp-background-process.php | 27 ++++++++++++++----- tests/Test_WP_Background_Process.php | 39 +++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 02baa91..2f2e743 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -650,6 +650,25 @@ protected function completed() { do_action( $this->identifier . '_completed' ); } + /** + * Get the cron healthcheck interval in minutes. + * + * Default is 5 minutes, minimum is 1 minute. + * + * @return int + */ + public function get_cron_interval() { + $interval = 5; + + if ( property_exists( $this, 'cron_interval' ) ) { + $interval = $this->cron_interval; + } + + $interval = apply_filters( $this->cron_interval_identifier, $interval ); + + return is_int( $interval ) && 0 < $interval ? $interval : 5; + } + /** * Schedule the cron healthcheck job. * @@ -660,11 +679,7 @@ protected function completed() { * @return mixed */ public function schedule_cron_healthcheck( $schedules ) { - $interval = apply_filters( $this->cron_interval_identifier, 5 ); - - if ( property_exists( $this, 'cron_interval' ) ) { - $interval = apply_filters( $this->cron_interval_identifier, $this->cron_interval ); - } + $interval = $this->get_cron_interval(); if ( 1 === $interval ) { $display = __( 'Every Minute' ); @@ -707,7 +722,7 @@ public function handle_cron_healthcheck() { */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { - wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier ); + wp_schedule_event( time() + ( $this->get_cron_interval() * MINUTE_IN_SECONDS ), $this->cron_interval_identifier, $this->cron_hook_identifier ); } } diff --git a/tests/Test_WP_Background_Process.php b/tests/Test_WP_Background_Process.php index fe9ff9a..0485234 100644 --- a/tests/Test_WP_Background_Process.php +++ b/tests/Test_WP_Background_Process.php @@ -5,7 +5,7 @@ * @package WP-Background-Processing */ - require_once __DIR__ . '/fixtures/Test_Batch_Data.php'; +require_once __DIR__ . '/fixtures/Test_Batch_Data.php'; use PHPUnit\Framework\MockObject\MockObject; @@ -208,31 +208,31 @@ public function test_get_batch() { $this->assertEquals( array( $batch_data_object ), $this->getWPBPProperty( 'data' ) ); $this->wpbp->save(); $third_batch = $this->executeWPBPMethod( 'get_batch' ); - $this->assertCount( 1, $third_batch->data ); + $this->assertCount( 1, $third_batch->data ); $this->assertInstanceOf( Test_Batch_Data::class, $third_batch->data[0] ); // Explicitly set allowed classes to Test_Batch_Data. $this->setWPBPProperty( 'allowed_batch_data_classes', array( Test_Batch_Data::class ) ); $third_batch = $this->executeWPBPMethod( 'get_batch' ); - $this->assertCount( 1, $third_batch->data ); + $this->assertCount( 1, $third_batch->data ); $this->assertInstanceOf( Test_Batch_Data::class, $third_batch->data[0] ); // Allow a different class. $this->setWPBPProperty( 'allowed_batch_data_classes', array( stdClass::class ) ); $third_batch = $this->executeWPBPMethod( 'get_batch' ); - $this->assertCount( 1, $third_batch->data ); + $this->assertCount( 1, $third_batch->data ); $this->assertInstanceOf( __PHP_Incomplete_Class::class, $third_batch->data[0] ); // Disallow all classes. $this->setWPBPProperty( 'allowed_batch_data_classes', false ); $third_batch = $this->executeWPBPMethod( 'get_batch' ); - $this->assertCount( 1, $third_batch->data ); + $this->assertCount( 1, $third_batch->data ); $this->assertInstanceOf( __PHP_Incomplete_Class::class, $third_batch->data[0] ); // Allow everything. $this->setWPBPProperty( 'allowed_batch_data_classes', true ); $third_batch = $this->executeWPBPMethod( 'get_batch' ); - $this->assertCount( 1, $third_batch->data ); + $this->assertCount( 1, $third_batch->data ); $this->assertInstanceOf( Test_Batch_Data::class, $third_batch->data[0] ); } @@ -642,4 +642,31 @@ public function test_is_active() { $this->wpbp->maybe_handle(); $this->assertFalse( $this->wpbp->is_active(), 'cancel handled, queue emptied, so no longer active' ); } + + /** + * Test get_cron_interval. + * + * @return void + */ + public function test_get_cron_interval() { + // Default value. + $this->assertEquals( 5, $this->wpbp->get_cron_interval() ); + + // Override via property (usually on subclass). + $this->wpbp->cron_interval = 3; + $this->assertEquals( 3, $this->wpbp->get_cron_interval() ); + + // Override via filter. + $callback = function ( $interval ) { + return 1; + }; + add_filter( $this->getWPBPProperty( 'identifier' ) . '_cron_interval', $callback ); + $this->assertEquals( 1, $this->wpbp->get_cron_interval() ); + + remove_filter( $this->getWPBPProperty( 'identifier' ) . '_cron_interval', $callback ); + $this->assertEquals( 3, $this->wpbp->get_cron_interval() ); + + unset( $this->wpbp->cron_interval ); + $this->assertEquals( 5, $this->wpbp->get_cron_interval() ); + } } From f7c67441cb171654934af1185368e6c8a4fba7fa Mon Sep 17 00:00:00 2001 From: Brian Hardie <777730+bhardie@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:21:46 -0700 Subject: [PATCH 58/65] Updated CODEOWNERS to use jira key --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f3381f7..37ee520 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ * @deliciousbrains/deli-eng -#jira:[18802] is where issues related to this repository should be ticketed] \ No newline at end of file +#jira:DELI is where issues related to this repository should be ticketed] \ No newline at end of file From 6ae6bd1889e3a773c04cd09aa5eda2cbe129f8f3 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Fri, 22 Nov 2024 17:06:29 +0000 Subject: [PATCH 59/65] Fix comments in WP_Async_Request --- classes/wp-async-request.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/classes/wp-async-request.php b/classes/wp-async-request.php index 0503a60..2e0e05c 100644 --- a/classes/wp-async-request.php +++ b/classes/wp-async-request.php @@ -101,9 +101,9 @@ protected function get_query_args() { ); /** - * Filters the post arguments used during an async request. + * Filters the query arguments used during an async request. * - * @param array $url + * @param array $args */ return apply_filters( $this->identifier . '_query_args', $args ); } @@ -121,7 +121,7 @@ protected function get_query_url() { $url = admin_url( 'admin-ajax.php' ); /** - * Filters the post arguments used during an async request. + * Filters the query URL used during an async request. * * @param string $url */ From d51a9fa40636ab47b0f228ce1a733df4edd602f0 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Fri, 22 Nov 2024 17:14:20 +0000 Subject: [PATCH 60/65] Add and use WP_Background_Process::get_status function --- classes/wp-background-process.php | 49 +++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 2f2e743..6066edb 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -211,9 +211,7 @@ public function cancel() { * @return bool */ public function is_cancelled() { - $status = get_site_option( $this->get_status_key(), 0 ); - - return absint( $status ) === self::STATUS_CANCELLED; + return $this->get_status() === self::STATUS_CANCELLED; } /** @@ -231,14 +229,12 @@ public function pause() { } /** - * Is the job paused? + * Has the process been paused? * * @return bool */ public function is_paused() { - $status = get_site_option( $this->get_status_key(), 0 ); - - return absint( $status ) === self::STATUS_PAUSED; + return $this->get_status() === self::STATUS_PAUSED; } /** @@ -311,6 +307,34 @@ protected function get_status_key() { return $this->identifier . '_status'; } + /** + * Get the status value for the process. + * + * @return int + */ + protected function get_status() { + global $wpdb; + + if ( is_multisite() ) { + $status = $wpdb->get_var( + $wpdb->prepare( + "SELECT meta_value FROM $wpdb->sitemeta WHERE meta_key = %s AND site_id = %d LIMIT 1", + $this->get_status_key(), + get_current_network_id() + ) + ); + } else { + $status = $wpdb->get_var( + $wpdb->prepare( + "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", + $this->get_status_key() + ) + ); + } + + return absint( $status ); + } + /** * Maybe process a batch of queued items. * @@ -477,7 +501,10 @@ public function get_batches( $limit = 0 ) { $args[] = $limit; } - $items = $wpdb->get_results( $wpdb->prepare( $sql, $args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $items = $wpdb->get_results( $wpdb->prepare( + $sql, + $args + ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $batches = array(); @@ -722,7 +749,11 @@ public function handle_cron_healthcheck() { */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { - wp_schedule_event( time() + ( $this->get_cron_interval() * MINUTE_IN_SECONDS ), $this->cron_interval_identifier, $this->cron_hook_identifier ); + wp_schedule_event( + time() + ( $this->get_cron_interval() * MINUTE_IN_SECONDS ), + $this->cron_interval_identifier, + $this->cron_hook_identifier + ); } } From b48f5af8d566c8e3a320a21bb645afb7ea7683ab Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Fri, 22 Nov 2024 17:16:22 +0000 Subject: [PATCH 61/65] Ensure WP_Background_Process:maybe_handle checks referrer early --- classes/wp-background-process.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 6066edb..1f646ec 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -345,11 +345,14 @@ public function maybe_handle() { // Don't lock up other requests while processing. session_write_close(); + check_ajax_referer( $this->identifier, 'nonce' ); + + // Background process already running. if ( $this->is_processing() ) { - // Background process already running. return $this->maybe_wp_die(); } + // Cancel requested. if ( $this->is_cancelled() ) { $this->clear_scheduled_event(); $this->delete_all(); @@ -357,6 +360,7 @@ public function maybe_handle() { return $this->maybe_wp_die(); } + // Pause requested. if ( $this->is_paused() ) { $this->clear_scheduled_event(); $this->paused(); @@ -364,13 +368,11 @@ public function maybe_handle() { return $this->maybe_wp_die(); } + // No data to process. if ( $this->is_queue_empty() ) { - // No data to process. return $this->maybe_wp_die(); } - check_ajax_referer( $this->identifier, 'nonce' ); - $this->handle(); return $this->maybe_wp_die(); From 8444cda415126a1ce894893ed669766aa4c8c16e Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Fri, 22 Nov 2024 17:20:35 +0000 Subject: [PATCH 62/65] Add chain_id to help identify unique background process chains --- classes/wp-background-process.php | 219 +++++++++++++++++++++++++++++- 1 file changed, 212 insertions(+), 7 deletions(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 1f646ec..6b30165 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -12,6 +12,17 @@ * @extends WP_Async_Request */ abstract class WP_Background_Process extends WP_Async_Request { + /** + * The default query arg name used for passing the chain ID to new processes. + */ + const CHAIN_ID_ARG_NAME = 'chain_id'; + + /** + * Unique background process chain ID. + * + * @var string + */ + private $chain_id; /** * Action @@ -97,6 +108,9 @@ public function __construct( $allowed_batch_data_classes = true ) { add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); + + // Ensure dispatch query args included extra data. + add_filter( $this->identifier . '_query_args', array( $this, 'filter_dispatch_query_args' ) ); } /** @@ -111,6 +125,18 @@ public function dispatch() { return false; } + /** + * Filter fired before background process dispatches its next process. + * + * @param bool $cancel Should the dispatch be cancelled? Default false. + * @param string $chain_id The background process chain ID. + */ + $cancel = apply_filters( $this->identifier . '_pre_dispatch', false, $this->get_chain_id() ); + + if ( $cancel ) { + return false; + } + // Schedule the cron healthcheck. $this->schedule_event(); @@ -422,14 +448,38 @@ public function is_processing() { * Lock the process so that multiple instances can't run simultaneously. * Override if applicable, but the duration should be greater than that * defined in the time_exceeded() method. + * + * @param bool $reset_start_time Optional, default true. */ - protected function lock_process() { - $this->start_time = time(); // Set start time of current process. + public function lock_process( $reset_start_time = true ) { + if ( $reset_start_time ) { + $this->start_time = time(); // Set start time of current process. + } $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); - set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); + $microtime = microtime(); + $locked = set_site_transient( $this->identifier . '_process_lock', $microtime, $lock_duration ); + + /** + * Action to note whether the background process managed to create its lock. + * + * The lock is used to signify that a process is running a task and no other + * process should be allowed to run the same task until the lock is released. + * + * @param bool $locked Whether the lock was successfully created. + * @param string $microtime Microtime string value used for the lock. + * @param int $lock_duration Max number of seconds that the lock will live for. + * @param string $chain_id Current background process chain ID. + */ + do_action( + $this->identifier . '_process_locked', + $locked, + $microtime, + $lock_duration, + $this->get_chain_id() + ); } /** @@ -440,7 +490,18 @@ protected function lock_process() { * @return $this */ protected function unlock_process() { - delete_site_transient( $this->identifier . '_process_lock' ); + $unlocked = delete_site_transient( $this->identifier . '_process_lock' ); + + /** + * Action to note whether the background process managed to release its lock. + * + * The lock is used to signify that a process is running a task and no other + * process should be allowed to run the same task until the lock is released. + * + * @param bool $unlocked Whether the lock was released. + * @param string $chain_id Current background process chain ID. + */ + do_action( $this->identifier . '_process_unlocked', $unlocked, $this->get_chain_id() ); return $this; } @@ -573,8 +634,8 @@ protected function handle() { // Let the server breathe a little. sleep( $throttle_seconds ); - // Batch limits reached, or pause or cancel request. - if ( $this->time_exceeded() || $this->memory_exceeded() || $this->is_paused() || $this->is_cancelled() ) { + // Batch limits reached, or pause or cancel requested. + if ( ! $this->should_continue() ) { break; } } @@ -583,7 +644,7 @@ protected function handle() { if ( empty( $batch->data ) ) { $this->delete( $batch->key ); } - } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() && ! $this->is_paused() && ! $this->is_cancelled() ); + } while ( ! $this->is_queue_empty() && $this->should_continue() ); $this->unlock_process(); @@ -816,4 +877,148 @@ protected static function maybe_unserialize( $data, $allowed_classes ) { return $data; } + + /** + * Should any processing continue? + * + * @return bool + */ + public function should_continue() { + /** + * Filter whether the current background process should continue running the task + * if there is data to be processed. + * + * If the processing time or memory limits have been exceeded, the value will be false. + * If pause or cancel have been requested, the value will be false. + * + * It is very unlikely that you would want to override a false value with true. + * + * If false is returned here, it does not necessarily mean background processing is + * complete. If there is batch data still to be processed and pause or cancel have not + * been requested, it simply means this background process should spawn a new process + * for the chain to continue processing and then close itself down. + * + * @param bool $continue Should the current process continue processing the task? + * @param string $chain_id The current background process chain's ID. + * + * @return bool + */ + return apply_filters( + $this->identifier . '_should_continue', + ! ( $this->time_exceeded() || $this->memory_exceeded() || $this->is_paused() || $this->is_cancelled() ), + $this->get_chain_id() + ); + } + + /** + * Get the string used to identify this type of background process. + * + * @return string + */ + public function get_identifier() { + return $this->identifier; + } + + /** + * Generates a unique background process chain ID. + * + * Basically a UUIDv4 to identify the current background process chain + * so that if another chain is started for the same background process type + * you can control them individually, i.e. shutdown the old chain. + * + * @return string + */ + private static function generate_chain_id() { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + + // 32 bits for "time_low". + mt_rand( 0, 0xffff ), + mt_rand( 0, 0xffff ), + + // 16 bits for "time_mid". + mt_rand( 0, 0xffff ), + + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 4. + mt_rand( 0, 0x0fff ) | 0x4000, + + // 16 bits, 8 bits for "clk_seq_hi_res", + // 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1. + mt_rand( 0, 0x3fff ) | 0x8000, + + // 48 bits for "node" + mt_rand( 0, 0xffff ), + mt_rand( 0, 0xffff ), + mt_rand( 0, 0xffff ) + ); + } + + /** + * Return the current background process chain's ID. + * + * If the chain's ID hasn't been set before this function is first used, + * and hasn't been passed as a query arg during dispatch, + * the chain ID will be generated before being returned. + * + * @return string + */ + public function get_chain_id() { + if ( empty( $this->chain_id ) && ! empty( $_GET[ $this->get_chain_id_arg_name() ] ) ) { + $this->chain_id = $_GET[ $this->get_chain_id_arg_name() ]; + + return $this->chain_id; + } + + if ( empty( $this->chain_id ) ) { + $this->chain_id = self::generate_chain_id(); + } + + return $this->chain_id; + } + + /** + * Filters the query arguments used during an async request. + * + * @param array $args + * + * @return array + */ + public function filter_dispatch_query_args( $args ) { + $args[ $this->get_chain_id_arg_name() ] = $this->get_chain_id(); + + return $args; + } + + /** + * Get the query arg name used for passing the chain ID to new processes. + * + * @return string + */ + private function get_chain_id_arg_name() { + static $chain_id_arg_name; + + if ( ! empty( $chain_id_arg_name ) ) { + return $chain_id_arg_name; + } + + /** + * Filter the query arg name used for passing the chain ID to new processes. + * + * If you encounter problems with using the default query arg name, you can + * change it with this filter. + * + * @param string $chain_id_arg_name Default "chain_id". + * + * @return string + */ + $chain_id_arg_name = apply_filters( $this->identifier . '_chain_id_arg_name', self::CHAIN_ID_ARG_NAME ); + + if ( ! is_string( $chain_id_arg_name ) || empty( $chain_id_arg_name ) ) { + $chain_id_arg_name = self::CHAIN_ID_ARG_NAME; + } + + return $chain_id_arg_name; + } } From 0da8e739a7658b70dffa97dd2eddbf222614de34 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Fri, 22 Nov 2024 17:33:31 +0000 Subject: [PATCH 63/65] Ensure .phpunit.result.cache is removed with clean Makefile target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7ad63b8..68aaf82 100644 --- a/Makefile +++ b/Makefile @@ -15,4 +15,4 @@ vendor: composer.json .PHONY: clean clean: rm -rf vendor - rm -f tests/.phpunit.result.cache + rm -f tests/.phpunit.result.cache .phpunit.result.cache From 79ef1fffffb152aeed78fad629882656424549b3 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Mon, 25 Nov 2024 16:36:26 +0000 Subject: [PATCH 64/65] Improve robustness of WP_Background_Process::get_chain_id --- classes/wp-background-process.php | 64 +++++++++---------------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 6b30165..3bbbb8b 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -564,10 +564,12 @@ public function get_batches( $limit = 0 ) { $args[] = $limit; } - $items = $wpdb->get_results( $wpdb->prepare( - $sql, - $args - ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $items = $wpdb->get_results( + $wpdb->prepare( + $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $args + ) + ); $batches = array(); @@ -919,42 +921,6 @@ public function get_identifier() { return $this->identifier; } - /** - * Generates a unique background process chain ID. - * - * Basically a UUIDv4 to identify the current background process chain - * so that if another chain is started for the same background process type - * you can control them individually, i.e. shutdown the old chain. - * - * @return string - */ - private static function generate_chain_id() { - return sprintf( - '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - - // 32 bits for "time_low". - mt_rand( 0, 0xffff ), - mt_rand( 0, 0xffff ), - - // 16 bits for "time_mid". - mt_rand( 0, 0xffff ), - - // 16 bits for "time_hi_and_version", - // four most significant bits holds version number 4. - mt_rand( 0, 0x0fff ) | 0x4000, - - // 16 bits, 8 bits for "clk_seq_hi_res", - // 8 bits for "clk_seq_low", - // two most significant bits holds zero and one for variant DCE1.1. - mt_rand( 0, 0x3fff ) | 0x8000, - - // 48 bits for "node" - mt_rand( 0, 0xffff ), - mt_rand( 0, 0xffff ), - mt_rand( 0, 0xffff ) - ); - } - /** * Return the current background process chain's ID. * @@ -965,14 +931,22 @@ private static function generate_chain_id() { * @return string */ public function get_chain_id() { - if ( empty( $this->chain_id ) && ! empty( $_GET[ $this->get_chain_id_arg_name() ] ) ) { - $this->chain_id = $_GET[ $this->get_chain_id_arg_name() ]; + if ( empty( $this->chain_id ) && wp_doing_ajax() ) { + check_ajax_referer( $this->identifier, 'nonce' ); + + if ( ! empty( $_GET[ $this->get_chain_id_arg_name() ] ) ) { + $chain_id = sanitize_key( $_GET[ $this->get_chain_id_arg_name() ] ); - return $this->chain_id; + if ( wp_is_uuid( $chain_id ) ) { + $this->chain_id = $chain_id; + + return $this->chain_id; + } + } } if ( empty( $this->chain_id ) ) { - $this->chain_id = self::generate_chain_id(); + $this->chain_id = wp_generate_uuid4(); } return $this->chain_id; @@ -981,7 +955,7 @@ public function get_chain_id() { /** * Filters the query arguments used during an async request. * - * @param array $args + * @param array $args Current query args. * * @return array */ From b9d5c6edef1d4726d9e082e52d52c41e02bf5a54 Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Thu, 28 Nov 2024 14:51:22 +0000 Subject: [PATCH 65/65] Add chain_id param to paused/resumed/cancelled/completed actions --- classes/wp-background-process.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/classes/wp-background-process.php b/classes/wp-background-process.php index 3bbbb8b..ccac98d 100644 --- a/classes/wp-background-process.php +++ b/classes/wp-background-process.php @@ -244,7 +244,7 @@ public function is_cancelled() { * Called when background process has been cancelled. */ protected function cancelled() { - do_action( $this->identifier . '_cancelled' ); + do_action( $this->identifier . '_cancelled', $this->get_chain_id() ); } /** @@ -267,7 +267,7 @@ public function is_paused() { * Called when background process has been paused. */ protected function paused() { - do_action( $this->identifier . '_paused' ); + do_action( $this->identifier . '_paused', $this->get_chain_id() ); } /** @@ -285,7 +285,7 @@ public function resume() { * Called when background process has been resumed. */ protected function resumed() { - do_action( $this->identifier . '_resumed' ); + do_action( $this->identifier . '_resumed', $this->get_chain_id() ); } /** @@ -739,7 +739,7 @@ protected function complete() { * Called when background process has completed. */ protected function completed() { - do_action( $this->identifier . '_completed' ); + do_action( $this->identifier . '_completed', $this->get_chain_id() ); } /**