From b77fcfcb193d9f01be81336563cc863a4217d9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Perona?= Date: Fri, 9 Aug 2024 10:25:54 -0400 Subject: [PATCH] Fixes #6826 Move Action Scheduler to prevent PSR-4 warning (#6836) --- composer.json | 2 +- .../ActionScheduler/README.md | 2 +- .../ActionScheduler/action-scheduler.php | 19 +- .../ActionScheduler/changelog.txt | 84 +++++++- .../classes/ActionScheduler_ActionClaim.php | 0 .../classes/ActionScheduler_ActionFactory.php | 131 ++++++++++++- .../classes/ActionScheduler_AdminView.php | 11 +- ...tionScheduler_AsyncRequest_QueueRunner.php | 0 .../classes/ActionScheduler_Compatibility.php | 18 +- .../ActionScheduler_DataController.php | 0 .../classes/ActionScheduler_DateTime.php | 0 .../classes/ActionScheduler_Exception.php | 0 .../ActionScheduler_FatalErrorMonitor.php | 0 ...ActionScheduler_InvalidActionException.php | 0 .../classes/ActionScheduler_ListTable.php | 21 +- .../classes/ActionScheduler_LogEntry.php | 0 .../classes/ActionScheduler_NullLogEntry.php | 0 .../classes/ActionScheduler_OptionLock.php | 135 +++++++++++++ .../classes/ActionScheduler_QueueCleaner.php | 129 ++++++++++--- .../classes/ActionScheduler_QueueRunner.php | 23 ++- .../classes/ActionScheduler_Versions.php | 0 .../ActionScheduler_WPCommentCleaner.php | 0 .../ActionScheduler_wcSystemStatus.php | 0 .../ActionScheduler_WPCLI_Clean_Command.php | 125 ++++++++++++ .../ActionScheduler_WPCLI_QueueRunner.php | 2 +- ...ctionScheduler_WPCLI_Scheduler_command.php | 50 +++-- .../classes/WP_CLI/Migration_Command.php | 0 .../classes/WP_CLI/ProgressBar.php | 0 .../classes/abstracts/ActionScheduler.php | 41 +++- .../ActionScheduler_Abstract_ListTable.php | 53 +++-- .../ActionScheduler_Abstract_QueueRunner.php | 121 +++++++++--- ...onScheduler_Abstract_RecurringSchedule.php | 4 +- .../ActionScheduler_Abstract_Schedule.php | 0 .../ActionScheduler_Abstract_Schema.php | 31 +-- .../abstracts/ActionScheduler_Lock.php | 2 + .../abstracts/ActionScheduler_Logger.php | 2 +- .../abstracts/ActionScheduler_Store.php | 11 +- .../ActionScheduler_TimezoneHelper.php | 4 +- .../actions/ActionScheduler_Action.php | 39 ++++ .../ActionScheduler_CanceledAction.php | 0 .../ActionScheduler_FinishedAction.php | 0 .../actions/ActionScheduler_NullAction.php | 0 .../data-stores/ActionScheduler_DBLogger.php | 2 +- .../data-stores/ActionScheduler_DBStore.php | 182 ++++++++++++++---- .../ActionScheduler_HybridStore.php | 0 .../ActionScheduler_wpCommentLogger.php | 0 .../ActionScheduler_wpPostStore.php | 27 ++- ...eduler_wpPostStore_PostStatusRegistrar.php | 0 ...cheduler_wpPostStore_PostTypeRegistrar.php | 2 +- ...cheduler_wpPostStore_TaxonomyRegistrar.php | 0 .../classes/migration/ActionMigrator.php | 1 + .../ActionScheduler_DBStoreMigrator.php | 1 + .../classes/migration/BatchFetcher.php | 0 .../classes/migration/Config.php | 2 +- .../classes/migration/Controller.php | 0 .../migration/DryRun_ActionMigrator.php | 0 .../classes/migration/DryRun_LogMigrator.php | 0 .../classes/migration/LogMigrator.php | 0 .../classes/migration/Runner.php | 2 +- .../classes/migration/Scheduler.php | 0 .../ActionScheduler_CanceledSchedule.php | 0 .../ActionScheduler_CronSchedule.php | 2 +- .../ActionScheduler_IntervalSchedule.php | 2 +- .../ActionScheduler_NullSchedule.php | 0 .../schedules/ActionScheduler_Schedule.php | 0 .../ActionScheduler_SimpleSchedule.php | 0 .../schema/ActionScheduler_LoggerSchema.php | 0 .../schema/ActionScheduler_StoreSchema.php | 8 +- ...eduler_Abstract_QueueRunner_Deprecated.php | 0 .../ActionScheduler_AdminView_Deprecated.php | 2 +- .../ActionScheduler_Schedule_Deprecated.php | 0 .../ActionScheduler_Store_Deprecated.php | 0 .../ActionScheduler/deprecated/functions.php | 0 .../ActionScheduler/functions.php | 115 ++++++++--- .../ActionScheduler/lib/WP_Async_Request.php | 0 .../lib/cron-expression/CronExpression.php | 0 .../CronExpression_AbstractField.php | 0 .../CronExpression_DayOfMonthField.php | 0 .../CronExpression_DayOfWeekField.php | 0 .../CronExpression_FieldFactory.php | 0 .../CronExpression_FieldInterface.php | 0 .../CronExpression_HoursField.php | 0 .../CronExpression_MinutesField.php | 0 .../CronExpression_MonthField.php | 0 .../CronExpression_YearField.php | 0 .../lib/cron-expression/LICENSE | 0 .../lib/cron-expression/README.md | 0 .../ActionScheduler/license.txt | 0 .../ActionScheduler/readme.txt | 94 ++++++++- .../classes/ActionScheduler_OptionLock.php | 49 ----- inc/Engine/Common/Queue/Cleaner.php | 137 +++++++++---- inc/main.php | 2 +- phpstan.neon.dist | 2 +- 93 files changed, 1373 insertions(+), 319 deletions(-) rename {inc/Dependencies => dependencies}/ActionScheduler/README.md (97%) rename {inc/Dependencies => dependencies}/ActionScheduler/action-scheduler.php (77%) rename {inc/Dependencies => dependencies}/ActionScheduler/changelog.txt (55%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_ActionClaim.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_ActionFactory.php (68%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_AdminView.php (96%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_Compatibility.php (87%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_DataController.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_DateTime.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_Exception.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_FatalErrorMonitor.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_InvalidActionException.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_ListTable.php (96%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_LogEntry.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_NullLogEntry.php (100%) create mode 100644 dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_QueueCleaner.php (50%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_QueueRunner.php (86%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_Versions.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_WPCommentCleaner.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/ActionScheduler_wcSystemStatus.php (100%) create mode 100644 dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php rename {inc/Dependencies => dependencies}/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php (99%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php (72%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/WP_CLI/Migration_Command.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/WP_CLI/ProgressBar.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/abstracts/ActionScheduler.php (87%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php (92%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php (71%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php (97%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php (87%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php (93%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/abstracts/ActionScheduler_Logger.php (97%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/abstracts/ActionScheduler_Store.php (97%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php (92%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/actions/ActionScheduler_Action.php (68%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/actions/ActionScheduler_CanceledAction.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/actions/ActionScheduler_FinishedAction.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/actions/ActionScheduler_NullAction.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php (98%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php (85%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/data-stores/ActionScheduler_HybridStore.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php (96%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php (97%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/migration/ActionMigrator.php (98%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/migration/ActionScheduler_DBStoreMigrator.php (97%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/migration/BatchFetcher.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/migration/Config.php (98%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/migration/Controller.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/migration/DryRun_ActionMigrator.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/migration/DryRun_LogMigrator.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/migration/LogMigrator.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/migration/Runner.php (98%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/migration/Scheduler.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/schedules/ActionScheduler_CanceledSchedule.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/schedules/ActionScheduler_CronSchedule.php (98%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/schedules/ActionScheduler_IntervalSchedule.php (97%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/schedules/ActionScheduler_NullSchedule.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/schedules/ActionScheduler_Schedule.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/schedules/ActionScheduler_SimpleSchedule.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/schema/ActionScheduler_LoggerSchema.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php (91%) rename {inc/Dependencies => dependencies}/ActionScheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/deprecated/ActionScheduler_AdminView_Deprecated.php (99%) rename {inc/Dependencies => dependencies}/ActionScheduler/deprecated/ActionScheduler_Schedule_Deprecated.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/deprecated/ActionScheduler_Store_Deprecated.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/deprecated/functions.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/functions.php (78%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/WP_Async_Request.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/CronExpression.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/CronExpression_AbstractField.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/CronExpression_DayOfMonthField.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/CronExpression_DayOfWeekField.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/CronExpression_FieldFactory.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/CronExpression_FieldInterface.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/CronExpression_HoursField.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/CronExpression_MinutesField.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/CronExpression_MonthField.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/CronExpression_YearField.php (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/LICENSE (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/lib/cron-expression/README.md (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/license.txt (100%) rename {inc/Dependencies => dependencies}/ActionScheduler/readme.txt (66%) delete mode 100644 inc/Dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php diff --git a/composer.json b/composer.json index c3dac045e9..60435e760f 100644 --- a/composer.json +++ b/composer.json @@ -104,7 +104,7 @@ }, "extra": { "installer-paths": { - "./inc/Dependencies/ActionScheduler/": ["woocommerce/action-scheduler"], + "dependencies/ActionScheduler/": ["woocommerce/action-scheduler"], "vendor/{$vendor}/{$name}/": ["type:wordpress-plugin"] }, "mozart": { diff --git a/inc/Dependencies/ActionScheduler/README.md b/dependencies/ActionScheduler/README.md similarity index 97% rename from inc/Dependencies/ActionScheduler/README.md rename to dependencies/ActionScheduler/README.md index bdfa2a62cb..09478b0b2b 100644 --- a/inc/Dependencies/ActionScheduler/README.md +++ b/dependencies/ActionScheduler/README.md @@ -2,7 +2,7 @@ Action Scheduler is a scalable, traceable job queue for background processing large sets of actions in WordPress. It's specially designed to be distributed in WordPress plugins. -Action Scheduler works by triggering an action hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occassions. +Action Scheduler works by triggering an action hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occasions. Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook. diff --git a/inc/Dependencies/ActionScheduler/action-scheduler.php b/dependencies/ActionScheduler/action-scheduler.php similarity index 77% rename from inc/Dependencies/ActionScheduler/action-scheduler.php rename to dependencies/ActionScheduler/action-scheduler.php index b950a70b26..1d5cf7b09f 100644 --- a/inc/Dependencies/ActionScheduler/action-scheduler.php +++ b/dependencies/ActionScheduler/action-scheduler.php @@ -5,8 +5,11 @@ * Description: A robust scheduling library for use in WordPress plugins. * Author: Automattic * Author URI: https://automattic.com/ - * Version: 3.5.4 + * Version: 3.8.1 * License: GPLv3 + * Requires at least: 6.2 + * Tested up to: 6.5 + * Requires PHP: 5.6 * * Copyright 2019 Automattic, Inc. (https://automattic.com/contact/) * @@ -26,27 +29,29 @@ * @package ActionScheduler */ -if ( ! function_exists( 'action_scheduler_register_3_dot_5_dot_4' ) && function_exists( 'add_action' ) ) { // WRCS: DEFINED_VERSION. +if ( ! function_exists( 'action_scheduler_register_3_dot_8_dot_1' ) && function_exists( 'add_action' ) ) { // WRCS: DEFINED_VERSION. if ( ! class_exists( 'ActionScheduler_Versions', false ) ) { require_once __DIR__ . '/classes/ActionScheduler_Versions.php'; add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 ); } - add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_5_dot_4', 0, 0 ); // WRCS: DEFINED_VERSION. + add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_8_dot_1', 0, 0 ); // WRCS: DEFINED_VERSION. + // phpcs:disable Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace /** * Registers this version of Action Scheduler. */ - function action_scheduler_register_3_dot_5_dot_4() { // WRCS: DEFINED_VERSION. + function action_scheduler_register_3_dot_8_dot_1() { // WRCS: DEFINED_VERSION. $versions = ActionScheduler_Versions::instance(); - $versions->register( '3.5.4', 'action_scheduler_initialize_3_dot_5_dot_4' ); // WRCS: DEFINED_VERSION. + $versions->register( '3.8.1', 'action_scheduler_initialize_3_dot_8_dot_1' ); // WRCS: DEFINED_VERSION. } + // phpcs:disable Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace /** * Initializes this version of Action Scheduler. */ - function action_scheduler_initialize_3_dot_5_dot_4() { // WRCS: DEFINED_VERSION. + function action_scheduler_initialize_3_dot_8_dot_1() { // WRCS: DEFINED_VERSION. // A final safety check is required even here, because historic versions of Action Scheduler // followed a different pattern (in some unusual cases, we could reach this point and the // ActionScheduler class is already defined—so we need to guard against that). @@ -58,7 +63,7 @@ function action_scheduler_initialize_3_dot_5_dot_4() { // WRCS: DEFINED_VERSION. // Support usage in themes - load this version if no plugin has loaded a version yet. if ( did_action( 'plugins_loaded' ) && ! doing_action( 'plugins_loaded' ) && ! class_exists( 'ActionScheduler', false ) ) { - action_scheduler_initialize_3_dot_5_dot_4(); // WRCS: DEFINED_VERSION. + action_scheduler_initialize_3_dot_8_dot_1(); // WRCS: DEFINED_VERSION. do_action( 'action_scheduler_pre_theme_init' ); ActionScheduler_Versions::initialize_latest_version(); } diff --git a/inc/Dependencies/ActionScheduler/changelog.txt b/dependencies/ActionScheduler/changelog.txt similarity index 55% rename from inc/Dependencies/ActionScheduler/changelog.txt rename to dependencies/ActionScheduler/changelog.txt index 69aef1ff75..97c537d9fd 100644 --- a/inc/Dependencies/ActionScheduler/changelog.txt +++ b/dependencies/ActionScheduler/changelog.txt @@ -1,5 +1,87 @@ *** Changelog *** += 3.8.1 - 2024-06-20 = +* Fix typos. +* Improve the messaging in our unidentified action exceptions. + += 3.8.0 - 2024-05-22 = +* Documentation - Fixed typos in perf.md. +* Update - We now require WordPress 6.3 or higher. +* Update - We now require PHP 7.0 or higher. + += 3.7.4 - 2024-04-05 = +* Give a clear description of how the $unique parameter works. +* Preserve the tab field if set. +* Tweak - WP 6.5 compatibility. + += 3.7.3 - 2024-03-20 = +* Do not iterate over all of GET when building form in list table. +* Fix a few issues reported by PCP (Plugin Check Plugin). +* Try to save actions as unique even when the store doesn't support it. +* Tweak - WP 6.4 compatibility. +* Update "Tested up to" tag to WordPress 6.5. +* update version in package-lock.json. + += 3.7.2 - 2024-02-14 = +* No longer user variables in `_n()` translation function. + += 3.7.1 - 2023-12-13 = +* update semver to 5.7.2 because of a security vulnerability in 5.7.1. + += 3.7.0 - 2023-11-20 = +* Important: starting with this release, Action Scheduler follows an L-2 version policy (WordPress, and consequently PHP). +* Add extended indexes for hook_status_scheduled_date_gmt and status_scheduled_date_gmt. +* Catch and log exceptions thrown when actions can't be created, e.g. under a corrupt database schema. +* Tweak - WP 6.4 compatibility. +* Update unit tests for upcoming dependency version policy. +* make sure hook action_scheduler_failed_execution can access original exception object. +* mention dependency version policy in usage.md. + += 3.6.4 - 2023-10-11 = +* Performance improvements when bulk cancelling actions. +* Dev-related fixes. + += 3.6.3 - 2023-09-13 = +* Use `_doing_it_wrong` in initialization check. + += 3.6.2 - 2023-08-09 = +* Add guidance about passing arguments. +* Atomic option locking. +* Improve bulk delete handling. +* Include database error in the exception message. +* Tweak - WP 6.3 compatibility. + += 3.6.1 - 2023-06-14 = +* Document new optional `$priority` arg for various API functions. +* Document the new `--exclude-groups` WP CLI option. +* Document the new `action_scheduler_init` hook. +* Ensure actions within each claim are executed in the expected order. +* Fix incorrect text domain. +* Remove SHOW TABLES usage when checking if tables exist. + += 3.6.0 - 2023-05-10 = +* Add $unique parameter to function signatures. +* Add a cast-to-int for extra safety before forming new DateTime object. +* Add a hook allowing exceptions for consistently failing recurring actions. +* Add action priorities. +* Add init hook. +* Always raise the time limit. +* Bump minimatch from 3.0.4 to 3.0.8. +* Bump yaml from 2.2.1 to 2.2.2. +* Defensive coding relating to gaps in declared schedule types. +* Do not process an action if it cannot be set to `in-progress`. +* Filter view labels (status names) should be translatable | #919. +* Fix WPCLI progress messages. +* Improve data-store initialization flow. +* Improve error handling across all supported PHP versions. +* Improve logic for flushing the runtime cache. +* Support exclusion of multiple groups. +* Update lint-staged and Node/NPM requirements. +* add CLI clean command. +* add CLI exclude-group filter. +* exclude past-due from list table all filter count. +* throwing an exception if as_schedule_recurring_action interval param is not of type integer. + = 3.5.4 - 2023-01-17 = * Add pre filters during action registration. * Async scheduling. @@ -43,7 +125,7 @@ * Dev - ActionScheduler_wcSystemStatus PHPCS fixes (props @ovidiul). #761 * Dev - ActionScheduler_DBLogger.php PHPCS fixes (props @ovidiul). #768 * Dev - Fixed phpcs for ActionScheduler_Schedule_Deprecated (props @ovidiul). #762 -* Dev - Improve actions table indicies (props @glagonikas). #774 & #777 +* Dev - Improve actions table indices (props @glagonikas). #774 & #777 * Dev - PHPCS fixes for ActionScheduler_DBStore.php (props @ovidiul). #769 & #778 * Dev - PHPCS Fixes for ActionScheduler_Abstract_ListTable (props @ovidiul). #763 & #779 * Dev - Adds new filter action_scheduler_claim_actions_order_by to allow tuning of the claim query (props @glagonikas). #773 diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ActionClaim.php b/dependencies/ActionScheduler/classes/ActionScheduler_ActionClaim.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_ActionClaim.php rename to dependencies/ActionScheduler/classes/ActionScheduler_ActionClaim.php diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ActionFactory.php b/dependencies/ActionScheduler/classes/ActionScheduler_ActionFactory.php similarity index 68% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_ActionFactory.php rename to dependencies/ActionScheduler/classes/ActionScheduler_ActionFactory.php index 8e2e650181..c8d68af3fa 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ActionFactory.php +++ b/dependencies/ActionScheduler/classes/ActionScheduler_ActionFactory.php @@ -13,10 +13,15 @@ class ActionScheduler_ActionFactory { * @param array $args Args to pass to callbacks when the hook is triggered. * @param ActionScheduler_Schedule $schedule The action's schedule. * @param string $group A group to put the action in. + * phpcs:ignore Squiz.Commenting.FunctionComment.ExtraParamComment + * @param int $priority The action priority. * * @return ActionScheduler_Action An instance of the stored action. */ public function get_stored_action( $status, $hook, array $args = array(), ActionScheduler_Schedule $schedule = null, $group = '' ) { + // The 6th parameter ($priority) is not formally declared in the method signature to maintain compatibility with + // third-party subclasses created before this param was added. + $priority = func_num_args() >= 6 ? (int) func_get_arg( 5 ) : 10; switch ( $status ) { case ActionScheduler_Store::STATUS_PENDING: @@ -36,17 +41,19 @@ public function get_stored_action( $status, $hook, array $args = array(), Action $action_class = apply_filters( 'action_scheduler_stored_action_class', $action_class, $status, $hook, $args, $schedule, $group ); $action = new $action_class( $hook, $args, $schedule, $group ); + $action->set_priority( $priority ); /** * Allow 3rd party code to change the instantiated action for a given hook, args, schedule and group. * - * @param ActionScheduler_Action $action The instantiated action. - * @param string $hook The instantiated action's hook. - * @param array $args The instantiated action's args. + * @param ActionScheduler_Action $action The instantiated action. + * @param string $hook The instantiated action's hook. + * @param array $args The instantiated action's args. * @param ActionScheduler_Schedule $schedule The instantiated action's schedule. - * @param string $group The instantiated action's group. + * @param string $group The instantiated action's group. + * @param int $priority The action priority. */ - return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group ); + return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group, $priority ); } /** @@ -229,9 +236,100 @@ public function repeat( $action ) { $schedule_class = get_class( $schedule ); $new_schedule = new $schedule( $next, $schedule->get_recurrence(), $schedule->get_first_date() ); $new_action = new ActionScheduler_Action( $action->get_hook(), $action->get_args(), $new_schedule, $action->get_group() ); + $new_action->set_priority( $action->get_priority() ); return $this->store( $new_action ); } + /** + * Creates a scheduled action. + * + * This general purpose method can be used in place of specific methods such as async(), + * async_unique(), single() or single_unique(), etc. + * + * @internal Not intended for public use, should not be overridden by subclasses. + * + * @param array $options { + * Describes the action we wish to schedule. + * + * @type string $type Must be one of 'async', 'cron', 'recurring', or 'single'. + * @type string $hook The hook to be executed. + * @type array $arguments Arguments to be passed to the callback. + * @type string $group The action group. + * @type bool $unique If the action should be unique. + * @type int $when Timestamp. Indicates when the action, or first instance of the action in the case + * of recurring or cron actions, becomes due. + * @type int|string $pattern Recurrence pattern. This is either an interval in seconds for recurring actions + * or a cron expression for cron actions. + * @type int $priority Lower values means higher priority. Should be in the range 0-255. + * } + * + * @return int The action ID. Zero if there was an error scheduling the action. + */ + public function create( array $options = array() ) { + $defaults = array( + 'type' => 'single', + 'hook' => '', + 'arguments' => array(), + 'group' => '', + 'unique' => false, + 'when' => time(), + 'pattern' => null, + 'priority' => 10, + ); + + $options = array_merge( $defaults, $options ); + + // Cron/recurring actions without a pattern are treated as single actions (this gives calling code the ability + // to use functions like as_schedule_recurring_action() to schedule recurring as well as single actions). + if ( ( 'cron' === $options['type'] || 'recurring' === $options['type'] ) && empty( $options['pattern'] ) ) { + $options['type'] = 'single'; + } + + switch ( $options['type'] ) { + case 'async': + $schedule = new ActionScheduler_NullSchedule(); + break; + + case 'cron': + $date = as_get_datetime_object( $options['when'] ); + $cron = CronExpression::factory( $options['pattern'] ); + $schedule = new ActionScheduler_CronSchedule( $date, $cron ); + break; + + case 'recurring': + $date = as_get_datetime_object( $options['when'] ); + $schedule = new ActionScheduler_IntervalSchedule( $date, $options['pattern'] ); + break; + + case 'single': + $date = as_get_datetime_object( $options['when'] ); + $schedule = new ActionScheduler_SimpleSchedule( $date ); + break; + + default: + error_log( "Unknown action type '{$options['type']}' specified when trying to create an action for '{$options['hook']}'." ); + return 0; + } + + $action = new ActionScheduler_Action( $options['hook'], $options['arguments'], $schedule, $options['group'] ); + $action->set_priority( $options['priority'] ); + + $action_id = 0; + try { + $action_id = $options['unique'] ? $this->store_unique_action( $action ) : $this->store( $action ); + } catch ( Exception $e ) { + error_log( + sprintf( + /* translators: %1$s is the name of the hook to be enqueued, %2$s is the exception message. */ + __( 'Caught exception while enqueuing action "%1$s": %2$s', 'action-scheduler' ), + $options['hook'], + $e->getMessage() + ) + ); + } + return $action_id; + } + /** * Save action to database. * @@ -253,7 +351,26 @@ protected function store( ActionScheduler_Action $action ) { */ protected function store_unique_action( ActionScheduler_Action $action ) { $store = ActionScheduler_Store::instance(); - return method_exists( $store, 'save_unique_action' ) ? - $store->save_unique_action( $action ) : $store->save_action( $action ); + if ( method_exists( $store, 'save_unique_action' ) ) { + return $store->save_unique_action( $action ); + } else { + /** + * Fallback to non-unique action if the store doesn't support unique actions. + * We try to save the action as unique, accepting that there might be a race condition. + * This is likely still better than giving up on unique actions entirely. + */ + $existing_action_id = (int) $store->find_action( + $action->get_hook(), + array( + 'args' => $action->get_args(), + 'status' => ActionScheduler_Store::STATUS_PENDING, + 'group' => $action->get_group(), + ) + ); + if ( $existing_action_id > 0 ) { + return 0; + } + return $store->save_action( $action ); + } } } diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_AdminView.php b/dependencies/ActionScheduler/classes/ActionScheduler_AdminView.php similarity index 96% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_AdminView.php rename to dependencies/ActionScheduler/classes/ActionScheduler_AdminView.php index b747b0a1b6..ed30950a73 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_AdminView.php +++ b/dependencies/ActionScheduler/classes/ActionScheduler_AdminView.php @@ -145,7 +145,7 @@ protected function check_pastdue_actions() { # Set thresholds. $threshold_seconds = ( int ) apply_filters( 'action_scheduler_pastdue_actions_seconds', DAY_IN_SECONDS ); - $threshhold_min = ( int ) apply_filters( 'action_scheduler_pastdue_actions_min', 1 ); + $threshold_min = ( int ) apply_filters( 'action_scheduler_pastdue_actions_min', 1 ); // Set fallback value for past-due actions count. $num_pastdue_actions = 0; @@ -162,7 +162,7 @@ protected function check_pastdue_actions() { $query_args = array( 'date' => as_get_datetime_object( time() - $threshold_seconds ), 'status' => ActionScheduler_Store::STATUS_PENDING, - 'per_page' => $threshhold_min, + 'per_page' => $threshold_min, ); # If no third-party preempted, run default check. @@ -171,8 +171,8 @@ protected function check_pastdue_actions() { $num_pastdue_actions = ( int ) $store->query_actions( $query_args, 'count' ); # Check if past-due actions count is greater than or equal to threshold. - $check = ( $num_pastdue_actions >= $threshhold_min ); - $check = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $check, $num_pastdue_actions, $threshold_seconds, $threshhold_min ); + $check = ( $num_pastdue_actions >= $threshold_min ); + $check = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $check, $num_pastdue_actions, $threshold_seconds, $threshold_min ); } # If check failed, set transient and abort. @@ -192,8 +192,8 @@ protected function check_pastdue_actions() { # Print notice. echo '

'; printf( + // translators: 1) is the number of affected actions, 2) is a link to an admin screen. _n( - // translators: 1) is the number of affected actions, 2) is a link to an admin screen. 'Action Scheduler: %1$d past-due action found; something may be wrong. Read documentation »', 'Action Scheduler: %1$d past-due actions found; something may be wrong. Read documentation »', $num_pastdue_actions, @@ -224,6 +224,7 @@ public function add_help_tabs() { 'id' => 'action_scheduler_about', 'title' => __( 'About', 'action-scheduler' ), 'content' => + // translators: %s is the Action Scheduler version. '

' . sprintf( __( 'About Action Scheduler %s', 'action-scheduler' ), $as_version ) . '

' . '

' . __( 'Action Scheduler is a scalable, traceable job queue for background processing large sets of actions. Action Scheduler works by triggering an action hook to run at some time in the future. Scheduled actions can also be scheduled to run on a recurring schedule.', 'action-scheduler' ) . diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php b/dependencies/ActionScheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php rename to dependencies/ActionScheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_Compatibility.php b/dependencies/ActionScheduler/classes/ActionScheduler_Compatibility.php similarity index 87% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_Compatibility.php rename to dependencies/ActionScheduler/classes/ActionScheduler_Compatibility.php index 85e0ed9da3..bb28023bc9 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_Compatibility.php +++ b/dependencies/ActionScheduler/classes/ActionScheduler_Compatibility.php @@ -4,7 +4,6 @@ * Class ActionScheduler_Compatibility */ class ActionScheduler_Compatibility { - /** * Converts a shorthand byte value to an integer byte value. * @@ -89,21 +88,18 @@ public static function raise_time_limit( $limit = 0 ) { $limit = (int) $limit; $max_execution_time = (int) ini_get( 'max_execution_time' ); - /* - * If the max execution time is already unlimited (zero), or if it exceeds or is equal to the proposed - * limit, there is no reason for us to make further changes (we never want to lower it). - */ - if ( - 0 === $max_execution_time - || ( $max_execution_time >= $limit && $limit !== 0 ) - ) { + // If the max execution time is already set to zero (unlimited), there is no reason to make a further change. + if ( 0 === $max_execution_time ) { return; } + // Whichever of $max_execution_time or $limit is higher is the amount by which we raise the time limit. + $raise_by = 0 === $limit || $limit > $max_execution_time ? $limit : $max_execution_time; + if ( function_exists( 'wc_set_time_limit' ) ) { - wc_set_time_limit( $limit ); + wc_set_time_limit( $raise_by ); } elseif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved - @set_time_limit( $limit ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + @set_time_limit( $raise_by ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } } } diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_DataController.php b/dependencies/ActionScheduler/classes/ActionScheduler_DataController.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_DataController.php rename to dependencies/ActionScheduler/classes/ActionScheduler_DataController.php diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_DateTime.php b/dependencies/ActionScheduler/classes/ActionScheduler_DateTime.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_DateTime.php rename to dependencies/ActionScheduler/classes/ActionScheduler_DateTime.php diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_Exception.php b/dependencies/ActionScheduler/classes/ActionScheduler_Exception.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_Exception.php rename to dependencies/ActionScheduler/classes/ActionScheduler_Exception.php diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_FatalErrorMonitor.php b/dependencies/ActionScheduler/classes/ActionScheduler_FatalErrorMonitor.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_FatalErrorMonitor.php rename to dependencies/ActionScheduler/classes/ActionScheduler_FatalErrorMonitor.php diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_InvalidActionException.php b/dependencies/ActionScheduler/classes/ActionScheduler_InvalidActionException.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_InvalidActionException.php rename to dependencies/ActionScheduler/classes/ActionScheduler_InvalidActionException.php diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ListTable.php b/dependencies/ActionScheduler/classes/ActionScheduler_ListTable.php similarity index 96% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_ListTable.php rename to dependencies/ActionScheduler/classes/ActionScheduler_ListTable.php index 9e631f7543..abf767ce65 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ListTable.php +++ b/dependencies/ActionScheduler/classes/ActionScheduler_ListTable.php @@ -234,7 +234,7 @@ private static function human_interval( $interval, $periods_to_include = 2 ) { if ( ! empty( $output ) ) { $output .= ' '; } - $output .= sprintf( _n( self::$time_periods[ $time_period_index ]['names'][0], self::$time_periods[ $time_period_index ]['names'][1], $periods_in_interval, 'action-scheduler' ), $periods_in_interval ); + $output .= sprintf( translate_nooped_plural( self::$time_periods[ $time_period_index ]['names'], $periods_in_interval, 'action-scheduler' ), $periods_in_interval ); $seconds_remaining -= $periods_in_interval * self::$time_periods[ $time_period_index ]['seconds']; $periods_included++; } @@ -252,7 +252,7 @@ private static function human_interval( $interval, $periods_to_include = 2 ) { */ protected function get_recurrence( $action ) { $schedule = $action->get_schedule(); - if ( $schedule->is_recurring() ) { + if ( $schedule->is_recurring() && method_exists( $schedule, 'get_recurrence' ) ) { $recurrence = $schedule->get_recurrence(); if ( is_numeric( $recurrence ) ) { @@ -471,7 +471,7 @@ protected function get_schedule_display_string( ActionScheduler_Schedule $schedu return __( 'async', 'action-scheduler' ); } - if ( ! $schedule->get_date() ) { + if ( ! method_exists( $schedule, 'get_date' ) || ! $schedule->get_date() ) { return '0000-00-00 00:00:00'; } @@ -502,7 +502,20 @@ protected function get_schedule_display_string( ActionScheduler_Schedule $schedu */ protected function bulk_delete( array $ids, $ids_sql ) { foreach ( $ids as $id ) { - $this->store->delete_action( $id ); + try { + $this->store->delete_action( $id ); + } catch ( Exception $e ) { + // A possible reason for an exception would include a scenario where the same action is deleted by a + // concurrent request. + error_log( + sprintf( + /* translators: 1: action ID 2: exception message. */ + __( 'Action Scheduler was unable to delete action %1$d. Reason: %2$s', 'action-scheduler' ), + $id, + $e->getMessage() + ) + ); + } } } diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_LogEntry.php b/dependencies/ActionScheduler/classes/ActionScheduler_LogEntry.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_LogEntry.php rename to dependencies/ActionScheduler/classes/ActionScheduler_LogEntry.php diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_NullLogEntry.php b/dependencies/ActionScheduler/classes/ActionScheduler_NullLogEntry.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_NullLogEntry.php rename to dependencies/ActionScheduler/classes/ActionScheduler_NullLogEntry.php diff --git a/dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php b/dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php new file mode 100644 index 0000000000..911f9b77c7 --- /dev/null +++ b/dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php @@ -0,0 +1,135 @@ +maybe_dispatch_async_request() uses a lock to avoid + * calling ActionScheduler_QueueRunner->has_maximum_concurrent_batches() every time the 'shutdown', + * hook is triggered, because that method calls ActionScheduler_QueueRunner->store->get_claim_count() + * to find the current number of claims in the database. + * + * @param string $lock_type A string to identify different lock types. + * @bool True if lock value has changed, false if not or if set failed. + */ + public function set( $lock_type ) { + global $wpdb; + + $lock_key = $this->get_key( $lock_type ); + $existing_lock_value = $this->get_existing_lock( $lock_type ); + $new_lock_value = $this->new_lock_value( $lock_type ); + + // The lock may not exist yet, or may have been deleted. + if ( empty( $existing_lock_value ) ) { + return (bool) $wpdb->insert( + $wpdb->options, + array( + 'option_name' => $lock_key, + 'option_value' => $new_lock_value, + 'autoload' => 'no', + ) + ); + } + + if ( $this->get_expiration_from( $existing_lock_value ) >= time() ) { + return false; + } + + // Otherwise, try to obtain the lock. + return (bool) $wpdb->update( + $wpdb->options, + array( 'option_value' => $new_lock_value ), + array( + 'option_name' => $lock_key, + 'option_value' => $existing_lock_value, + ) + ); + } + + /** + * If a lock is set, return the timestamp it was set to expiry. + * + * @param string $lock_type A string to identify different lock types. + * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. + */ + public function get_expiration( $lock_type ) { + return $this->get_expiration_from( $this->get_existing_lock( $lock_type ) ); + } + + /** + * Given the lock string, derives the lock expiration timestamp (or false if it cannot be determined). + * + * @param string $lock_value String containing a timestamp, or pipe-separated combination of unique value and timestamp. + * + * @return false|int + */ + private function get_expiration_from( $lock_value ) { + $lock_string = explode( '|', $lock_value ); + + // Old style lock? + if ( count( $lock_string ) === 1 && is_numeric( $lock_string[0] ) ) { + return (int) $lock_string[0]; + } + + // New style lock? + if ( count( $lock_string ) === 2 && is_numeric( $lock_string[1] ) ) { + return (int) $lock_string[1]; + } + + return false; + } + + /** + * Get the key to use for storing the lock in the transient + * + * @param string $lock_type A string to identify different lock types. + * @return string + */ + protected function get_key( $lock_type ) { + return sprintf( 'action_scheduler_lock_%s', $lock_type ); + } + + /** + * Supplies the existing lock value, or an empty string if not set. + * + * @param string $lock_type A string to identify different lock types. + * + * @return string + */ + private function get_existing_lock( $lock_type ) { + global $wpdb; + + // Now grab the existing lock value, if there is one. + return (string) $wpdb->get_var( + $wpdb->prepare( + "SELECT option_value FROM $wpdb->options WHERE option_name = %s", + $this->get_key( $lock_type ) + ) + ); + } + + /** + * Supplies a lock value consisting of a unique value and the current timestamp, which are separated by a pipe + * character. + * + * Example: (string) "649de012e6b262.09774912|1688068114" + * + * @param string $lock_type A string to identify different lock types. + * + * @return string + */ + private function new_lock_value( $lock_type ) { + return uniqid( '', true ) . '|' . ( time() + $this->get_duration( $lock_type ) ); + } +} diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueCleaner.php b/dependencies/ActionScheduler/classes/ActionScheduler_QueueCleaner.php similarity index 50% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueCleaner.php rename to dependencies/ActionScheduler/classes/ActionScheduler_QueueCleaner.php index 49cd44bb2a..6b066dca02 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueCleaner.php +++ b/dependencies/ActionScheduler/classes/ActionScheduler_QueueCleaner.php @@ -18,6 +18,14 @@ class ActionScheduler_QueueCleaner { */ private $month_in_seconds = 2678400; + /** + * @var string[] Default list of statuses purged by the cleaner process. + */ + private $default_statuses_to_purge = [ + ActionScheduler_Store::STATUS_COMPLETE, + ActionScheduler_Store::STATUS_CANCELED, + ]; + /** * ActionScheduler_QueueCleaner constructor. * @@ -29,46 +37,113 @@ public function __construct( ActionScheduler_Store $store = null, $batch_size = $this->batch_size = $batch_size; } + /** + * Default queue cleaner process used by queue runner. + * + * @return array + */ public function delete_old_actions() { + /** + * Filter the minimum scheduled date age for action deletion. + * + * @param int $retention_period Minimum scheduled age in seconds of the actions to be deleted. + */ $lifespan = apply_filters( 'action_scheduler_retention_period', $this->month_in_seconds ); - $cutoff = as_get_datetime_object($lifespan.' seconds ago'); - $statuses_to_purge = array( - ActionScheduler_Store::STATUS_COMPLETE, - ActionScheduler_Store::STATUS_CANCELED, - ); + try { + $cutoff = as_get_datetime_object( $lifespan . ' seconds ago' ); + } catch ( Exception $e ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* Translators: %s is the exception message. */ + esc_html__( 'It was not possible to determine a valid cut-off time: %s.', 'action-scheduler' ), + esc_html( $e->getMessage() ) + ), + '3.5.5' + ); + + return array(); + } + + + /** + * Filter the statuses when cleaning the queue. + * + * @param string[] $default_statuses_to_purge Action statuses to clean. + */ + $statuses_to_purge = (array) apply_filters( 'action_scheduler_default_cleaner_statuses', $this->default_statuses_to_purge ); + + return $this->clean_actions( $statuses_to_purge, $cutoff, $this->get_batch_size() ); + } + + /** + * Delete selected actions limited by status and date. + * + * @param string[] $statuses_to_purge List of action statuses to purge. Defaults to canceled, complete. + * @param DateTime $cutoff_date Date limit for selecting actions. Defaults to 31 days ago. + * @param int|null $batch_size Maximum number of actions per status to delete. Defaults to 20. + * @param string $context Calling process context. Defaults to `old`. + * @return array Actions deleted. + */ + public function clean_actions( array $statuses_to_purge, DateTime $cutoff_date, $batch_size = null, $context = 'old' ) { + $batch_size = $batch_size !== null ? $batch_size : $this->batch_size; + $cutoff = $cutoff_date !== null ? $cutoff_date : as_get_datetime_object( $this->month_in_seconds . ' seconds ago' ); + $lifespan = time() - $cutoff->getTimestamp(); + if ( empty( $statuses_to_purge ) ) { + $statuses_to_purge = $this->default_statuses_to_purge; + } + $deleted_actions = []; foreach ( $statuses_to_purge as $status ) { $actions_to_delete = $this->store->query_actions( array( 'status' => $status, 'modified' => $cutoff, 'modified_compare' => '<=', - 'per_page' => $this->get_batch_size(), + 'per_page' => $batch_size, 'orderby' => 'none', ) ); - foreach ( $actions_to_delete as $action_id ) { - try { - $this->store->delete_action( $action_id ); - } catch ( Exception $e ) { - - /** - * Notify 3rd party code of exceptions when deleting a completed action older than the retention period - * - * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their - * actions. - * - * @since 2.0.0 - * - * @param int $action_id The scheduled actions ID in the data store - * @param Exception $e The exception thrown when attempting to delete the action from the data store - * @param int $lifespan The retention period, in seconds, for old actions - * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch - */ - do_action( 'action_scheduler_failed_old_action_deletion', $action_id, $e, $lifespan, count( $actions_to_delete ) ); - } + $deleted_actions = array_merge( $deleted_actions, $this->delete_actions( $actions_to_delete, $lifespan, $context ) ); + } + + return $deleted_actions; + } + + /** + * @param int[] $actions_to_delete List of action IDs to delete. + * @param int $lifespan Minimum scheduled age in seconds of the actions being deleted. + * @param string $context Context of the delete request. + * @return array Deleted action IDs. + */ + private function delete_actions( array $actions_to_delete, $lifespan = null, $context = 'old' ) { + $deleted_actions = []; + if ( $lifespan === null ) { + $lifespan = $this->month_in_seconds; + } + + foreach ( $actions_to_delete as $action_id ) { + try { + $this->store->delete_action( $action_id ); + $deleted_actions[] = $action_id; + } catch ( Exception $e ) { + /** + * Notify 3rd party code of exceptions when deleting a completed action older than the retention period + * + * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their + * actions. + * + * @param int $action_id The scheduled actions ID in the data store + * @param Exception $e The exception thrown when attempting to delete the action from the data store + * @param int $lifespan The retention period, in seconds, for old actions + * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch + * @since 2.0.0 + * + */ + do_action( "action_scheduler_failed_{$context}_action_deletion", $action_id, $e, $lifespan, count( $actions_to_delete ) ); } } + return $deleted_actions; } /** @@ -102,7 +177,7 @@ public function reset_timeouts( $time_limit = 300 ) { /** * Mark actions that have been running for more than a given time limit as failed, based on - * the assumption some uncatachable and unloggable fatal error occurred during processing. + * the assumption some uncatchable and unloggable fatal error occurred during processing. * * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed * as a parameter is 10x the time limit used for queue processing. diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueRunner.php b/dependencies/ActionScheduler/classes/ActionScheduler_QueueRunner.php similarity index 86% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueRunner.php rename to dependencies/ActionScheduler/classes/ActionScheduler_QueueRunner.php index b890dca137..90e64fc46e 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueRunner.php +++ b/dependencies/ActionScheduler/classes/ActionScheduler_QueueRunner.php @@ -103,9 +103,12 @@ public function unhook_dispatch_async_request() { * should dispatch a request to process pending actions. */ public function maybe_dispatch_async_request() { - if ( is_admin() && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) ) { - // Only start an async queue at most once every 60 seconds - ActionScheduler::lock()->set( 'async-request-runner' ); + // Only start an async queue at most once every 60 seconds. + if ( + is_admin() + && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) + && ActionScheduler::lock()->set( 'async-request-runner' ) + ) { $this->async_request->maybe_dispatch(); } } @@ -119,7 +122,7 @@ public function maybe_dispatch_async_request() { * should set a context as the first parameter. For an example of this, refer to the code seen in * @see ActionScheduler_AsyncRequest_QueueRunner::handle() * - * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. * @return int The number of actions processed. */ @@ -149,7 +152,7 @@ public function run( $context = 'WP Cron' ) { * size is completed, or memory or time limits are reached, defined by @see $this->batch_limits_exceeded(). * * @param int $size The maximum number of actions to process in the batch. - * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. * @return int The number of actions processed. */ @@ -185,9 +188,15 @@ protected function do_batch( $size = 100, $context = '' ) { protected function clear_caches() { /* * Calling wp_cache_flush_runtime() lets us clear the runtime cache without invalidating the external object - * cache, so we will always prefer this when it is available (but it was only introduced in WordPress 6.0). + * cache, so we will always prefer this method (as compared to calling wp_cache_flush()) when it is available. + * + * However, this function was only introduced in WordPress 6.0. Additionally, the preferred way of detecting if + * it is supported changed in WordPress 6.1 so we use two different methods to decide if we should utilize it. */ - if ( function_exists( 'wp_cache_flush_runtime' ) ) { + $flushing_runtime_cache_explicitly_supported = function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_runtime' ); + $flushing_runtime_cache_implicitly_supported = ! function_exists( 'wp_cache_supports' ) && function_exists( 'wp_cache_flush_runtime' ); + + if ( $flushing_runtime_cache_explicitly_supported || $flushing_runtime_cache_implicitly_supported ) { wp_cache_flush_runtime(); } elseif ( ! wp_using_ext_object_cache() diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_Versions.php b/dependencies/ActionScheduler/classes/ActionScheduler_Versions.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_Versions.php rename to dependencies/ActionScheduler/classes/ActionScheduler_Versions.php diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_WPCommentCleaner.php b/dependencies/ActionScheduler/classes/ActionScheduler_WPCommentCleaner.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_WPCommentCleaner.php rename to dependencies/ActionScheduler/classes/ActionScheduler_WPCommentCleaner.php diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_wcSystemStatus.php b/dependencies/ActionScheduler/classes/ActionScheduler_wcSystemStatus.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/ActionScheduler_wcSystemStatus.php rename to dependencies/ActionScheduler/classes/ActionScheduler_wcSystemStatus.php diff --git a/dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php b/dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php new file mode 100644 index 0000000000..ff6e57aa31 --- /dev/null +++ b/dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php @@ -0,0 +1,125 @@ +] + * : The maximum number of actions to delete per batch. Defaults to 20. + * + * [--batches=] + * : Limit execution to a number of batches. Defaults to 0, meaning batches will continue all eligible actions are deleted. + * + * [--status=] + * : Only clean actions with the specified status. Defaults to Canceled, Completed. Define multiple statuses as a comma separated string (without spaces), e.g. `--status=complete,failed,canceled` + * + * [--before=] + * : Only delete actions with scheduled date older than this. Defaults to 31 days. e.g `--before='7 days ago'`, `--before='02-Feb-2020 20:20:20'` + * + * [--pause=] + * : The number of seconds to pause between batches. Default no pause. + * + * @param array $args Positional arguments. + * @param array $assoc_args Keyed arguments. + * @throws \WP_CLI\ExitException When an error occurs. + * + * @subcommand clean + */ + public function clean( $args, $assoc_args ) { + // Handle passed arguments. + $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 20 ) ); + $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); + $status = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'status', '' ) ); + $status = array_filter( array_map( 'trim', $status ) ); + $before = \WP_CLI\Utils\get_flag_value( $assoc_args, 'before', '' ); + $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); + + $batches_completed = 0; + $actions_deleted = 0; + $unlimited = $batches === 0; + try { + $lifespan = as_get_datetime_object( $before ); + } catch ( Exception $e ) { + $lifespan = null; + } + + try { + // Custom queue cleaner instance. + $cleaner = new ActionScheduler_QueueCleaner( null, $batch ); + + // Clean actions for as long as possible. + while ( $unlimited || $batches_completed < $batches ) { + if ( $sleep && $batches_completed > 0 ) { + sleep( $sleep ); + } + + $deleted = count( $cleaner->clean_actions( $status, $lifespan, null,'CLI' ) ); + if ( $deleted <= 0 ) { + break; + } + $actions_deleted += $deleted; + $batches_completed++; + $this->print_success( $deleted ); + } + } catch ( Exception $e ) { + $this->print_error( $e ); + } + + $this->print_total_batches( $batches_completed ); + if ( $batches_completed > 1 ) { + $this->print_success( $actions_deleted ); + } + } + + /** + * Print WP CLI message about how many batches of actions were processed. + * + * @param int $batches_processed + */ + protected function print_total_batches( int $batches_processed ) { + WP_CLI::log( + sprintf( + /* translators: %d refers to the total number of batches processed */ + _n( '%d batch processed.', '%d batches processed.', $batches_processed, 'action-scheduler' ), + $batches_processed + ) + ); + } + + /** + * Convert an exception into a WP CLI error. + * + * @param Exception $e The error object. + * + * @throws \WP_CLI\ExitException + */ + protected function print_error( Exception $e ) { + WP_CLI::error( + sprintf( + /* translators: %s refers to the exception error message */ + __( 'There was an error deleting an action: %s', 'action-scheduler' ), + $e->getMessage() + ) + ); + } + + /** + * Print a success message with the number of completed actions. + * + * @param int $actions_deleted + */ + protected function print_success( int $actions_deleted ) { + WP_CLI::success( + sprintf( + /* translators: %d refers to the total number of actions deleted */ + _n( '%d action deleted.', '%d actions deleted.', $actions_deleted, 'action-scheduler' ), + $actions_deleted + ) + ); + } +} diff --git a/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php b/dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php similarity index 99% rename from inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php rename to dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php index c33de68672..4681daa492 100644 --- a/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php +++ b/dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php @@ -90,7 +90,7 @@ protected function setup_progress_bar() { $count = count( $this->actions ); $this->progress_bar = new ProgressBar( /* translators: %d: amount of actions */ - sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'action-scheduler' ), number_format_i18n( $count ) ), + sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'action-scheduler' ), $count ), $count ); } diff --git a/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php b/dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php similarity index 72% rename from inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php rename to dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php index 70b052e58d..2c68a3860d 100644 --- a/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php +++ b/dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php @@ -55,6 +55,9 @@ public function fix_schema( $args, $assoc_args ) { * [--group=] * : Only run actions from the specified group. Omitting this option runs actions from all groups. * + * [--exclude-groups=] + * : Run actions from all groups except the specified group(s). Define multiple groups as a comma separated string (without spaces), e.g. '--group_a,group_b'. This option is ignored when `--group` is used. + * * [--free-memory-on=] * : The number of actions to process between freeing memory. 0 disables freeing memory. Default 50. * @@ -72,15 +75,16 @@ public function fix_schema( $args, $assoc_args ) { */ public function run( $args, $assoc_args ) { // Handle passed arguments. - $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) ); - $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); - $clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) ); - $hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) ); - $hooks = array_filter( array_map( 'trim', $hooks ) ); - $group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' ); - $free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 ); - $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); - $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false ); + $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) ); + $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); + $clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) ); + $hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) ); + $hooks = array_filter( array_map( 'trim', $hooks ) ); + $group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' ); + $exclude_groups = \WP_CLI\Utils\get_flag_value( $assoc_args, 'exclude-groups', '' ); + $free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 ); + $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); + $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false ); ActionScheduler_DataController::set_free_ticks( $free_on ); ActionScheduler_DataController::set_sleep_time( $sleep ); @@ -88,6 +92,13 @@ public function run( $args, $assoc_args ) { $batches_completed = 0; $actions_completed = 0; $unlimited = $batches === 0; + if ( is_callable( [ ActionScheduler::store(), 'set_claim_filter' ] ) ) { + $exclude_groups = $this->parse_comma_separated_string( $exclude_groups ); + + if ( ! empty( $exclude_groups ) ) { + ActionScheduler::store()->set_claim_filter('exclude-groups', $exclude_groups ); + } + } try { // Custom queue cleaner instance. @@ -116,6 +127,17 @@ public function run( $args, $assoc_args ) { $this->print_success( $actions_completed ); } + /** + * Converts a string of comma-separated values into an array of those same values. + * + * @param string $string The string of one or more comma separated values. + * + * @return array + */ + private function parse_comma_separated_string( $string ): array { + return array_filter( str_getcsv( $string ) ); + } + /** * Print WP CLI message about how many actions are about to be processed. * @@ -126,9 +148,9 @@ public function run( $args, $assoc_args ) { protected function print_total_actions( $total ) { WP_CLI::log( sprintf( - /* translators: %d refers to how many scheduled taks were found to run */ + /* translators: %d refers to how many scheduled tasks were found to run */ _n( 'Found %d scheduled task', 'Found %d scheduled tasks', $total, 'action-scheduler' ), - number_format_i18n( $total ) + $total ) ); } @@ -145,7 +167,7 @@ protected function print_total_batches( $batches_completed ) { sprintf( /* translators: %d refers to the total number of batches executed */ _n( '%d batch executed.', '%d batches executed.', $batches_completed, 'action-scheduler' ), - number_format_i18n( $batches_completed ) + $batches_completed ) ); } @@ -179,9 +201,9 @@ protected function print_error( Exception $e ) { protected function print_success( $actions_completed ) { WP_CLI::success( sprintf( - /* translators: %d refers to the total number of taskes completed */ + /* translators: %d refers to the total number of tasks completed */ _n( '%d scheduled task completed.', '%d scheduled tasks completed.', $actions_completed, 'action-scheduler' ), - number_format_i18n( $actions_completed ) + $actions_completed ) ); } diff --git a/inc/Dependencies/ActionScheduler/classes/WP_CLI/Migration_Command.php b/dependencies/ActionScheduler/classes/WP_CLI/Migration_Command.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/WP_CLI/Migration_Command.php rename to dependencies/ActionScheduler/classes/WP_CLI/Migration_Command.php diff --git a/inc/Dependencies/ActionScheduler/classes/WP_CLI/ProgressBar.php b/dependencies/ActionScheduler/classes/WP_CLI/ProgressBar.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/WP_CLI/ProgressBar.php rename to dependencies/ActionScheduler/classes/WP_CLI/ProgressBar.php diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler.php b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler.php similarity index 87% rename from inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler.php rename to dependencies/ActionScheduler/classes/abstracts/ActionScheduler.php index e8873f11e9..0163f7072d 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler.php +++ b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler.php @@ -153,11 +153,41 @@ public static function init( $plugin_file ) { add_action( 'init', array( $store, 'init' ), 1, 0 ); add_action( 'init', array( $logger, 'init' ), 1, 0 ); add_action( 'init', array( $runner, 'init' ), 1, 0 ); + + add_action( + 'init', + /** + * Runs after the active store's init() method has been called. + * + * It would probably be preferable to have $store->init() (or it's parent method) set this itself, + * once it has initialized, however that would cause problems in cases where a custom data store is in + * use and it has not yet been updated to follow that same logic. + */ + function () { + self::$data_store_initialized = true; + + /** + * Fires when Action Scheduler is ready: it is safe to use the procedural API after this point. + * + * @since 3.5.5 + */ + do_action( 'action_scheduler_init' ); + }, + 1 + ); } else { $admin_view->init(); $store->init(); $logger->init(); $runner->init(); + self::$data_store_initialized = true; + + /** + * Fires when Action Scheduler is ready: it is safe to use the procedural API after this point. + * + * @since 3.5.5 + */ + do_action( 'action_scheduler_init' ); } if ( apply_filters( 'action_scheduler_load_deprecated_functions', true ) ) { @@ -166,14 +196,13 @@ public static function init( $plugin_file ) { if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Scheduler_command' ); + WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Clean_Command' ); if ( ! ActionScheduler_DataController::is_migration_complete() && Controller::instance()->allow_migration() ) { $command = new Migration_Command(); $command->register(); } } - self::$data_store_initialized = true; - /** * Handle WP comment cleanup after migration. */ @@ -192,8 +221,12 @@ public static function init( $plugin_file ) { */ public static function is_initialized( $function_name = null ) { if ( ! self::$data_store_initialized && ! empty( $function_name ) ) { - $message = sprintf( __( '%s() was called before the Action Scheduler data store was initialized', 'action-scheduler' ), esc_attr( $function_name ) ); - error_log( $message, E_WARNING ); + $message = sprintf( + /* translators: %s function name. */ + __( '%s() was called before the Action Scheduler data store was initialized', 'action-scheduler' ), + esc_attr( $function_name ) + ); + _doing_it_wrong( $function_name, $message, '3.1.6' ); } return self::$data_store_initialized; diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php similarity index 92% rename from inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php rename to dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php index ccc997f2fd..3c480e3dad 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php +++ b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php @@ -313,6 +313,24 @@ protected function get_items_query_order() { return "ORDER BY {$orderby} {$order}"; } + /** + * Querystring arguments to persist between form submissions. + * + * @since 3.7.3 + * + * @return string[] + */ + protected function get_request_query_args_to_persist() { + return array_merge( + $this->sort_by, + array( + 'page', + 'status', + 'tab', + ) + ); + } + /** * Return the sortable column specified for this request to order the results by, if any. * @@ -437,7 +455,7 @@ protected function get_items_query_filters() { /** * Prepares the data to feed WP_Table_List. * - * This has the core for selecting, sorting and filting data. To keep the code simple + * This has the core for selecting, sorting and filtering data. To keep the code simple * its logic is split among many methods (get_items_query_*). * * Beside populating the items this function will also count all the records that matches @@ -526,7 +544,7 @@ public function extra_tablenav( $which ) { /** * Set the data for displaying. It will attempt to unserialize (There is a chance that some columns - * are serialized). This can be override in child classes for futher data transformation. + * are serialized). This can be override in child classes for further data transformation. * * @param array $items Items array. */ @@ -627,7 +645,7 @@ protected function process_row_actions() { } /** - * Default column formatting, it will escape everythig for security. + * Default column formatting, it will escape everything for security. * * @param array $item The item array. * @param string $column_name Column name to display. @@ -673,24 +691,34 @@ protected function display_filter_by_status() { // Helper to set 'all' filter when not set on status counts passed in. if ( ! isset( $this->status_counts['all'] ) ) { - $this->status_counts = array( 'all' => array_sum( $this->status_counts ) ) + $this->status_counts; + $all_count = array_sum( $this->status_counts ); + if ( isset( $this->status_counts['past-due'] ) ) { + $all_count -= $this->status_counts['past-due']; + } + $this->status_counts = array( 'all' => $all_count ) + $this->status_counts; } - foreach ( $this->status_counts as $status_name => $count ) { + // Translated status labels. + $status_labels = ActionScheduler_Store::instance()->get_status_labels(); + $status_labels['all'] = esc_html_x( 'All', 'status labels', 'action-scheduler' ); + $status_labels['past-due'] = esc_html_x( 'Past-due', 'status labels', 'action-scheduler' ); + + foreach ( $this->status_counts as $status_slug => $count ) { if ( 0 === $count ) { continue; } - if ( $status_name === $request_status || ( empty( $request_status ) && 'all' === $status_name ) ) { + if ( $status_slug === $request_status || ( empty( $request_status ) && 'all' === $status_slug ) ) { $status_list_item = '

  • %3$s (%4$d)
  • '; } else { $status_list_item = '
  • %3$s (%4$d)
  • '; } - $status_filter_url = ( 'all' === $status_name ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_name ); + $status_name = isset( $status_labels[ $status_slug ] ) ? $status_labels[ $status_slug ] : ucfirst( $status_slug ); + $status_filter_url = ( 'all' === $status_slug ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_slug ); $status_filter_url = remove_query_arg( array( 'paged', 's' ), $status_filter_url ); - $status_list_items[] = sprintf( $status_list_item, esc_attr( $status_name ), esc_url( $status_filter_url ), esc_html( ucfirst( $status_name ) ), absint( $count ) ); + $status_list_items[] = sprintf( $status_list_item, esc_attr( $status_slug ), esc_url( $status_filter_url ), esc_html( $status_name ), absint( $count ) ); } if ( $status_list_items ) { @@ -707,12 +735,15 @@ protected function display_filter_by_status() { */ protected function display_table() { echo '
    '; - foreach ( $_GET as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( '_' === $key[0] || 'paged' === $key || 'ID' === $key ) { + foreach ( $this->get_request_query_args_to_persist() as $arg ) { + $arg_value = isset( $_GET[ $arg ] ) ? sanitize_text_field( wp_unslash( $_GET[ $arg ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! $arg_value ) { continue; } - echo ''; + + echo ''; } + if ( ! empty( $this->search_by ) ) { echo $this->search_box( $this->get_search_box_button_text(), 'plugin' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php similarity index 71% rename from inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php rename to dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php index 3440f0016c..371917e3ca 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php +++ b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php @@ -44,34 +44,60 @@ public function __construct( ActionScheduler_Store $store = null, ActionSchedule * Process an individual action. * * @param int $action_id The action ID to process. - * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. */ public function process_action( $action_id, $context = '' ) { - try { - $valid_action = false; - do_action( 'action_scheduler_before_execute', $action_id, $context ); + // Temporarily override the error handler while we process the current action. + set_error_handler( + /** + * Temporary error handler which can catch errors and convert them into exceptions. This facilitates more + * robust error handling across all supported PHP versions. + * + * @throws Exception + * + * @param int $type Error level expressed as an integer. + * @param string $message Error message. + */ + function ( $type, $message ) { + throw new Exception( $message ); + }, + E_USER_ERROR | E_RECOVERABLE_ERROR + ); - if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) { - do_action( 'action_scheduler_execution_ignored', $action_id, $context ); - return; + /* + * The nested try/catch structure is required because we potentially need to convert thrown errors into + * exceptions (and an exception thrown from a catch block cannot be caught by a later catch block in the *same* + * structure). + */ + try { + try { + $valid_action = false; + do_action( 'action_scheduler_before_execute', $action_id, $context ); + + if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) { + do_action( 'action_scheduler_execution_ignored', $action_id, $context ); + return; + } + + $valid_action = true; + do_action( 'action_scheduler_begin_execute', $action_id, $context ); + + $action = $this->store->fetch_action( $action_id ); + $this->store->log_execution( $action_id ); + $action->execute(); + do_action( 'action_scheduler_after_execute', $action_id, $action, $context ); + $this->store->mark_complete( $action_id ); + } catch ( Throwable $e ) { + // Throwable is defined when executing under PHP 7.0 and up. We convert it to an exception, for + // compatibility with ActionScheduler_Logger. + throw new Exception( $e->getMessage(), $e->getCode(), $e ); } - - $valid_action = true; - do_action( 'action_scheduler_begin_execute', $action_id, $context ); - - $action = $this->store->fetch_action( $action_id ); - $this->store->log_execution( $action_id ); - $action->execute(); - do_action( 'action_scheduler_after_execute', $action_id, $action, $context ); - $this->store->mark_complete( $action_id ); } catch ( Exception $e ) { - if ( $valid_action ) { - $this->store->mark_failure( $action_id ); - do_action( 'action_scheduler_failed_execution', $action_id, $e, $context ); - } else { - do_action( 'action_scheduler_failed_validation', $action_id, $e, $context ); - } + // This catch block exists for compatibility with PHP 5.6. + $this->handle_action_error( $action_id, $e, $context, $valid_action ); + } finally { + restore_error_handler(); } if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) && $action->get_schedule()->is_recurring() ) { @@ -79,6 +105,39 @@ public function process_action( $action_id, $context = '' ) { } } + /** + * Marks actions as either having failed execution or failed validation, as appropriate. + * + * @param int $action_id Action ID. + * @param Exception $e Exception instance. + * @param string $context Execution context. + * @param bool $valid_action If the action is valid. + * + * @return void + */ + private function handle_action_error( $action_id, $e, $context, $valid_action ) { + if ( $valid_action ) { + $this->store->mark_failure( $action_id ); + /** + * Runs when action execution fails. + * + * @param int $action_id Action ID. + * @param Exception $e Exception instance. + * @param string $context Execution context. + */ + do_action( 'action_scheduler_failed_execution', $action_id, $e, $context ); + } else { + /** + * Runs when action validation fails. + * + * @param int $action_id Action ID. + * @param Exception $e Exception instance. + * @param string $context Execution context. + */ + do_action( 'action_scheduler_failed_validation', $action_id, $e, $context ); + } + } + /** * Schedule the next instance of the action if necessary. * @@ -143,12 +202,22 @@ private function recurring_action_is_consistently_failing( ActionScheduler_Actio return false; } - // Now let's fetch the first action (having the same hook) of *any status*ithin the same window. + // Now let's fetch the first action (having the same hook) of *any status* within the same window. unset( $query_args['status'] ); $first_action_id_with_the_same_hook = $this->store->query_actions( $query_args ); - // If the IDs match, then actions for this hook must be consistently failing. - return $first_action_id_with_the_same_hook === $first_failing_action_id; + /** + * If a recurring action is assessed as consistently failing, it will not be rescheduled. This hook provides a + * way to observe and optionally override that assessment. + * + * @param bool $is_consistently_failing If the action is considered to be consistently failing. + * @param ActionScheduler_Action $action The action being assessed. + */ + return (bool) apply_filters( + 'action_scheduler_recurring_action_is_consistently_failing', + $first_action_id_with_the_same_hook === $first_failing_action_id, + $action + ); } /** @@ -295,7 +364,7 @@ protected function batch_limits_exceeded( $processed_actions ) { * Process actions in the queue. * * @author Jeremy Pry - * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. * @return int The number of actions processed. */ diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php similarity index 97% rename from inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php rename to dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php index 131d4757d8..0ed8bc8e50 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php +++ b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php @@ -25,7 +25,7 @@ abstract class ActionScheduler_Abstract_RecurringSchedule extends ActionSchedule protected $first_timestamp = NULL; /** - * The recurrance between each time an action is run using this schedule. + * The recurrence between each time an action is run using this schedule. * Used to calculate the start date & time. Can be a number of seconds, in the * case of ActionScheduler_IntervalSchedule, or a cron expression, as in the * case of ActionScheduler_CronSchedule. Or something else. @@ -36,7 +36,7 @@ abstract class ActionScheduler_Abstract_RecurringSchedule extends ActionSchedule /** * @param DateTime $date The date & time to run the action. - * @param mixed $recurrence The data used to determine the schedule's recurrance. + * @param mixed $recurrence The data used to determine the schedule's recurrence. * @param DateTime|null $first (Optional) The date & time the first instance of this interval schedule ran. Default null, meaning this is the first instance. */ public function __construct( DateTime $date, $recurrence, DateTime $first = null ) { diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php rename to dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php similarity index 87% rename from inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php rename to dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php index 2334fda10e..3fd259ea71 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php +++ b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php @@ -25,7 +25,7 @@ abstract class ActionScheduler_Abstract_Schema { /** * @var array Names of tables that will be registered by this class. */ - protected $tables = []; + protected $tables = array(); /** * Can optionally be used by concrete classes to carry out additional initialization work @@ -90,10 +90,10 @@ private function schema_update_required() { $plugin_option_name = 'schema-'; switch ( static::class ) { - case 'ActionScheduler_StoreSchema' : + case 'ActionScheduler_StoreSchema': $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Store_Table_Maker'; break; - case 'ActionScheduler_LoggerSchema' : + case 'ActionScheduler_LoggerSchema': $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Logger_Table_Maker'; break; } @@ -129,7 +129,7 @@ private function mark_schema_update_complete() { * @return void */ private function update_table( $table ) { - require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $definition = $this->get_table_definition( $table ); if ( $definition ) { $updated = dbDelta( $definition ); @@ -148,7 +148,7 @@ private function update_table( $table ) { * table prefix for the current blog */ protected function get_full_table_name( $table ) { - return $GLOBALS[ 'wpdb' ]->prefix . $table; + return $GLOBALS['wpdb']->prefix . $table; } /** @@ -159,14 +159,19 @@ protected function get_full_table_name( $table ) { public function tables_exist() { global $wpdb; - $existing_tables = $wpdb->get_col( 'SHOW TABLES' ); - $expected_tables = array_map( - function ( $table_name ) use ( $wpdb ) { - return $wpdb->prefix . $table_name; - }, - $this->tables - ); + $tables_exist = true; - return count( array_intersect( $existing_tables, $expected_tables ) ) === count( $expected_tables ); + foreach ( $this->tables as $table_name ) { + $table_name = $wpdb->prefix . $table_name; + $pattern = str_replace( '_', '\\_', $table_name ); + $existing_table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pattern ) ); + + if ( $existing_table !== $table_name ) { + $tables_exist = false; + break; + } + } + + return $tables_exist; } } diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php similarity index 93% rename from inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php rename to dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php index 86e8528512..e388a58faf 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php +++ b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php @@ -26,6 +26,8 @@ public function is_locked( $lock_type ) { /** * Set a lock. * + * To prevent race conditions, implementations should avoid setting the lock if the lock is already held. + * * @param string $lock_type A string to identify different lock types. * @return bool */ diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Logger.php b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Logger.php similarity index 97% rename from inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Logger.php rename to dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Logger.php index c3afd04b63..0627251cbb 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Logger.php +++ b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Logger.php @@ -135,7 +135,7 @@ public function log_ignored_action( $action_id, $context = '' ) { /** * @param string $action_id - * @param Exception|NULL $exception The exception which occured when fetching the action. NULL by default for backward compatibility. + * @param Exception|NULL $exception The exception which occurred when fetching the action. NULL by default for backward compatibility. * * @return ActionScheduler_LogEntry[] */ diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Store.php b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Store.php similarity index 97% rename from inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Store.php rename to dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Store.php index a555293325..421f5a6716 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Store.php +++ b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Store.php @@ -248,7 +248,7 @@ protected function validate_sql_comparator( $comparison_operator ) { } /** - * Get the time MySQL formated date/time string for an action's (next) scheduled date. + * Get the time MySQL formatted date/time string for an action's (next) scheduled date. * * @param ActionScheduler_Action $action * @param DateTime $scheduled_date (optional) @@ -265,7 +265,7 @@ protected function get_scheduled_date_string( ActionScheduler_Action $action, Da } /** - * Get the time MySQL formated date/time string for an action's (next) scheduled date. + * Get the time MySQL formatted date/time string for an action's (next) scheduled date. * * @param ActionScheduler_Action $action * @param DateTime $scheduled_date (optional) @@ -325,7 +325,8 @@ protected function validate_schedule( $schedule, $action_id ) { * @throws InvalidArgumentException When json encoded args is too long. */ protected function validate_action( ActionScheduler_Action $action ) { - if ( strlen( json_encode( $action->get_args() ) ) > static::$max_args_length ) { + if ( strlen( wp_json_encode( $action->get_args() ) ) > static::$max_args_length ) { + // translators: %d is a number (maximum length of action arguments). throw new InvalidArgumentException( sprintf( __( 'ActionScheduler_Action::$args too long. To ensure the args column can be indexed, action args should not be more than %d characters when encoded as JSON.', 'action-scheduler' ), static::$max_args_length ) ); } } @@ -347,7 +348,7 @@ public function cancel_actions_by_hook( $hook ) { 'hook' => $hook, 'status' => self::STATUS_PENDING, 'per_page' => 1000, - 'orderby' => 'action_id', + 'orderby' => 'none', ) ); @@ -372,7 +373,7 @@ public function cancel_actions_by_group( $group ) { 'group' => $group, 'status' => self::STATUS_PENDING, 'per_page' => 1000, - 'orderby' => 'action_id', + 'orderby' => 'none', ) ); diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php similarity index 92% rename from inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php rename to dependencies/ActionScheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php index fd01449412..36529181dc 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php +++ b/dependencies/ActionScheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php @@ -69,7 +69,7 @@ protected static function get_local_timezone_string( $reset = false ) { // Last try, guess timezone string manually. foreach ( timezone_abbreviations_list() as $abbr ) { foreach ( $abbr as $city ) { - if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { + if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- we are actually interested in the runtime timezone. return $city['timezone_id']; } } @@ -122,7 +122,7 @@ public static function get_local_timezone( $reset = FALSE ) { // Try mapping to the first abbreviation we can find. if ( false === $tzstring ) { - $is_dst = date( 'I' ); + $is_dst = date( 'I' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- we are actually interested in the runtime timezone. foreach ( timezone_abbreviations_list() as $abbr ) { foreach ( $abbr as $city ) { if ( $city['dst'] == $is_dst && $city['offset'] == $gmt_offset ) { diff --git a/inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_Action.php b/dependencies/ActionScheduler/classes/actions/ActionScheduler_Action.php similarity index 68% rename from inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_Action.php rename to dependencies/ActionScheduler/classes/actions/ActionScheduler_Action.php index f538f506b5..ddf33d5d99 100644 --- a/inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_Action.php +++ b/dependencies/ActionScheduler/classes/actions/ActionScheduler_Action.php @@ -10,6 +10,19 @@ class ActionScheduler_Action { protected $schedule = NULL; protected $group = ''; + /** + * Priorities are conceptually similar to those used for regular WordPress actions. + * Like those, a lower priority takes precedence over a higher priority and the default + * is 10. + * + * Unlike regular WordPress actions, the priority of a scheduled action is strictly an + * integer and should be kept within the bounds 0-255 (anything outside the bounds will + * be brought back into the acceptable range). + * + * @var int + */ + protected $priority = 10; + public function __construct( $hook, array $args = array(), ActionScheduler_Schedule $schedule = NULL, $group = '' ) { $schedule = empty( $schedule ) ? new ActionScheduler_NullSchedule() : $schedule; $this->set_hook($hook); @@ -93,4 +106,30 @@ public function get_group() { public function is_finished() { return FALSE; } + + /** + * Sets the priority of the action. + * + * @param int $priority Priority level (lower is higher priority). Should be in the range 0-255. + * + * @return void + */ + public function set_priority( $priority ) { + if ( $priority < 0 ) { + $priority = 0; + } elseif ( $priority > 255 ) { + $priority = 255; + } + + $this->priority = (int) $priority; + } + + /** + * Gets the action priority. + * + * @return int + */ + public function get_priority() { + return $this->priority; + } } diff --git a/inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_CanceledAction.php b/dependencies/ActionScheduler/classes/actions/ActionScheduler_CanceledAction.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_CanceledAction.php rename to dependencies/ActionScheduler/classes/actions/ActionScheduler_CanceledAction.php diff --git a/inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_FinishedAction.php b/dependencies/ActionScheduler/classes/actions/ActionScheduler_FinishedAction.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_FinishedAction.php rename to dependencies/ActionScheduler/classes/actions/ActionScheduler_FinishedAction.php diff --git a/inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_NullAction.php b/dependencies/ActionScheduler/classes/actions/ActionScheduler_NullAction.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_NullAction.php rename to dependencies/ActionScheduler/classes/actions/ActionScheduler_NullAction.php diff --git a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php similarity index 98% rename from inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php rename to dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php index 37bfd0d44e..d285c8d270 100644 --- a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php +++ b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php @@ -82,7 +82,7 @@ private function create_entry_from_db_record( $record ) { } /** - * Retrieve the an action's log entries from the database. + * Retrieve an action's log entries from the database. * * @param int $action_id Action ID. * diff --git a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php similarity index 85% rename from inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php rename to dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php index 5009454f74..e19711ebd6 100644 --- a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php +++ b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php @@ -25,6 +25,13 @@ class ActionScheduler_DBStore extends ActionScheduler_Store { /** @var int */ protected static $max_index_length = 191; + /** @var array List of claim filters. */ + protected $claim_filters = [ + 'group' => '', + 'hooks' => '', + 'exclude-groups' => '', + ]; + /** * Initialize the data store * @@ -84,7 +91,8 @@ private function save_action_to_db( ActionScheduler_Action $action, DateTime $da 'scheduled_date_gmt' => $this->get_scheduled_date_string( $action, $date ), 'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ), 'schedule' => serialize( $action->get_schedule() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize - 'group_id' => $this->get_group_id( $action->get_group() ), + 'group_id' => current( $this->get_group_ids( $action->get_group() ) ), + 'priority' => $action->get_priority(), ); $args = wp_json_encode( $action->get_args() ); @@ -138,7 +146,7 @@ private function build_insert_sql( array $data, $unique ) { $column_sql = '`' . implode( '`, `', $columns ) . '`'; $placeholder_sql = implode( ', ', $placeholders ); $where_clause = $this->build_where_clause_for_insert( $data, $table_name, $unique ); - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $column_sql and $where_clause are already prepared. $placeholder_sql is hardcoded. + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $column_sql and $where_clause are already prepared. $placeholder_sql is hardcoded. $insert_query = $wpdb->prepare( " INSERT INTO $table_name ( $column_sql ) @@ -172,7 +180,8 @@ private function build_where_clause_for_insert( $data, $table_name, $unique ) { ActionScheduler_Store::STATUS_RUNNING, ); $pending_status_placeholders = implode( ', ', array_fill( 0, count( $pending_statuses ), '%s' ) ); - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $pending_status_placeholders is hardcoded. + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $pending_status_placeholders is hardcoded. $where_clause = $wpdb->prepare( " SELECT action_id FROM $table_name @@ -242,23 +251,35 @@ protected function get_args_for_query( $args ) { /** * Get a group's ID based on its name/slug. * - * @param string $slug The string name of a group. - * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group. + * @param string|array $slugs The string name of a group, or names for several groups. + * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group. * - * @return int The group's ID, if it exists or is created, or 0 if it does not exist and is not created. + * @return array The group IDs, if they exist or were successfully created. May be empty. */ - protected function get_group_id( $slug, $create_if_not_exists = true ) { - if ( empty( $slug ) ) { - return 0; + protected function get_group_ids( $slugs, $create_if_not_exists = true ) { + $slugs = (array) $slugs; + $group_ids = array(); + + if ( empty( $slugs ) ) { + return array(); } + /** @var \wpdb $wpdb */ global $wpdb; - $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) ); - if ( empty( $group_id ) && $create_if_not_exists ) { - $group_id = $this->create_group( $slug ); + + foreach ( $slugs as $slug ) { + $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) ); + + if ( empty( $group_id ) && $create_if_not_exists ) { + $group_id = $this->create_group( $slug ); + } + + if ( $group_id ) { + $group_ids[] = $group_id; + } } - return $group_id; + return $group_ids; } /** @@ -355,7 +376,7 @@ protected function make_action_from_db_record( $data ) { } $group = $data->group ? $data->group : ''; - return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group ); + return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group, $data->priority ); } /** @@ -459,7 +480,7 @@ protected function get_query_actions_sql( array $query, $select_or_count = 'sele case 'like': foreach ( $query['args'] as $key => $value ) { $sql .= ' AND a.args LIKE %s'; - $json_partial = $wpdb->esc_like( trim( json_encode( array( $key => $value ) ), '{}' ) ); + $json_partial = $wpdb->esc_like( trim( wp_json_encode( array( $key => $value ) ), '{}' ) ); $sql_params[] = "%{$json_partial}%"; } break; @@ -629,7 +650,7 @@ public function cancel_action( $action_id ) { ); if ( false === $updated ) { /* translators: %s: action ID */ - throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to cancel this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } do_action( 'action_scheduler_canceled_action', $action_id ); } @@ -684,7 +705,7 @@ protected function bulk_cancel_actions( $query_args ) { array( 'per_page' => 1000, 'status' => self::STATUS_PENDING, - 'orderby' => 'action_id', + 'orderby' => 'none', ) ); @@ -721,7 +742,8 @@ public function delete_action( $action_id ) { global $wpdb; $deleted = $wpdb->delete( $wpdb->actionscheduler_actions, array( 'action_id' => $action_id ), array( '%d' ) ); if ( empty( $deleted ) ) { - throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment + /* translators: %s is the action ID */ + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to delete this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } do_action( 'action_scheduler_deleted_action', $action_id ); } @@ -752,7 +774,8 @@ protected function get_date_gmt( $action_id ) { global $wpdb; $record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d", $action_id ) ); if ( empty( $record ) ) { - throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment + /* translators: %s is the action ID */ + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to determine the date of this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } if ( self::STATUS_PENDING === $record->status ) { return as_get_datetime_object( $record->scheduled_date_gmt ); @@ -796,6 +819,33 @@ protected function generate_claim_id() { return $wpdb->insert_id; } + /** + * Set a claim filter. + * + * @param string $filter_name Claim filter name. + * @param mixed $filter_values Values to filter. + * @return void + */ + public function set_claim_filter( $filter_name, $filter_values ) { + if ( isset( $this->claim_filters[ $filter_name ] ) ) { + $this->claim_filters[ $filter_name ] = $filter_values; + } + } + + /** + * Get the claim filter value. + * + * @param string $filter_name Claim filter name. + * @return mixed + */ + public function get_claim_filter( $filter_name ) { + if ( isset( $this->claim_filters[ $filter_name ] ) ) { + return $this->claim_filters[ $filter_name ]; + } + + return ''; + } + /** * Mark actions claimed. * @@ -813,9 +863,8 @@ protected function claim_actions( $claim_id, $limit, \DateTime $before_date = nu /** @var \wpdb $wpdb */ global $wpdb; - $now = as_get_datetime_object(); - $date = is_null( $before_date ) ? $now : clone $before_date; - + $now = as_get_datetime_object(); + $date = is_null( $before_date ) ? $now : clone $before_date; // can't use $wpdb->update() because of the <= condition. $update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s"; $params = array( @@ -824,6 +873,18 @@ protected function claim_actions( $claim_id, $limit, \DateTime $before_date = nu current_time( 'mysql' ), ); + // Set claim filters. + if ( ! empty( $hooks ) ) { + $this->set_claim_filter( 'hooks', $hooks ); + } else { + $hooks = $this->get_claim_filter( 'hooks' ); + } + if ( ! empty( $group ) ) { + $this->set_claim_filter( 'group', $group ); + } else { + $group = $this->get_claim_filter( 'group' ); + } + $where = 'WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s'; $params[] = $date->format( 'Y-m-d H:i:s' ); $params[] = self::STATUS_PENDING; @@ -834,18 +895,33 @@ protected function claim_actions( $claim_id, $limit, \DateTime $before_date = nu $params = array_merge( $params, array_values( $hooks ) ); } - if ( ! empty( $group ) ) { - - $group_id = $this->get_group_id( $group, false ); + $group_operator = 'IN'; + if ( empty( $group ) ) { + $group = $this->get_claim_filter( 'exclude-groups' ); + $group_operator = 'NOT IN'; + } - // throw exception if no matching group found, this matches ActionScheduler_wpPostStore's behaviour. - if ( empty( $group_id ) ) { - /* translators: %s: group name */ - throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) ); + if ( ! empty( $group ) ) { + $group_ids = $this->get_group_ids( $group, false ); + + // throw exception if no matching group(s) found, this matches ActionScheduler_wpPostStore's behaviour. + if ( empty( $group_ids ) ) { + throw new InvalidArgumentException( + sprintf( + /* translators: %s: group name(s) */ + _n( + 'The group "%s" does not exist.', + 'The groups "%s" do not exist.', + is_array( $group ) ? count( $group ) : 1, + 'action-scheduler' + ), + $group + ) + ); } - $where .= ' AND group_id = %d'; - $params[] = $group_id; + $id_list = implode( ',', array_map( 'intval', $group_ids ) ); + $where .= " AND group_id {$group_operator} ( $id_list )"; } /** @@ -855,13 +931,23 @@ protected function claim_actions( $claim_id, $limit, \DateTime $before_date = nu * * @param string $order_by_sql */ - $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY attempts ASC, scheduled_date_gmt ASC, action_id ASC' ); + $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC' ); $params[] = $limit; $sql = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders $rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( false === $rows_affected ) { - throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) ); + $error = empty( $wpdb->last_error ) + ? _x( 'unknown', 'database error', 'action-scheduler' ) + : $wpdb->last_error; + + throw new \RuntimeException( + sprintf( + /* translators: %s database error. */ + __( 'Unable to claim actions. Database error: %s.', 'action-scheduler' ), + $error + ) + ); } return (int) $rows_affected; @@ -912,7 +998,7 @@ public function find_actions_by_claim_id( $claim_id ) { $cut_off = $before_date->format( 'Y-m-d H:i:s' ); $sql = $wpdb->prepare( - "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d", + "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC", $claim_id ); @@ -947,7 +1033,7 @@ public function release_claim( ActionScheduler_ActionClaim $claim ) { $row_updates = 0; if ( count( $action_ids ) > 0 ) { $action_id_string = implode( ',', array_map( 'absint', $action_ids ) ); - $row_updates = $wpdb->query( "UPDATE {$wpdb->actionscheduler_actions} SET claim_id = 0 WHERE action_id IN ({$action_id_string})" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $row_updates = $wpdb->query( "UPDATE {$wpdb->actionscheduler_actions} SET claim_id = 0 WHERE action_id IN ({$action_id_string})" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared } $wpdb->delete( $wpdb->actionscheduler_claims, array( 'claim_id' => $claim->get_id() ), array( '%d' ) ); @@ -955,7 +1041,8 @@ public function release_claim( ActionScheduler_ActionClaim $claim ) { if ( $row_updates < count( $action_ids ) ) { throw new RuntimeException( sprintf( - __( 'Unable to release actions from claim id %d.', 'woocommerce' ), + // translators: %d is an id. + __( 'Unable to release actions from claim id %d.', 'action-scheduler' ), $claim->get_id() ) ); @@ -998,13 +1085,16 @@ public function mark_failure( $action_id ) { array( '%d' ) ); if ( empty( $updated ) ) { - throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment + /* translators: %s is the action ID */ + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having failed. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } } /** * Add execution message to action log. * + * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress'). + * * @param int $action_id Action ID. * * @return void @@ -1015,7 +1105,20 @@ public function log_execution( $action_id ) { $sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d"; $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $status_updated = $wpdb->query( $sql ); + + if ( ! $status_updated ) { + throw new Exception( + sprintf( + /* translators: 1: action ID. 2: status slug. */ + __( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ), + $action_id, + self::STATUS_RUNNING + ) + ); + } } /** @@ -1041,7 +1144,8 @@ public function mark_complete( $action_id ) { array( '%d' ) ); if ( empty( $updated ) ) { - throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment + /* translators: %s is the action ID */ + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having completed. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } /** diff --git a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_HybridStore.php b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_HybridStore.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_HybridStore.php rename to dependencies/ActionScheduler/classes/data-stores/ActionScheduler_HybridStore.php diff --git a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php rename to dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php diff --git a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php similarity index 96% rename from inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php rename to dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php index 7883ca82bf..3971b3395b 100644 --- a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php +++ b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php @@ -512,7 +512,7 @@ public function cancel_action( $action_id ) { $post = get_post( $action_id ); if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { /* translators: %s is the action ID */ - throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to cancel this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } do_action( 'action_scheduler_canceled_action', $action_id ); add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); @@ -531,7 +531,7 @@ public function delete_action( $action_id ) { $post = get_post( $action_id ); if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { /* translators: %s is the action ID */ - throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to delete this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } do_action( 'action_scheduler_deleted_action', $action_id ); @@ -561,7 +561,7 @@ public function get_date_gmt( $action_id ) { $post = get_post( $action_id ); if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { /* translators: %s is the action ID */ - throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to determine the date of this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } if ( 'publish' === $post->post_status ) { return as_get_datetime_object( $post->post_modified_gmt ); @@ -690,7 +690,7 @@ protected function claim_actions( $claim_id, $limit, DateTime $before_date = nul $params[] = $limit; // Run the query and gather results. - $rows_affected = $wpdb->query( $wpdb->prepare( "{$update} {$where} {$order}", $params ) ); // phpcs:ignore // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + $rows_affected = $wpdb->query( $wpdb->prepare( "{$update} {$where} {$order}", $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare if ( false === $rows_affected ) { throw new RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) ); @@ -725,7 +725,7 @@ protected function get_actions_by_group( $group, $limit, DateTime $date ) { 'post_status' => ActionScheduler_Store::STATUS_PENDING, 'has_password' => false, 'posts_per_page' => $limit * 3, - 'suppress_filters' => true, + 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters 'no_found_rows' => true, 'orderby' => array( 'menu_order' => 'ASC', @@ -936,6 +936,8 @@ private function get_post_column( $action_id, $column_name ) { /** * Log Execution. * + * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress'). + * * @param string $action_id Action ID. */ public function log_execution( $action_id ) { @@ -947,7 +949,7 @@ public function log_execution( $action_id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->query( + $status_updated = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET menu_order = menu_order+1, post_status=%s, post_modified_gmt = %s, post_modified = %s WHERE ID = %d AND post_type = %s", self::STATUS_RUNNING, @@ -957,6 +959,17 @@ public function log_execution( $action_id ) { self::POST_TYPE ) ); + + if ( ! $status_updated ) { + throw new Exception( + sprintf( + /* translators: 1: action ID. 2: status slug. */ + __( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ), + $action_id, + self::STATUS_RUNNING + ) + ); + } } /** @@ -971,7 +984,7 @@ public function mark_complete( $action_id ) { $post = get_post( $action_id ); if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { /* translators: %s is the action ID */ - throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having completed. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 ); add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); diff --git a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php rename to dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php diff --git a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php similarity index 97% rename from inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php rename to dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php index 8c63bd0f7a..e1bcf6bead 100644 --- a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php +++ b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php @@ -17,7 +17,7 @@ public function register() { protected function post_type_args() { $args = array( 'label' => __( 'Scheduled Actions', 'action-scheduler' ), - 'description' => __( 'Scheduled actions are hooks triggered on a cetain date and time.', 'action-scheduler' ), + 'description' => __( 'Scheduled actions are hooks triggered on a certain date and time.', 'action-scheduler' ), 'public' => false, 'map_meta_cap' => true, 'hierarchical' => false, diff --git a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php b/dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php rename to dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php diff --git a/inc/Dependencies/ActionScheduler/classes/migration/ActionMigrator.php b/dependencies/ActionScheduler/classes/migration/ActionMigrator.php similarity index 98% rename from inc/Dependencies/ActionScheduler/classes/migration/ActionMigrator.php rename to dependencies/ActionScheduler/classes/migration/ActionMigrator.php index c77d0832cd..1469de02fd 100644 --- a/inc/Dependencies/ActionScheduler/classes/migration/ActionMigrator.php +++ b/dependencies/ActionScheduler/classes/migration/ActionMigrator.php @@ -92,6 +92,7 @@ public function migrate( $source_action_id ) { $test_action = $this->source->fetch_action( $source_action_id ); if ( ! is_a( $test_action, 'ActionScheduler_NullAction' ) ) { + // translators: %s is an action ID. throw new \RuntimeException( sprintf( __( 'Unable to remove source migrated action %s', 'action-scheduler' ), $source_action_id ) ); } do_action( 'action_scheduler/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination ); diff --git a/inc/Dependencies/ActionScheduler/classes/migration/ActionScheduler_DBStoreMigrator.php b/dependencies/ActionScheduler/classes/migration/ActionScheduler_DBStoreMigrator.php similarity index 97% rename from inc/Dependencies/ActionScheduler/classes/migration/ActionScheduler_DBStoreMigrator.php rename to dependencies/ActionScheduler/classes/migration/ActionScheduler_DBStoreMigrator.php index 41c21da256..ae0785d44f 100644 --- a/inc/Dependencies/ActionScheduler/classes/migration/ActionScheduler_DBStoreMigrator.php +++ b/dependencies/ActionScheduler/classes/migration/ActionScheduler_DBStoreMigrator.php @@ -41,6 +41,7 @@ public function save_action( ActionScheduler_Action $action, \DateTime $schedule return $action_id; } catch ( \Exception $e ) { + // translators: %s is an error message. throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 ); } } diff --git a/inc/Dependencies/ActionScheduler/classes/migration/BatchFetcher.php b/dependencies/ActionScheduler/classes/migration/BatchFetcher.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/migration/BatchFetcher.php rename to dependencies/ActionScheduler/classes/migration/BatchFetcher.php diff --git a/inc/Dependencies/ActionScheduler/classes/migration/Config.php b/dependencies/ActionScheduler/classes/migration/Config.php similarity index 98% rename from inc/Dependencies/ActionScheduler/classes/migration/Config.php rename to dependencies/ActionScheduler/classes/migration/Config.php index 50f41ff49a..cf91a5e246 100644 --- a/inc/Dependencies/ActionScheduler/classes/migration/Config.php +++ b/dependencies/ActionScheduler/classes/migration/Config.php @@ -65,7 +65,7 @@ public function set_source_store( Store $store ) { } /** - * Get the configured source loger. + * Get the configured source logger. * * @return ActionScheduler_Logger */ diff --git a/inc/Dependencies/ActionScheduler/classes/migration/Controller.php b/dependencies/ActionScheduler/classes/migration/Controller.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/migration/Controller.php rename to dependencies/ActionScheduler/classes/migration/Controller.php diff --git a/inc/Dependencies/ActionScheduler/classes/migration/DryRun_ActionMigrator.php b/dependencies/ActionScheduler/classes/migration/DryRun_ActionMigrator.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/migration/DryRun_ActionMigrator.php rename to dependencies/ActionScheduler/classes/migration/DryRun_ActionMigrator.php diff --git a/inc/Dependencies/ActionScheduler/classes/migration/DryRun_LogMigrator.php b/dependencies/ActionScheduler/classes/migration/DryRun_LogMigrator.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/migration/DryRun_LogMigrator.php rename to dependencies/ActionScheduler/classes/migration/DryRun_LogMigrator.php diff --git a/inc/Dependencies/ActionScheduler/classes/migration/LogMigrator.php b/dependencies/ActionScheduler/classes/migration/LogMigrator.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/migration/LogMigrator.php rename to dependencies/ActionScheduler/classes/migration/LogMigrator.php diff --git a/inc/Dependencies/ActionScheduler/classes/migration/Runner.php b/dependencies/ActionScheduler/classes/migration/Runner.php similarity index 98% rename from inc/Dependencies/ActionScheduler/classes/migration/Runner.php rename to dependencies/ActionScheduler/classes/migration/Runner.php index 867c5de681..2304a79adc 100644 --- a/inc/Dependencies/ActionScheduler/classes/migration/Runner.php +++ b/dependencies/ActionScheduler/classes/migration/Runner.php @@ -79,7 +79,7 @@ public function run( $batch_size = 10 ) { if ( $this->progress_bar ) { /* translators: %d: amount of actions */ - $this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), number_format_i18n( $batch_size ) ) ); + $this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), $batch_size ) ); $this->progress_bar->set_count( $batch_size ); } diff --git a/inc/Dependencies/ActionScheduler/classes/migration/Scheduler.php b/dependencies/ActionScheduler/classes/migration/Scheduler.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/migration/Scheduler.php rename to dependencies/ActionScheduler/classes/migration/Scheduler.php diff --git a/inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_CanceledSchedule.php b/dependencies/ActionScheduler/classes/schedules/ActionScheduler_CanceledSchedule.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_CanceledSchedule.php rename to dependencies/ActionScheduler/classes/schedules/ActionScheduler_CanceledSchedule.php diff --git a/inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_CronSchedule.php b/dependencies/ActionScheduler/classes/schedules/ActionScheduler_CronSchedule.php similarity index 98% rename from inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_CronSchedule.php rename to dependencies/ActionScheduler/classes/schedules/ActionScheduler_CronSchedule.php index 7859307ac8..bc70c0f24b 100644 --- a/inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_CronSchedule.php +++ b/dependencies/ActionScheduler/classes/schedules/ActionScheduler_CronSchedule.php @@ -58,7 +58,7 @@ public function get_recurrence() { /** * Serialize cron schedules with data required prior to AS 3.0.0 * - * Prior to Action Scheduler 3.0.0, reccuring schedules used different property names to + * Prior to Action Scheduler 3.0.0, recurring schedules used different property names to * refer to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 * aligned properties and property names for better inheritance. To guard against the diff --git a/inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_IntervalSchedule.php b/dependencies/ActionScheduler/classes/schedules/ActionScheduler_IntervalSchedule.php similarity index 97% rename from inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_IntervalSchedule.php rename to dependencies/ActionScheduler/classes/schedules/ActionScheduler_IntervalSchedule.php index 11a591e80b..ba4d508dff 100644 --- a/inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_IntervalSchedule.php +++ b/dependencies/ActionScheduler/classes/schedules/ActionScheduler_IntervalSchedule.php @@ -38,7 +38,7 @@ public function interval_in_seconds() { /** * Serialize interval schedules with data required prior to AS 3.0.0 * - * Prior to Action Scheduler 3.0.0, reccuring schedules used different property names to + * Prior to Action Scheduler 3.0.0, recurring schedules used different property names to * refer to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 * aligned properties and property names for better inheritance. To guard against the diff --git a/inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_NullSchedule.php b/dependencies/ActionScheduler/classes/schedules/ActionScheduler_NullSchedule.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_NullSchedule.php rename to dependencies/ActionScheduler/classes/schedules/ActionScheduler_NullSchedule.php diff --git a/inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_Schedule.php b/dependencies/ActionScheduler/classes/schedules/ActionScheduler_Schedule.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_Schedule.php rename to dependencies/ActionScheduler/classes/schedules/ActionScheduler_Schedule.php diff --git a/inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_SimpleSchedule.php b/dependencies/ActionScheduler/classes/schedules/ActionScheduler_SimpleSchedule.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/schedules/ActionScheduler_SimpleSchedule.php rename to dependencies/ActionScheduler/classes/schedules/ActionScheduler_SimpleSchedule.php diff --git a/inc/Dependencies/ActionScheduler/classes/schema/ActionScheduler_LoggerSchema.php b/dependencies/ActionScheduler/classes/schema/ActionScheduler_LoggerSchema.php similarity index 100% rename from inc/Dependencies/ActionScheduler/classes/schema/ActionScheduler_LoggerSchema.php rename to dependencies/ActionScheduler/classes/schema/ActionScheduler_LoggerSchema.php diff --git a/inc/Dependencies/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php b/dependencies/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php similarity index 91% rename from inc/Dependencies/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php rename to dependencies/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php index d52f27f6fc..a0bd8cb20f 100644 --- a/inc/Dependencies/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php +++ b/dependencies/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php @@ -16,7 +16,7 @@ class ActionScheduler_StoreSchema extends ActionScheduler_Abstract_Schema { /** * @var int Increment this value to trigger a schema update. */ - protected $schema_version = 6; + protected $schema_version = 7; public function __construct() { $this->tables = [ @@ -38,6 +38,7 @@ protected function get_table_definition( $table ) { $table_name = $wpdb->$table; $charset_collate = $wpdb->get_charset_collate(); $max_index_length = 191; // @see wp_get_db_schema() + $hook_status_scheduled_date_gmt_max_index_length = $max_index_length - 20 - 8; // - status, - scheduled_date_gmt $default_date = self::DEFAULT_DATE; switch ( $table ) { @@ -49,6 +50,7 @@ protected function get_table_definition( $table ) { status varchar(20) NOT NULL, scheduled_date_gmt datetime NULL default '{$default_date}', scheduled_date_local datetime NULL default '{$default_date}', + priority tinyint unsigned NOT NULL default '10', args varchar($max_index_length), schedule longtext, group_id bigint(20) unsigned NOT NULL default '0', @@ -58,8 +60,8 @@ protected function get_table_definition( $table ) { claim_id bigint(20) unsigned NOT NULL default '0', extended_args varchar(8000) DEFAULT NULL, PRIMARY KEY (action_id), - KEY hook (hook($max_index_length)), - KEY status (status), + KEY hook_status_scheduled_date_gmt (hook($hook_status_scheduled_date_gmt_max_index_length), status, scheduled_date_gmt), + KEY status_scheduled_date_gmt (status, scheduled_date_gmt), KEY scheduled_date_gmt (scheduled_date_gmt), KEY args (args($max_index_length)), KEY group_id (group_id), diff --git a/inc/Dependencies/ActionScheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php b/dependencies/ActionScheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php similarity index 100% rename from inc/Dependencies/ActionScheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php rename to dependencies/ActionScheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php diff --git a/inc/Dependencies/ActionScheduler/deprecated/ActionScheduler_AdminView_Deprecated.php b/dependencies/ActionScheduler/deprecated/ActionScheduler_AdminView_Deprecated.php similarity index 99% rename from inc/Dependencies/ActionScheduler/deprecated/ActionScheduler_AdminView_Deprecated.php rename to dependencies/ActionScheduler/deprecated/ActionScheduler_AdminView_Deprecated.php index 69b46d7bd4..62d964e0ff 100644 --- a/inc/Dependencies/ActionScheduler/deprecated/ActionScheduler_AdminView_Deprecated.php +++ b/dependencies/ActionScheduler/deprecated/ActionScheduler_AdminView_Deprecated.php @@ -100,7 +100,7 @@ public static function maybe_execute_action() { * Convert an interval of seconds into a two part human friendly string. * * The WordPress human_time_diff() function only calculates the time difference to one degree, meaning - * even if an action is 1 day and 11 hours away, it will display "1 day". This funciton goes one step + * even if an action is 1 day and 11 hours away, it will display "1 day". This function goes one step * further to display two degrees of accuracy. * * Based on Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/ diff --git a/inc/Dependencies/ActionScheduler/deprecated/ActionScheduler_Schedule_Deprecated.php b/dependencies/ActionScheduler/deprecated/ActionScheduler_Schedule_Deprecated.php similarity index 100% rename from inc/Dependencies/ActionScheduler/deprecated/ActionScheduler_Schedule_Deprecated.php rename to dependencies/ActionScheduler/deprecated/ActionScheduler_Schedule_Deprecated.php diff --git a/inc/Dependencies/ActionScheduler/deprecated/ActionScheduler_Store_Deprecated.php b/dependencies/ActionScheduler/deprecated/ActionScheduler_Store_Deprecated.php similarity index 100% rename from inc/Dependencies/ActionScheduler/deprecated/ActionScheduler_Store_Deprecated.php rename to dependencies/ActionScheduler/deprecated/ActionScheduler_Store_Deprecated.php diff --git a/inc/Dependencies/ActionScheduler/deprecated/functions.php b/dependencies/ActionScheduler/deprecated/functions.php similarity index 100% rename from inc/Dependencies/ActionScheduler/deprecated/functions.php rename to dependencies/ActionScheduler/deprecated/functions.php diff --git a/inc/Dependencies/ActionScheduler/functions.php b/dependencies/ActionScheduler/functions.php similarity index 78% rename from inc/Dependencies/ActionScheduler/functions.php rename to dependencies/ActionScheduler/functions.php index 09ef353d9a..66bc54ff53 100644 --- a/inc/Dependencies/ActionScheduler/functions.php +++ b/dependencies/ActionScheduler/functions.php @@ -11,11 +11,12 @@ * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. - * @param bool $unique Whether the action should be unique. + * @param bool $unique Whether the action should be unique. It will not be scheduled if another pending or running action has the same hook and group parameters. + * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * - * @return int The action ID. + * @return int The action ID. Zero if there was an error scheduling the action. */ -function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = false ) { +function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } @@ -33,13 +34,23 @@ function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. + * @param int $priority Action priority. */ - $pre = apply_filters( 'pre_as_enqueue_async_action', null, $hook, $args, $group ); + $pre = apply_filters( 'pre_as_enqueue_async_action', null, $hook, $args, $group, $priority ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } - return ActionScheduler::factory()->async_unique( $hook, $args, $group, $unique ); + return ActionScheduler::factory()->create( + array( + 'type' => 'async', + 'hook' => $hook, + 'arguments' => $args, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** @@ -49,11 +60,12 @@ function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. - * @param bool $unique Whether the action should be unique. + * @param bool $unique Whether the action should be unique. It will not be scheduled if another pending or running action has the same hook and group parameters. + * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * - * @return int The action ID. + * @return int The action ID. Zero if there was an error scheduling the action. */ -function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false ) { +function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } @@ -72,13 +84,24 @@ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. + * @param int $priorities Action priority. */ - $pre = apply_filters( 'pre_as_schedule_single_action', null, $timestamp, $hook, $args, $group ); + $pre = apply_filters( 'pre_as_schedule_single_action', null, $timestamp, $hook, $args, $group, $priority ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } - return ActionScheduler::factory()->single_unique( $hook, $args, $timestamp, $group, $unique ); + return ActionScheduler::factory()->create( + array( + 'type' => 'single', + 'hook' => $hook, + 'arguments' => $args, + 'when' => $timestamp, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** @@ -89,15 +112,35 @@ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. - * @param bool $unique Whether the action should be unique. + * @param bool $unique Whether the action should be unique. It will not be scheduled if another pending or running action has the same hook and group parameters. + * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * - * @return int The action ID. + * @return int The action ID. Zero if there was an error scheduling the action. */ -function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '', $unique = false ) { +function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } + $interval = (int) $interval_in_seconds; + + // We expect an integer and allow it to be passed using float and string types, but otherwise + // should reject unexpected values. + if ( ! is_numeric( $interval_in_seconds ) || $interval_in_seconds != $interval ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: provided value 2: provided type. */ + esc_html__( 'An integer was expected but "%1$s" (%2$s) was received.', 'action-scheduler' ), + esc_html( $interval_in_seconds ), + esc_html( gettype( $interval_in_seconds ) ) + ), + '3.6.0' + ); + + return 0; + } + /** * Provides an opportunity to short-circuit the default process for enqueuing recurring * actions. @@ -113,13 +156,25 @@ function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. + * @param int $priority Action priority. */ - $pre = apply_filters( 'pre_as_schedule_recurring_action', null, $timestamp, $interval_in_seconds, $hook, $args, $group ); + $pre = apply_filters( 'pre_as_schedule_recurring_action', null, $timestamp, $interval_in_seconds, $hook, $args, $group, $priority ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } - return ActionScheduler::factory()->recurring_unique( $hook, $args, $timestamp, $interval_in_seconds, $group, $unique ); + return ActionScheduler::factory()->create( + array( + 'type' => 'recurring', + 'hook' => $hook, + 'arguments' => $args, + 'when' => $timestamp, + 'pattern' => $interval_in_seconds, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** @@ -142,11 +197,12 @@ function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. - * @param bool $unique Whether the action should be unique. + * @param bool $unique Whether the action should be unique. It will not be scheduled if another pending or running action has the same hook and group parameters. + * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * - * @return int The action ID. + * @return int The action ID. Zero if there was an error scheduling the action. */ -function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '', $unique = false ) { +function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } @@ -166,13 +222,25 @@ function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. + * @param int $priority Action priority. */ - $pre = apply_filters( 'pre_as_schedule_cron_action', null, $timestamp, $schedule, $hook, $args, $group ); + $pre = apply_filters( 'pre_as_schedule_cron_action', null, $timestamp, $schedule, $hook, $args, $group, $priority ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } - return ActionScheduler::factory()->cron_unique( $hook, $args, $timestamp, $schedule, $group, $unique ); + return ActionScheduler::factory()->create( + array( + 'type' => 'cron', + 'hook' => $hook, + 'arguments' => $args, + 'when' => $timestamp, + 'pattern' => $schedule, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** @@ -215,9 +283,10 @@ function as_unschedule_action( $hook, $args = array(), $group = '' ) { ActionScheduler::logger()->log( $action_id, sprintf( - /* translators: %s is the name of the hook to be cancelled. */ - __( 'Caught exception while cancelling action: %s', 'action-scheduler' ), - esc_attr( $hook ) + /* translators: %1$s is the name of the hook to be cancelled, %2$s is the exception message. */ + __( 'Caught exception while cancelling action "%1$s": %2$s', 'action-scheduler' ), + $hook, + $exception->getMessage() ) ); diff --git a/inc/Dependencies/ActionScheduler/lib/WP_Async_Request.php b/dependencies/ActionScheduler/lib/WP_Async_Request.php similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/WP_Async_Request.php rename to dependencies/ActionScheduler/lib/WP_Async_Request.php diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression.php b/dependencies/ActionScheduler/lib/cron-expression/CronExpression.php similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression.php rename to dependencies/ActionScheduler/lib/cron-expression/CronExpression.php diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_AbstractField.php b/dependencies/ActionScheduler/lib/cron-expression/CronExpression_AbstractField.php similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_AbstractField.php rename to dependencies/ActionScheduler/lib/cron-expression/CronExpression_AbstractField.php diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_DayOfMonthField.php b/dependencies/ActionScheduler/lib/cron-expression/CronExpression_DayOfMonthField.php similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_DayOfMonthField.php rename to dependencies/ActionScheduler/lib/cron-expression/CronExpression_DayOfMonthField.php diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_DayOfWeekField.php b/dependencies/ActionScheduler/lib/cron-expression/CronExpression_DayOfWeekField.php similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_DayOfWeekField.php rename to dependencies/ActionScheduler/lib/cron-expression/CronExpression_DayOfWeekField.php diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_FieldFactory.php b/dependencies/ActionScheduler/lib/cron-expression/CronExpression_FieldFactory.php similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_FieldFactory.php rename to dependencies/ActionScheduler/lib/cron-expression/CronExpression_FieldFactory.php diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_FieldInterface.php b/dependencies/ActionScheduler/lib/cron-expression/CronExpression_FieldInterface.php similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_FieldInterface.php rename to dependencies/ActionScheduler/lib/cron-expression/CronExpression_FieldInterface.php diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_HoursField.php b/dependencies/ActionScheduler/lib/cron-expression/CronExpression_HoursField.php similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_HoursField.php rename to dependencies/ActionScheduler/lib/cron-expression/CronExpression_HoursField.php diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_MinutesField.php b/dependencies/ActionScheduler/lib/cron-expression/CronExpression_MinutesField.php similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_MinutesField.php rename to dependencies/ActionScheduler/lib/cron-expression/CronExpression_MinutesField.php diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_MonthField.php b/dependencies/ActionScheduler/lib/cron-expression/CronExpression_MonthField.php similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_MonthField.php rename to dependencies/ActionScheduler/lib/cron-expression/CronExpression_MonthField.php diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_YearField.php b/dependencies/ActionScheduler/lib/cron-expression/CronExpression_YearField.php similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/CronExpression_YearField.php rename to dependencies/ActionScheduler/lib/cron-expression/CronExpression_YearField.php diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/LICENSE b/dependencies/ActionScheduler/lib/cron-expression/LICENSE similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/LICENSE rename to dependencies/ActionScheduler/lib/cron-expression/LICENSE diff --git a/inc/Dependencies/ActionScheduler/lib/cron-expression/README.md b/dependencies/ActionScheduler/lib/cron-expression/README.md similarity index 100% rename from inc/Dependencies/ActionScheduler/lib/cron-expression/README.md rename to dependencies/ActionScheduler/lib/cron-expression/README.md diff --git a/inc/Dependencies/ActionScheduler/license.txt b/dependencies/ActionScheduler/license.txt similarity index 100% rename from inc/Dependencies/ActionScheduler/license.txt rename to dependencies/ActionScheduler/license.txt diff --git a/inc/Dependencies/ActionScheduler/readme.txt b/dependencies/ActionScheduler/readme.txt similarity index 66% rename from inc/Dependencies/ActionScheduler/readme.txt rename to dependencies/ActionScheduler/readme.txt index 3518b15444..d2ee033bf3 100644 --- a/inc/Dependencies/ActionScheduler/readme.txt +++ b/dependencies/ActionScheduler/readme.txt @@ -1,11 +1,11 @@ === Action Scheduler === Contributors: Automattic, wpmuguru, claudiosanches, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, royho, barryhughes-1 Tags: scheduler, cron -Requires at least: 5.2 -Tested up to: 6.0 -Stable tag: 3.5.4 +Stable tag: 3.8.1 License: GPLv3 -Requires PHP: 5.6 +Requires at least: 6.3 +Tested up to: 6.5 +Requires PHP: 7.0 Action Scheduler - Job Queue for WordPress @@ -13,7 +13,7 @@ Action Scheduler - Job Queue for WordPress Action Scheduler is a scalable, traceable job queue for background processing large sets of actions in WordPress. It's specially designed to be distributed in WordPress plugins. -Action Scheduler works by triggering an action hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occassions. +Action Scheduler works by triggering an action hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occasions. Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook. @@ -47,6 +47,88 @@ Collaboration is cool. We'd love to work with you to improve Action Scheduler. [ == Changelog == += 3.8.1 - 2024-06-20 = +* Fix typos. +* Improve the messaging in our unidentified action exceptions. + += 3.8.0 - 2024-05-22 = +* Documentation - Fixed typos in perf.md. +* Update - We now require WordPress 6.3 or higher. +* Update - We now require PHP 7.0 or higher. + += 3.7.4 - 2024-04-05 = +* Give a clear description of how the $unique parameter works. +* Preserve the tab field if set. +* Tweak - WP 6.5 compatibility. + += 3.7.3 - 2024-03-20 = +* Do not iterate over all of GET when building form in list table. +* Fix a few issues reported by PCP (Plugin Check Plugin). +* Try to save actions as unique even when the store doesn't support it. +* Tweak - WP 6.4 compatibility. +* Update "Tested up to" tag to WordPress 6.5. +* update version in package-lock.json. + += 3.7.2 - 2024-02-14 = +* No longer user variables in `_n()` translation function. + += 3.7.1 - 2023-12-13 = +* update semver to 5.7.2 because of a security vulnerability in 5.7.1. + += 3.7.0 - 2023-11-20 = +* Important: starting with this release, Action Scheduler follows an L-2 version policy (WordPress, and consequently PHP). +* Add extended indexes for hook_status_scheduled_date_gmt and status_scheduled_date_gmt. +* Catch and log exceptions thrown when actions can't be created, e.g. under a corrupt database schema. +* Tweak - WP 6.4 compatibility. +* Update unit tests for upcoming dependency version policy. +* make sure hook action_scheduler_failed_execution can access original exception object. +* mention dependency version policy in usage.md. + += 3.6.4 - 2023-10-11 = +* Performance improvements when bulk cancelling actions. +* Dev-related fixes. + += 3.6.3 - 2023-09-13 = +* Use `_doing_it_wrong` in initialization check. + += 3.6.2 - 2023-08-09 = +* Add guidance about passing arguments. +* Atomic option locking. +* Improve bulk delete handling. +* Include database error in the exception message. +* Tweak - WP 6.3 compatibility. + += 3.6.1 - 2023-06-14 = +* Document new optional `$priority` arg for various API functions. +* Document the new `--exclude-groups` WP CLI option. +* Document the new `action_scheduler_init` hook. +* Ensure actions within each claim are executed in the expected order. +* Fix incorrect text domain. +* Remove SHOW TABLES usage when checking if tables exist. + += 3.6.0 - 2023-05-10 = +* Add $unique parameter to function signatures. +* Add a cast-to-int for extra safety before forming new DateTime object. +* Add a hook allowing exceptions for consistently failing recurring actions. +* Add action priorities. +* Add init hook. +* Always raise the time limit. +* Bump minimatch from 3.0.4 to 3.0.8. +* Bump yaml from 2.2.1 to 2.2.2. +* Defensive coding relating to gaps in declared schedule types. +* Do not process an action if it cannot be set to `in-progress`. +* Filter view labels (status names) should be translatable | #919. +* Fix WPCLI progress messages. +* Improve data-store initialization flow. +* Improve error handling across all supported PHP versions. +* Improve logic for flushing the runtime cache. +* Support exclusion of multiple groups. +* Update lint-staged and Node/NPM requirements. +* add CLI clean command. +* add CLI exclude-group filter. +* exclude past-due from list table all filter count. +* throwing an exception if as_schedule_recurring_action interval param is not of type integer. + = 3.5.4 - 2023-01-17 = * Add pre filters during action registration. * Async scheduling. @@ -90,7 +172,7 @@ Collaboration is cool. We'd love to work with you to improve Action Scheduler. [ * Dev - ActionScheduler_wcSystemStatus PHPCS fixes (props @ovidiul). #761 * Dev - ActionScheduler_DBLogger.php PHPCS fixes (props @ovidiul). #768 * Dev - Fixed phpcs for ActionScheduler_Schedule_Deprecated (props @ovidiul). #762 -* Dev - Improve actions table indicies (props @glagonikas). #774 & #777 +* Dev - Improve actions table indices (props @glagonikas). #774 & #777 * Dev - PHPCS fixes for ActionScheduler_DBStore.php (props @ovidiul). #769 & #778 * Dev - PHPCS Fixes for ActionScheduler_Abstract_ListTable (props @ovidiul). #763 & #779 * Dev - Adds new filter action_scheduler_claim_actions_order_by to allow tuning of the claim query (props @glagonikas). #773 diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php deleted file mode 100644 index 4bc9a3fc26..0000000000 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php +++ /dev/null @@ -1,49 +0,0 @@ -maybe_dispatch_async_request() uses a lock to avoid - * calling ActionScheduler_QueueRunner->has_maximum_concurrent_batches() every time the 'shutdown', - * hook is triggered, because that method calls ActionScheduler_QueueRunner->store->get_claim_count() - * to find the current number of claims in the database. - * - * @param string $lock_type A string to identify different lock types. - * @bool True if lock value has changed, false if not or if set failed. - */ - public function set( $lock_type ) { - return update_option( $this->get_key( $lock_type ), time() + $this->get_duration( $lock_type ) ); - } - - /** - * If a lock is set, return the timestamp it was set to expiry. - * - * @param string $lock_type A string to identify different lock types. - * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. - */ - public function get_expiration( $lock_type ) { - return get_option( $this->get_key( $lock_type ) ); - } - - /** - * Get the key to use for storing the lock in the transient - * - * @param string $lock_type A string to identify different lock types. - * @return string - */ - protected function get_key( $lock_type ) { - return sprintf( 'action_scheduler_lock_%s', $lock_type ); - } -} diff --git a/inc/Engine/Common/Queue/Cleaner.php b/inc/Engine/Common/Queue/Cleaner.php index fb313d785d..dde6dcc586 100644 --- a/inc/Engine/Common/Queue/Cleaner.php +++ b/inc/Engine/Common/Queue/Cleaner.php @@ -3,7 +3,10 @@ namespace WP_Rocket\Engine\Common\Queue; -class Cleaner extends \ActionScheduler_QueueCleaner { +use ActionScheduler_QueueCleaner; +use ActionScheduler_Store; + +class Cleaner extends ActionScheduler_QueueCleaner { /** * The duration of clean Hour In seconds. @@ -12,6 +15,16 @@ class Cleaner extends \ActionScheduler_QueueCleaner { */ protected $hour_in_seconds = 60 * 60; + /** + * Default list of statuses purged by the cleaner process. + * + * @var string[] + */ + private $default_statuses_to_purge = [ + ActionScheduler_Store::STATUS_COMPLETE, + ActionScheduler_Store::STATUS_CANCELED, + ]; + /** * Store instance. * @@ -29,13 +42,13 @@ class Cleaner extends \ActionScheduler_QueueCleaner { /** * Cleaner constructor. * - * @param \ActionScheduler_Store|null $store The store instance. - * @param int $batch_size The batch size. - * @param string $group Current queue group. + * @param ActionScheduler_Store|null $store The store instance. + * @param int $batch_size The batch size. + * @param string $group Current queue group. */ - public function __construct( \ActionScheduler_Store $store = null, $batch_size = 20, $group = '' ) { + public function __construct( ActionScheduler_Store $store = null, $batch_size = 20, $group = '' ) { parent::__construct( $store, $batch_size ); - $this->store = $store ? $store : \ActionScheduler_Store::instance(); + $this->store = $store ? $store : ActionScheduler_Store::instance(); $this->group = $group; } @@ -45,7 +58,12 @@ public function __construct( \ActionScheduler_Store $store = null, $batch_size = * @return array */ public function delete_old_actions() { - $lifespan = (int) apply_filters( 'action_scheduler_retention_period', $this->hour_in_seconds );// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + /** + * Filter the minimum scheduled date age for action deletion. + * + * @param int $retention_period Minimum scheduled age in seconds of the actions to be deleted. + */ + $lifespan = (int) apply_filters( 'action_scheduler_retention_period', $this->hour_in_seconds ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound /** * Filters the retention period for our tasks only. @@ -58,50 +76,103 @@ public function delete_old_actions() { * @return int */ $lifespan = (int) apply_filters( 'rocket_action_scheduler_retention_period', $lifespan, $this->group ); - $cutoff = as_get_datetime_object( $lifespan . ' seconds ago' ); + + try { + $cutoff = as_get_datetime_object( $lifespan . ' seconds ago' ); + } catch ( \Exception $e ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* Translators: %s is the exception message. */ + esc_html__( 'It was not possible to determine a valid cut-off time: %s.', 'rocket' ), + esc_html( $e->getMessage() ) + ), + '3.5.5' + ); + + return []; + } $statuses_to_purge = [ \ActionScheduler_Store::STATUS_COMPLETE, \ActionScheduler_Store::STATUS_CANCELED, \ActionScheduler_Store::STATUS_FAILED, ]; + + return $this->clean_actions( $statuses_to_purge, $cutoff, $this->get_batch_size() ); + } + + /** + * Delete selected actions limited by status and date. + * + * @param string[] $statuses_to_purge List of action statuses to purge. Defaults to canceled, complete. + * @param \DateTime $cutoff_date Date limit for selecting actions. Defaults to 31 days ago. + * @param int|null $batch_size Maximum number of actions per status to delete. Defaults to 20. + * @param string $context Calling process context. Defaults to `old`. + * @return array Actions deleted. + */ + public function clean_actions( array $statuses_to_purge, \DateTime $cutoff_date, $batch_size = null, $context = 'old' ) { + $batch_size = null !== $batch_size ? $batch_size : $this->batch_size; + $cutoff = null !== $cutoff_date ? $cutoff_date : as_get_datetime_object( $this->hour_in_seconds . ' seconds ago' ); + $lifespan = time() - $cutoff->getTimestamp(); + + if ( empty( $statuses_to_purge ) ) { + $statuses_to_purge = $this->default_statuses_to_purge; + } + + $deleted_actions = []; foreach ( $statuses_to_purge as $status ) { $actions_to_delete = $this->store->query_actions( [ 'status' => $status, 'modified' => $cutoff, 'modified_compare' => '<=', - 'per_page' => $this->get_batch_size(), + 'per_page' => $batch_size, 'orderby' => 'none', 'group' => $this->group, ] ); - foreach ( $actions_to_delete as $action_id ) { - try { - $this->store->delete_action( $action_id ); - } catch ( \Exception $e ) { - - /** - * Notify 3rd party code of exceptions when deleting a completed action older than the retention period - * - * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their - * actions. - * - * @since 2.0.0 - * - * @param int $action_id The scheduled actions ID in the data store - * @param \Exception $e The exception thrown when attempting to delete the action from the data store - * @param int $lifespan The retention period, in seconds, for old actions - * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch - */ - do_action( 'action_scheduler_failed_old_action_deletion', $action_id, $e, $lifespan, count( $actions_to_delete ) );// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound - - return []; - } - } + $deleted_actions = array_merge( $deleted_actions, $this->delete_actions( $actions_to_delete, $lifespan, $context ) ); } - return []; + return $deleted_actions; + } + + /** + * Delete actions + * + * @param int[] $actions_to_delete List of action IDs to delete. + * @param int $lifespan Minimum scheduled age in seconds of the actions being deleted. + * @param string $context Context of the delete request. + * @return array Deleted action IDs. + */ + private function delete_actions( array $actions_to_delete, $lifespan = null, $context = 'old' ) { + $deleted_actions = []; + if ( null === $lifespan ) { + $lifespan = $this->hour_in_seconds; + } + + foreach ( $actions_to_delete as $action_id ) { + try { + $this->store->delete_action( $action_id ); + $deleted_actions[] = $action_id; + } catch ( \Exception $e ) { + /** + * Notify 3rd party code of exceptions when deleting a completed action older than the retention period + * + * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their + * actions. + * + * @param int $action_id The scheduled actions ID in the data store + * @param \Exception $e The exception thrown when attempting to delete the action from the data store + * @param int $lifespan The retention period, in seconds, for old actions + * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch + * @since 2.0.0 + */ + do_action( "action_scheduler_failed_{$context}_action_deletion", $action_id, $e, $lifespan, count( $actions_to_delete ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + } + } + return $deleted_actions; } } diff --git a/inc/main.php b/inc/main.php index c09b7ec41f..3c5a1e5e89 100644 --- a/inc/main.php +++ b/inc/main.php @@ -15,7 +15,7 @@ Cloudflare::fix_cf_flexible_ssl(); -require_once WP_ROCKET_INC_PATH . 'Dependencies' . DIRECTORY_SEPARATOR . 'ActionScheduler' . DIRECTORY_SEPARATOR . 'action-scheduler.php'; +require_once WP_ROCKET_PATH . 'dependencies/ActionScheduler' . DIRECTORY_SEPARATOR . 'action-scheduler.php'; /** * Tell WP what to do when plugin is loaded. diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 612faecfcd..fab0cfbadd 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -32,7 +32,7 @@ parameters: - %currentWorkingDirectory%/inc/classes/class-wp-rocket-requirements-check.php - %currentWorkingDirectory%/inc/vendors/classes/class-rocket-mobile-detect.php scanDirectories: - - %currentWorkingDirectory%/inc/Dependencies/ActionScheduler + - %currentWorkingDirectory%/dependencies/ActionScheduler - %currentWorkingDirectory%/tests/Fixtures/ - %currentWorkingDirectory%/vendor/antecedent/patchwork/ - %currentWorkingDirectory%/vendor/wpackagist-plugin/cloudflare/