diff --git a/inc/global-hooks.php b/inc/global-hooks.php
index 23362a9..5eeca97 100644
--- a/inc/global-hooks.php
+++ b/inc/global-hooks.php
@@ -4,14 +4,29 @@
die( 'Access Denied.' );
}
-function mailrelay_new_user_auto_sync( $user_id ) {
+function mailrelay_user_register_hook( $user_id ) {
if ( '1' !== get_option( 'mailrelay_auto_sync' ) ) {
// Autosync isn't enabled
return;
}
- $user = new WP_User( $user_id );
+ // Schedule the sync to run in the background
+ as_enqueue_async_action(
+ 'mailrelay_sync_user_background',
+ array( $user_id ),
+ 'mailrelay',
+ true
+ );
+}
+add_action( 'user_register', 'mailrelay_user_register_hook' );
+/**
+ * Background handler for syncing users with Mailrelay
+ *
+ * @param int $user_id The WordPress user ID to sync.
+ */
+function mailrelay_sync_user_background( $user_id ) {
+ $user = new WP_User( $user_id );
$groups = get_option( 'mailrelay_auto_sync_groups' );
if ( ! empty( $groups ) ) {
@@ -22,4 +37,4 @@ function mailrelay_new_user_auto_sync( $user_id ) {
}
}
}
-add_action( 'user_register', 'mailrelay_new_user_auto_sync' );
+add_action( 'mailrelay_sync_user_background', 'mailrelay_sync_user_background', 10, 1 );
diff --git a/libraries/action-scheduler/.editorconfig b/libraries/action-scheduler/.editorconfig
new file mode 100644
index 0000000..c3dfa83
--- /dev/null
+++ b/libraries/action-scheduler/.editorconfig
@@ -0,0 +1,24 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+
+# WordPress Coding Standards
+# https://make.wordpress.org/core/handbook/coding-standards/
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+tab_width = 4
+indent_style = tab
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.txt]
+trim_trailing_whitespace = false
+
+[*.{md,json,yml}]
+trim_trailing_whitespace = false
+indent_style = space
+indent_size = 2
diff --git a/libraries/action-scheduler/.gitattributes b/libraries/action-scheduler/.gitattributes
new file mode 100644
index 0000000..a7666ea
--- /dev/null
+++ b/libraries/action-scheduler/.gitattributes
@@ -0,0 +1,14 @@
+docs export-ignore
+tests export-ignore
+codecov.yml export-ignore
+.editorconfig export-ignore
+.github export-ignore
+.travis.yml export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+composer.* export-ignore
+Gruntfile.js export-ignore
+package.json export-ignore
+package-lock.json export-ignore
+phpcs.xml export-ignore
+phpunit.* export-ignore
diff --git a/libraries/action-scheduler/.github/release-drafter.yml b/libraries/action-scheduler/.github/release-drafter.yml
new file mode 100644
index 0000000..702a690
--- /dev/null
+++ b/libraries/action-scheduler/.github/release-drafter.yml
@@ -0,0 +1,15 @@
+template: |
+ ## next release – date
+
+
+
+ $CHANGES
+
+ **Added**
+ **Changed**
+ **Deprecated**
+ **Removed**
+ **Fixed**
+ **Security**
+
+change-template: '* $TITLE (PR #$NUMBER)'
diff --git a/libraries/action-scheduler/.github/workflows/pr-unit-tests.yml b/libraries/action-scheduler/.github/workflows/pr-unit-tests.yml
new file mode 100644
index 0000000..c86ba59
--- /dev/null
+++ b/libraries/action-scheduler/.github/workflows/pr-unit-tests.yml
@@ -0,0 +1,97 @@
+name: Run unit tests on PR
+on:
+ pull_request
+jobs:
+ test:
+ name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} MU ${{ matrix.multisite }}
+ timeout-minutes: 15
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ # We test against the earliest and latest PHP versions for each major supported version.
+ php: [ '7.1', '7.4', '8.0', '8.3' ]
+ wp: [ '6.4', '6.5', '6.6', 'latest', 'nightly' ]
+ multisite: [ '0', '1' ]
+ exclude:
+ # WordPress 6.6+ requires PHP 7.2+
+ - php: 7.1
+ wp: 6.6
+ - php: 7.1
+ wp: latest
+ - php: 7.1
+ wp: nightly
+ services:
+ database:
+ image: mysql:5.6
+ env:
+ MYSQL_ROOT_PASSWORD: root
+ ports:
+ - 3306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ tools: composer
+ extensions: mysql
+ coverage: none
+
+ - name: Tool versions
+ run: |
+ php --version
+ composer --version
+
+ - name: Get composer cache directory
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - name: Cache dependencies
+ uses: actions/cache@v4
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist
+
+ - name: Setup PHPUnit
+ run: |
+ # PHPUnit 7.5 when using PHP 7.1 - 7.4.
+ if [ "$(php -r "echo version_compare( PHP_VERSION, '8.0', '<' );")" ]; then
+ curl -L https://phar.phpunit.de/phpunit-7.5.phar -o /tmp/phpunit
+ OVERWRITE=1
+ # PHPUnit 7.5 (Custom Fork) when using PHP 8.0+.
+ else
+ curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip
+ unzip -d /tmp/phpunit-fork /tmp/phpunit-7.5-fork.zip
+ composer --working-dir=/tmp/phpunit-fork/phpunit-add-compatibility-with-php8-to-phpunit-7 install
+ rm ./vendor/bin/phpunit
+ ln -sf /tmp/phpunit-fork/phpunit-add-compatibility-with-php8-to-phpunit-7/phpunit ./vendor/bin/phpunit
+ fi
+
+ if [ $OVERWRITE ]; then
+ rm ./vendor/bin/phpunit
+ chmod +x /tmp/phpunit
+ mv /tmp/phpunit ./vendor/bin/phpunit
+ fi
+
+ - name: Install Subversion
+ run: sudo apt-get update && sudo apt-get install -y subversion
+
+ - name: Init DB and WP
+ run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }}
+
+ - name: Run tests
+ run: |
+ ./vendor/bin/phpunit --version
+ WP_MULTISITE=${{ matrix.multisite }} ./vendor/bin/phpunit -c ./tests/phpunit.xml.dist
+
+ - name: Code Coverage
+ run: |
+ bash <(curl -s https://codecov.io/bash)
diff --git a/libraries/action-scheduler/.gitignore b/libraries/action-scheduler/.gitignore
new file mode 100644
index 0000000..6716d1c
--- /dev/null
+++ b/libraries/action-scheduler/.gitignore
@@ -0,0 +1,25 @@
+# Operating System files
+.DS_Store
+Thumbs.db
+
+# IDE files
+.idea
+.vscode/*
+project.xml
+project.properties
+.project
+.settings*
+*.sublime-project
+*.sublime-workspace
+.sublimelinterrc
+
+# Project files
+node_modules/
+vendor/
+
+#PHP Unit
+.phpunit.result.cache
+phpunit.xml
+
+# Build files
+action-scheduler.zip
diff --git a/libraries/action-scheduler/Gruntfile.js b/libraries/action-scheduler/Gruntfile.js
new file mode 100644
index 0000000..4477049
--- /dev/null
+++ b/libraries/action-scheduler/Gruntfile.js
@@ -0,0 +1,57 @@
+module.exports = function( grunt ) {
+ 'use strict';
+
+ grunt.initConfig({
+ // Check textdomain errors.
+ checktextdomain: {
+ options:{
+ text_domain: 'action-scheduler',
+ keywords: [
+ '__:1,2d',
+ '_e:1,2d',
+ '_x:1,2c,3d',
+ 'esc_html__:1,2d',
+ 'esc_html_e:1,2d',
+ 'esc_html_x:1,2c,3d',
+ 'esc_attr__:1,2d',
+ 'esc_attr_e:1,2d',
+ 'esc_attr_x:1,2c,3d',
+ '_ex:1,2c,3d',
+ '_n:1,2,4d',
+ '_nx:1,2,4c,5d',
+ '_n_noop:1,2,3d',
+ '_nx_noop:1,2,3c,4d'
+ ]
+ },
+ files: {
+ src: [
+ '**/*.php',
+ '!node_modules/**',
+ '!tests/**',
+ '!vendor/**',
+ '!tmp/**'
+ ],
+ expand: true
+ }
+ },
+
+ // PHP Code Sniffer.
+ phpcs: {
+ options: {
+ bin: 'vendor/bin/phpcs'
+ },
+ dist: {
+ src: [
+ '**/*.php', // Include all php files.
+ '!deprecated/**',
+ '!node_modules/**',
+ '!vendor/**'
+ ]
+ }
+ }
+ });
+
+ // Load NPM tasks to be used here.
+ grunt.loadNpmTasks( 'grunt-phpcs' );
+ grunt.loadNpmTasks( 'grunt-checktextdomain' );
+};
diff --git a/libraries/action-scheduler/README.md b/libraries/action-scheduler/README.md
new file mode 100644
index 0000000..09478b0
--- /dev/null
+++ b/libraries/action-scheduler/README.md
@@ -0,0 +1,35 @@
+# Action Scheduler - Job Queue for WordPress [](https://travis-ci.org/woocommerce/action-scheduler) [](https://codecov.io/gh/woocommerce/action-scheduler)
+
+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 occasions.
+
+Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook.
+
+## Battle-Tested Background Processing
+
+Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins.
+
+It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, at a sustained rate of over 10,000 / hour without negatively impacting normal site operations.
+
+This is all on infrastructure and WordPress sites outside the control of the plugin author.
+
+If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help.
+
+## Learn More
+
+To learn more about how to Action Scheduler works, and how to use it in your plugin, check out the docs on [ActionScheduler.org](https://actionscheduler.org).
+
+There you will find:
+
+* [Usage guide](https://actionscheduler.org/usage/): instructions on installing and using Action Scheduler
+* [WP CLI guide](https://actionscheduler.org/wp-cli/): instructions on running Action Scheduler at scale via WP CLI
+* [API Reference](https://actionscheduler.org/api/): complete reference guide for all API functions
+* [Administration Guide](https://actionscheduler.org/admin/): guide to managing scheduled actions via the administration screen
+* [Guide to Background Processing at Scale](https://actionscheduler.org/perf/): instructions for running Action Scheduler at scale via the default WP Cron queue runner
+
+## Credits
+
+Action Scheduler is developed and maintained by [Automattic](http://automattic.com/) with significant early development completed by [Flightless](https://flightless.us/).
+
+Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/woocommerce/action-scheduler/pulls) welcome.
diff --git a/libraries/action-scheduler/action-scheduler.php b/libraries/action-scheduler/action-scheduler.php
new file mode 100644
index 0000000..ef12ca2
--- /dev/null
+++ b/libraries/action-scheduler/action-scheduler.php
@@ -0,0 +1,70 @@
+.
+ *
+ * @package ActionScheduler
+ */
+
+if ( ! function_exists( 'action_scheduler_register_3_dot_9_dot_2' ) && 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_9_dot_2', 0, 0 ); // WRCS: DEFINED_VERSION.
+
+ // phpcs:disable Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace
+ /**
+ * Registers this version of Action Scheduler.
+ */
+ function action_scheduler_register_3_dot_9_dot_2() { // WRCS: DEFINED_VERSION.
+ $versions = ActionScheduler_Versions::instance();
+ $versions->register( '3.9.2', 'action_scheduler_initialize_3_dot_9_dot_2' ); // WRCS: DEFINED_VERSION.
+ }
+
+ // phpcs:disable Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace
+ /**
+ * Initializes this version of Action Scheduler.
+ */
+ function action_scheduler_initialize_3_dot_9_dot_2() { // 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).
+ if ( ! class_exists( 'ActionScheduler', false ) ) {
+ require_once __DIR__ . '/classes/abstracts/ActionScheduler.php';
+ ActionScheduler::init( __FILE__ );
+ }
+ }
+
+ // 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_9_dot_2(); // WRCS: DEFINED_VERSION.
+ do_action( 'action_scheduler_pre_theme_init' );
+ ActionScheduler_Versions::initialize_latest_version();
+ }
+}
diff --git a/libraries/action-scheduler/changelog.txt b/libraries/action-scheduler/changelog.txt
new file mode 100644
index 0000000..43ca782
--- /dev/null
+++ b/libraries/action-scheduler/changelog.txt
@@ -0,0 +1,187 @@
+*** Changelog ***
+
+= 3.9.2 - 2025-02-03 =
+* Fixed fatal errors by moving version info methods to a new class and deprecating conflicting ones in ActionScheduler_Versions
+
+= 3.9.1 - 2025-01-21 =
+* A number of new WP CLI commands have been added, making it easier to manage actions in the terminal and from scripts.
+* New wp action-scheduler source command to help determine how Action Scheduler is being loaded.
+* Additional information about the active instance of Action Scheduler is now available in the Help pull-down drawer.
+* Make some other nullable parameters explicitly nullable.
+* Set option value to `no` rather than deleting.
+
+= 3.9.0 - 2024-11-14 =
+* Minimum required version of PHP is now 7.1.
+* Performance improvements for the `as_pending_actions_due()` function.
+* Existing filter hook `action_scheduler_claim_actions_order_by` enhanced to provide callbacks with additional information.
+* Improved compatibility with PHP 8.4, specifically by making implicitly nullable parameters explicitly nullable.
+* A large number of coding standards-enhancements, to help reduce friction when submitting plugins to marketplaces and plugin directories. Special props @crstauf for this effort.
+* Minor documentation tweaks and improvements.
+
+= 3.8.2 - 2024-09-12 =
+* Add missing parameter to the `pre_as_enqueue_async_action` hook.
+* Bump minimum PHP version to 7.0.
+* Bump minimum WordPress version to 6.4.
+* Make the batch size adjustable during processing.
+
+= 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.
+* Calculate timeouts based on total actions.
+* Correctly order the parameters for `ActionScheduler_ActionFactory`'s calls to `single_unique`.
+* Fetch action in memory first before releasing claim to avoid deadlock.
+* PHP 8.2: declare property to fix creation of dynamic property warning.
+* PHP 8.2: fix "Using ${var} in strings is deprecated, use {$var} instead".
+* Prevent `undefined variable` warning for `$num_pastdue_actions`.
+
+= 3.5.3 - 2022-11-09 =
+* Query actions with partial match.
+
+= 3.5.2 - 2022-09-16 =
+* Fix - erroneous 3.5.1 release.
+
+= 3.5.1 - 2022-09-13 =
+* Maintenance on A/S docs.
+* fix: PHP 8.2 deprecated notice.
+
+= 3.5.0 - 2022-08-25 =
+* Add - The active view link within the "Tools > Scheduled Actions" screen is now clickable.
+* Add - A warning when there are past-due actions.
+* Enhancement - Added the ability to schedule unique actions via an atomic operation.
+* Enhancement - Improvements to cache invalidation when processing batches (when running on WordPress 6.0+).
+* Enhancement - If a recurring action is found to be consistently failing, it will stop being rescheduled.
+* Enhancement - Adds a new "Past Due" view to the scheduled actions list table.
+
+= 3.4.2 - 2022-06-08 =
+* Fix - Change the include for better linting.
+* Fix - update: Added Action scheduler completed action hook.
+
+= 3.4.1 - 2022-05-24 =
+* Fix - Change the include for better linting.
+* Fix - Fix the documented return type.
+
+= 3.4.0 - 2021-10-29 =
+* Enhancement - Number of items per page can now be set for the Scheduled Actions view (props @ovidiul). #771
+* Fix - Do not lower the max_execution_time if it is already set to 0 (unlimited) (props @barryhughes). #755
+* Fix - Avoid triggering autoloaders during the version resolution process (props @olegabr). #731 & #776
+* 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 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
+* Dev - PHPCS fixes for ActionScheduler_WpPostStore class (props @ovidiul). #780
+
+= 3.3.0 - 2021-09-15 =
+* Enhancement - Adds as_has_scheduled_action() to provide a performant way to test for existing actions. #645
+* Fix - Improves compatibility with environments where NO_ZERO_DATE is enabled. #519
+* Fix - Adds safety checks to guard against errors when our database tables cannot be created. #645
+* Dev - Now supports queries that use multiple statuses. #649
+* Dev - Minimum requirements for WordPress and PHP bumped (to 5.2 and 5.6 respectively). #723
+
+= 3.2.1 - 2021-06-21 =
+* Fix - Add extra safety/account for different versions of AS and different loading patterns. #714
+* Fix - Handle hidden columns (Tools → Scheduled Actions) | #600.
+
+= 3.2.0 - 2021-06-03 =
+* Fix - Add "no ordering" option to as_next_scheduled_action().
+* Fix - Add secondary scheduled date checks when claiming actions (DBStore) | #634.
+* Fix - Add secondary scheduled date checks when claiming actions (wpPostStore) | #634.
+* Fix - Adds a new index to the action table, reducing the potential for deadlocks (props: @glagonikas).
+* Fix - Fix unit tests infrastructure and adapt tests to PHP 8.
+* Fix - Identify in-use data store.
+* Fix - Improve test_migration_is_scheduled.
+* Fix - PHP notice on list table.
+* Fix - Speed up clean up and batch selects.
+* Fix - Update pending dependencies.
+* Fix - [PHP 8.0] Only pass action arg values through to do_action_ref_array().
+* Fix - [PHP 8] Set the PHP version to 7.1 in composer.json for PHP 8 compatibility.
+* Fix - add is_initialized() to docs.
+* Fix - fix file permissions.
+* Fix - fixes #664 by replacing __ with esc_html__.
+
+= 3.1.6 - 2020-05-12 =
+* Change log starts.
diff --git a/libraries/action-scheduler/classes/ActionScheduler_ActionClaim.php b/libraries/action-scheduler/classes/ActionScheduler_ActionClaim.php
new file mode 100644
index 0000000..8eb01ea
--- /dev/null
+++ b/libraries/action-scheduler/classes/ActionScheduler_ActionClaim.php
@@ -0,0 +1,45 @@
+id = $id;
+ $this->action_ids = $action_ids;
+ }
+
+ /**
+ * Get claim ID.
+ */
+ public function get_id() {
+ return $this->id;
+ }
+
+ /**
+ * Get IDs of claimed actions.
+ */
+ public function get_actions() {
+ return $this->action_ids;
+ }
+}
diff --git a/libraries/action-scheduler/classes/ActionScheduler_ActionFactory.php b/libraries/action-scheduler/classes/ActionScheduler_ActionFactory.php
new file mode 100644
index 0000000..ea60d60
--- /dev/null
+++ b/libraries/action-scheduler/classes/ActionScheduler_ActionFactory.php
@@ -0,0 +1,378 @@
+= 6 ? (int) func_get_arg( 5 ) : 10;
+
+ switch ( $status ) {
+ case ActionScheduler_Store::STATUS_PENDING:
+ $action_class = 'ActionScheduler_Action';
+ break;
+ case ActionScheduler_Store::STATUS_CANCELED:
+ $action_class = 'ActionScheduler_CanceledAction';
+ if ( ! is_null( $schedule ) && ! is_a( $schedule, 'ActionScheduler_CanceledSchedule' ) && ! is_a( $schedule, 'ActionScheduler_NullSchedule' ) ) {
+ $schedule = new ActionScheduler_CanceledSchedule( $schedule->get_date() );
+ }
+ break;
+ default:
+ $action_class = 'ActionScheduler_FinishedAction';
+ break;
+ }
+
+ $action_class = apply_filters( 'action_scheduler_stored_action_class', $action_class, $status, $hook, $args, $schedule, $group );
+
+ $action = new $action_class( $hook, $args, $schedule, $group );
+ $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_Schedule $schedule The instantiated action's schedule.
+ * @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, $priority );
+ }
+
+ /**
+ * Enqueue an action to run one time, as soon as possible (rather a specific scheduled time).
+ *
+ * This method creates a new action using the NullSchedule. In practice, this results in an action scheduled to
+ * execute "now". Therefore, it will generally run as soon as possible but is not prioritized ahead of other actions
+ * that are already past-due.
+ *
+ * @param string $hook The hook to trigger when this action runs.
+ * @param array $args Args to pass when the hook is triggered.
+ * @param string $group A group to put the action in.
+ *
+ * @return int The ID of the stored action.
+ */
+ public function async( $hook, $args = array(), $group = '' ) {
+ return $this->async_unique( $hook, $args, $group, false );
+ }
+
+ /**
+ * Same as async, but also supports $unique param.
+ *
+ * @param string $hook The hook to trigger when this action runs.
+ * @param array $args Args to pass when the hook is triggered.
+ * @param string $group A group to put the action in.
+ * @param bool $unique Whether to ensure the action is unique.
+ *
+ * @return int The ID of the stored action.
+ */
+ public function async_unique( $hook, $args = array(), $group = '', $unique = true ) {
+ $schedule = new ActionScheduler_NullSchedule();
+ $action = new ActionScheduler_Action( $hook, $args, $schedule, $group );
+ return $unique ? $this->store_unique_action( $action, $unique ) : $this->store( $action );
+ }
+
+ /**
+ * Create single action.
+ *
+ * @param string $hook The hook to trigger when this action runs.
+ * @param array $args Args to pass when the hook is triggered.
+ * @param int $when Unix timestamp when the action will run.
+ * @param string $group A group to put the action in.
+ *
+ * @return int The ID of the stored action.
+ */
+ public function single( $hook, $args = array(), $when = null, $group = '' ) {
+ return $this->single_unique( $hook, $args, $when, $group, false );
+ }
+
+ /**
+ * Create single action only if there is no pending or running action with same name and params.
+ *
+ * @param string $hook The hook to trigger when this action runs.
+ * @param array $args Args to pass when the hook is triggered.
+ * @param int $when Unix timestamp when the action will run.
+ * @param string $group A group to put the action in.
+ * @param bool $unique Whether action scheduled should be unique.
+ *
+ * @return int The ID of the stored action.
+ */
+ public function single_unique( $hook, $args = array(), $when = null, $group = '', $unique = true ) {
+ $date = as_get_datetime_object( $when );
+ $schedule = new ActionScheduler_SimpleSchedule( $date );
+ $action = new ActionScheduler_Action( $hook, $args, $schedule, $group );
+ return $unique ? $this->store_unique_action( $action ) : $this->store( $action );
+ }
+
+ /**
+ * Create the first instance of an action recurring on a given interval.
+ *
+ * @param string $hook The hook to trigger when this action runs.
+ * @param array $args Args to pass when the hook is triggered.
+ * @param int $first Unix timestamp for the first run.
+ * @param int $interval Seconds between runs.
+ * @param string $group A group to put the action in.
+ *
+ * @return int The ID of the stored action.
+ */
+ public function recurring( $hook, $args = array(), $first = null, $interval = null, $group = '' ) {
+ return $this->recurring_unique( $hook, $args, $first, $interval, $group, false );
+ }
+
+ /**
+ * Create the first instance of an action recurring on a given interval only if there is no pending or running action with same name and params.
+ *
+ * @param string $hook The hook to trigger when this action runs.
+ * @param array $args Args to pass when the hook is triggered.
+ * @param int $first Unix timestamp for the first run.
+ * @param int $interval Seconds between runs.
+ * @param string $group A group to put the action in.
+ * @param bool $unique Whether action scheduled should be unique.
+ *
+ * @return int The ID of the stored action.
+ */
+ public function recurring_unique( $hook, $args = array(), $first = null, $interval = null, $group = '', $unique = true ) {
+ if ( empty( $interval ) ) {
+ return $this->single_unique( $hook, $args, $first, $group, $unique );
+ }
+ $date = as_get_datetime_object( $first );
+ $schedule = new ActionScheduler_IntervalSchedule( $date, $interval );
+ $action = new ActionScheduler_Action( $hook, $args, $schedule, $group );
+ return $unique ? $this->store_unique_action( $action ) : $this->store( $action );
+ }
+
+ /**
+ * Create the first instance of an action recurring on a Cron schedule.
+ *
+ * @param string $hook The hook to trigger when this action runs.
+ * @param array $args Args to pass when the hook is triggered.
+ * @param int $base_timestamp The first instance of the action will be scheduled
+ * to run at a time calculated after this timestamp matching the cron
+ * expression. This can be used to delay the first instance of the action.
+ * @param int $schedule A cron definition string.
+ * @param string $group A group to put the action in.
+ *
+ * @return int The ID of the stored action.
+ */
+ public function cron( $hook, $args = array(), $base_timestamp = null, $schedule = null, $group = '' ) {
+ return $this->cron_unique( $hook, $args, $base_timestamp, $schedule, $group, false );
+ }
+
+
+ /**
+ * Create the first instance of an action recurring on a Cron schedule only if there is no pending or running action with same name and params.
+ *
+ * @param string $hook The hook to trigger when this action runs.
+ * @param array $args Args to pass when the hook is triggered.
+ * @param int $base_timestamp The first instance of the action will be scheduled
+ * to run at a time calculated after this timestamp matching the cron
+ * expression. This can be used to delay the first instance of the action.
+ * @param int $schedule A cron definition string.
+ * @param string $group A group to put the action in.
+ * @param bool $unique Whether action scheduled should be unique.
+ *
+ * @return int The ID of the stored action.
+ **/
+ public function cron_unique( $hook, $args = array(), $base_timestamp = null, $schedule = null, $group = '', $unique = true ) {
+ if ( empty( $schedule ) ) {
+ return $this->single_unique( $hook, $args, $base_timestamp, $group, $unique );
+ }
+ $date = as_get_datetime_object( $base_timestamp );
+ $cron = CronExpression::factory( $schedule );
+ $schedule = new ActionScheduler_CronSchedule( $date, $cron );
+ $action = new ActionScheduler_Action( $hook, $args, $schedule, $group );
+ return $unique ? $this->store_unique_action( $action ) : $this->store( $action );
+ }
+
+ /**
+ * Create a successive instance of a recurring or cron action.
+ *
+ * Importantly, the action will be rescheduled to run based on the current date/time.
+ * That means when the action is scheduled to run in the past, the next scheduled date
+ * will be pushed forward. For example, if a recurring action set to run every hour
+ * was scheduled to run 5 seconds ago, it will be next scheduled for 1 hour in the
+ * future, which is 1 hour and 5 seconds from when it was last scheduled to run.
+ *
+ * Alternatively, if the action is scheduled to run in the future, and is run early,
+ * likely via manual intervention, then its schedule will change based on the time now.
+ * For example, if a recurring action set to run every day, and is run 12 hours early,
+ * it will run again in 24 hours, not 36 hours.
+ *
+ * This slippage is less of an issue with Cron actions, as the specific run time can
+ * be set for them to run, e.g. 1am each day. In those cases, and entire period would
+ * need to be missed before there was any change is scheduled, e.g. in the case of an
+ * action scheduled for 1am each day, the action would need to run an entire day late.
+ *
+ * @param ActionScheduler_Action $action The existing action.
+ *
+ * @return string The ID of the stored action
+ * @throws InvalidArgumentException If $action is not a recurring action.
+ */
+ public function repeat( $action ) {
+ $schedule = $action->get_schedule();
+ $next = $schedule->get_next( as_get_datetime_object() );
+
+ if ( is_null( $next ) || ! $schedule->is_recurring() ) {
+ throw new InvalidArgumentException( __( 'Invalid action - must be a recurring action.', 'action-scheduler' ) );
+ }
+
+ $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:
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ 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 ) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ 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.
+ *
+ * @param ActionScheduler_Action $action Action object to save.
+ *
+ * @return int The ID of the stored action
+ */
+ protected function store( ActionScheduler_Action $action ) {
+ $store = ActionScheduler_Store::instance();
+ return $store->save_action( $action );
+ }
+
+ /**
+ * Store action if it's unique.
+ *
+ * @param ActionScheduler_Action $action Action object to store.
+ *
+ * @return int ID of the created action. Will be 0 if action was not created.
+ */
+ protected function store_unique_action( ActionScheduler_Action $action ) {
+ $store = ActionScheduler_Store::instance();
+ 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/libraries/action-scheduler/classes/ActionScheduler_AdminView.php b/libraries/action-scheduler/classes/ActionScheduler_AdminView.php
new file mode 100644
index 0000000..1c8306e
--- /dev/null
+++ b/libraries/action-scheduler/classes/ActionScheduler_AdminView.php
@@ -0,0 +1,311 @@
+render();
+ }
+
+ /**
+ * Registers action-scheduler into WooCommerce > System status.
+ *
+ * @param array $tabs An associative array of tab key => label.
+ * @return array $tabs An associative array of tab key => label, including Action Scheduler's tabs
+ */
+ public function register_system_status_tab( array $tabs ) {
+ $tabs['action-scheduler'] = __( 'Scheduled Actions', 'action-scheduler' );
+
+ return $tabs;
+ }
+
+ /**
+ * Include Action Scheduler's administration under the Tools menu.
+ *
+ * A menu under the Tools menu is important for backward compatibility (as that's
+ * where it started), and also provides more convenient access than the WooCommerce
+ * System Status page, and for sites where WooCommerce isn't active.
+ */
+ public function register_menu() {
+ $hook_suffix = add_submenu_page(
+ 'tools.php',
+ __( 'Scheduled Actions', 'action-scheduler' ),
+ __( 'Scheduled Actions', 'action-scheduler' ),
+ 'manage_options',
+ 'action-scheduler',
+ array( $this, 'render_admin_ui' )
+ );
+ add_action( 'load-' . $hook_suffix, array( $this, 'process_admin_ui' ) );
+ }
+
+ /**
+ * Triggers processing of any pending actions.
+ */
+ public function process_admin_ui() {
+ $this->get_list_table();
+ }
+
+ /**
+ * Renders the Admin UI
+ */
+ public function render_admin_ui() {
+ $table = $this->get_list_table();
+ $table->display_page();
+ }
+
+ /**
+ * Get the admin UI object and process any requested actions.
+ *
+ * @return ActionScheduler_ListTable
+ */
+ protected function get_list_table() {
+ if ( null === $this->list_table ) {
+ $this->list_table = new ActionScheduler_ListTable( ActionScheduler::store(), ActionScheduler::logger(), ActionScheduler::runner() );
+ $this->list_table->process_actions();
+ }
+
+ return $this->list_table;
+ }
+
+ /**
+ * Action: admin_notices
+ *
+ * Maybe check past-due actions, and print notice.
+ *
+ * @uses $this->check_pastdue_actions()
+ */
+ public function maybe_check_pastdue_actions() {
+
+ // Filter to prevent checking actions (ex: inappropriate user).
+ if ( ! apply_filters( 'action_scheduler_check_pastdue_actions', current_user_can( 'manage_options' ) ) ) {
+ return;
+ }
+
+ // Get last check transient.
+ $last_check = get_transient( 'action_scheduler_last_pastdue_actions_check' );
+
+ // If transient exists, we're within interval, so bail.
+ if ( ! empty( $last_check ) ) {
+ return;
+ }
+
+ // Perform the check.
+ $this->check_pastdue_actions();
+ }
+
+ /**
+ * Check past-due actions, and print notice.
+ */
+ protected function check_pastdue_actions() {
+
+ // Set thresholds.
+ $threshold_seconds = (int) apply_filters( 'action_scheduler_pastdue_actions_seconds', DAY_IN_SECONDS );
+ $threshold_min = (int) apply_filters( 'action_scheduler_pastdue_actions_min', 1 );
+
+ // Set fallback value for past-due actions count.
+ $num_pastdue_actions = 0;
+
+ // Allow third-parties to preempt the default check logic.
+ $check = apply_filters( 'action_scheduler_pastdue_actions_check_pre', null );
+
+ // If no third-party preempted and there are no past-due actions, return early.
+ if ( ! is_null( $check ) ) {
+ return;
+ }
+
+ // Scheduled actions query arguments.
+ $query_args = array(
+ 'date' => as_get_datetime_object( time() - $threshold_seconds ),
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ 'per_page' => $threshold_min,
+ );
+
+ // If no third-party preempted, run default check.
+ if ( is_null( $check ) ) {
+ $store = ActionScheduler_Store::instance();
+ $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 >= $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.
+ if ( ! boolval( $check ) ) {
+ $interval = apply_filters( 'action_scheduler_pastdue_actions_check_interval', round( $threshold_seconds / 4 ), $threshold_seconds );
+ set_transient( 'action_scheduler_last_pastdue_actions_check', time(), $interval );
+
+ return;
+ }
+
+ $actions_url = add_query_arg(
+ array(
+ 'page' => 'action-scheduler',
+ 'status' => 'past-due',
+ 'order' => 'asc',
+ ),
+ admin_url( 'tools.php' )
+ );
+
+ // Print notice.
+ echo '
';
+ printf(
+ wp_kses(
+ // translators: 1) is the number of affected actions, 2) is a link to an admin screen.
+ _n(
+ '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,
+ 'action-scheduler'
+ ),
+ array(
+ 'strong' => array(),
+ 'a' => array(
+ 'href' => true,
+ 'target' => true,
+ ),
+ )
+ ),
+ absint( $num_pastdue_actions ),
+ esc_attr( esc_url( $actions_url ) )
+ );
+ echo '
';
+
+ // Facilitate third-parties to evaluate and print notices.
+ do_action( 'action_scheduler_pastdue_actions_extra_notices', $query_args );
+ }
+
+ /**
+ * Provide more information about the screen and its data in the help tab.
+ */
+ public function add_help_tabs() {
+ $screen = get_current_screen();
+
+ if ( ! $screen || self::$screen_id !== $screen->id ) {
+ return;
+ }
+
+ $as_version = ActionScheduler_Versions::instance()->latest_version();
+ $as_source = ActionScheduler_SystemInformation::active_source();
+ $as_source_path = ActionScheduler_SystemInformation::active_source_path();
+ $as_source_markup = sprintf( '%s', esc_html( $as_source_path ) );
+
+ if ( ! empty( $as_source ) ) {
+ $as_source_markup = sprintf(
+ '%s: %s',
+ ucfirst( $as_source['type'] ),
+ esc_attr( $as_source_path ),
+ esc_html( $as_source['name'] )
+ );
+ }
+
+ $screen->add_help_tab(
+ array(
+ 'id' => 'action_scheduler_about',
+ 'title' => __( 'About', 'action-scheduler' ),
+ 'content' =>
+ // translators: %s is the Action Scheduler 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' ) .
+ '
' .
+ esc_html__( 'Action Scheduler is currently being loaded from the following location. This can be useful when debugging, or if requested by the support team.', 'action-scheduler' ) .
+ '
';
+
+ return apply_filters( 'action_scheduler_list_table_column_args', $row_html, $row );
+ }
+
+ /**
+ * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
+ *
+ * @param array $row Action array.
+ * @return string
+ */
+ public function column_log_entries( array $row ) {
+
+ $log_entries_html = '';
+
+ $timezone = new DateTimezone( 'UTC' );
+
+ foreach ( $row['log_entries'] as $log_entry ) {
+ $log_entries_html .= $this->get_log_entry_html( $log_entry, $timezone );
+ }
+
+ $log_entries_html .= '';
+
+ return $log_entries_html;
+ }
+
+ /**
+ * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
+ *
+ * @param ActionScheduler_LogEntry $log_entry Log entry object.
+ * @param DateTimezone $timezone Timestamp.
+ * @return string
+ */
+ protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) {
+ $date = $log_entry->get_date();
+ $date->setTimezone( $timezone );
+ return sprintf( '
%s %s
', esc_html( $date->format( 'Y-m-d H:i:s O' ) ), esc_html( $log_entry->get_message() ) );
+ }
+
+ /**
+ * Only display row actions for pending actions.
+ *
+ * @param array $row Row to render.
+ * @param string $column_name Current row.
+ *
+ * @return string
+ */
+ protected function maybe_render_actions( $row, $column_name ) {
+ if ( 'pending' === strtolower( $row['status_name'] ) ) {
+ return parent::maybe_render_actions( $row, $column_name );
+ }
+
+ return '';
+ }
+
+ /**
+ * Renders admin notifications
+ *
+ * Notifications:
+ * 1. When the maximum number of tasks are being executed simultaneously.
+ * 2. Notifications when a task is manually executed.
+ * 3. Tables are missing.
+ */
+ public function display_admin_notices() {
+ global $wpdb;
+
+ if ( ( is_a( $this->store, 'ActionScheduler_HybridStore' ) || is_a( $this->store, 'ActionScheduler_DBStore' ) ) && apply_filters( 'action_scheduler_enable_recreate_data_store', true ) ) {
+ $table_list = array(
+ 'actionscheduler_actions',
+ 'actionscheduler_logs',
+ 'actionscheduler_groups',
+ 'actionscheduler_claims',
+ );
+
+ $found_tables = $wpdb->get_col( "SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ foreach ( $table_list as $table_name ) {
+ if ( ! in_array( $wpdb->prefix . $table_name, $found_tables, true ) ) {
+ $this->admin_notices[] = array(
+ 'class' => 'error',
+ 'message' => __( 'It appears one or more database tables were missing. Attempting to re-create the missing table(s).', 'action-scheduler' ),
+ );
+ $this->recreate_tables();
+ parent::display_admin_notices();
+
+ return;
+ }
+ }
+ }
+
+ if ( $this->runner->has_maximum_concurrent_batches() ) {
+ $claim_count = $this->store->get_claim_count();
+ $this->admin_notices[] = array(
+ 'class' => 'updated',
+ 'message' => sprintf(
+ /* translators: %s: amount of claims */
+ _n(
+ 'Maximum simultaneous queues already in progress (%s queue). No additional queues will begin processing until the current queues are complete.',
+ 'Maximum simultaneous queues already in progress (%s queues). No additional queues will begin processing until the current queues are complete.',
+ $claim_count,
+ 'action-scheduler'
+ ),
+ $claim_count
+ ),
+ );
+ } elseif ( $this->store->has_pending_actions_due() ) {
+
+ $async_request_lock_expiration = ActionScheduler::lock()->get_expiration( 'async-request-runner' );
+
+ // No lock set or lock expired.
+ if ( false === $async_request_lock_expiration || $async_request_lock_expiration < time() ) {
+ $in_progress_url = add_query_arg( 'status', 'in-progress', remove_query_arg( 'status' ) );
+ /* translators: %s: process URL */
+ $async_request_message = sprintf( __( 'A new queue has begun processing. View actions in-progress »', 'action-scheduler' ), esc_url( $in_progress_url ) );
+ } else {
+ /* translators: %d: seconds */
+ $async_request_message = sprintf( __( 'The next queue will begin processing in approximately %d seconds.', 'action-scheduler' ), $async_request_lock_expiration - time() );
+ }
+
+ $this->admin_notices[] = array(
+ 'class' => 'notice notice-info',
+ 'message' => $async_request_message,
+ );
+ }
+
+ $notification = get_transient( 'action_scheduler_admin_notice' );
+
+ if ( is_array( $notification ) ) {
+ delete_transient( 'action_scheduler_admin_notice' );
+
+ $action = $this->store->fetch_action( $notification['action_id'] );
+ $action_hook_html = '' . $action->get_hook() . '';
+
+ if ( 1 === absint( $notification['success'] ) ) {
+ $class = 'updated';
+ switch ( $notification['row_action_type'] ) {
+ case 'run':
+ /* translators: %s: action HTML */
+ $action_message_html = sprintf( __( 'Successfully executed action: %s', 'action-scheduler' ), $action_hook_html );
+ break;
+ case 'cancel':
+ /* translators: %s: action HTML */
+ $action_message_html = sprintf( __( 'Successfully canceled action: %s', 'action-scheduler' ), $action_hook_html );
+ break;
+ default:
+ /* translators: %s: action HTML */
+ $action_message_html = sprintf( __( 'Successfully processed change for action: %s', 'action-scheduler' ), $action_hook_html );
+ break;
+ }
+ } else {
+ $class = 'error';
+ /* translators: 1: action HTML 2: action ID 3: error message */
+ $action_message_html = sprintf( __( 'Could not process change for action: "%1$s" (ID: %2$d). Error: %3$s', 'action-scheduler' ), $action_hook_html, esc_html( $notification['action_id'] ), esc_html( $notification['error_message'] ) );
+ }
+
+ $action_message_html = apply_filters( 'action_scheduler_admin_notice_html', $action_message_html, $action, $notification );
+
+ $this->admin_notices[] = array(
+ 'class' => $class,
+ 'message' => $action_message_html,
+ );
+ }
+
+ parent::display_admin_notices();
+ }
+
+ /**
+ * Prints the scheduled date in a human friendly format.
+ *
+ * @param array $row The array representation of the current row of the table.
+ *
+ * @return string
+ */
+ public function column_schedule( $row ) {
+ return $this->get_schedule_display_string( $row['schedule'] );
+ }
+
+ /**
+ * Get the scheduled date in a human friendly format.
+ *
+ * @param ActionScheduler_Schedule $schedule Action's schedule.
+ * @return string
+ */
+ protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) {
+
+ $schedule_display_string = '';
+
+ if ( is_a( $schedule, 'ActionScheduler_NullSchedule' ) ) {
+ return __( 'async', 'action-scheduler' );
+ }
+
+ if ( ! method_exists( $schedule, 'get_date' ) || ! $schedule->get_date() ) {
+ return '0000-00-00 00:00:00';
+ }
+
+ $next_timestamp = $schedule->get_date()->getTimestamp();
+
+ $schedule_display_string .= $schedule->get_date()->format( 'Y-m-d H:i:s O' );
+ $schedule_display_string .= ' ';
+
+ if ( gmdate( 'U' ) > $next_timestamp ) {
+ /* translators: %s: date interval */
+ $schedule_display_string .= sprintf( __( ' (%s ago)', 'action-scheduler' ), self::human_interval( gmdate( 'U' ) - $next_timestamp ) );
+ } else {
+ /* translators: %s: date interval */
+ $schedule_display_string .= sprintf( __( ' (%s)', 'action-scheduler' ), self::human_interval( $next_timestamp - gmdate( 'U' ) ) );
+ }
+
+ return $schedule_display_string;
+ }
+
+ /**
+ * Bulk delete.
+ *
+ * Deletes actions based on their ID. This is the handler for the bulk delete. It assumes the data
+ * properly validated by the callee and it will delete the actions without any extra validation.
+ *
+ * @param int[] $ids Action IDs.
+ * @param string $ids_sql Inherited and unused.
+ */
+ protected function bulk_delete( array $ids, $ids_sql ) {
+ foreach ( $ids as $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.
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ 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()
+ )
+ );
+ }
+ }
+ }
+
+ /**
+ * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
+ * parameters are valid.
+ *
+ * @param int $action_id Action ID.
+ */
+ protected function row_action_cancel( $action_id ) {
+ $this->process_row_action( $action_id, 'cancel' );
+ }
+
+ /**
+ * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
+ * parameters are valid.
+ *
+ * @param int $action_id Action ID.
+ */
+ protected function row_action_run( $action_id ) {
+ $this->process_row_action( $action_id, 'run' );
+ }
+
+ /**
+ * Force the data store schema updates.
+ */
+ protected function recreate_tables() {
+ if ( is_a( $this->store, 'ActionScheduler_HybridStore' ) ) {
+ $store = $this->store;
+ } else {
+ $store = new ActionScheduler_HybridStore();
+ }
+ add_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10, 2 );
+
+ $store_schema = new ActionScheduler_StoreSchema();
+ $logger_schema = new ActionScheduler_LoggerSchema();
+ $store_schema->register_tables( true );
+ $logger_schema->register_tables( true );
+
+ remove_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10 );
+ }
+ /**
+ * Implements the logic behind processing an action once an action link is clicked on the list table.
+ *
+ * @param int $action_id Action ID.
+ * @param string $row_action_type The type of action to perform on the action.
+ */
+ protected function process_row_action( $action_id, $row_action_type ) {
+ try {
+ switch ( $row_action_type ) {
+ case 'run':
+ $this->runner->process_action( $action_id, 'Admin List Table' );
+ break;
+ case 'cancel':
+ $this->store->cancel_action( $action_id );
+ break;
+ }
+ $success = 1;
+ $error_message = '';
+ } catch ( Exception $e ) {
+ $success = 0;
+ $error_message = $e->getMessage();
+ }
+
+ set_transient( 'action_scheduler_admin_notice', compact( 'action_id', 'success', 'error_message', 'row_action_type' ), 30 );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function prepare_items() {
+ $this->prepare_column_headers();
+
+ $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
+
+ $query = array(
+ 'per_page' => $per_page,
+ 'offset' => $this->get_items_offset(),
+ 'status' => $this->get_request_status(),
+ 'orderby' => $this->get_request_orderby(),
+ 'order' => $this->get_request_order(),
+ 'search' => $this->get_request_search_query(),
+ );
+
+ /**
+ * Change query arguments to query for past-due actions.
+ * Past-due actions have the 'pending' status and are in the past.
+ * This is needed because registering 'past-due' as a status is overkill.
+ */
+ if ( 'past-due' === $this->get_request_status() ) {
+ $query['status'] = ActionScheduler_Store::STATUS_PENDING;
+ $query['date'] = as_get_datetime_object();
+ }
+
+ $this->items = array();
+
+ $total_items = $this->store->query_actions( $query, 'count' );
+
+ $status_labels = $this->store->get_status_labels();
+
+ foreach ( $this->store->query_actions( $query ) as $action_id ) {
+ try {
+ $action = $this->store->fetch_action( $action_id );
+ } catch ( Exception $e ) {
+ continue;
+ }
+ if ( is_a( $action, 'ActionScheduler_NullAction' ) ) {
+ continue;
+ }
+ $this->items[ $action_id ] = array(
+ 'ID' => $action_id,
+ 'hook' => $action->get_hook(),
+ 'status_name' => $this->store->get_status( $action_id ),
+ 'status' => $status_labels[ $this->store->get_status( $action_id ) ],
+ 'args' => $action->get_args(),
+ 'group' => $action->get_group(),
+ 'log_entries' => $this->logger->get_logs( $action_id ),
+ 'claim_id' => $this->store->get_claim_id( $action_id ),
+ 'recurrence' => $this->get_recurrence( $action ),
+ 'schedule' => $action->get_schedule(),
+ );
+ }
+
+ $this->set_pagination_args(
+ array(
+ 'total_items' => $total_items,
+ 'per_page' => $per_page,
+ 'total_pages' => ceil( $total_items / $per_page ),
+ )
+ );
+ }
+
+ /**
+ * Prints the available statuses so the user can click to filter.
+ */
+ protected function display_filter_by_status() {
+ $this->status_counts = $this->store->action_counts() + $this->store->extra_action_counts();
+ parent::display_filter_by_status();
+ }
+
+ /**
+ * Get the text to display in the search box on the list table.
+ */
+ protected function get_search_box_button_text() {
+ return __( 'Search hook, args and claim ID', 'action-scheduler' );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function get_per_page_option_name() {
+ return str_replace( '-', '_', $this->screen->id ) . '_per_page';
+ }
+}
diff --git a/libraries/action-scheduler/classes/ActionScheduler_LogEntry.php b/libraries/action-scheduler/classes/ActionScheduler_LogEntry.php
new file mode 100644
index 0000000..726fea2
--- /dev/null
+++ b/libraries/action-scheduler/classes/ActionScheduler_LogEntry.php
@@ -0,0 +1,78 @@
+comment_type
+ * to ActionScheduler_LogEntry::__construct(), goodness knows why, and the Follow-up Emails plugin
+ * hard-codes loading its own version of ActionScheduler_wpCommentLogger with that out-dated method,
+ * goodness knows why, so we need to guard against that here instead of using a DateTime type declaration
+ * for the constructor's 3rd param of $date and causing a fatal error with older versions of FUE.
+ */
+ if ( null !== $date && ! is_a( $date, 'DateTime' ) ) {
+ _doing_it_wrong( __METHOD__, 'The third parameter must be a valid DateTime instance, or null.', '2.0.0' );
+ $date = null;
+ }
+
+ $this->action_id = $action_id;
+ $this->message = $message;
+ $this->date = $date ? $date : new Datetime();
+ }
+
+ /**
+ * Returns the date when this log entry was created
+ *
+ * @return Datetime
+ */
+ public function get_date() {
+ return $this->date;
+ }
+
+ /**
+ * Get action ID of log entry.
+ */
+ public function get_action_id() {
+ return $this->action_id;
+ }
+
+ /**
+ * Get log entry message.
+ */
+ public function get_message() {
+ return $this->message;
+ }
+}
+
diff --git a/libraries/action-scheduler/classes/ActionScheduler_NullLogEntry.php b/libraries/action-scheduler/classes/ActionScheduler_NullLogEntry.php
new file mode 100644
index 0000000..4df0f05
--- /dev/null
+++ b/libraries/action-scheduler/classes/ActionScheduler_NullLogEntry.php
@@ -0,0 +1,18 @@
+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/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php b/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php
new file mode 100644
index 0000000..7029d0b
--- /dev/null
+++ b/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php
@@ -0,0 +1,254 @@
+store = $store ? $store : ActionScheduler_Store::instance();
+ $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 );
+
+ 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 = ! is_null( $batch_size ) ? $batch_size : $this->batch_size;
+ $cutoff = ! is_null( $cutoff_date ) ? $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 = array();
+
+ foreach ( $statuses_to_purge as $status ) {
+ $actions_to_delete = $this->store->query_actions(
+ array(
+ 'status' => $status,
+ 'modified' => $cutoff,
+ 'modified_compare' => '<=',
+ 'per_page' => $batch_size,
+ 'orderby' => 'none',
+ )
+ );
+
+ $deleted_actions = array_merge( $deleted_actions, $this->delete_actions( $actions_to_delete, $lifespan, $context ) );
+ }
+
+ 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 = array();
+
+ if ( is_null( $lifespan ) ) {
+ $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;
+ }
+
+ /**
+ * Unclaim pending actions that have not been run within a given time limit.
+ *
+ * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
+ * as a parameter is 10x the time limit used for queue processing.
+ *
+ * @param int $time_limit The number of seconds to allow a queue to run before unclaiming its pending actions. Default 300 (5 minutes).
+ */
+ public function reset_timeouts( $time_limit = 300 ) {
+ $timeout = apply_filters( 'action_scheduler_timeout_period', $time_limit );
+
+ if ( $timeout < 0 ) {
+ return;
+ }
+
+ $cutoff = as_get_datetime_object( $timeout . ' seconds ago' );
+ $actions_to_reset = $this->store->query_actions(
+ array(
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ 'modified' => $cutoff,
+ 'modified_compare' => '<=',
+ 'claimed' => true,
+ 'per_page' => $this->get_batch_size(),
+ 'orderby' => 'none',
+ )
+ );
+
+ foreach ( $actions_to_reset as $action_id ) {
+ $this->store->unclaim_action( $action_id );
+ do_action( 'action_scheduler_reset_action', $action_id );
+ }
+ }
+
+ /**
+ * Mark actions that have been running for more than a given time limit as failed, based on
+ * the assumption some 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.
+ *
+ * @param int $time_limit The number of seconds to allow an action to run before it is considered to have failed. Default 300 (5 minutes).
+ */
+ public function mark_failures( $time_limit = 300 ) {
+ $timeout = apply_filters( 'action_scheduler_failure_period', $time_limit );
+
+ if ( $timeout < 0 ) {
+ return;
+ }
+
+ $cutoff = as_get_datetime_object( $timeout . ' seconds ago' );
+ $actions_to_reset = $this->store->query_actions(
+ array(
+ 'status' => ActionScheduler_Store::STATUS_RUNNING,
+ 'modified' => $cutoff,
+ 'modified_compare' => '<=',
+ 'per_page' => $this->get_batch_size(),
+ 'orderby' => 'none',
+ )
+ );
+
+ foreach ( $actions_to_reset as $action_id ) {
+ $this->store->mark_failure( $action_id );
+ do_action( 'action_scheduler_failed_action', $action_id, $timeout );
+ }
+ }
+
+ /**
+ * Do all of the cleaning actions.
+ *
+ * @param int $time_limit The number of seconds to use as the timeout and failure period. Default 300 (5 minutes).
+ */
+ public function clean( $time_limit = 300 ) {
+ $this->delete_old_actions();
+ $this->reset_timeouts( $time_limit );
+ $this->mark_failures( $time_limit );
+ }
+
+ /**
+ * Get the batch size for cleaning the queue.
+ *
+ * @return int
+ */
+ protected function get_batch_size() {
+ /**
+ * Filter the batch size when cleaning the queue.
+ *
+ * @param int $batch_size The number of actions to clean in one batch.
+ */
+ return absint( apply_filters( 'action_scheduler_cleanup_batch_size', $this->batch_size ) );
+ }
+}
diff --git a/libraries/action-scheduler/classes/ActionScheduler_QueueRunner.php b/libraries/action-scheduler/classes/ActionScheduler_QueueRunner.php
new file mode 100644
index 0000000..13a71e7
--- /dev/null
+++ b/libraries/action-scheduler/classes/ActionScheduler_QueueRunner.php
@@ -0,0 +1,254 @@
+store );
+ }
+
+ $this->async_request = $async_request;
+ }
+
+ /**
+ * Initialize.
+ *
+ * @codeCoverageIgnore
+ */
+ public function init() {
+
+ add_filter( 'cron_schedules', array( self::instance(), 'add_wp_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.CronSchedulesInterval
+
+ // Check for and remove any WP Cron hook scheduled by Action Scheduler < 3.0.0, which didn't include the $context param.
+ $next_timestamp = wp_next_scheduled( self::WP_CRON_HOOK );
+ if ( $next_timestamp ) {
+ wp_unschedule_event( $next_timestamp, self::WP_CRON_HOOK );
+ }
+
+ $cron_context = array( 'WP Cron' );
+
+ if ( ! wp_next_scheduled( self::WP_CRON_HOOK, $cron_context ) ) {
+ $schedule = apply_filters( 'action_scheduler_run_schedule', self::WP_CRON_SCHEDULE );
+ wp_schedule_event( time(), $schedule, self::WP_CRON_HOOK, $cron_context );
+ }
+
+ add_action( self::WP_CRON_HOOK, array( self::instance(), 'run' ) );
+ $this->hook_dispatch_async_request();
+ }
+
+ /**
+ * Hook check for dispatching an async request.
+ */
+ public function hook_dispatch_async_request() {
+ add_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) );
+ }
+
+ /**
+ * Unhook check for dispatching an async request.
+ */
+ public function unhook_dispatch_async_request() {
+ remove_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) );
+ }
+
+ /**
+ * Check if we should dispatch an async request to process actions.
+ *
+ * This method is attached to 'shutdown', so is called frequently. To avoid slowing down
+ * the site, it mitigates the work performed in each request by:
+ * 1. checking if it's in the admin context and then
+ * 2. haven't run on the 'shutdown' hook within the lock time (60 seconds by default)
+ * 3. haven't exceeded the number of allowed batches.
+ *
+ * The order of these checks is important, because they run from a check on a value:
+ * 1. in memory - is_admin() maps to $GLOBALS or the WP_ADMIN constant
+ * 2. in memory - transients use autoloaded options by default
+ * 3. from a database query - has_maximum_concurrent_batches() run the query
+ * $this->store->get_claim_count() to find the current number of claims in the DB.
+ *
+ * If all of these conditions are met, then we request an async runner check whether it
+ * should dispatch a request to process pending actions.
+ */
+ public function maybe_dispatch_async_request() {
+ // 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();
+ }
+ }
+
+ /**
+ * Process actions in the queue. Attached to self::WP_CRON_HOOK i.e. 'action_scheduler_run_queue'
+ *
+ * The $context param of this method defaults to 'WP Cron', because prior to Action Scheduler 3.0.0
+ * that was the only context in which this method was run, and the self::WP_CRON_HOOK hook had no context
+ * passed along with it. New code calling this method directly, or by triggering the self::WP_CRON_HOOK,
+ * 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 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.
+ */
+ public function run( $context = 'WP Cron' ) {
+ ActionScheduler_Compatibility::raise_memory_limit();
+ ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() );
+ do_action( 'action_scheduler_before_process_queue' );
+ $this->run_cleanup();
+
+ $this->processed_actions_count = 0;
+ if ( false === $this->has_maximum_concurrent_batches() ) {
+ do {
+ $batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 );
+ $processed_actions_in_batch = $this->do_batch( $batch_size, $context );
+ $this->processed_actions_count += $processed_actions_in_batch;
+ } while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $this->processed_actions_count ) ); // keep going until we run out of actions, time, or memory.
+ }
+
+ do_action( 'action_scheduler_after_process_queue' );
+ return $this->processed_actions_count;
+ }
+
+ /**
+ * Process a batch of actions pending in the queue.
+ *
+ * Actions are processed by claiming a set of pending actions then processing each one until either the batch
+ * 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 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.
+ */
+ protected function do_batch( $size = 100, $context = '' ) {
+ $claim = $this->store->stake_claim( $size );
+ $this->monitor->attach( $claim );
+ $processed_actions = 0;
+
+ foreach ( $claim->get_actions() as $action_id ) {
+ // bail if we lost the claim.
+ if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $claim->get_id() ), true ) ) {
+ break;
+ }
+ $this->process_action( $action_id, $context );
+ $processed_actions++;
+
+ if ( $this->batch_limits_exceeded( $processed_actions + $this->processed_actions_count ) ) {
+ break;
+ }
+ }
+ $this->store->release_claim( $claim );
+ $this->monitor->detach();
+ $this->clear_caches();
+ return $processed_actions;
+ }
+
+ /**
+ * Flush the cache if possible (intended for use after a batch of actions has been processed).
+ *
+ * This is useful because running large batches can eat up memory and because invalid data can accrue in the
+ * runtime cache, which may lead to unexpected results.
+ */
+ 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 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.
+ */
+ $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()
+ /**
+ * When an external object cache is in use, and when wp_cache_flush_runtime() is not available, then
+ * normally the cache will not be flushed after processing a batch of actions (to avoid a performance
+ * penalty for other processes).
+ *
+ * This filter makes it possible to override this behavior and always flush the cache, even if an external
+ * object cache is in use.
+ *
+ * @since 1.0
+ *
+ * @param bool $flush_cache If the cache should be flushed.
+ */
+ || apply_filters( 'action_scheduler_queue_runner_flush_cache', false )
+ ) {
+ wp_cache_flush();
+ }
+ }
+
+ /**
+ * Add schedule to WP cron.
+ *
+ * @param array> $schedules Schedules.
+ * @return array>
+ */
+ public function add_wp_cron_schedule( $schedules ) {
+ $schedules['every_minute'] = array(
+ 'interval' => 60, // in seconds.
+ 'display' => __( 'Every minute', 'action-scheduler' ),
+ );
+
+ return $schedules;
+ }
+}
diff --git a/libraries/action-scheduler/classes/ActionScheduler_SystemInformation.php b/libraries/action-scheduler/classes/ActionScheduler_SystemInformation.php
new file mode 100644
index 0000000..188751e
--- /dev/null
+++ b/libraries/action-scheduler/classes/ActionScheduler_SystemInformation.php
@@ -0,0 +1,93 @@
+ 'plugin', # or 'theme'
+ * 'name' => 'Name',
+ * ]
+ *
+ * @return array
+ */
+ public static function active_source(): array {
+ $plugins = get_plugins();
+ $plugin_files = array_keys( $plugins );
+
+ foreach ( $plugin_files as $plugin_file ) {
+ $plugin_path = trailingslashit( WP_PLUGIN_DIR ) . dirname( $plugin_file );
+ $plugin_file = trailingslashit( WP_PLUGIN_DIR ) . $plugin_file;
+
+ if ( 0 !== strpos( dirname( __DIR__ ), $plugin_path ) ) {
+ continue;
+ }
+
+ $plugin_data = get_plugin_data( $plugin_file );
+
+ if ( ! is_array( $plugin_data ) || empty( $plugin_data['Name'] ) ) {
+ continue;
+ }
+
+ return array(
+ 'type' => 'plugin',
+ 'name' => $plugin_data['Name'],
+ );
+ }
+
+ $themes = (array) search_theme_directories();
+
+ foreach ( $themes as $slug => $data ) {
+ $needle = trailingslashit( $data['theme_root'] ) . $slug . '/';
+
+ if ( 0 !== strpos( __FILE__, $needle ) ) {
+ continue;
+ }
+
+ $theme = wp_get_theme( $slug );
+
+ if ( ! is_object( $theme ) || ! is_a( $theme, \WP_Theme::class ) ) {
+ continue;
+ }
+
+ return array(
+ 'type' => 'theme',
+ // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
+ 'name' => $theme->Name,
+ );
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns the directory path for the currently active installation of Action Scheduler.
+ *
+ * @return string
+ */
+ public static function active_source_path(): string {
+ return trailingslashit( dirname( __DIR__ ) );
+ }
+
+ /**
+ * Get registered sources.
+ *
+ * It is not always possible to obtain this information. For instance, if earlier versions (<=3.9.0) of
+ * Action Scheduler register themselves first, then the necessary data about registered sources will
+ * not be available.
+ *
+ * @return array
+ */
+ public static function get_sources() {
+ $versions = ActionScheduler_Versions::instance();
+ return method_exists( $versions, 'get_sources' ) ? $versions->get_sources() : array();
+ }
+}
diff --git a/libraries/action-scheduler/classes/ActionScheduler_Versions.php b/libraries/action-scheduler/classes/ActionScheduler_Versions.php
new file mode 100644
index 0000000..c23e464
--- /dev/null
+++ b/libraries/action-scheduler/classes/ActionScheduler_Versions.php
@@ -0,0 +1,151 @@
+
+ */
+ private $versions = array();
+
+ /**
+ * Registered sources.
+ *
+ * @var array
+ */
+ private $sources = array();
+
+ /**
+ * Register version's callback.
+ *
+ * @param string $version_string Action Scheduler version.
+ * @param callable $initialization_callback Callback to initialize the version.
+ */
+ public function register( $version_string, $initialization_callback ) {
+ if ( isset( $this->versions[ $version_string ] ) ) {
+ return false;
+ }
+
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
+ $backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS );
+ $source = $backtrace[0]['file'];
+
+ $this->versions[ $version_string ] = $initialization_callback;
+ $this->sources[ $source ] = $version_string;
+ return true;
+ }
+
+ /**
+ * Get all versions.
+ */
+ public function get_versions() {
+ return $this->versions;
+ }
+
+ /**
+ * Get registered sources.
+ *
+ * Use with caution: this method is only available as of Action Scheduler's 3.9.1
+ * release and, owing to the way Action Scheduler is loaded, it's possible that the
+ * class definition used at runtime will belong to an earlier version.
+ *
+ * @since 3.9.1
+ *
+ * @return array
+ */
+ public function get_sources() {
+ return $this->sources;
+ }
+
+ /**
+ * Get latest version registered.
+ */
+ public function latest_version() {
+ $keys = array_keys( $this->versions );
+ if ( empty( $keys ) ) {
+ return false;
+ }
+ uasort( $keys, 'version_compare' );
+ return end( $keys );
+ }
+
+ /**
+ * Get callback for latest registered version.
+ */
+ public function latest_version_callback() {
+ $latest = $this->latest_version();
+
+ if ( empty( $latest ) || ! isset( $this->versions[ $latest ] ) ) {
+ return '__return_null';
+ }
+
+ return $this->versions[ $latest ];
+ }
+
+ /**
+ * Get instance.
+ *
+ * @return ActionScheduler_Versions
+ * @codeCoverageIgnore
+ */
+ public static function instance() {
+ if ( empty( self::$instance ) ) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Initialize.
+ *
+ * @codeCoverageIgnore
+ */
+ public static function initialize_latest_version() {
+ $self = self::instance();
+ call_user_func( $self->latest_version_callback() );
+ }
+
+ /**
+ * Returns information about the plugin or theme which contains the current active version
+ * of Action Scheduler.
+ *
+ * If this cannot be determined, or if Action Scheduler is being loaded via some other
+ * method, then it will return an empty array. Otherwise, if populated, the array will
+ * look like the following:
+ *
+ * [
+ * 'type' => 'plugin', # or 'theme'
+ * 'name' => 'Name',
+ * ]
+ *
+ * @deprecated 3.9.2 Use ActionScheduler_SystemInformation::active_source().
+ *
+ * @return array
+ */
+ public function active_source(): array {
+ _deprecated_function( __METHOD__, '3.9.2', 'ActionScheduler_SystemInformation::active_source()' );
+ return ActionScheduler_SystemInformation::active_source();
+ }
+
+ /**
+ * Returns the directory path for the currently active installation of Action Scheduler.
+ *
+ * @deprecated 3.9.2 Use ActionScheduler_SystemInformation::active_source_path().
+ *
+ * @return string
+ */
+ public function active_source_path(): string {
+ _deprecated_function( __METHOD__, '3.9.2', 'ActionScheduler_SystemInformation::active_source_path()' );
+ return ActionScheduler_SystemInformation::active_source_path();
+ }
+}
diff --git a/libraries/action-scheduler/classes/ActionScheduler_WPCommentCleaner.php b/libraries/action-scheduler/classes/ActionScheduler_WPCommentCleaner.php
new file mode 100644
index 0000000..2725d1f
--- /dev/null
+++ b/libraries/action-scheduler/classes/ActionScheduler_WPCommentCleaner.php
@@ -0,0 +1,133 @@
+ Status administration screen.
+ add_action( 'load-tools_page_action-scheduler', array( __CLASS__, 'register_admin_notice' ) );
+ add_action( 'load-woocommerce_page_wc-status', array( __CLASS__, 'register_admin_notice' ) );
+ }
+
+ /**
+ * Determines if there are log entries in the wp comments table.
+ *
+ * Uses the flag set on migration completion set by @see self::maybe_schedule_cleanup().
+ *
+ * @return boolean Whether there are scheduled action comments in the comments table.
+ */
+ public static function has_logs() {
+ return 'yes' === get_option( self::$has_logs_option_key );
+ }
+
+ /**
+ * Schedules the WP Post comment table cleanup to run in 6 months if it's not already scheduled.
+ * Attached to the migration complete hook 'action_scheduler/migration_complete'.
+ */
+ public static function maybe_schedule_cleanup() {
+ $has_logs = 'no';
+
+ $args = array(
+ 'type' => ActionScheduler_wpCommentLogger::TYPE,
+ 'number' => 1,
+ 'fields' => 'ids',
+ );
+
+ if ( (bool) get_comments( $args ) ) {
+ $has_logs = 'yes';
+
+ if ( ! as_next_scheduled_action( self::$cleanup_hook ) ) {
+ as_schedule_single_action( gmdate( 'U' ) + ( 6 * MONTH_IN_SECONDS ), self::$cleanup_hook );
+ }
+ }
+
+ update_option( self::$has_logs_option_key, $has_logs, true );
+ }
+
+ /**
+ * Delete all action comments from the WP Comments table.
+ */
+ public static function delete_all_action_comments() {
+ global $wpdb;
+
+ $wpdb->delete(
+ $wpdb->comments,
+ array(
+ 'comment_type' => ActionScheduler_wpCommentLogger::TYPE,
+ 'comment_agent' => ActionScheduler_wpCommentLogger::AGENT,
+ )
+ );
+
+ update_option( self::$has_logs_option_key, 'no', true );
+ }
+
+ /**
+ * Registers admin notices about the orphaned action logs.
+ */
+ public static function register_admin_notice() {
+ add_action( 'admin_notices', array( __CLASS__, 'print_admin_notice' ) );
+ }
+
+ /**
+ * Prints details about the orphaned action logs and includes information on where to learn more.
+ */
+ public static function print_admin_notice() {
+ $next_cleanup_message = '';
+ $next_scheduled_cleanup_hook = as_next_scheduled_action( self::$cleanup_hook );
+
+ if ( $next_scheduled_cleanup_hook ) {
+ /* translators: %s: date interval */
+ $next_cleanup_message = sprintf( __( 'This data will be deleted in %s.', 'action-scheduler' ), human_time_diff( gmdate( 'U' ), $next_scheduled_cleanup_hook ) );
+ }
+
+ $notice = sprintf(
+ /* translators: 1: next cleanup message 2: github issue URL */
+ __( 'Action Scheduler has migrated data to custom tables; however, orphaned log entries exist in the WordPress Comments table. %1$s Learn more »', 'action-scheduler' ),
+ $next_cleanup_message,
+ 'https://github.com/woocommerce/action-scheduler/issues/368'
+ );
+
+ echo '
' . wp_kses_post( $notice ) . '
';
+ }
+}
diff --git a/libraries/action-scheduler/classes/ActionScheduler_wcSystemStatus.php b/libraries/action-scheduler/classes/ActionScheduler_wcSystemStatus.php
new file mode 100644
index 0000000..bca63e7
--- /dev/null
+++ b/libraries/action-scheduler/classes/ActionScheduler_wcSystemStatus.php
@@ -0,0 +1,166 @@
+store = $store;
+ }
+
+ /**
+ * Display action data, including number of actions grouped by status and the oldest & newest action in each status.
+ *
+ * Helpful to identify issues, like a clogged queue.
+ */
+ public function render() {
+ $action_counts = $this->store->action_counts();
+ $status_labels = $this->store->get_status_labels();
+ $oldest_and_newest = $this->get_oldest_and_newest( array_keys( $status_labels ) );
+
+ $this->get_template( $status_labels, $action_counts, $oldest_and_newest );
+ }
+
+ /**
+ * Get oldest and newest scheduled dates for a given set of statuses.
+ *
+ * @param array $status_keys Set of statuses to find oldest & newest action for.
+ * @return array
+ */
+ protected function get_oldest_and_newest( $status_keys ) {
+
+ $oldest_and_newest = array();
+
+ foreach ( $status_keys as $status ) {
+ $oldest_and_newest[ $status ] = array(
+ 'oldest' => '–',
+ 'newest' => '–',
+ );
+
+ if ( 'in-progress' === $status ) {
+ continue;
+ }
+
+ $oldest_and_newest[ $status ]['oldest'] = $this->get_action_status_date( $status, 'oldest' );
+ $oldest_and_newest[ $status ]['newest'] = $this->get_action_status_date( $status, 'newest' );
+ }
+
+ return $oldest_and_newest;
+ }
+
+ /**
+ * Get oldest or newest scheduled date for a given status.
+ *
+ * @param string $status Action status label/name string.
+ * @param string $date_type Oldest or Newest.
+ * @return DateTime
+ */
+ protected function get_action_status_date( $status, $date_type = 'oldest' ) {
+
+ $order = 'oldest' === $date_type ? 'ASC' : 'DESC';
+
+ $action = $this->store->query_actions(
+ array(
+ 'claimed' => false,
+ 'status' => $status,
+ 'per_page' => 1,
+ 'order' => $order,
+ )
+ );
+
+ if ( ! empty( $action ) ) {
+ $date_object = $this->store->get_date( $action[0] );
+ $action_date = $date_object->format( 'Y-m-d H:i:s O' );
+ } else {
+ $action_date = '–';
+ }
+
+ return $action_date;
+ }
+
+ /**
+ * Get oldest or newest scheduled date for a given status.
+ *
+ * @param array $status_labels Set of statuses to find oldest & newest action for.
+ * @param array $action_counts Number of actions grouped by status.
+ * @param array $oldest_and_newest Date of the oldest and newest action with each status.
+ */
+ protected function get_template( $status_labels, $action_counts, $oldest_and_newest ) {
+ $as_version = ActionScheduler_Versions::instance()->latest_version();
+ $as_datastore = get_class( ActionScheduler_Store::instance() );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $count ) {
+ // WC uses the 3rd column for export, so we need to display more data in that (hidden when viewed as part of the table) and add an empty 2nd column.
+ printf(
+ '
';
+ }
+
+ /**
+ * Set the data for displaying. It will attempt to unserialize (There is a chance that some columns
+ * are serialized). This can be override in child classes for further data transformation.
+ *
+ * @param array $items Items array.
+ */
+ protected function set_items( array $items ) {
+ $this->items = array();
+ foreach ( $items as $item ) {
+ $this->items[ $item[ $this->ID ] ] = array_map( 'maybe_unserialize', $item );
+ }
+ }
+
+ /**
+ * Renders the checkbox for each row, this is the first column and it is named ID regardless
+ * of how the primary key is named (to keep the code simpler). The bulk actions will do the proper
+ * name transformation though using `$this->ID`.
+ *
+ * @param array $row The row to render.
+ */
+ public function column_cb( $row ) {
+ return '';
+ }
+
+ /**
+ * Renders the row-actions.
+ *
+ * This method renders the action menu, it reads the definition from the $row_actions property,
+ * and it checks that the row action method exists before rendering it.
+ *
+ * @param array $row Row to be rendered.
+ * @param string $column_name Column name.
+ * @return string
+ */
+ protected function maybe_render_actions( $row, $column_name ) {
+ if ( empty( $this->row_actions[ $column_name ] ) ) {
+ return;
+ }
+
+ $row_id = $row[ $this->ID ];
+
+ $actions = '
';
+ }
+ }
+
+ /**
+ * Renders the table list, we override the original class to render the table inside a form
+ * and to render any needed HTML (like the search box). By doing so the callee of a function can simple
+ * forget about any extra HTML.
+ */
+ protected function display_table() {
+ echo '';
+ }
+
+ /**
+ * Process any pending actions.
+ */
+ public function process_actions() {
+ $this->process_bulk_action();
+ $this->process_row_actions();
+
+ if ( ! empty( $_REQUEST['_wp_http_referer'] ) && ! empty( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ // _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
+ wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) );
+ exit;
+ }
+ }
+
+ /**
+ * Render the list table page, including header, notices, status filters and table.
+ */
+ public function display_page() {
+ $this->prepare_items();
+
+ echo '
A scalable, traceable job queue for background processing large queues of tasks in WordPress. Designed for distribution in WordPress plugins - no server access required.
+
+
+
+
+
+ {{ content }}
+
+
+
+
+
+ {% if site.google_analytics %}
+
+ {% endif %}
+
+
\ No newline at end of file
diff --git a/libraries/action-scheduler/docs/admin.md b/libraries/action-scheduler/docs/admin.md
new file mode 100644
index 0000000..18769eb
--- /dev/null
+++ b/libraries/action-scheduler/docs/admin.md
@@ -0,0 +1,22 @@
+---
+description: Learn how to administer background jobs with the Action Scheduler job queue for WordPress.
+---
+# Scheduled Actions Administration Screen
+
+Action Scheduler has a built in administration screen for monitoring, debugging and manually triggering scheduled actions.
+
+The administration interface is accessible through both:
+
+1. **Tools > Scheduled Actions**
+1. **WooCommerce > Status > Scheduled Actions**, when WooCommerce is installed.
+
+Among other tasks, from the admin screen you can:
+
+* run a pending action
+* view the scheduled actions with a specific status, like the all actions which have failed or are in-progress (https://cldup.com/NNTwE88Xl8.png).
+* view the log entries for a specific action to find out why it failed.
+* sort scheduled actions by hook name, scheduled date, claim ID or group name.
+
+Still have questions? Check out the [FAQ](/faq).
+
+
diff --git a/libraries/action-scheduler/docs/android-chrome-192x192.png b/libraries/action-scheduler/docs/android-chrome-192x192.png
new file mode 100644
index 0000000..36475ec
Binary files /dev/null and b/libraries/action-scheduler/docs/android-chrome-192x192.png differ
diff --git a/libraries/action-scheduler/docs/android-chrome-256x256.png b/libraries/action-scheduler/docs/android-chrome-256x256.png
new file mode 100644
index 0000000..881cb44
Binary files /dev/null and b/libraries/action-scheduler/docs/android-chrome-256x256.png differ
diff --git a/libraries/action-scheduler/docs/api.md b/libraries/action-scheduler/docs/api.md
new file mode 100644
index 0000000..e60f201
--- /dev/null
+++ b/libraries/action-scheduler/docs/api.md
@@ -0,0 +1,253 @@
+---
+description: Reference guide for background processing functions provided by the Action Scheduler job queue for WordPress.
+---
+# API Reference
+
+Action Scheduler provides a range of functions for scheduling hooks to run at some time in the future on one or more occasions.
+
+To understand the scheduling functions, it can help to think of them as extensions to WordPress' `do_action()` function that add the ability to delay and repeat when the hook will be triggered.
+
+## WP-Cron APIs vs. Action Scheduler APIs
+
+The Action Scheduler API functions are designed to mirror the WordPress [WP-Cron API functions](http://codex.wordpress.org/Category:WP-Cron_Functions).
+
+Functions return similar values and accept similar arguments to their WP-Cron counterparts. The notable differences are:
+
+* `as_schedule_single_action()` & `as_schedule_recurring_action()` will return the ID of the scheduled action rather than boolean indicating whether the event was scheduled
+* `as_schedule_recurring_action()` takes an interval in seconds as the recurring interval rather than an arbitrary string
+* `as_schedule_single_action()` & `as_schedule_recurring_action()` can accept a `$group` parameter to group different actions for the one plugin together.
+* the `wp_` prefix is substituted with `as_` and the term `event` is replaced with `action`
+
+## API Function Availability
+
+As mentioned in the [Usage - Load Order](usage.md#load-order) section, Action Scheduler will initialize itself on the `'init'` hook with priority `1`. While API functions are loaded prior to this and can be called, they should not be called until after `'init'` with priority `1`, because each component, like the data store, has not yet been initialized.
+
+Do not use Action Scheduler API functions prior to `'init'` hook with priority `1`. Doing so could lead to unexpected results, like data being stored in the incorrect location. To make this easier:
+
+- Action Scheduler provides `Action_Scheduler::is_initialized()` for use in hooks to confirm that the data stores have been initialized.
+- It also provides the `'action_scheduler_init'` action hook. It is safe to call API functions during or after this event has fired (tip: you can take advantage of WordPress's [did_action()](https://developer.wordpress.org/reference/functions/did_action/) function to check this).
+
+## Function Reference / `as_enqueue_async_action()`
+
+### Description
+
+Enqueue an action to run one time, as soon as possible.
+
+### Usage
+
+```php
+as_enqueue_async_action( $hook, $args, $group, $unique, $priority );
+```
+
+### Parameters
+
+- **$hook** (string)(required) Name of the action hook.
+- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (string) The group to assign this job to. Default: _''_.
+- **$unique** (boolean) Whether the action should be unique. Default: _`false`_.
+- **$priority** (integer) Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255.
+
+### Return value
+
+`(integer)` the action's ID. Zero if there was an error scheduling the action. The error will be sent to `error_log`.
+
+## Function Reference / `as_schedule_single_action()`
+
+### Description
+
+Schedule an action to run one time at some defined point in the future.
+
+### Usage
+
+```php
+as_schedule_single_action( $timestamp, $hook, $args, $group, $unique, $priority );
+```
+
+### Parameters
+
+- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run.
+- **$hook** (string)(required) Name of the action hook.
+- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (string) The group to assign this job to. Default: _''_.
+- **$unique** (boolean) Whether the action should be unique. Default: _`false`_.
+- **$priority** (integer) Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255.)
+
+### Return value
+
+`(integer)` the action's ID. Zero if there was an error scheduling the action. The error will be sent to `error_log`.
+
+## Function Reference / `as_schedule_recurring_action()`
+
+### Description
+
+Schedule an action to run repeatedly with a specified interval in seconds.
+
+### Usage
+
+```php
+as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group, $unique, $priority );
+```
+
+### Parameters
+
+- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run.
+- **$interval_in_seconds** (integer)(required) How long to wait between runs.
+- **$hook** (string)(required) Name of the action hook.
+- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (string) The group to assign this job to. Default: _''_.
+- **$unique** (boolean) Whether the action should be unique. Default: _`false`_.
+- **$priority** (integer) Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255.
+
+### Return value
+
+`(integer)` the action's ID. Zero if there was an error scheduling the action. The error will be sent to `error_log`.
+
+## Function Reference / `as_schedule_cron_action()`
+
+### Description
+
+Schedule an action that recurs on a cron-like schedule.
+
+If execution of a cron-like action is delayed, the next attempt will still be scheduled according to the provided cron expression.
+
+### Usage
+
+```php
+as_schedule_cron_action( $timestamp, $schedule, $hook, $args, $group, $unique, $priority );
+```
+
+### Parameters
+
+- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run.
+- **$schedule** (string)(required) $schedule A cron-like schedule string, see http://en.wikipedia.org/wiki/Cron.
+- **$hook** (string)(required) Name of the action hook.
+- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (string) The group to assign this job to. Default: _''_.
+- **$unique** (boolean) Whether the action should be unique. Default: _`false`_.
+- **$priority** (integer) Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255.
+
+### Return value
+
+`(integer)` the action's ID. Zero if there was an error scheduling the action. The error will be sent to `error_log`.
+
+## Function Reference / `as_unschedule_action()`
+
+### Description
+
+Cancel the next occurrence of a scheduled action.
+
+### Usage
+
+```php
+as_unschedule_action( $hook, $args, $group );
+```
+
+### Parameters
+
+- **$hook** (string)(required) Name of the action hook.
+- **$args** (array) Arguments passed to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (string) The group the job is assigned to. Default: _''_.
+
+### Return value
+
+`(null)`
+
+## Function Reference / `as_unschedule_all_actions()`
+
+### Description
+
+Cancel all occurrences of a scheduled action.
+
+### Usage
+
+```php
+as_unschedule_all_actions( $hook, $args, $group )
+```
+
+### Parameters
+
+- **$hook** (string)(required) Name of the action hook.
+- **$args** (array) Arguments passed to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (string) The group the job is assigned to. Default: _''_.
+
+### Return value
+
+`(string|null)` The scheduled action ID if a scheduled action was found, or null if no matching action found.
+
+## Function Reference / `as_next_scheduled_action()`
+
+### Description
+
+Returns the next timestamp for a scheduled action.
+
+### Usage
+
+```php
+as_next_scheduled_action( $hook, $args, $group );
+```
+
+### Parameters
+
+- **$hook** (string)(required) Name of the action hook. Default: _none_.
+- **$args** (array) Arguments passed to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (string) The group the job is assigned to. Default: _''_.
+
+### Return value
+
+`(integer|boolean)` The timestamp for the next occurrence of a pending scheduled action, true for an async or in-progress action or false if there is no matching action.
+
+## Function Reference / `as_has_scheduled_action()`
+
+### Description
+
+Check if there is a scheduled action in the queue, but more efficiently than as_next_scheduled_action(). It's recommended to use this function when you need to know whether a specific action is currently scheduled. _Available since 3.3.0._
+
+### Usage
+
+```php
+as_has_scheduled_action( $hook, $args, $group );
+```
+
+### Parameters
+
+- **$hook** (string)(required) Name of the action hook. Default: _none_.
+- **$args** (array) Arguments passed to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (string) The group the job is assigned to. Default: _''_.
+
+### Return value
+
+`(boolean)` True if a matching action is pending or in-progress, false otherwise.
+
+## Function Reference / `as_get_scheduled_actions()`
+
+### Description
+
+Find scheduled actions.
+
+### Usage
+
+```php
+as_get_scheduled_actions( $args, $return_format );
+```
+
+### Parameters
+
+- **$args** (array) Arguments to search and filter results by. Possible arguments, with their default values:
+ * `'hook' => ''` - the name of the action that will be triggered
+ * `'args' => NULL` - the args array that will be passed with the action
+ * `'date' => NULL` - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime().
+ * `'date_compare' => '<=`' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='
+ * `'modified' => NULL` - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime().
+ * `'modified_compare' => '<='` - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='
+ * `'group' => ''` - the group the action belongs to
+ * `'status' => ''` - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING
+ * `'claimed' => NULL` - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID
+ * `'per_page' => 5` - Number of results to return
+ * `'offset' => 0`
+ * `'orderby' => 'date'` - accepted values are 'hook', 'group', 'modified', or 'date'
+ * `'order' => 'ASC'`
+- **$return_format** (string) The format in which to return the scheduled actions: 'OBJECT', 'ARRAY_A', or 'ids'. Default: _'OBJECT'_.
+
+### Return value
+
+`(array)` Array of action rows matching the criteria specified with `$args`.
diff --git a/libraries/action-scheduler/docs/apple-touch-icon.png b/libraries/action-scheduler/docs/apple-touch-icon.png
new file mode 100644
index 0000000..36475ec
Binary files /dev/null and b/libraries/action-scheduler/docs/apple-touch-icon.png differ
diff --git a/libraries/action-scheduler/docs/assets/css/style.scss b/libraries/action-scheduler/docs/assets/css/style.scss
new file mode 100644
index 0000000..a13de0d
--- /dev/null
+++ b/libraries/action-scheduler/docs/assets/css/style.scss
@@ -0,0 +1,57 @@
+---
+---
+
+@import "{{ site.theme }}";
+
+a {
+ text-shadow: none;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+header h1 a {
+ color: #b5e853;
+}
+
+.container {
+ max-width: 700px;
+}
+
+footer {
+ margin-top: 6em;
+ padding: 1.6em 0;
+ border-top: 1px dashed #b5e853;
+}
+
+.footer-image {
+ text-align: center;
+ padding-top: 1em;
+}
+
+.github-corner:hover .octo-arm {
+ animation:octocat-wave 560ms ease-in-out
+}
+
+@keyframes octocat-wave {
+ 0%,100%{
+ transform:rotate(0)
+ }
+ 20%,60%{
+ transform:rotate(-25deg)
+ }
+ 40%,80%{
+ transform:rotate(10deg)
+ }
+}
+
+@media (max-width:500px){
+ .github-corner:hover .octo-arm {
+ animation:none
+ }
+ .github-corner .octo-arm {
+ animation:octocat-wave 560ms ease-in-out
+ }
+}
\ No newline at end of file
diff --git a/libraries/action-scheduler/docs/browserconfig.xml b/libraries/action-scheduler/docs/browserconfig.xml
new file mode 100644
index 0000000..f6244e6
--- /dev/null
+++ b/libraries/action-scheduler/docs/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ #151515
+
+
+
diff --git a/libraries/action-scheduler/docs/faq.md b/libraries/action-scheduler/docs/faq.md
new file mode 100644
index 0000000..e937032
--- /dev/null
+++ b/libraries/action-scheduler/docs/faq.md
@@ -0,0 +1,129 @@
+## FAQ
+
+### Is it safe to release Action Scheduler in my plugin? Won't its functions conflict with another copy of the library?
+
+Action Scheduler is designed to be used and released in plugins. It avoids redeclaring public API functions when more than one copy of the library is being loaded by different plugins. It will also load only the most recent version of itself (by checking registered versions after all plugins are loaded on the `'plugins_loaded'` hook).
+
+To use it in your plugin (or theme), simply require the `action-scheduler/action-scheduler.php` file. Action Scheduler will take care of the rest. __Note:__ Action Scheduler is only loaded from a theme if it is not included in any active plugins.
+
+### I don't want to use WP-Cron. Does Action Scheduler depend on WP-Cron?
+
+By default, Action Scheduler is initiated by WP-Cron (and the `'shutdown'` hook on admin requests). However, it has no dependency on the WP-Cron system. You can initiate the Action Scheduler queue in other ways with just one or two lines of code.
+
+For example, you can start a queue directly by calling:
+
+```php
+ActionScheduler::runner()->run();
+```
+
+Or trigger the `'action_scheduler_run_queue'` hook and let Action Scheduler do it for you:
+
+```php
+do_action( 'action_scheduler_run_queue', $context_identifier );
+```
+
+Further customization can be done by extending the `ActionScheduler_Abstract_QueueRunner` class to create a custom Queue Runner. For an example of a customized queue runner, see the [`ActionScheduler_WPCLI_QueueRunner`](https://github.com/woocommerce/action-scheduler/blob/trunk/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php), which is used when running WP CLI.
+
+Want to create some other method for initiating Action Scheduler? [Open a new issue](https://github.com/woocommerce/action-scheduler/issues/new), we'd love to help you with it.
+
+### I don't want to use WP-Cron, ever. Does Action Scheduler replace WP-Cron?
+
+By default, Action Scheduler is designed to work alongside WP-Cron and not change any of its behaviour. This helps avoid unexpectedly overriding WP-Cron on sites installing your plugin, which may have nothing to do with WP-Cron.
+
+However, we can understand why you might want to replace WP-Cron completely in environments within your control, especially as it gets you the advantages of Action Scheduler. This should be possible without too much code.
+
+You could use the `'schedule_event'` hook in WordPress to use Action Scheduler for only newly scheduled WP-Cron jobs and map the `$event` param to Action Scheduler API functions.
+
+Alternatively, you can use a combination of the `'pre_update_option_cron'` and `'pre_option_cron'` hooks to override all new and previously scheduled WP-Cron jobs (similar to the way [Cavalcade](https://github.com/humanmade/Cavalcade) does it).
+
+If you'd like to create a plugin to do this automatically and want to share your work with others, [open a new issue to let us know](https://github.com/woocommerce/action-scheduler/issues/new), we'd love to help you with it.
+
+### How does Action Scheduler store its data?
+
+Action Scheduler 3.0 and newer stores data in custom tables prefixed with `actionscheduler_`. For the list of all tables and their schemas, refer to the `ActionScheduler_StoreSchema` class.
+
+Prior to Action 3.0, actions were a custom post type, and data was stored in `wp_posts`, `wp_postmeta` and related tables.
+
+Action Scheduler 3+ migrates data from the custom post type to custom tables.
+
+### Can I use a different storage scheme?
+
+Of course! Action Scheduler data storage is completely swappable, and always has been.
+
+If you choose to, you can actually store them anywhere, like in a remote storage service from Amazon Web Services.
+
+To implement a custom store:
+
+1. extend the abstract `ActionScheduler_Store` class, being careful to implement each of its methods
+2. attach a callback to `'action_scheduler_store_class'` to tell Action Scheduler your class is the one which should be used to manage storage, e.g.
+
+```
+function eg_define_custom_store( $existing_storage_class ) {
+ return 'My_Radical_Action_Scheduler_Store';
+}
+add_filter( 'action_scheduler_store_class', 'eg_define_custom_store', 10, 1 );
+```
+
+Take a look at the `classes/data-stores/ActionScheduler_DBStore.php` class for an example implementation of `ActionScheduler_Store`.
+
+If you'd like to create a plugin to do this automatically and release it publicly to help others, [open a new issue to let us know](https://github.com/woocommerce/action-scheduler/issues/new), we'd love to help you with it.
+
+### Can I use a different storage scheme just for logging?
+
+Of course! Action Scheduler's logger is completely swappable, and always has been. You can also customise where logs are stored, and the storage mechanism.
+
+To implement a custom logger:
+
+1. extend the abstract `ActionScheduler_Logger` class, being careful to implement each of its methods
+2. attach a callback to `'action_scheduler_logger_class'` to tell Action Scheduler your class is the one which should be used to manage logging, e.g.
+
+```
+function eg_define_custom_logger( $existing_storage_class ) {
+ return 'My_Radical_Action_Scheduler_Logger';
+}
+add_filter( 'action_scheduler_logger_class', 'eg_define_custom_logger', 10, 1 );
+```
+
+Take a look at the `classes/data-stores/ActionScheduler_DBLogger.php` class for an example implementation of `ActionScheduler_Logger`.
+
+### I want to run Action Scheduler only on a dedicated application server in my cluster. Can I do that?
+
+Wow, now you're really asking the tough questions. In theory, yes, this is possible. The `ActionScheduler_QueueRunner` class, which is responsible for running queues, is swappable via the `'action_scheduler_queue_runner_class'` filter.
+
+Because of this, you can effectively customise queue running however you need. Whether that means tweaking minor things, like not using WP-Cron at all to initiate queues by overriding `ActionScheduler_QueueRunner::init()`, or completely changing how and where queues are run, by overriding `ActionScheduler_QueueRunner::run()`.
+
+### Is Action Scheduler safe to use on my production site?
+
+Yes, absolutely! Action Scheduler is actively used on tens of thousands of production sites already. Right now it's responsible for scheduling everything from emails to payments.
+
+In fact, every month, Action Scheduler processes millions of payments as part of the [WooCommerce Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/) extension.
+
+It requires no setup, and won't override any WordPress APIs (unless you want it to).
+
+### How does Action Scheduler work on WordPress Multisite?
+
+Action Scheduler is designed to manage the scheduled actions on a single site. It has no special handling for running queues across multiple sites in a multisite network. That said, because its storage and Queue Runner are completely swappable, it would be possible to write multisite handling classes to use with it.
+
+If you'd like to create a multisite plugin to do this and release it publicly to help others, [open a new issue to let us know](https://github.com/woocommerce/action-scheduler/issues/new), we'd love to help you with it.
+
+### How can I change the Action Scheduler User-Agent to better identify its requests?
+
+Action Scheduler has a filter available named `as_async_request_queue_runner_post_args` which can be used to filter the arguments that are being sent to the `wp_remote_post` call.
+
+The User-Agent parameter is just one of them and can be adjusted as follows:
+
+```
+function eg_define_custom_user_agent( $args ) {
+ $versions = ActionScheduler_Versions::instance();
+ $args['user-agent'] = 'Action Scheduler/' . $versions->latest_version();
+
+ return $args;
+}
+add_filter( 'as_async_request_queue_runner_post_args', 'eg_define_custom_user_agent', 10, 1 );
+```
+
+### My site has past-due actions; what can I do?
+
+Actions that are past-due have missed their scheduled date; because of [how WP Cron works](https://developer.wordpress.org/plugins/cron/), it is normal to have some past-due actions.
+
+If there are several past-due actions more than one day old, there may be something wrong with your site. If you are using WooCommerce and need help determining the issue, please [contact us on our forum page](https://wordpress.org/support/plugin/woocommerce/).
diff --git a/libraries/action-scheduler/docs/favicon-16x16.png b/libraries/action-scheduler/docs/favicon-16x16.png
new file mode 100644
index 0000000..c46600f
Binary files /dev/null and b/libraries/action-scheduler/docs/favicon-16x16.png differ
diff --git a/libraries/action-scheduler/docs/favicon-32x32.png b/libraries/action-scheduler/docs/favicon-32x32.png
new file mode 100644
index 0000000..7d08869
Binary files /dev/null and b/libraries/action-scheduler/docs/favicon-32x32.png differ
diff --git a/libraries/action-scheduler/docs/favicon.ico b/libraries/action-scheduler/docs/favicon.ico
new file mode 100644
index 0000000..03560e3
Binary files /dev/null and b/libraries/action-scheduler/docs/favicon.ico differ
diff --git a/libraries/action-scheduler/docs/google14ef723abb376cd3.html b/libraries/action-scheduler/docs/google14ef723abb376cd3.html
new file mode 100644
index 0000000..f3bf171
--- /dev/null
+++ b/libraries/action-scheduler/docs/google14ef723abb376cd3.html
@@ -0,0 +1 @@
+google-site-verification: google14ef723abb376cd3.html
\ No newline at end of file
diff --git a/libraries/action-scheduler/docs/index.md b/libraries/action-scheduler/docs/index.md
new file mode 100644
index 0000000..ae7af33
--- /dev/null
+++ b/libraries/action-scheduler/docs/index.md
@@ -0,0 +1,66 @@
+---
+title: Action Scheduler - Background Processing Job Queue for WordPress
+---
+## WordPress Job Queue with Background Processing
+
+Action Scheduler is a library for triggering a WordPress hook to run at some time in the future (or as soon as possible, in the case of an async action). 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.
+
+It just so happens, this functionality also creates a robust job queue for background processing large queues of tasks in WordPress. With the addition of logging and an [administration interface](/admin/), it also provides traceability on your tasks processed in the background.
+
+### Battle-Tested Background Processing
+
+Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins.
+
+It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, in 10 concurrent queues at a rate of over 10,000 actions / hour without negatively impacting normal site operations.
+
+This is all possible on infrastructure and WordPress sites outside the control of the plugin author.
+
+Action Scheduler is specifically designed for distribution in WordPress plugins (and themes) - no server access required. If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help.
+
+### How it Works
+
+Action Scheduler stores the hook name, arguments and scheduled date for an action that should be triggered at some time in the future.
+
+The scheduler will attempt to run every minute by attaching itself as a callback to the `'action_scheduler_run_schedule'` hook, which is scheduled using WordPress's built-in [WP-Cron](http://codex.wordpress.org/Function_Reference/wp_cron) system. Once per minute on, it will also check on the `'shutdown'` hook of WP Admin requests whether there are pending actions, and if there are, it will initiate a queue via an async loopback request.
+
+When triggered, Action Scheduler will check for scheduled actions that have a due date at or before this point in time i.e. actions scheduled to run now or at sometime in the past. Action scheduled to run asynchronously, i.e. not scheduled, have a zero date, meaning they will always be due no matter when the check occurs.
+
+### Batch Processing Background Jobs
+
+If there are actions to be processed, Action Scheduler will stake a unique claim for a batch of 25 actions and begin processing that batch. The PHP process spawned to run the batch will then continue processing batches of 25 actions until it uses 90% of available memory or has been processing for 30 seconds.
+
+At that point, if there are additional actions to process, an asynchronous loopback request will be made to the site to continue processing actions in a new request.
+
+This process and the loopback requests will continue until all actions are processed.
+
+### Housekeeping
+
+Before processing a batch, the scheduler will remove any existing claims on actions which have been sitting in a queue for more than five minutes (or more specifically, 10 times the allowed time limit, which defaults to 30 seconds).
+
+Action Scheduler will also delete any actions which were completed or canceled more than a month ago.
+
+If an action runs for more than 5 minutes, Action Scheduler will assume the action has timed out and will mark it as failed. However, if all callbacks attached to the action were to successfully complete sometime after that 5 minute timeout, its status would later be updated to completed.
+
+### Traceable Background Processing
+
+Did your background job run?
+
+Never be left wondering with Action Scheduler's built-in record keeping.
+
+All events for each action are logged in the `actionscheduler_logs` table and displayed in the [administration interface](/admin/).
+
+The events logged by default include when an action:
+ * is created
+ * starts (including details of how it was run, e.g. via [WP CLI](/wp-cli/) or WP Cron)
+ * completes
+ * fails
+
+If it fails with an error that can be recorded, that error will be recorded in the log and visible in administration interface, making it possible to trace what went wrong at some point in the past on a site you didn't have access to in the past.
+
+## Credits
+
+Action Scheduler is developed and maintained by folks at [Automattic](http://automattic.com/) with significant early development completed by [Flightless](https://flightless.us/).
+
+Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/woocommerce/action-scheduler/pulls) welcome.
diff --git a/libraries/action-scheduler/docs/mstile-150x150.png b/libraries/action-scheduler/docs/mstile-150x150.png
new file mode 100644
index 0000000..3a3ed19
Binary files /dev/null and b/libraries/action-scheduler/docs/mstile-150x150.png differ
diff --git a/libraries/action-scheduler/docs/perf.md b/libraries/action-scheduler/docs/perf.md
new file mode 100644
index 0000000..f272b95
--- /dev/null
+++ b/libraries/action-scheduler/docs/perf.md
@@ -0,0 +1,149 @@
+---
+title: WordPress Background Processing at Scale - Action Scheduler Job Queue
+description: Learn how to do WordPress background processing at scale by tuning the Action Scheduler job queue's default WP Cron runner.
+---
+# Background Processing at Scale
+
+Action Scheduler's default processing is designed to work reliably across all different hosting environments. To achieve that, the default processing thresholds are more conservative than many servers could support.
+
+Specifically, Action Scheduler will only process actions in a request until:
+
+* 90% of available memory is used
+* processing another 3 actions would exceed 30 seconds of total request time, based on the average processing time for the current batch
+* in a single concurrent queue
+
+While Action Scheduler will initiate an asynchronous loopback request to process additional actions, on sites with very large queues, this can result in slow processing times.
+
+While using [WP CLI to process queues](/wp-cli/) is the best approach to increasing processing speed, on occasion, that is not a viable option. In these cases, it's also possible to increase the processing thresholds in Action Scheduler to increase the rate at which actions are processed by the default queue runner.
+
+## Increasing Time Limit
+
+By default, Action Scheduler will only process actions for a maximum of 30 seconds in each request. This time limit minimizes the risk of a script timeout on unknown hosting environments, some of which enforce 30-second timeouts.
+
+If you know your host supports longer than this time limit for web requests, you can increase this time limit. This allows more actions to be processed in each request and reduces the lag between processing each queue, greatly speeding up the processing rate of scheduled actions.
+
+For example, the following snippet will increase the time limit to 2 minutes (120 seconds):
+
+```php
+function eg_increase_time_limit( $time_limit ) {
+ return 120;
+}
+add_filter( 'action_scheduler_queue_runner_time_limit', 'eg_increase_time_limit' );
+```
+
+Some of the known host time limits are:
+
+* 60 seconds on WP Engine
+* 120 seconds on Pantheon
+* 120 seconds on SiteGround
+
+## Increasing Batch Size
+
+By default, Action Scheduler will claim a batch of 25 actions. This small batch size is because the default time limit is only 30 seconds; however, if you know your actions are processing very quickly, e.g. taking milliseconds not seconds, or that you have more than 30 seconds available to process each batch, increasing the batch size can slightly improve performance.
+
+This is because claiming a batch has some overhead, so the less often a batch needs to be claimed, the faster actions can be processed.
+
+For example, to increase the batch size to 100, we can use the following function:
+
+```php
+function eg_increase_action_scheduler_batch_size( $batch_size ) {
+ return 100;
+}
+add_filter( 'action_scheduler_queue_runner_batch_size', 'eg_increase_action_scheduler_batch_size' );
+```
+
+## Increasing Concurrent Batches
+
+By default, Action Scheduler will run only one concurrent batch of actions. This is to prevent consuming a lot of available connections or processes on your web server.
+
+However, your server may allow a large number of connections, for example, because it has a high value for Apache's `MaxClients` setting or PHP-FPM's `pm.max_children` setting.
+
+If this is the case, you can use the `'action_scheduler_queue_runner_concurrent_batches'` filter to increase the number of concurrent batches allowed, and therefore speed up large numbers of actions scheduled to be processed simultaneously.
+
+For example, to increase the allowed number of concurrent queues to 10, we can use the following code:
+
+```php
+function eg_increase_action_scheduler_concurrent_batches( $concurrent_batches ) {
+ return 10;
+}
+add_filter( 'action_scheduler_queue_runner_concurrent_batches', 'eg_increase_action_scheduler_concurrent_batches' );
+```
+
+> WARNING: because the async queue runner introduced in Action Scheduler 3.0 will continue asynchronous loopback requests to process actions, increasing the number of concurrent batches can substantially increase server load and take down a site. [WP CLI](/wp-cli/) is a better method to achieve higher throughput.
+
+## Increasing Initialisation Rate of Runners
+
+By default, Action Scheduler initiates at most one queue runner every time the `'action_scheduler_run_queue'` action is triggered by WP Cron.
+
+Because this action is only triggered at most once every minute, then there will rarely be more than one queue processing actions even if the concurrent runners are increased.
+
+To handle larger queues on more powerful servers, it's possible to initiate additional queue runners whenever the `'action_scheduler_run_queue'` action is run.
+
+That can be done by initiating additional secure requests to our server via loopback requests.
+
+The code below demonstrates how to create 5 loopback requests each time a queue begins:
+
+```php
+/**
+ * Trigger 5 additional loopback requests with unique URL params.
+ */
+function eg_request_additional_runners() {
+
+ // allow self-signed SSL certificates
+ add_filter( 'https_local_ssl_verify', '__return_false', 100 );
+
+ for ( $i = 0; $i < 5; $i++ ) {
+ $response = wp_remote_post( admin_url( 'admin-ajax.php' ), array(
+ 'method' => 'POST',
+ 'timeout' => 45,
+ 'redirection' => 5,
+ 'httpversion' => '1.0',
+ 'blocking' => false,
+ 'headers' => array(),
+ 'body' => array(
+ 'action' => 'eg_create_additional_runners',
+ 'instance' => $i,
+ 'eg_nonce' => wp_create_nonce( 'eg_additional_runner_' . $i ),
+ ),
+ 'cookies' => array(),
+ ) );
+ }
+}
+add_action( 'action_scheduler_run_queue', 'eg_request_additional_runners', 0 );
+
+/**
+ * Handle requests initiated by eg_request_additional_runners() and start a queue runner if the request is valid.
+ */
+function eg_create_additional_runners() {
+
+ if ( isset( $_POST['eg_nonce'] ) && isset( $_POST['instance'] ) && wp_verify_nonce( $_POST['eg_nonce'], 'eg_additional_runner_' . $_POST['instance'] ) ) {
+ ActionScheduler_QueueRunner::instance()->run();
+ }
+
+ wp_die();
+}
+add_action( 'wp_ajax_nopriv_eg_create_additional_runners', 'eg_create_additional_runners', 0 );
+```
+
+> WARNING: because of the processing rate of scheduled actions, this kind of increase can very easily take down a site. Use only on high-powered servers and be sure to test before attempting to use it in production.
+
+## Cleaning Failed Actions
+
+By default, Action Scheduler does not automatically delete old failed actions. There are two optional methods of removing these actions:
+
+- Include the failed status in the list of statuses to purge:
+```php
+add_filter( 'action_scheduler_default_cleaner_statuses', function( $statuses ) {
+ $statuses[] = ActionScheduler_Store::STATUS_FAILED;
+ return $statuses;
+} );
+```
+- Use [WP CLI](/wp-cli/):
+```shell
+// Example
+wp action-scheduler clean --status=failed --batch-size=50 --before='90 days ago' --pause=2
+```
+
+## High Volume Plugin
+
+It's not necessary to add all of this code yourself, there is a handy plugin to get access to each of these increases - the [Action Scheduler - High Volume](https://github.com/woocommerce/action-scheduler-high-volume) plugin.
diff --git a/libraries/action-scheduler/docs/safari-pinned-tab.svg b/libraries/action-scheduler/docs/safari-pinned-tab.svg
new file mode 100644
index 0000000..b67c32b
--- /dev/null
+++ b/libraries/action-scheduler/docs/safari-pinned-tab.svg
@@ -0,0 +1,40 @@
+
+
+
diff --git a/libraries/action-scheduler/docs/site.webmanifest b/libraries/action-scheduler/docs/site.webmanifest
new file mode 100644
index 0000000..de65106
--- /dev/null
+++ b/libraries/action-scheduler/docs/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "",
+ "short_name": "",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-256x256.png",
+ "sizes": "256x256",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/libraries/action-scheduler/docs/usage.md b/libraries/action-scheduler/docs/usage.md
new file mode 100644
index 0000000..5a779d1
--- /dev/null
+++ b/libraries/action-scheduler/docs/usage.md
@@ -0,0 +1,154 @@
+---
+description: Learn how to use the Action Scheduler background processing job queue for WordPress in your WordPress plugin.
+---
+# Usage
+
+Using Action Scheduler requires:
+
+1. installing the library
+1. scheduling an action
+
+## Installation
+
+There are two ways to install Action Scheduler:
+
+1. regular WordPress plugin; or
+1. a library within your plugin's codebase.
+
+Note that [Action Scheduler follows an L-2 dependency version policy](https://developer.woocommerce.com/2023/10/24/action-scheduler-to-adopt-l-2-dependency-version-policy/). That is, the library requires at least the "latest minus two" version of WordPress and the PHP minimum version requirement of that WordPress version.
+
+### Usage as a Plugin
+
+Action Scheduler includes the necessary file headers to be used as a standard WordPress plugin.
+
+To install it as a plugin:
+
+1. Download the .zip archive of the latest [stable release](https://github.com/woocommerce/action-scheduler/releases)
+1. Go to the **Plugins > Add New > Upload** administration screen on your WordPress site
+1. Select the archive file you just downloaded
+1. Click **Install Now**
+1. Click **Activate**
+
+Or clone the Git repository into your site's `wp-content/plugins` folder.
+
+Using Action Scheduler as a plugin can be handy for developing against newer versions, rather than having to update the subtree in your codebase. **When installed as a plugin, Action Scheduler does not provide any user interfaces for scheduling actions**. The only way to interact with Action Scheduler is via code.
+
+### Usage as a Library
+
+To use Action Scheduler as a library:
+
+1. include the Action Scheduler codebase
+1. load the library by including the `action-scheduler.php` file
+
+Using a [subtree in your plugin, theme or site's Git repository](https://www.atlassian.com/blog/git/alternatives-to-git-submodule-git-subtree) to include Action Scheduler is the recommended method. Composer can also be used.
+
+To include Action Scheduler as a git subtree:
+
+#### Step 1. Add the Repository as a Remote
+
+```
+git remote add -f subtree-action-scheduler https://github.com/woocommerce/action-scheduler.git
+```
+
+Adding the subtree as a remote allows us to refer to it in short from via the name `subtree-action-scheduler`, instead of the full GitHub URL.
+
+#### Step 2. Add the Repo as a Subtree
+
+```
+git subtree add --prefix libraries/action-scheduler subtree-action-scheduler trunk --squash
+```
+
+This will add the `trunk` branch of Action Scheduler to your repository in the folder `libraries/action-scheduler`.
+
+You can change the `--prefix` to change where the code is included. Or change the `trunk` branch to a tag, like `2.1.0` to include only a stable version.
+
+#### Step 3. Update the Subtree
+
+To update Action Scheduler to a new version, use the commands:
+
+```
+git fetch subtree-action-scheduler trunk
+git subtree pull --prefix libraries/action-scheduler subtree-action-scheduler trunk --squash
+```
+
+### Loading Action Scheduler
+
+Regardless of how it is installed, to load Action Scheduler, you only need to include the `action-scheduler.php` file, e.g.
+
+```php
+ Action Scheduler
+1. Take a screenshot of the action status counts at the top of the page. Example screenshot: https://cld.wthms.co/kwIqv7
+
+
+#### Stage 2: Install & Activate Action Scheduler 3.0+ as a plugin
+
+1. The migration will start almost immediately
+1. Keep an eye on your error log
+1. Report any notices, errors or other issues on GitHub
+
+#### Stage 3: Verify Migration
+
+1. Go to Tools > Action Scheduler
+1. Take a screenshot of the action status counts at the top of the page.
+1. Verify the counts match the status counts taken in Stage 1 (the Completed counts could be higher because actions will have been completed to run the migration).
diff --git a/libraries/action-scheduler/docs/wp-cli.md b/libraries/action-scheduler/docs/wp-cli.md
new file mode 100644
index 0000000..63287d7
--- /dev/null
+++ b/libraries/action-scheduler/docs/wp-cli.md
@@ -0,0 +1,100 @@
+---
+description: Learn how to do WordPress background processing at scale with WP CLI and the Action Scheduler job queue.
+---
+# WP CLI
+
+Action Scheduler has custom [WP CLI](http://wp-cli.org) commands available for processing actions.
+
+For large sites, WP CLI is a much better choice for running queues of actions than the default WP Cron runner. These are some common cases where WP CLI is a better option:
+
+* long-running tasks - Tasks that take a significant amount of time to run
+* large queues - A large number of tasks will naturally take a longer time
+* other plugins with extensive WP Cron usage - WP Cron's limited resources are spread across more tasks
+
+With a regular web request, you may have to deal with script timeouts enforced by hosts, or other restraints that make it more challenging to run Action Scheduler tasks. Utilizing WP CLI to run commands directly on the server give you more freedom. This means that you typically don't have the same constraints of a normal web request.
+
+If you choose to utilize WP CLI exclusively, you can disable the normal WP CLI queue runner by installing the [Action Scheduler - Disable Default Queue Runner](https://github.com/woocommerce/action-scheduler-disable-default-runner) plugin. Note that if you do this, you **must** run Action Scheduler via WP CLI or another method, otherwise no scheduled actions will be processed.
+
+## Commands
+
+These are the commands available to use with Action Scheduler:
+
+* `action-scheduler clean`
+
+ Options:
+ * `--batch-size` - This is the number of actions per status to clean in a single batch. Default is `20`.
+ * `--batches` - This is the number of batches to process. Default 0 means that batches will continue to process until there are no more actions to delete.
+ * `--status` - Process only actions with specific status or statuses. Default is `canceled` and `complete`. Define multiple statuses as a comma separated string (without spaces), e.g. `--status=complete,failed,canceled`
+ * `--before` - Process only 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.
+
+* `action-scheduler migrate`
+
+ **Note**: This command is only available while the migration to custom tables is in progress.
+
+* `action-scheduler run`
+
+ Options:
+ * `--batch-size` - This is the number of actions to run in a single batch. The default is `100`.
+ * `--batches` - This is the number of batches to run. Using 0 means that batches will continue running until there are no more actions to run.
+ * `--hooks` - Process only actions with specific hook or hooks, like `'woocommerce_scheduled_subscription_payment'`. By default, actions with any hook will be processed. Define multiple hooks as a comma separated string (without spaces), e.g. `--hooks=woocommerce_scheduled_subscription_trial_end,woocommerce_scheduled_subscription_payment,woocommerce_scheduled_subscription_expiration`
+ * `--group` - Process only actions in a specific group, like `'woocommerce-memberships'`. By default, actions in any group (or no group) will be processed.
+ * `--exclude-groups` - Ignore actions from the specified group or groups (to specify multiple groups, supply a comma-separated list of slugs). This option is ignored if `--group` is also specified.
+ * `--force` - By default, Action Scheduler limits the number of concurrent batches that can be run at once to ensure the server does not get overwhelmed. Using the `--force` flag overrides this behavior to force the WP CLI queue to run.
+
+* `action-scheduler action cancel`
+* `action-scheduler action create`
+* `action-scheduler action delete`
+* `action-scheduler action generate`
+* `action-scheduler action get`
+* `action-scheduler action list`
+* `action-scheduler action next`
+* `action-scheduler action run`
+* `action-scheduler datastore`
+* `action-scheduler runner`
+* `action-scheduler status`
+* `action-scheduler version`
+
+The best way to get a full list of commands and their available options is to use WP CLI itself. This can be done by running `wp action-scheduler` to list all Action Scheduler commands, or by including the `--help` flag with any of the individual commands. This will provide all relevant parameters and flags for the command.
+
+## Cautionary Note on Action Dependencies when using `--group` or `--hooks` Options
+
+The `--group` and `--hooks` options should be used with caution if you have an implicit dependency between scheduled actions based on their schedule.
+
+For example, consider two scheduled actions for the same subscription:
+
+* `scheduled_payment` scheduled for `2015-11-13 00:00:00` and
+* `scheduled_expiration` scheduled for `2015-11-13 00:01:00`.
+
+Under normal conditions, Action Scheduler will ensure the `scheduled_payment` action is run before the `scheduled_expiration` action. Because that's how they are scheduled.
+
+However, when using the `--hooks` option, the `scheduled_payment` and `scheduled_expiration` actions will be processed in separate queues. As a result, this dependency is not guaranteed.
+
+For example, consider a site with both:
+
+* 100,000 `scheduled_payment` actions, scheduled for `2015-11-13 00:00:00`
+* 100 `scheduled_expiration` actions, scheduled for `2015-11-13 00:01:00`
+
+If two queue runners are running alongside each other with each runner dedicated to just one of these hooks, the queue runner handling expiration hooks will complete the processing of the expiration hooks more quickly than the queue runner handling all the payment actions.
+
+**Because of this, the `--group` and `--hooks` options should be used with caution to avoid processing actions with an implicit dependency based on their schedule in separate queues.**
+
+## Improving Performance with `--group` or `--hooks`
+
+Being able to run queues for specific hooks or groups of actions is valuable at scale. Why? Because it means you can restrict the concurrency for similar actions.
+
+For example, let's say you have 300,000 actions queued up comprised of:
+
+* 100,000 renewals payments
+* 100,000 email notifications
+* 100,000 membership status updates
+
+Action Scheduler's default WP Cron queue runner will process them all together. e.g. when it claims a batch of actions, some may be emails, some membership updates and some renewals.
+
+When you add concurrency to that, you can end up with issues. For example, if you have 3 queues running, they may all be attempting to process similar actions at the same time, which can lead to querying the same database tables with similar queries. Depending on the code/queries running, this can lead to database locks or other issues.
+
+If you can batch based on each action's group, then you can improve performance by processing like actions consecutively, but still processing the full set of actions concurrently.
+
+For example, if one queue is created to process emails, another to process membership updates, and another to process renewal payments, then the same queries won't be run at the same time, and 3 separate queues will be able to run more efficiently.
+
+The WP CLI runner can achieve this using the `--group` option to create queue runners that focus on a specific group and, optionally, the `--exclude-groups` option to create one or more 'catch-all' queue runners that ignore those groups.
diff --git a/libraries/action-scheduler/functions.php b/libraries/action-scheduler/functions.php
new file mode 100644
index 0000000..59e5542
--- /dev/null
+++ b/libraries/action-scheduler/functions.php
@@ -0,0 +1,495 @@
+create(
+ array(
+ 'type' => 'async',
+ 'hook' => $hook,
+ 'arguments' => $args,
+ 'group' => $group,
+ 'unique' => $unique,
+ 'priority' => $priority,
+ )
+ );
+}
+
+/**
+ * Schedule an action to run one time
+ *
+ * @param int $timestamp When the job will run.
+ * @param string $hook The hook to trigger.
+ * @param array $args Arguments to pass when the hook triggers.
+ * @param string $group The group to assign this job to.
+ * @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. Zero if there was an error scheduling the action.
+ */
+function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) {
+ if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) {
+ return 0;
+ }
+
+ /**
+ * Provides an opportunity to short-circuit the default process for enqueuing single
+ * actions.
+ *
+ * Returning a value other than null from the filter will short-circuit the normal
+ * process. The expectation in such a scenario is that callbacks will return an integer
+ * representing the scheduled action ID (scheduled using some alternative process) or else
+ * zero.
+ *
+ * @param int|null $pre_option The value to return instead of the option value.
+ * @param int $timestamp When the action will run.
+ * @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, $priority );
+ if ( null !== $pre ) {
+ return is_int( $pre ) ? $pre : 0;
+ }
+
+ return ActionScheduler::factory()->create(
+ array(
+ 'type' => 'single',
+ 'hook' => $hook,
+ 'arguments' => $args,
+ 'when' => $timestamp,
+ 'group' => $group,
+ 'unique' => $unique,
+ 'priority' => $priority,
+ )
+ );
+}
+
+/**
+ * Schedule a recurring action
+ *
+ * @param int $timestamp When the first instance of the job will run.
+ * @param int $interval_in_seconds How long to wait between runs.
+ * @param string $hook The hook to trigger.
+ * @param array $args Arguments to pass when the hook triggers.
+ * @param string $group The group to assign this job to.
+ * @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. Zero if there was an error scheduling the action.
+ */
+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.
+ // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
+ 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.
+ *
+ * Returning a value other than null from the filter will short-circuit the normal
+ * process. The expectation in such a scenario is that callbacks will return an integer
+ * representing the scheduled action ID (scheduled using some alternative process) or else
+ * zero.
+ *
+ * @param int|null $pre_option The value to return instead of the option value.
+ * @param int $timestamp When the action will run.
+ * @param int $interval_in_seconds How long to wait between runs.
+ * @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, $priority );
+ if ( null !== $pre ) {
+ return is_int( $pre ) ? $pre : 0;
+ }
+
+ return ActionScheduler::factory()->create(
+ array(
+ 'type' => 'recurring',
+ 'hook' => $hook,
+ 'arguments' => $args,
+ 'when' => $timestamp,
+ 'pattern' => $interval_in_seconds,
+ 'group' => $group,
+ 'unique' => $unique,
+ 'priority' => $priority,
+ )
+ );
+}
+
+/**
+ * Schedule an action that recurs on a cron-like schedule.
+ *
+ * @param int $timestamp The first instance of the action will be scheduled
+ * to run at a time calculated after this timestamp matching the cron
+ * expression. This can be used to delay the first instance of the action.
+ * @param string $schedule A cron-link schedule string.
+ * @see http://en.wikipedia.org/wiki/Cron
+ * * * * * * *
+ * ┬ ┬ ┬ ┬ ┬ ┬
+ * | | | | | |
+ * | | | | | + year [optional]
+ * | | | | +----- day of week (0 - 7) (Sunday=0 or 7)
+ * | | | +---------- month (1 - 12)
+ * | | +--------------- day of month (1 - 31)
+ * | +-------------------- hour (0 - 23)
+ * +------------------------- min (0 - 59)
+ * @param string $hook The hook to trigger.
+ * @param array $args Arguments to pass when the hook triggers.
+ * @param string $group The group to assign this job to.
+ * @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. Zero if there was an error scheduling the action.
+ */
+function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) {
+ if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) {
+ return 0;
+ }
+
+ /**
+ * Provides an opportunity to short-circuit the default process for enqueuing cron
+ * actions.
+ *
+ * Returning a value other than null from the filter will short-circuit the normal
+ * process. The expectation in such a scenario is that callbacks will return an integer
+ * representing the scheduled action ID (scheduled using some alternative process) or else
+ * zero.
+ *
+ * @param int|null $pre_option The value to return instead of the option value.
+ * @param int $timestamp When the action will run.
+ * @param string $schedule Cron-like schedule string.
+ * @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, $priority );
+ if ( null !== $pre ) {
+ return is_int( $pre ) ? $pre : 0;
+ }
+
+ return ActionScheduler::factory()->create(
+ array(
+ 'type' => 'cron',
+ 'hook' => $hook,
+ 'arguments' => $args,
+ 'when' => $timestamp,
+ 'pattern' => $schedule,
+ 'group' => $group,
+ 'unique' => $unique,
+ 'priority' => $priority,
+ )
+ );
+}
+
+/**
+ * Cancel the next occurrence of a scheduled action.
+ *
+ * While only the next instance of a recurring or cron action is unscheduled by this method, that will also prevent
+ * all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled in
+ * a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled
+ * only after the former action is run. If the next instance is never run, because it's unscheduled by this function,
+ * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled
+ * by this method also.
+ *
+ * @param string $hook The hook that the job will trigger.
+ * @param array $args Args that would have been passed to the job.
+ * @param string $group The group the job is assigned to.
+ *
+ * @return int|null The scheduled action ID if a scheduled action was found, or null if no matching action found.
+ */
+function as_unschedule_action( $hook, $args = array(), $group = '' ) {
+ if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) {
+ return 0;
+ }
+ $params = array(
+ 'hook' => $hook,
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ 'orderby' => 'date',
+ 'order' => 'ASC',
+ 'group' => $group,
+ );
+ if ( is_array( $args ) ) {
+ $params['args'] = $args;
+ }
+
+ $action_id = ActionScheduler::store()->query_action( $params );
+
+ if ( $action_id ) {
+ try {
+ ActionScheduler::store()->cancel_action( $action_id );
+ } catch ( Exception $exception ) {
+ ActionScheduler::logger()->log(
+ $action_id,
+ sprintf(
+ /* 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()
+ )
+ );
+
+ $action_id = null;
+ }
+ }
+
+ return $action_id;
+}
+
+/**
+ * Cancel all occurrences of a scheduled action.
+ *
+ * @param string $hook The hook that the job will trigger.
+ * @param array $args Args that would have been passed to the job.
+ * @param string $group The group the job is assigned to.
+ */
+function as_unschedule_all_actions( $hook, $args = array(), $group = '' ) {
+ if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) {
+ return;
+ }
+ if ( empty( $args ) ) {
+ if ( ! empty( $hook ) && empty( $group ) ) {
+ ActionScheduler_Store::instance()->cancel_actions_by_hook( $hook );
+ return;
+ }
+ if ( ! empty( $group ) && empty( $hook ) ) {
+ ActionScheduler_Store::instance()->cancel_actions_by_group( $group );
+ return;
+ }
+ }
+ do {
+ $unscheduled_action = as_unschedule_action( $hook, $args, $group );
+ } while ( ! empty( $unscheduled_action ) );
+}
+
+/**
+ * Check if there is an existing action in the queue with a given hook, args and group combination.
+ *
+ * An action in the queue could be pending, in-progress or async. If the is pending for a time in
+ * future, its scheduled date will be returned as a timestamp. If it is currently being run, or an
+ * async action sitting in the queue waiting to be processed, in which case boolean true will be
+ * returned. Or there may be no async, in-progress or pending action for this hook, in which case,
+ * boolean false will be the return value.
+ *
+ * @param string $hook Name of the hook to search for.
+ * @param array $args Arguments of the action to be searched.
+ * @param string $group Group of the action to be searched.
+ *
+ * @return int|bool The timestamp for the next occurrence of a pending scheduled action, true for an async or in-progress action or false if there is no matching action.
+ */
+function as_next_scheduled_action( $hook, $args = null, $group = '' ) {
+ if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) {
+ return false;
+ }
+
+ $params = array(
+ 'hook' => $hook,
+ 'orderby' => 'date',
+ 'order' => 'ASC',
+ 'group' => $group,
+ );
+
+ if ( is_array( $args ) ) {
+ $params['args'] = $args;
+ }
+
+ $params['status'] = ActionScheduler_Store::STATUS_RUNNING;
+ $action_id = ActionScheduler::store()->query_action( $params );
+ if ( $action_id ) {
+ return true;
+ }
+
+ $params['status'] = ActionScheduler_Store::STATUS_PENDING;
+ $action_id = ActionScheduler::store()->query_action( $params );
+ if ( null === $action_id ) {
+ return false;
+ }
+
+ $action = ActionScheduler::store()->fetch_action( $action_id );
+ $scheduled_date = $action->get_schedule()->get_date();
+ if ( $scheduled_date ) {
+ return (int) $scheduled_date->format( 'U' );
+ } elseif ( null === $scheduled_date ) { // pending async action with NullSchedule.
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Check if there is a scheduled action in the queue but more efficiently than as_next_scheduled_action().
+ *
+ * It's recommended to use this function when you need to know whether a specific action is currently scheduled
+ * (pending or in-progress).
+ *
+ * @since 3.3.0
+ *
+ * @param string $hook The hook of the action.
+ * @param array $args Args that have been passed to the action. Null will matches any args.
+ * @param string $group The group the job is assigned to.
+ *
+ * @return bool True if a matching action is pending or in-progress, false otherwise.
+ */
+function as_has_scheduled_action( $hook, $args = null, $group = '' ) {
+ if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) {
+ return false;
+ }
+
+ $query_args = array(
+ 'hook' => $hook,
+ 'status' => array( ActionScheduler_Store::STATUS_RUNNING, ActionScheduler_Store::STATUS_PENDING ),
+ 'group' => $group,
+ 'orderby' => 'none',
+ );
+
+ if ( null !== $args ) {
+ $query_args['args'] = $args;
+ }
+
+ $action_id = ActionScheduler::store()->query_action( $query_args );
+
+ return null !== $action_id;
+}
+
+/**
+ * Find scheduled actions
+ *
+ * @param array $args Possible arguments, with their default values.
+ * 'hook' => '' - the name of the action that will be triggered.
+ * 'args' => NULL - the args array that will be passed with the action.
+ * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone.
+ * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='.
+ * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone.
+ * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='.
+ * 'group' => '' - the group the action belongs to.
+ * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING.
+ * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID.
+ * 'per_page' => 5 - Number of results to return.
+ * 'offset' => 0.
+ * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', 'date' or 'none'.
+ * 'order' => 'ASC'.
+ *
+ * @param string $return_format OBJECT, ARRAY_A, or ids.
+ *
+ * @return array
+ */
+function as_get_scheduled_actions( $args = array(), $return_format = OBJECT ) {
+ if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) {
+ return array();
+ }
+ $store = ActionScheduler::store();
+ foreach ( array( 'date', 'modified' ) as $key ) {
+ if ( isset( $args[ $key ] ) ) {
+ $args[ $key ] = as_get_datetime_object( $args[ $key ] );
+ }
+ }
+ $ids = $store->query_actions( $args );
+
+ if ( 'ids' === $return_format || 'int' === $return_format ) {
+ return $ids;
+ }
+
+ $actions = array();
+ foreach ( $ids as $action_id ) {
+ $actions[ $action_id ] = $store->fetch_action( $action_id );
+ }
+
+ if ( ARRAY_A === $return_format ) {
+ foreach ( $actions as $action_id => $action_object ) {
+ $actions[ $action_id ] = get_object_vars( $action_object );
+ }
+ }
+
+ return $actions;
+}
+
+/**
+ * Helper function to create an instance of DateTime based on a given
+ * string and timezone. By default, will return the current date/time
+ * in the UTC timezone.
+ *
+ * Needed because new DateTime() called without an explicit timezone
+ * will create a date/time in PHP's timezone, but we need to have
+ * assurance that a date/time uses the right timezone (which we almost
+ * always want to be UTC), which means we need to always include the
+ * timezone when instantiating datetimes rather than leaving it up to
+ * the PHP default.
+ *
+ * @param mixed $date_string A date/time string. Valid formats are explained in http://php.net/manual/en/datetime.formats.php.
+ * @param string $timezone A timezone identifier, like UTC or Europe/Lisbon. The list of valid identifiers is available http://php.net/manual/en/timezones.php.
+ *
+ * @return ActionScheduler_DateTime
+ */
+function as_get_datetime_object( $date_string = null, $timezone = 'UTC' ) {
+ if ( is_object( $date_string ) && $date_string instanceof DateTime ) {
+ $date = new ActionScheduler_DateTime( $date_string->format( 'Y-m-d H:i:s' ), new DateTimeZone( $timezone ) );
+ } elseif ( is_numeric( $date_string ) ) {
+ $date = new ActionScheduler_DateTime( '@' . $date_string, new DateTimeZone( $timezone ) );
+ } else {
+ $date = new ActionScheduler_DateTime( null === $date_string ? 'now' : $date_string, new DateTimeZone( $timezone ) );
+ }
+ return $date;
+}
diff --git a/libraries/action-scheduler/lib/WP_Async_Request.php b/libraries/action-scheduler/lib/WP_Async_Request.php
new file mode 100644
index 0000000..a40c80a
--- /dev/null
+++ b/libraries/action-scheduler/lib/WP_Async_Request.php
@@ -0,0 +1,188 @@
+identifier = $this->prefix . '_' . $this->action;
+
+ add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) );
+ add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) );
+ }
+
+ /**
+ * Set data used during the request
+ *
+ * @param array $data Data.
+ *
+ * @return $this
+ */
+ public function data( $data ) {
+ $this->data = $data;
+
+ return $this;
+ }
+
+ /**
+ * Dispatch the async request
+ *
+ * @return array|WP_Error
+ */
+ public function dispatch() {
+ $url = add_query_arg( $this->get_query_args(), $this->get_query_url() );
+ $args = $this->get_post_args();
+
+ return wp_remote_post( esc_url_raw( $url ), $args );
+ }
+
+ /**
+ * Get query args
+ *
+ * @return array
+ */
+ protected function get_query_args() {
+ if ( property_exists( $this, 'query_args' ) ) {
+ return $this->query_args;
+ }
+
+ $args = array(
+ 'action' => $this->identifier,
+ 'nonce' => wp_create_nonce( $this->identifier ),
+ );
+
+ /**
+ * Filters the post arguments used during an async request.
+ *
+ * @param array $url
+ */
+ return apply_filters( $this->identifier . '_query_args', $args );
+ }
+
+ /**
+ * Get query URL
+ *
+ * @return string
+ */
+ protected function get_query_url() {
+ if ( property_exists( $this, 'query_url' ) ) {
+ return $this->query_url;
+ }
+
+ $url = admin_url( 'admin-ajax.php' );
+
+ /**
+ * Filters the post arguments used during an async request.
+ *
+ * @param string $url
+ */
+ return apply_filters( $this->identifier . '_query_url', $url );
+ }
+
+ /**
+ * Get post args
+ *
+ * @return array
+ */
+ protected function get_post_args() {
+ if ( property_exists( $this, 'post_args' ) ) {
+ return $this->post_args;
+ }
+
+ $args = array(
+ 'timeout' => 0.01,
+ 'blocking' => false,
+ 'body' => $this->data,
+ 'cookies' => $_COOKIE,
+ 'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
+ );
+
+ /**
+ * Filters the post arguments used during an async request.
+ *
+ * @param array $args
+ */
+ return apply_filters( $this->identifier . '_post_args', $args );
+ }
+
+ /**
+ * Maybe handle
+ *
+ * Check for correct nonce and pass to handler.
+ */
+ public function maybe_handle() {
+ // Don't lock up other requests while processing.
+ session_write_close();
+
+ check_ajax_referer( $this->identifier, 'nonce' );
+
+ $this->handle();
+
+ wp_die();
+ }
+
+ /**
+ * Handle
+ *
+ * Override this method to perform any actions required
+ * during the async request.
+ */
+ abstract protected function handle();
+
+ }
+}
diff --git a/libraries/action-scheduler/lib/cron-expression/CronExpression.php b/libraries/action-scheduler/lib/cron-expression/CronExpression.php
new file mode 100644
index 0000000..33040ed
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/CronExpression.php
@@ -0,0 +1,318 @@
+
+ * @link http://en.wikipedia.org/wiki/Cron
+ */
+class CronExpression
+{
+ const MINUTE = 0;
+ const HOUR = 1;
+ const DAY = 2;
+ const MONTH = 3;
+ const WEEKDAY = 4;
+ const YEAR = 5;
+
+ /**
+ * @var array CRON expression parts
+ */
+ private $cronParts;
+
+ /**
+ * @var CronExpression_FieldFactory CRON field factory
+ */
+ private $fieldFactory;
+
+ /**
+ * @var array Order in which to test of cron parts
+ */
+ private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE);
+
+ /**
+ * Factory method to create a new CronExpression.
+ *
+ * @param string $expression The CRON expression to create. There are
+ * several special predefined values which can be used to substitute the
+ * CRON expression:
+ *
+ * @yearly, @annually) - Run once a year, midnight, Jan. 1 - 0 0 1 1 *
+ * @monthly - Run once a month, midnight, first of month - 0 0 1 * *
+ * @weekly - Run once a week, midnight on Sun - 0 0 * * 0
+ * @daily - Run once a day, midnight - 0 0 * * *
+ * @hourly - Run once an hour, first minute - 0 * * * *
+ *
+*@param CronExpression_FieldFactory $fieldFactory (optional) Field factory to use
+ *
+ * @return CronExpression
+ */
+ public static function factory($expression, ?CronExpression_FieldFactory $fieldFactory = null)
+ {
+ $mappings = array(
+ '@yearly' => '0 0 1 1 *',
+ '@annually' => '0 0 1 1 *',
+ '@monthly' => '0 0 1 * *',
+ '@weekly' => '0 0 * * 0',
+ '@daily' => '0 0 * * *',
+ '@hourly' => '0 * * * *'
+ );
+
+ if (isset($mappings[$expression])) {
+ $expression = $mappings[$expression];
+ }
+
+ return new self($expression, $fieldFactory ? $fieldFactory : new CronExpression_FieldFactory());
+ }
+
+ /**
+ * Parse a CRON expression
+ *
+ * @param string $expression CRON expression (e.g. '8 * * * *')
+ * @param CronExpression_FieldFactory $fieldFactory Factory to create cron fields
+ */
+ public function __construct($expression, CronExpression_FieldFactory $fieldFactory)
+ {
+ $this->fieldFactory = $fieldFactory;
+ $this->setExpression($expression);
+ }
+
+ /**
+ * Set or change the CRON expression
+ *
+ * @param string $value CRON expression (e.g. 8 * * * *)
+ *
+ * @return CronExpression
+ * @throws InvalidArgumentException if not a valid CRON expression
+ */
+ public function setExpression($value)
+ {
+ $this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
+ if (count($this->cronParts) < 5) {
+ throw new InvalidArgumentException(
+ $value . ' is not a valid CRON expression'
+ );
+ }
+
+ foreach ($this->cronParts as $position => $part) {
+ $this->setPart($position, $part);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set part of the CRON expression
+ *
+ * @param int $position The position of the CRON expression to set
+ * @param string $value The value to set
+ *
+ * @return CronExpression
+ * @throws InvalidArgumentException if the value is not valid for the part
+ */
+ public function setPart($position, $value)
+ {
+ if (!$this->fieldFactory->getField($position)->validate($value)) {
+ throw new InvalidArgumentException(
+ 'Invalid CRON field value ' . $value . ' as position ' . $position
+ );
+ }
+
+ $this->cronParts[$position] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get a next run date relative to the current date or a specific date
+ *
+ * @param string|DateTime $currentTime (optional) Relative calculation date
+ * @param int $nth (optional) Number of matches to skip before returning a
+ * matching next run date. 0, the default, will return the current
+ * date and time if the next run date falls on the current date and
+ * time. Setting this value to 1 will skip the first match and go to
+ * the second match. Setting this value to 2 will skip the first 2
+ * matches and so on.
+ * @param bool $allowCurrentDate (optional) Set to TRUE to return the
+ * current date if it matches the cron expression
+ *
+ * @return DateTime
+ * @throws RuntimeException on too many iterations
+ */
+ public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
+ {
+ return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate);
+ }
+
+ /**
+ * Get a previous run date relative to the current date or a specific date
+ *
+ * @param string|DateTime $currentTime (optional) Relative calculation date
+ * @param int $nth (optional) Number of matches to skip before returning
+ * @param bool $allowCurrentDate (optional) Set to TRUE to return the
+ * current date if it matches the cron expression
+ *
+ * @return DateTime
+ * @throws RuntimeException on too many iterations
+ * @see CronExpression::getNextRunDate
+ */
+ public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
+ {
+ return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate);
+ }
+
+ /**
+ * Get multiple run dates starting at the current date or a specific date
+ *
+ * @param int $total Set the total number of dates to calculate
+ * @param string|DateTime $currentTime (optional) Relative calculation date
+ * @param bool $invert (optional) Set to TRUE to retrieve previous dates
+ * @param bool $allowCurrentDate (optional) Set to TRUE to return the
+ * current date if it matches the cron expression
+ *
+ * @return array Returns an array of run dates
+ */
+ public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false)
+ {
+ $matches = array();
+ for ($i = 0; $i < max(0, $total); $i++) {
+ $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate);
+ }
+
+ return $matches;
+ }
+
+ /**
+ * Get all or part of the CRON expression
+ *
+ * @param string $part (optional) Specify the part to retrieve or NULL to
+ * get the full cron schedule string.
+ *
+ * @return string|null Returns the CRON expression, a part of the
+ * CRON expression, or NULL if the part was specified but not found
+ */
+ public function getExpression($part = null)
+ {
+ if (null === $part) {
+ return implode(' ', $this->cronParts);
+ } elseif (array_key_exists($part, $this->cronParts)) {
+ return $this->cronParts[$part];
+ }
+
+ return null;
+ }
+
+ /**
+ * Helper method to output the full expression.
+ *
+ * @return string Full CRON expression
+ */
+ public function __toString()
+ {
+ return $this->getExpression();
+ }
+
+ /**
+ * Determine if the cron is due to run based on the current date or a
+ * specific date. This method assumes that the current number of
+ * seconds are irrelevant, and should be called once per minute.
+ *
+ * @param string|DateTime $currentTime (optional) Relative calculation date
+ *
+ * @return bool Returns TRUE if the cron is due to run or FALSE if not
+ */
+ public function isDue($currentTime = 'now')
+ {
+ if ('now' === $currentTime) {
+ $currentDate = date('Y-m-d H:i');
+ $currentTime = strtotime($currentDate);
+ } elseif ($currentTime instanceof DateTime) {
+ $currentDate = $currentTime->format('Y-m-d H:i');
+ $currentTime = strtotime($currentDate);
+ } else {
+ $currentTime = new DateTime($currentTime);
+ $currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0);
+ $currentDate = $currentTime->format('Y-m-d H:i');
+ $currentTime = (int)($currentTime->getTimestamp());
+ }
+
+ return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime;
+ }
+
+ /**
+ * Get the next or previous run date of the expression relative to a date
+ *
+ * @param string|DateTime $currentTime (optional) Relative calculation date
+ * @param int $nth (optional) Number of matches to skip before returning
+ * @param bool $invert (optional) Set to TRUE to go backwards in time
+ * @param bool $allowCurrentDate (optional) Set to TRUE to return the
+ * current date if it matches the cron expression
+ *
+ * @return DateTime
+ * @throws RuntimeException on too many iterations
+ */
+ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false)
+ {
+ if ($currentTime instanceof DateTime) {
+ $currentDate = $currentTime;
+ } else {
+ $currentDate = new DateTime($currentTime ? $currentTime : 'now');
+ $currentDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
+ }
+
+ $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0);
+ $nextRun = clone $currentDate;
+ $nth = (int) $nth;
+
+ // Set a hard limit to bail on an impossible date
+ for ($i = 0; $i < 1000; $i++) {
+
+ foreach (self::$order as $position) {
+ $part = $this->getExpression($position);
+ if (null === $part) {
+ continue;
+ }
+
+ $satisfied = false;
+ // Get the field object used to validate this part
+ $field = $this->fieldFactory->getField($position);
+ // Check if this is singular or a list
+ if (strpos($part, ',') === false) {
+ $satisfied = $field->isSatisfiedBy($nextRun, $part);
+ } else {
+ foreach (array_map('trim', explode(',', $part)) as $listPart) {
+ if ($field->isSatisfiedBy($nextRun, $listPart)) {
+ $satisfied = true;
+ break;
+ }
+ }
+ }
+
+ // If the field is not satisfied, then start over
+ if (!$satisfied) {
+ $field->increment($nextRun, $invert);
+ continue 2;
+ }
+ }
+
+ // Skip this match if needed
+ if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) {
+ $this->fieldFactory->getField(0)->increment($nextRun, $invert);
+ continue;
+ }
+
+ return $nextRun;
+ }
+
+ // @codeCoverageIgnoreStart
+ throw new RuntimeException('Impossible CRON expression');
+ // @codeCoverageIgnoreEnd
+ }
+}
diff --git a/libraries/action-scheduler/lib/cron-expression/CronExpression_AbstractField.php b/libraries/action-scheduler/lib/cron-expression/CronExpression_AbstractField.php
new file mode 100644
index 0000000..f8d5c00
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/CronExpression_AbstractField.php
@@ -0,0 +1,100 @@
+
+ */
+abstract class CronExpression_AbstractField implements CronExpression_FieldInterface
+{
+ /**
+ * Check to see if a field is satisfied by a value
+ *
+ * @param string $dateValue Date value to check
+ * @param string $value Value to test
+ *
+ * @return bool
+ */
+ public function isSatisfied($dateValue, $value)
+ {
+ if ($this->isIncrementsOfRanges($value)) {
+ return $this->isInIncrementsOfRanges($dateValue, $value);
+ } elseif ($this->isRange($value)) {
+ return $this->isInRange($dateValue, $value);
+ }
+
+ return $value == '*' || $dateValue == $value;
+ }
+
+ /**
+ * Check if a value is a range
+ *
+ * @param string $value Value to test
+ *
+ * @return bool
+ */
+ public function isRange($value)
+ {
+ return strpos($value, '-') !== false;
+ }
+
+ /**
+ * Check if a value is an increments of ranges
+ *
+ * @param string $value Value to test
+ *
+ * @return bool
+ */
+ public function isIncrementsOfRanges($value)
+ {
+ return strpos($value, '/') !== false;
+ }
+
+ /**
+ * Test if a value is within a range
+ *
+ * @param string $dateValue Set date value
+ * @param string $value Value to test
+ *
+ * @return bool
+ */
+ public function isInRange($dateValue, $value)
+ {
+ $parts = array_map('trim', explode('-', $value, 2));
+
+ return $dateValue >= $parts[0] && $dateValue <= $parts[1];
+ }
+
+ /**
+ * Test if a value is within an increments of ranges (offset[-to]/step size)
+ *
+ * @param string $dateValue Set date value
+ * @param string $value Value to test
+ *
+ * @return bool
+ */
+ public function isInIncrementsOfRanges($dateValue, $value)
+ {
+ $parts = array_map('trim', explode('/', $value, 2));
+ $stepSize = isset($parts[1]) ? $parts[1] : 0;
+ if ($parts[0] == '*' || $parts[0] === '0') {
+ return (int) $dateValue % $stepSize == 0;
+ }
+
+ $range = explode('-', $parts[0], 2);
+ $offset = $range[0];
+ $to = isset($range[1]) ? $range[1] : $dateValue;
+ // Ensure that the date value is within the range
+ if ($dateValue < $offset || $dateValue > $to) {
+ return false;
+ }
+
+ for ($i = $offset; $i <= $to; $i+= $stepSize) {
+ if ($i == $dateValue) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/libraries/action-scheduler/lib/cron-expression/CronExpression_DayOfMonthField.php b/libraries/action-scheduler/lib/cron-expression/CronExpression_DayOfMonthField.php
new file mode 100644
index 0000000..40c1d6c
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/CronExpression_DayOfMonthField.php
@@ -0,0 +1,110 @@
+
+ */
+class CronExpression_DayOfMonthField extends CronExpression_AbstractField
+{
+ /**
+ * Get the nearest day of the week for a given day in a month
+ *
+ * @param int $currentYear Current year
+ * @param int $currentMonth Current month
+ * @param int $targetDay Target day of the month
+ *
+ * @return DateTime Returns the nearest date
+ */
+ private static function getNearestWeekday($currentYear, $currentMonth, $targetDay)
+ {
+ $tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT);
+ $target = new DateTime("$currentYear-$currentMonth-$tday");
+ $currentWeekday = (int) $target->format('N');
+
+ if ($currentWeekday < 6) {
+ return $target;
+ }
+
+ $lastDayOfMonth = $target->format('t');
+
+ foreach (array(-1, 1, -2, 2) as $i) {
+ $adjusted = $targetDay + $i;
+ if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) {
+ $target->setDate($currentYear, $currentMonth, $adjusted);
+ if ($target->format('N') < 6 && $target->format('m') == $currentMonth) {
+ return $target;
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isSatisfiedBy(DateTime $date, $value)
+ {
+ // ? states that the field value is to be skipped
+ if ($value == '?') {
+ return true;
+ }
+
+ $fieldValue = $date->format('d');
+
+ // Check to see if this is the last day of the month
+ if ($value == 'L') {
+ return $fieldValue == $date->format('t');
+ }
+
+ // Check to see if this is the nearest weekday to a particular value
+ if (strpos($value, 'W')) {
+ // Parse the target day
+ $targetDay = substr($value, 0, strpos($value, 'W'));
+ // Find out if the current day is the nearest day of the week
+ return $date->format('j') == self::getNearestWeekday(
+ $date->format('Y'),
+ $date->format('m'),
+ $targetDay
+ )->format('j');
+ }
+
+ return $this->isSatisfied($date->format('d'), $value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function increment(DateTime $date, $invert = false)
+ {
+ if ($invert) {
+ $date->modify('previous day');
+ $date->setTime(23, 59);
+ } else {
+ $date->modify('next day');
+ $date->setTime(0, 0);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($value)
+ {
+ return (bool) preg_match('/[\*,\/\-\?LW0-9A-Za-z]+/', $value);
+ }
+}
diff --git a/libraries/action-scheduler/lib/cron-expression/CronExpression_DayOfWeekField.php b/libraries/action-scheduler/lib/cron-expression/CronExpression_DayOfWeekField.php
new file mode 100644
index 0000000..e9f68a7
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/CronExpression_DayOfWeekField.php
@@ -0,0 +1,124 @@
+
+ */
+class CronExpression_DayOfWeekField extends CronExpression_AbstractField
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isSatisfiedBy(DateTime $date, $value)
+ {
+ if ($value == '?') {
+ return true;
+ }
+
+ // Convert text day of the week values to integers
+ $value = str_ireplace(
+ array('SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'),
+ range(0, 6),
+ $value
+ );
+
+ $currentYear = $date->format('Y');
+ $currentMonth = $date->format('m');
+ $lastDayOfMonth = $date->format('t');
+
+ // Find out if this is the last specific weekday of the month
+ if (strpos($value, 'L')) {
+ $weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L')));
+ $tdate = clone $date;
+ $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth);
+ while ($tdate->format('w') != $weekday) {
+ $tdate->setDate($currentYear, $currentMonth, --$lastDayOfMonth);
+ }
+
+ return $date->format('j') == $lastDayOfMonth;
+ }
+
+ // Handle # hash tokens
+ if (strpos($value, '#')) {
+ list($weekday, $nth) = explode('#', $value);
+ // Validate the hash fields
+ if ($weekday < 1 || $weekday > 5) {
+ throw new InvalidArgumentException("Weekday must be a value between 1 and 5. {$weekday} given");
+ }
+ if ($nth > 5) {
+ throw new InvalidArgumentException('There are never more than 5 of a given weekday in a month');
+ }
+ // The current weekday must match the targeted weekday to proceed
+ if ($date->format('N') != $weekday) {
+ return false;
+ }
+
+ $tdate = clone $date;
+ $tdate->setDate($currentYear, $currentMonth, 1);
+ $dayCount = 0;
+ $currentDay = 1;
+ while ($currentDay < $lastDayOfMonth + 1) {
+ if ($tdate->format('N') == $weekday) {
+ if (++$dayCount >= $nth) {
+ break;
+ }
+ }
+ $tdate->setDate($currentYear, $currentMonth, ++$currentDay);
+ }
+
+ return $date->format('j') == $currentDay;
+ }
+
+ // Handle day of the week values
+ if (strpos($value, '-')) {
+ $parts = explode('-', $value);
+ if ($parts[0] == '7') {
+ $parts[0] = '0';
+ } elseif ($parts[1] == '0') {
+ $parts[1] = '7';
+ }
+ $value = implode('-', $parts);
+ }
+
+ // Test to see which Sunday to use -- 0 == 7 == Sunday
+ $format = in_array(7, str_split($value)) ? 'N' : 'w';
+ $fieldValue = $date->format($format);
+
+ return $this->isSatisfied($fieldValue, $value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function increment(DateTime $date, $invert = false)
+ {
+ if ($invert) {
+ $date->modify('-1 day');
+ $date->setTime(23, 59, 0);
+ } else {
+ $date->modify('+1 day');
+ $date->setTime(0, 0, 0);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($value)
+ {
+ return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value);
+ }
+}
diff --git a/libraries/action-scheduler/lib/cron-expression/CronExpression_FieldFactory.php b/libraries/action-scheduler/lib/cron-expression/CronExpression_FieldFactory.php
new file mode 100644
index 0000000..556ba1a
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/CronExpression_FieldFactory.php
@@ -0,0 +1,55 @@
+
+ * @link http://en.wikipedia.org/wiki/Cron
+ */
+class CronExpression_FieldFactory
+{
+ /**
+ * @var array Cache of instantiated fields
+ */
+ private $fields = array();
+
+ /**
+ * Get an instance of a field object for a cron expression position
+ *
+ * @param int $position CRON expression position value to retrieve
+ *
+ * @return CronExpression_FieldInterface
+ * @throws InvalidArgumentException if a position is not valid
+ */
+ public function getField($position)
+ {
+ if (!isset($this->fields[$position])) {
+ switch ($position) {
+ case 0:
+ $this->fields[$position] = new CronExpression_MinutesField();
+ break;
+ case 1:
+ $this->fields[$position] = new CronExpression_HoursField();
+ break;
+ case 2:
+ $this->fields[$position] = new CronExpression_DayOfMonthField();
+ break;
+ case 3:
+ $this->fields[$position] = new CronExpression_MonthField();
+ break;
+ case 4:
+ $this->fields[$position] = new CronExpression_DayOfWeekField();
+ break;
+ case 5:
+ $this->fields[$position] = new CronExpression_YearField();
+ break;
+ default:
+ throw new InvalidArgumentException(
+ $position . ' is not a valid position'
+ );
+ }
+ }
+
+ return $this->fields[$position];
+ }
+}
diff --git a/libraries/action-scheduler/lib/cron-expression/CronExpression_FieldInterface.php b/libraries/action-scheduler/lib/cron-expression/CronExpression_FieldInterface.php
new file mode 100644
index 0000000..5d5109b
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/CronExpression_FieldInterface.php
@@ -0,0 +1,39 @@
+
+ */
+interface CronExpression_FieldInterface
+{
+ /**
+ * Check if the respective value of a DateTime field satisfies a CRON exp
+ *
+ * @param DateTime $date DateTime object to check
+ * @param string $value CRON expression to test against
+ *
+ * @return bool Returns TRUE if satisfied, FALSE otherwise
+ */
+ public function isSatisfiedBy(DateTime $date, $value);
+
+ /**
+ * When a CRON expression is not satisfied, this method is used to increment
+ * or decrement a DateTime object by the unit of the cron field
+ *
+ * @param DateTime $date DateTime object to change
+ * @param bool $invert (optional) Set to TRUE to decrement
+ *
+ * @return CronExpression_FieldInterface
+ */
+ public function increment(DateTime $date, $invert = false);
+
+ /**
+ * Validates a CRON expression for a given field
+ *
+ * @param string $value CRON expression value to validate
+ *
+ * @return bool Returns TRUE if valid, FALSE otherwise
+ */
+ public function validate($value);
+}
diff --git a/libraries/action-scheduler/lib/cron-expression/CronExpression_HoursField.php b/libraries/action-scheduler/lib/cron-expression/CronExpression_HoursField.php
new file mode 100644
index 0000000..088ca73
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/CronExpression_HoursField.php
@@ -0,0 +1,47 @@
+
+ */
+class CronExpression_HoursField extends CronExpression_AbstractField
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isSatisfiedBy(DateTime $date, $value)
+ {
+ return $this->isSatisfied($date->format('H'), $value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function increment(DateTime $date, $invert = false)
+ {
+ // Change timezone to UTC temporarily. This will
+ // allow us to go back or forwards and hour even
+ // if DST will be changed between the hours.
+ $timezone = $date->getTimezone();
+ $date->setTimezone(new DateTimeZone('UTC'));
+ if ($invert) {
+ $date->modify('-1 hour');
+ $date->setTime($date->format('H'), 59);
+ } else {
+ $date->modify('+1 hour');
+ $date->setTime($date->format('H'), 0);
+ }
+ $date->setTimezone($timezone);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($value)
+ {
+ return (bool) preg_match('/[\*,\/\-0-9]+/', $value);
+ }
+}
diff --git a/libraries/action-scheduler/lib/cron-expression/CronExpression_MinutesField.php b/libraries/action-scheduler/lib/cron-expression/CronExpression_MinutesField.php
new file mode 100644
index 0000000..436acf2
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/CronExpression_MinutesField.php
@@ -0,0 +1,39 @@
+
+ */
+class CronExpression_MinutesField extends CronExpression_AbstractField
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isSatisfiedBy(DateTime $date, $value)
+ {
+ return $this->isSatisfied($date->format('i'), $value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function increment(DateTime $date, $invert = false)
+ {
+ if ($invert) {
+ $date->modify('-1 minute');
+ } else {
+ $date->modify('+1 minute');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($value)
+ {
+ return (bool) preg_match('/[\*,\/\-0-9]+/', $value);
+ }
+}
diff --git a/libraries/action-scheduler/lib/cron-expression/CronExpression_MonthField.php b/libraries/action-scheduler/lib/cron-expression/CronExpression_MonthField.php
new file mode 100644
index 0000000..d3deb12
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/CronExpression_MonthField.php
@@ -0,0 +1,55 @@
+
+ */
+class CronExpression_MonthField extends CronExpression_AbstractField
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isSatisfiedBy(DateTime $date, $value)
+ {
+ // Convert text month values to integers
+ $value = str_ireplace(
+ array(
+ 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
+ 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'
+ ),
+ range(1, 12),
+ $value
+ );
+
+ return $this->isSatisfied($date->format('m'), $value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function increment(DateTime $date, $invert = false)
+ {
+ if ($invert) {
+ // $date->modify('last day of previous month'); // remove for php 5.2 compat
+ $date->modify('previous month');
+ $date->modify($date->format('Y-m-t'));
+ $date->setTime(23, 59);
+ } else {
+ //$date->modify('first day of next month'); // remove for php 5.2 compat
+ $date->modify('next month');
+ $date->modify($date->format('Y-m-01'));
+ $date->setTime(0, 0);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($value)
+ {
+ return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value);
+ }
+}
diff --git a/libraries/action-scheduler/lib/cron-expression/CronExpression_YearField.php b/libraries/action-scheduler/lib/cron-expression/CronExpression_YearField.php
new file mode 100644
index 0000000..f11562e
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/CronExpression_YearField.php
@@ -0,0 +1,43 @@
+
+ */
+class CronExpression_YearField extends CronExpression_AbstractField
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isSatisfiedBy(DateTime $date, $value)
+ {
+ return $this->isSatisfied($date->format('Y'), $value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function increment(DateTime $date, $invert = false)
+ {
+ if ($invert) {
+ $date->modify('-1 year');
+ $date->setDate($date->format('Y'), 12, 31);
+ $date->setTime(23, 59, 0);
+ } else {
+ $date->modify('+1 year');
+ $date->setDate($date->format('Y'), 1, 1);
+ $date->setTime(0, 0, 0);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($value)
+ {
+ return (bool) preg_match('/[\*,\/\-0-9]+/', $value);
+ }
+}
diff --git a/libraries/action-scheduler/lib/cron-expression/LICENSE b/libraries/action-scheduler/lib/cron-expression/LICENSE
new file mode 100644
index 0000000..c6d88ac
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2011 Michael Dowling and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libraries/action-scheduler/lib/cron-expression/README.md b/libraries/action-scheduler/lib/cron-expression/README.md
new file mode 100644
index 0000000..d4d9d5a
--- /dev/null
+++ b/libraries/action-scheduler/lib/cron-expression/README.md
@@ -0,0 +1,92 @@
+PHP Cron Expression Parser
+==========================
+
+[](https://packagist.org/packages/mtdowling/cron-expression) [](https://packagist.org/packages/mtdowling/cron-expression) [](http://travis-ci.org/mtdowling/cron-expression)
+
+The PHP cron expression parser can parse a CRON expression, determine if it is
+due to run, calculate the next run date of the expression, and calculate the previous
+run date of the expression. You can calculate dates far into the future or past by
+skipping n number of matching dates.
+
+The parser can handle increments of ranges (e.g. */12, 2-59/3), intervals (e.g. 0-9),
+lists (e.g. 1,2,3), W to find the nearest weekday for a given day of the month, L to
+find the last day of the month, L to find the last given weekday of a month, and hash
+(#) to find the nth weekday of a given month.
+
+Credits
+==========
+
+Created by Micheal Dowling. Ported to PHP 5.2 by Flightless, Inc.
+Based on version 1.0.3: https://github.com/mtdowling/cron-expression/tree/v1.0.3
+
+Installing
+==========
+
+Add the following to your project's composer.json:
+
+```javascript
+{
+ "require": {
+ "mtdowling/cron-expression": "1.0.*"
+ }
+}
+```
+
+Usage
+=====
+```php
+isDue();
+echo $cron->getNextRunDate()->format('Y-m-d H:i:s');
+echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s');
+
+// Works with complex expressions
+$cron = Cron\CronExpression::factory('3-59/15 2,6-12 */15 1 2-5');
+echo $cron->getNextRunDate()->format('Y-m-d H:i:s');
+
+// Calculate a run date two iterations into the future
+$cron = Cron\CronExpression::factory('@daily');
+echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s');
+
+// Calculate a run date relative to a specific time
+$cron = Cron\CronExpression::factory('@monthly');
+echo $cron->getNextRunDate('2010-01-12 00:00:00')->format('Y-m-d H:i:s');
+```
+
+CRON Expressions
+================
+
+A CRON expression is a string representing the schedule for a particular command to execute. The parts of a CRON schedule are as follows:
+
+ * * * * * *
+ - - - - - -
+ | | | | | |
+ | | | | | + year [optional]
+ | | | | +----- day of week (0 - 7) (Sunday=0 or 7)
+ | | | +---------- month (1 - 12)
+ | | +--------------- day of month (1 - 31)
+ | +-------------------- hour (0 - 23)
+ +------------------------- min (0 - 59)
+
+Requirements
+============
+
+- PHP 5.3+
+- PHPUnit is required to run the unit tests
+- Composer is required to run the unit tests
+
+CHANGELOG
+=========
+
+1.0.3 (2013-11-23)
+------------------
+
+* Only set default timezone if the given $currentTime is not a DateTime instance (#34)
+* Fixes issue #28 where PHP increments of ranges were failing due to PHP casting hyphens to 0
+* Now supports expressions with any number of extra spaces, tabs, or newlines
+* Using static instead of self in `CronExpression::factory`
diff --git a/libraries/action-scheduler/license.txt b/libraries/action-scheduler/license.txt
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/libraries/action-scheduler/license.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/libraries/action-scheduler/package-lock.json b/libraries/action-scheduler/package-lock.json
new file mode 100644
index 0000000..8584cb9
--- /dev/null
+++ b/libraries/action-scheduler/package-lock.json
@@ -0,0 +1,4995 @@
+{
+ "name": "action-scheduler",
+ "version": "3.9.2",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "action-scheduler",
+ "version": "3.8.0",
+ "license": "GPL-3.0+",
+ "devDependencies": {
+ "grunt": "1.5.3",
+ "grunt-checktextdomain": "1.0.1",
+ "grunt-phpcs": "0.4.0",
+ "husky": "3.0.9",
+ "lint-staged": "13.2.1"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=8.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
+ "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz",
+ "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^2.0.0",
+ "esutils": "^2.0.2",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "node_modules/@types/normalize-package-data": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
+ "dev": true
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "dev": true,
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/argparse/node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "node_modules/array-each": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
+ "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-slice": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
+ "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
+ "dev": true
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/caller-callsite": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
+ "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/caller-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
+ "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
+ "dev": true,
+ "dependencies": {
+ "caller-callsite": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
+ "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
+ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
+ "dev": true
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz",
+ "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==",
+ "dev": true,
+ "dependencies": {
+ "slice-ansi": "^5.0.0",
+ "string-width": "^5.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true
+ },
+ "node_modules/colors": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+ "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "node_modules/cosmiconfig": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
+ "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
+ "dev": true,
+ "dependencies": {
+ "import-fresh": "^2.0.0",
+ "is-directory": "^0.3.1",
+ "js-yaml": "^3.13.1",
+ "parse-json": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cosmiconfig/node_modules/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+ "dev": true,
+ "dependencies": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/dateformat": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
+ "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eventemitter2": {
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
+ "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==",
+ "dev": true
+ },
+ "node_modules/execa": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^6.0.0",
+ "get-stream": "^4.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
+ "dev": true,
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/findup-sync": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz",
+ "integrity": "sha512-z8Nrwhi6wzxNMIbxlrTzuUW6KWuKkogZ/7OdDVq+0+kxn77KUH1nipx8iU6suqkHqc4y6n7a9A8IpmxY/pTjWg==",
+ "dev": true,
+ "dependencies": {
+ "glob": "~5.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/glob": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+ "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==",
+ "dev": true,
+ "dependencies": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/fined": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
+ "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "is-plain-object": "^2.0.3",
+ "object.defaults": "^1.1.0",
+ "object.pick": "^1.2.0",
+ "parse-filepath": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/flagged-respawn": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
+ "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/for-own": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+ "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==",
+ "dev": true,
+ "dependencies": {
+ "for-in": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "dev": true,
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/getobject": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz",
+ "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/global-modules": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+ "dev": true,
+ "dependencies": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+ "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz",
+ "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==",
+ "dev": true,
+ "dependencies": {
+ "dateformat": "~3.0.3",
+ "eventemitter2": "~0.4.13",
+ "exit": "~0.1.2",
+ "findup-sync": "~0.3.0",
+ "glob": "~7.1.6",
+ "grunt-cli": "~1.4.3",
+ "grunt-known-options": "~2.0.0",
+ "grunt-legacy-log": "~3.0.0",
+ "grunt-legacy-util": "~2.0.1",
+ "iconv-lite": "~0.4.13",
+ "js-yaml": "~3.14.0",
+ "minimatch": "~3.0.4",
+ "mkdirp": "~1.0.4",
+ "nopt": "~3.0.6",
+ "rimraf": "~3.0.2"
+ },
+ "bin": {
+ "grunt": "bin/grunt"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-checktextdomain": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-checktextdomain/-/grunt-checktextdomain-1.0.1.tgz",
+ "integrity": "sha1-slTQHh3pEwBdTbHFMD2QI7mD4Zs=",
+ "dev": true,
+ "dependencies": {
+ "chalk": "~0.2.1",
+ "text-table": "~0.2.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ },
+ "peerDependencies": {
+ "grunt": ">=0.4.1"
+ }
+ },
+ "node_modules/grunt-checktextdomain/node_modules/ansi-styles": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz",
+ "integrity": "sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/grunt-checktextdomain/node_modules/chalk": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.2.1.tgz",
+ "integrity": "sha1-dhPhV1FFshOGSD9/SFql/6jL0Qw=",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "~0.2.0",
+ "has-color": "~0.1.0"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/grunt-known-options": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz",
+ "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-legacy-log": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz",
+ "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==",
+ "dev": true,
+ "dependencies": {
+ "colors": "~1.1.2",
+ "grunt-legacy-log-utils": "~2.1.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.19"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz",
+ "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "~4.1.0",
+ "lodash": "~4.17.19"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-legacy-util": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz",
+ "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==",
+ "dev": true,
+ "dependencies": {
+ "async": "~3.2.0",
+ "exit": "~0.1.2",
+ "getobject": "~1.0.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.21",
+ "underscore.string": "~3.3.5",
+ "which": "~2.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/grunt-legacy-util/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/grunt-phpcs": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/grunt-phpcs/-/grunt-phpcs-0.4.0.tgz",
+ "integrity": "sha1-oI1iX8ZEZeRTsr2T+BCyqB6Uvao=",
+ "dev": true,
+ "engines": {
+ "node": "0.10.x"
+ }
+ },
+ "node_modules/grunt/node_modules/grunt-cli": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz",
+ "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==",
+ "dev": true,
+ "dependencies": {
+ "grunt-known-options": "~2.0.0",
+ "interpret": "~1.1.0",
+ "liftup": "~3.0.1",
+ "nopt": "~4.0.1",
+ "v8flags": "~3.2.0"
+ },
+ "bin": {
+ "grunt": "bin/grunt"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/grunt/node_modules/grunt-cli/node_modules/nopt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
+ "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ }
+ },
+ "node_modules/grunt/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-color": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz",
+ "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/homedir-polyfill": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+ "dev": true,
+ "dependencies": {
+ "parse-passwd": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/hooker": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz",
+ "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true
+ },
+ "node_modules/human-signals": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
+ "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/husky": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.9.tgz",
+ "integrity": "sha512-Yolhupm7le2/MqC1VYLk/cNmYxsSsqKkTyBhzQHhPK1jFnC89mmmNVuGtLNabjDI6Aj8UNIr0KpRNuBkiC4+sg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "chalk": "^2.4.2",
+ "ci-info": "^2.0.0",
+ "cosmiconfig": "^5.2.1",
+ "execa": "^1.0.0",
+ "get-stdin": "^7.0.0",
+ "opencollective-postinstall": "^2.0.2",
+ "pkg-dir": "^4.2.0",
+ "please-upgrade-node": "^3.2.0",
+ "read-pkg": "^5.2.0",
+ "run-node": "^1.0.0",
+ "slash": "^3.0.0"
+ },
+ "bin": {
+ "husky-run": "run.js",
+ "husky-upgrade": "lib/upgrader/bin.js"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/husky/node_modules/get-stdin": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz",
+ "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/husky/node_modules/parse-json": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz",
+ "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/husky/node_modules/read-pkg": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
+ "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+ "dev": true,
+ "dependencies": {
+ "@types/normalize-package-data": "^2.4.0",
+ "normalize-package-data": "^2.5.0",
+ "parse-json": "^5.0.0",
+ "type-fest": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
+ "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
+ "dev": true,
+ "dependencies": {
+ "caller-path": "^2.0.0",
+ "resolve-from": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true
+ },
+ "node_modules/interpret": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
+ "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==",
+ "dev": true
+ },
+ "node_modules/is-absolute": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
+ "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
+ "dev": true,
+ "dependencies": {
+ "is-relative": "^1.0.0",
+ "is-windows": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+ "dev": true
+ },
+ "node_modules/is-core-module": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz",
+ "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-directory": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
+ "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-relative": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
+ "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
+ "dev": true,
+ "dependencies": {
+ "is-unc-path": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-unc-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
+ "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
+ "dev": true,
+ "dependencies": {
+ "unc-path-regex": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "node_modules/js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+ "dev": true
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/liftup": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz",
+ "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==",
+ "dev": true,
+ "dependencies": {
+ "extend": "^3.0.2",
+ "findup-sync": "^4.0.0",
+ "fined": "^1.2.0",
+ "flagged-respawn": "^1.0.1",
+ "is-plain-object": "^2.0.4",
+ "object.map": "^1.0.1",
+ "rechoir": "^0.7.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/liftup/node_modules/findup-sync": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz",
+ "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==",
+ "dev": true,
+ "dependencies": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "micromatch": "^4.0.2",
+ "resolve-dir": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/liftup/node_modules/resolve": {
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
+ "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.8.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+ "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
+ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
+ "dev": true
+ },
+ "node_modules/lint-staged": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.1.tgz",
+ "integrity": "sha512-8gfzinVXoPfga5Dz/ZOn8I2GOhf81Wvs+KwbEXQn/oWZAvCVS2PivrXfVbFJc93zD16uC0neS47RXHIjXKYZQw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "5.2.0",
+ "cli-truncate": "^3.1.0",
+ "commander": "^10.0.0",
+ "debug": "^4.3.4",
+ "execa": "^7.0.0",
+ "lilconfig": "2.1.0",
+ "listr2": "^5.0.7",
+ "micromatch": "^4.0.5",
+ "normalize-path": "^3.0.0",
+ "object-inspect": "^1.12.3",
+ "pidtree": "^0.6.0",
+ "string-argv": "^0.3.1",
+ "yaml": "^2.2.1"
+ },
+ "bin": {
+ "lint-staged": "bin/lint-staged.js"
+ },
+ "engines": {
+ "node": "^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/lint-staged"
+ }
+ },
+ "node_modules/lint-staged/node_modules/chalk": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz",
+ "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/lint-staged/node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/lint-staged/node_modules/execa": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz",
+ "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.1",
+ "human-signals": "^4.3.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^3.0.7",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/lint-staged/node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lint-staged/node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lint-staged/node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lint-staged/node_modules/npm-run-path": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
+ "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lint-staged/node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lint-staged/node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lint-staged/node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lint-staged/node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lint-staged/node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lint-staged/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/listr2": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-5.0.8.tgz",
+ "integrity": "sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==",
+ "dev": true,
+ "dependencies": {
+ "cli-truncate": "^2.1.0",
+ "colorette": "^2.0.19",
+ "log-update": "^4.0.0",
+ "p-map": "^4.0.0",
+ "rfdc": "^1.3.0",
+ "rxjs": "^7.8.0",
+ "through": "^2.3.8",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": "^14.13.1 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "enquirer": ">= 2.3.0 < 3"
+ },
+ "peerDependenciesMeta": {
+ "enquirer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/cli-truncate": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+ "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+ "dev": true,
+ "dependencies": {
+ "slice-ansi": "^3.0.0",
+ "string-width": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/listr2/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/listr2/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/listr2/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/listr2/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/listr2/node_modules/slice-ansi": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+ "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/listr2/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/listr2/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "node_modules/log-update": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+ "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-escapes": "^4.3.0",
+ "cli-cursor": "^3.1.0",
+ "slice-ansi": "^4.0.0",
+ "wrap-ansi": "^6.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/log-update/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/log-update/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/log-update/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-update/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-update/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/make-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
+ "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/map-cache": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+ "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
+ "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "node_modules/nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+ "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.defaults": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
+ "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==",
+ "dev": true,
+ "dependencies": {
+ "array-each": "^1.0.1",
+ "array-slice": "^1.0.0",
+ "for-own": "^1.0.0",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
+ "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==",
+ "dev": true,
+ "dependencies": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.pick": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+ "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/opencollective-postinstall": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz",
+ "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==",
+ "dev": true,
+ "bin": {
+ "opencollective-postinstall": "index.js"
+ }
+ },
+ "node_modules/os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "dev": true,
+ "dependencies": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "node_modules/p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
+ "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "dev": true,
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-filepath": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
+ "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==",
+ "dev": true,
+ "dependencies": {
+ "is-absolute": "^1.0.0",
+ "map-cache": "^0.2.0",
+ "path-root": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-root": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
+ "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==",
+ "dev": true,
+ "dependencies": {
+ "path-root-regex": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-root-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
+ "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pidtree": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+ "dev": true,
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/please-upgrade-node": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
+ "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
+ "dev": true,
+ "dependencies": {
+ "semver-compare": "^1.0.0"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dev": true,
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
+ "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.9.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
+ "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==",
+ "dev": true,
+ "dependencies": {
+ "path-parse": "^1.0.6"
+ }
+ },
+ "node_modules/resolve-dir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+ "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
+ "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
+ "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==",
+ "dev": true
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/run-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz",
+ "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==",
+ "dev": true,
+ "bin": {
+ "run-node": "run-node"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
+ "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
+ "dev": true
+ },
+ "node_modules/shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+ "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
+ "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
+ "dev": true,
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
+ "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
+ "dev": true
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+ "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+ "dev": true,
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
+ "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
+ "dev": true
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
+ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
+ "dev": true
+ },
+ "node_modules/string-argv": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
+ "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
+ "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-eof": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+ "dev": true
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
+ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==",
+ "dev": true
+ },
+ "node_modules/type-fest": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+ "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/unc-path-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
+ "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/underscore.string": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz",
+ "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "^1.1.1",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
+ },
+ "node_modules/v8flags": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz",
+ "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==",
+ "dev": true,
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "node_modules/yaml": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
+ "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 14"
+ }
+ }
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
+ "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.0.0"
+ }
+ },
+ "@babel/highlight": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz",
+ "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.0.0",
+ "esutils": "^2.0.2",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@types/normalize-package-data": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
+ "dev": true
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
+ "aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "dev": true,
+ "requires": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ }
+ },
+ "ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.21.3"
+ },
+ "dependencies": {
+ "type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true
+ }
+ }
+ },
+ "ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ },
+ "dependencies": {
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ }
+ }
+ },
+ "array-each": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
+ "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==",
+ "dev": true
+ },
+ "array-slice": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
+ "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==",
+ "dev": true
+ },
+ "astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true
+ },
+ "async": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.1.1"
+ }
+ },
+ "caller-callsite": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
+ "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
+ "dev": true,
+ "requires": {
+ "callsites": "^2.0.0"
+ }
+ },
+ "caller-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
+ "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
+ "dev": true,
+ "requires": {
+ "caller-callsite": "^2.0.0"
+ }
+ },
+ "callsites": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
+ "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "ci-info": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
+ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
+ "dev": true
+ },
+ "clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true
+ },
+ "cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^3.1.0"
+ }
+ },
+ "cli-truncate": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz",
+ "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==",
+ "dev": true,
+ "requires": {
+ "slice-ansi": "^5.0.0",
+ "string-width": "^5.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true
+ },
+ "colors": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+ "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==",
+ "dev": true
+ },
+ "commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "cosmiconfig": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
+ "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
+ "dev": true,
+ "requires": {
+ "import-fresh": "^2.0.0",
+ "is-directory": "^0.3.1",
+ "js-yaml": "^3.13.1",
+ "parse-json": "^4.0.0"
+ },
+ "dependencies": {
+ "parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+ "dev": true,
+ "requires": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ }
+ }
+ }
+ },
+ "cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "requires": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "dateformat": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
+ "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==",
+ "dev": true
+ },
+ "eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dev": true,
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
+ "eventemitter2": {
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
+ "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==",
+ "dev": true
+ },
+ "execa": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^6.0.0",
+ "get-stream": "^4.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ }
+ },
+ "exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
+ "dev": true
+ },
+ "expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
+ "dev": true,
+ "requires": {
+ "homedir-polyfill": "^1.0.1"
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true
+ },
+ "fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "findup-sync": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz",
+ "integrity": "sha512-z8Nrwhi6wzxNMIbxlrTzuUW6KWuKkogZ/7OdDVq+0+kxn77KUH1nipx8iU6suqkHqc4y6n7a9A8IpmxY/pTjWg==",
+ "dev": true,
+ "requires": {
+ "glob": "~5.0.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+ "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==",
+ "dev": true,
+ "requires": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
+ }
+ },
+ "fined": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
+ "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.2",
+ "is-plain-object": "^2.0.3",
+ "object.defaults": "^1.1.0",
+ "object.pick": "^1.2.0",
+ "parse-filepath": "^1.0.1"
+ }
+ },
+ "flagged-respawn": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
+ "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==",
+ "dev": true
+ },
+ "for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==",
+ "dev": true
+ },
+ "for-own": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+ "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==",
+ "dev": true,
+ "requires": {
+ "for-in": "^1.0.1"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "dev": true,
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "getobject": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz",
+ "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "global-modules": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+ "dev": true,
+ "requires": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ }
+ },
+ "global-prefix": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+ "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ }
+ },
+ "grunt": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz",
+ "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==",
+ "dev": true,
+ "requires": {
+ "dateformat": "~3.0.3",
+ "eventemitter2": "~0.4.13",
+ "exit": "~0.1.2",
+ "findup-sync": "~0.3.0",
+ "glob": "~7.1.6",
+ "grunt-cli": "~1.4.3",
+ "grunt-known-options": "~2.0.0",
+ "grunt-legacy-log": "~3.0.0",
+ "grunt-legacy-util": "~2.0.1",
+ "iconv-lite": "~0.4.13",
+ "js-yaml": "~3.14.0",
+ "minimatch": "~3.0.4",
+ "mkdirp": "~1.0.4",
+ "nopt": "~3.0.6",
+ "rimraf": "~3.0.2"
+ },
+ "dependencies": {
+ "grunt-cli": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz",
+ "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==",
+ "dev": true,
+ "requires": {
+ "grunt-known-options": "~2.0.0",
+ "interpret": "~1.1.0",
+ "liftup": "~3.0.1",
+ "nopt": "~4.0.1",
+ "v8flags": "~3.2.0"
+ },
+ "dependencies": {
+ "nopt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
+ "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
+ "dev": true,
+ "requires": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ }
+ }
+ }
+ },
+ "js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ }
+ }
+ },
+ "grunt-checktextdomain": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-checktextdomain/-/grunt-checktextdomain-1.0.1.tgz",
+ "integrity": "sha1-slTQHh3pEwBdTbHFMD2QI7mD4Zs=",
+ "dev": true,
+ "requires": {
+ "chalk": "~0.2.1",
+ "text-table": "~0.2.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz",
+ "integrity": "sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.2.1.tgz",
+ "integrity": "sha1-dhPhV1FFshOGSD9/SFql/6jL0Qw=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "~0.2.0",
+ "has-color": "~0.1.0"
+ }
+ }
+ }
+ },
+ "grunt-known-options": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz",
+ "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==",
+ "dev": true
+ },
+ "grunt-legacy-log": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz",
+ "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==",
+ "dev": true,
+ "requires": {
+ "colors": "~1.1.2",
+ "grunt-legacy-log-utils": "~2.1.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.19"
+ }
+ },
+ "grunt-legacy-log-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz",
+ "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==",
+ "dev": true,
+ "requires": {
+ "chalk": "~4.1.0",
+ "lodash": "~4.17.19"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "grunt-legacy-util": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz",
+ "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==",
+ "dev": true,
+ "requires": {
+ "async": "~3.2.0",
+ "exit": "~0.1.2",
+ "getobject": "~1.0.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.21",
+ "underscore.string": "~3.3.5",
+ "which": "~2.0.2"
+ },
+ "dependencies": {
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ }
+ }
+ },
+ "grunt-phpcs": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/grunt-phpcs/-/grunt-phpcs-0.4.0.tgz",
+ "integrity": "sha1-oI1iX8ZEZeRTsr2T+BCyqB6Uvao=",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-color": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz",
+ "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "homedir-polyfill": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+ "dev": true,
+ "requires": {
+ "parse-passwd": "^1.0.0"
+ }
+ },
+ "hooker": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz",
+ "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==",
+ "dev": true
+ },
+ "hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true
+ },
+ "human-signals": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
+ "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
+ "dev": true
+ },
+ "husky": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.9.tgz",
+ "integrity": "sha512-Yolhupm7le2/MqC1VYLk/cNmYxsSsqKkTyBhzQHhPK1jFnC89mmmNVuGtLNabjDI6Aj8UNIr0KpRNuBkiC4+sg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.4.2",
+ "ci-info": "^2.0.0",
+ "cosmiconfig": "^5.2.1",
+ "execa": "^1.0.0",
+ "get-stdin": "^7.0.0",
+ "opencollective-postinstall": "^2.0.2",
+ "pkg-dir": "^4.2.0",
+ "please-upgrade-node": "^3.2.0",
+ "read-pkg": "^5.2.0",
+ "run-node": "^1.0.0",
+ "slash": "^3.0.0"
+ },
+ "dependencies": {
+ "get-stdin": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz",
+ "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==",
+ "dev": true
+ },
+ "parse-json": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz",
+ "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1",
+ "lines-and-columns": "^1.1.6"
+ }
+ },
+ "read-pkg": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
+ "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+ "dev": true,
+ "requires": {
+ "@types/normalize-package-data": "^2.4.0",
+ "normalize-package-data": "^2.5.0",
+ "parse-json": "^5.0.0",
+ "type-fest": "^0.6.0"
+ }
+ }
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "import-fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
+ "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
+ "dev": true,
+ "requires": {
+ "caller-path": "^2.0.0",
+ "resolve-from": "^3.0.0"
+ }
+ },
+ "indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true
+ },
+ "interpret": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
+ "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==",
+ "dev": true
+ },
+ "is-absolute": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
+ "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
+ "dev": true,
+ "requires": {
+ "is-relative": "^1.0.0",
+ "is-windows": "^1.0.1"
+ }
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+ "dev": true
+ },
+ "is-core-module": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz",
+ "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-directory": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
+ "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
+ "dev": true
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "requires": {
+ "isobject": "^3.0.1"
+ }
+ },
+ "is-relative": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
+ "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
+ "dev": true,
+ "requires": {
+ "is-unc-path": "^1.0.0"
+ }
+ },
+ "is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+ "dev": true
+ },
+ "is-unc-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
+ "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
+ "dev": true,
+ "requires": {
+ "unc-path-regex": "^0.1.2"
+ }
+ },
+ "is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "dev": true
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+ "dev": true
+ },
+ "kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true
+ },
+ "liftup": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz",
+ "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==",
+ "dev": true,
+ "requires": {
+ "extend": "^3.0.2",
+ "findup-sync": "^4.0.0",
+ "fined": "^1.2.0",
+ "flagged-respawn": "^1.0.1",
+ "is-plain-object": "^2.0.4",
+ "object.map": "^1.0.1",
+ "rechoir": "^0.7.0",
+ "resolve": "^1.19.0"
+ },
+ "dependencies": {
+ "findup-sync": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz",
+ "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==",
+ "dev": true,
+ "requires": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "micromatch": "^4.0.2",
+ "resolve-dir": "^1.0.1"
+ }
+ },
+ "resolve": {
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
+ "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.8.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ }
+ }
+ },
+ "lilconfig": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+ "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+ "dev": true
+ },
+ "lines-and-columns": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
+ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
+ "dev": true
+ },
+ "lint-staged": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.1.tgz",
+ "integrity": "sha512-8gfzinVXoPfga5Dz/ZOn8I2GOhf81Wvs+KwbEXQn/oWZAvCVS2PivrXfVbFJc93zD16uC0neS47RXHIjXKYZQw==",
+ "dev": true,
+ "requires": {
+ "chalk": "5.2.0",
+ "cli-truncate": "^3.1.0",
+ "commander": "^10.0.0",
+ "debug": "^4.3.4",
+ "execa": "^7.0.0",
+ "lilconfig": "2.1.0",
+ "listr2": "^5.0.7",
+ "micromatch": "^4.0.5",
+ "normalize-path": "^3.0.0",
+ "object-inspect": "^1.12.3",
+ "pidtree": "^0.6.0",
+ "string-argv": "^0.3.1",
+ "yaml": "^2.2.1"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz",
+ "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "execa": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz",
+ "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.1",
+ "human-signals": "^4.3.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^3.0.7",
+ "strip-final-newline": "^3.0.0"
+ }
+ },
+ "get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true
+ },
+ "is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true
+ },
+ "mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true
+ },
+ "npm-run-path": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
+ "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
+ "dev": true,
+ "requires": {
+ "path-key": "^4.0.0"
+ },
+ "dependencies": {
+ "path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true
+ }
+ }
+ },
+ "onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^4.0.0"
+ }
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ }
+ }
+ },
+ "listr2": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-5.0.8.tgz",
+ "integrity": "sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==",
+ "dev": true,
+ "requires": {
+ "cli-truncate": "^2.1.0",
+ "colorette": "^2.0.19",
+ "log-update": "^4.0.0",
+ "p-map": "^4.0.0",
+ "rfdc": "^1.3.0",
+ "rxjs": "^7.8.0",
+ "through": "^2.3.8",
+ "wrap-ansi": "^7.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "cli-truncate": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+ "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+ "dev": true,
+ "requires": {
+ "slice-ansi": "^3.0.0",
+ "string-width": "^4.2.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "slice-ansi": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+ "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ }
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
+ }
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "log-update": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+ "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^4.3.0",
+ "cli-cursor": "^3.1.0",
+ "slice-ansi": "^4.0.0",
+ "wrap-ansi": "^6.2.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ }
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ }
+ }
+ },
+ "make-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
+ "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.2"
+ }
+ },
+ "map-cache": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+ "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==",
+ "dev": true
+ },
+ "merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ }
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
+ "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==",
+ "dev": true,
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "requires": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+ "dev": true,
+ "requires": {
+ "path-key": "^2.0.0"
+ }
+ },
+ "object-inspect": {
+ "version": "1.12.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+ "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "dev": true
+ },
+ "object.defaults": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
+ "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==",
+ "dev": true,
+ "requires": {
+ "array-each": "^1.0.1",
+ "array-slice": "^1.0.0",
+ "for-own": "^1.0.0",
+ "isobject": "^3.0.0"
+ }
+ },
+ "object.map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
+ "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==",
+ "dev": true,
+ "requires": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ }
+ },
+ "object.pick": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+ "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==",
+ "dev": true,
+ "requires": {
+ "isobject": "^3.0.1"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ },
+ "opencollective-postinstall": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz",
+ "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==",
+ "dev": true
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
+ "dev": true
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "dev": true
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "dev": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+ "dev": true
+ },
+ "p-limit": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
+ "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "dev": true,
+ "requires": {
+ "aggregate-error": "^3.0.0"
+ }
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true
+ },
+ "parse-filepath": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
+ "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==",
+ "dev": true,
+ "requires": {
+ "is-absolute": "^1.0.0",
+ "map-cache": "^0.2.0",
+ "path-root": "^0.1.1"
+ }
+ },
+ "parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "path-root": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
+ "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==",
+ "dev": true,
+ "requires": {
+ "path-root-regex": "^0.1.0"
+ }
+ },
+ "path-root-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
+ "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true
+ },
+ "pidtree": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+ "dev": true
+ },
+ "pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "requires": {
+ "find-up": "^4.0.0"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ }
+ }
+ },
+ "please-upgrade-node": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
+ "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
+ "dev": true,
+ "requires": {
+ "semver-compare": "^1.0.0"
+ }
+ },
+ "pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dev": true,
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "rechoir": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
+ "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==",
+ "dev": true,
+ "requires": {
+ "resolve": "^1.9.0"
+ }
+ },
+ "resolve": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
+ "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==",
+ "dev": true,
+ "requires": {
+ "path-parse": "^1.0.6"
+ }
+ },
+ "resolve-dir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+ "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ }
+ },
+ "resolve-from": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
+ "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
+ "dev": true
+ },
+ "restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "requires": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "rfdc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
+ "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "run-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz",
+ "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==",
+ "dev": true
+ },
+ "rxjs": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
+ "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
+ "dev": true,
+ "requires": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true
+ },
+ "semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
+ "dev": true
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "dev": true
+ },
+ "signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true
+ },
+ "slice-ansi": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+ "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true
+ }
+ }
+ },
+ "spdx-correct": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
+ "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
+ "dev": true,
+ "requires": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-exceptions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
+ "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
+ "dev": true
+ },
+ "spdx-expression-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+ "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+ "dev": true,
+ "requires": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-license-ids": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
+ "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
+ "dev": true
+ },
+ "sprintf-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
+ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
+ "dev": true
+ },
+ "string-argv": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
+ "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "requires": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
+ "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^6.0.1"
+ }
+ },
+ "strip-eof": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+ "dev": true
+ },
+ "strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+ "dev": true
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "tslib": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
+ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==",
+ "dev": true
+ },
+ "type-fest": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+ "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+ "dev": true
+ },
+ "unc-path-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
+ "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==",
+ "dev": true
+ },
+ "underscore.string": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz",
+ "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "^1.1.1",
+ "util-deprecate": "^1.0.2"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
+ },
+ "v8flags": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz",
+ "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==",
+ "dev": true,
+ "requires": {
+ "homedir-polyfill": "^1.0.1"
+ }
+ },
+ "validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "requires": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "yaml": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
+ "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
+ "dev": true
+ }
+ }
+}
diff --git a/libraries/action-scheduler/package.json b/libraries/action-scheduler/package.json
new file mode 100644
index 0000000..4df5b07
--- /dev/null
+++ b/libraries/action-scheduler/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "action-scheduler",
+ "title": "Action Scheduler",
+ "version": "3.9.2",
+ "homepage": "https://actionscheduler.org/",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/woocommerce/action-scheduler.git"
+ },
+ "license": "GPL-3.0+",
+ "main": "Gruntfile.js",
+ "scripts": {
+ "build": "composer install --no-dev && npm install --only=prod && composer archive --file=$npm_package_name --format=zip && npm run postarchive",
+ "postarchive": "rm -rf $npm_package_name && unzip $npm_package_name.zip -d $npm_package_name && rm $npm_package_name.zip && zip -r $npm_package_name.zip $npm_package_name && rm -rf $npm_package_name",
+ "check-textdomain": "grunt checktextdomain",
+ "phpcs": "grunt phpcs"
+ },
+ "woorelease": {
+ "wp_org_slug": "action-scheduler"
+ },
+ "devDependencies": {
+ "grunt": "1.5.3",
+ "grunt-checktextdomain": "1.0.1",
+ "grunt-phpcs": "0.4.0",
+ "husky": "3.0.9",
+ "lint-staged": "13.2.1"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=8.0.0"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "lint-staged"
+ }
+ },
+ "lint-staged": {
+ "*.php": [
+ "php -d display_errors=1 -l",
+ "composer run-script phpcs-pre-commit"
+ ]
+ }
+}
diff --git a/libraries/action-scheduler/phpcs.xml b/libraries/action-scheduler/phpcs.xml
new file mode 100644
index 0000000..4b72a7f
--- /dev/null
+++ b/libraries/action-scheduler/phpcs.xml
@@ -0,0 +1,63 @@
+
+
+ WooCommerce dev PHP_CodeSniffer ruleset.
+
+
+ docs/
+ */node_modules/*
+ */vendor/*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ classes/*
+ deprecated/*
+ lib/*
+ tests/*
+
+
+ classes/*
+ deprecated/*
+ lib/*
+ tests/*
+
+
+
+ tests/
+
+
+
+ classes/*
+ deprecated/*
+ lib/*
+ tests/*
+
+
+
+ classes/ActionScheduler_wcSystemStatus.php
+ classes/data-stores/ActionScheduler_wpCommentLogger.php
+ classes/data-stores/ActionScheduler_wpPostStore.php
+ classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php
+ classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php
+ classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php
+ classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php
+ tests/phpunit/jobstore/ActionScheduler_wpPostStore_Test.php
+ tests/phpunit/logging/ActionScheduler_wpCommentLogger_Test.php
+ tests/phpunit/procedural_api/wc_get_scheduled_actions_Test.php
+
+
+
+ tests/*
+
+
diff --git a/libraries/action-scheduler/readme.txt b/libraries/action-scheduler/readme.txt
new file mode 100644
index 0000000..9ef478b
--- /dev/null
+++ b/libraries/action-scheduler/readme.txt
@@ -0,0 +1,231 @@
+=== Action Scheduler ===
+Contributors: Automattic, wpmuguru, claudiosanches, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, royho, barryhughes-1
+Tags: scheduler, cron
+Stable tag: 3.9.2
+License: GPLv3
+Requires at least: 6.5
+Tested up to: 6.7
+Requires PHP: 7.1
+
+Action Scheduler - Job Queue for WordPress
+
+== Description ==
+
+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 occasions.
+
+Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook.
+
+## Battle-Tested Background Processing
+
+Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins.
+
+It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, at a sustained rate of over 10,000 / hour without negatively impacting normal site operations.
+
+This is all on infrastructure and WordPress sites outside the control of the plugin author.
+
+If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help.
+
+## Learn More
+
+To learn more about how to Action Scheduler works, and how to use it in your plugin, check out the docs on [ActionScheduler.org](https://actionscheduler.org).
+
+There you will find:
+
+* [Usage guide](https://actionscheduler.org/usage/): instructions on installing and using Action Scheduler
+* [WP CLI guide](https://actionscheduler.org/wp-cli/): instructions on running Action Scheduler at scale via WP CLI
+* [API Reference](https://actionscheduler.org/api/): complete reference guide for all API functions
+* [Administration Guide](https://actionscheduler.org/admin/): guide to managing scheduled actions via the administration screen
+* [Guide to Background Processing at Scale](https://actionscheduler.org/perf/): instructions for running Action Scheduler at scale via the default WP Cron queue runner
+
+## Credits
+
+Action Scheduler is developed and maintained by [Automattic](http://automattic.com/) with significant early development completed by [Flightless](https://flightless.us/).
+
+Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/woocommerce/action-scheduler/pulls) welcome.
+
+== Changelog ==
+
+= 3.9.2 - 2025-02-03 =
+* Fixed fatal errors by moving version info methods to a new class and deprecating conflicting ones in ActionScheduler_Versions
+
+= 3.9.1 - 2025-01-21 =
+* A number of new WP CLI commands have been added, making it easier to manage actions in the terminal and from scripts.
+* New wp action-scheduler source command to help determine how Action Scheduler is being loaded.
+* Additional information about the active instance of Action Scheduler is now available in the Help pull-down drawer.
+* Make some other nullable parameters explicitly nullable.
+* Set option value to `no` rather than deleting.
+
+= 3.9.0 - 2024-11-14 =
+* Minimum required version of PHP is now 7.1.
+* Performance improvements for the `as_pending_actions_due()` function.
+* Existing filter hook `action_scheduler_claim_actions_order_by` enhanced to provide callbacks with additional information.
+* Improved compatibility with PHP 8.4, specifically by making implicitly nullable parameters explicitly nullable.
+* A large number of coding standards-enhancements, to help reduce friction when submitting plugins to marketplaces and plugin directories. Special props @crstauf for this effort.
+* Minor documentation tweaks and improvements.
+
+= 3.8.2 - 2024-09-12 =
+* Add missing parameter to the `pre_as_enqueue_async_action` hook.
+* Bump minimum PHP version to 7.0.
+* Bump minimum WordPress version to 6.4.
+* Make the batch size adjustable during processing.
+
+= 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.
+* Calculate timeouts based on total actions.
+* Correctly order the parameters for `ActionScheduler_ActionFactory`'s calls to `single_unique`.
+* Fetch action in memory first before releasing claim to avoid deadlock.
+* PHP 8.2: declare property to fix creation of dynamic property warning.
+* PHP 8.2: fix "Using ${var} in strings is deprecated, use {$var} instead".
+* Prevent `undefined variable` warning for `$num_pastdue_actions`.
+
+= 3.5.3 - 2022-11-09 =
+* Query actions with partial match.
+
+= 3.5.2 - 2022-09-16 =
+* Fix - erroneous 3.5.1 release.
+
+= 3.5.1 - 2022-09-13 =
+* Maintenance on A/S docs.
+* fix: PHP 8.2 deprecated notice.
+
+= 3.5.0 - 2022-08-25 =
+* Add - The active view link within the "Tools > Scheduled Actions" screen is now clickable.
+* Add - A warning when there are past-due actions.
+* Enhancement - Added the ability to schedule unique actions via an atomic operation.
+* Enhancement - Improvements to cache invalidation when processing batches (when running on WordPress 6.0+).
+* Enhancement - If a recurring action is found to be consistently failing, it will stop being rescheduled.
+* Enhancement - Adds a new "Past Due" view to the scheduled actions list table.
+
+= 3.4.2 - 2022-06-08 =
+* Fix - Change the include for better linting.
+* Fix - update: Added Action scheduler completed action hook.
+
+= 3.4.1 - 2022-05-24 =
+* Fix - Change the include for better linting.
+* Fix - Fix the documented return type.
+
+= 3.4.0 - 2021-10-29 =
+* Enhancement - Number of items per page can now be set for the Scheduled Actions view (props @ovidiul). #771
+* Fix - Do not lower the max_execution_time if it is already set to 0 (unlimited) (props @barryhughes). #755
+* Fix - Avoid triggering autoloaders during the version resolution process (props @olegabr). #731 & #776
+* 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 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
+* Dev - PHPCS fixes for ActionScheduler_WpPostStore class (props @ovidiul). #780
+
+= 3.3.0 - 2021-09-15 =
+* Enhancement - Adds as_has_scheduled_action() to provide a performant way to test for existing actions. #645
+* Fix - Improves compatibility with environments where NO_ZERO_DATE is enabled. #519
+* Fix - Adds safety checks to guard against errors when our database tables cannot be created. #645
+* Dev - Now supports queries that use multiple statuses. #649
+* Dev - Minimum requirements for WordPress and PHP bumped (to 5.2 and 5.6 respectively). #723
+
+= 3.2.1 - 2021-06-21 =
+* Fix - Add extra safety/account for different versions of AS and different loading patterns. #714
+* Fix - Handle hidden columns (Tools → Scheduled Actions) | #600.
+
+= 3.2.0 - 2021-06-03 =
+* Fix - Add "no ordering" option to as_next_scheduled_action().
+* Fix - Add secondary scheduled date checks when claiming actions (DBStore) | #634.
+* Fix - Add secondary scheduled date checks when claiming actions (wpPostStore) | #634.
+* Fix - Adds a new index to the action table, reducing the potential for deadlocks (props: @glagonikas).
+* Fix - Fix unit tests infrastructure and adapt tests to PHP 8.
+* Fix - Identify in-use data store.
+* Fix - Improve test_migration_is_scheduled.
+* Fix - PHP notice on list table.
+* Fix - Speed up clean up and batch selects.
+* Fix - Update pending dependencies.
+* Fix - [PHP 8.0] Only pass action arg values through to do_action_ref_array().
+* Fix - [PHP 8] Set the PHP version to 7.1 in composer.json for PHP 8 compatibility.
+* Fix - add is_initialized() to docs.
+* Fix - fix file permissions.
+* Fix - fixes #664 by replacing __ with esc_html__.
diff --git a/libraries/action-scheduler/tests/ActionScheduler_UnitTestCase.php b/libraries/action-scheduler/tests/ActionScheduler_UnitTestCase.php
new file mode 100644
index 0000000..8b7217a
--- /dev/null
+++ b/libraries/action-scheduler/tests/ActionScheduler_UnitTestCase.php
@@ -0,0 +1,70 @@
+createResult();
+ }
+
+ $this->existing_timezone = date_default_timezone_get();
+
+ if ( 'UTC' !== $this->existing_timezone ) {
+ date_default_timezone_set( 'UTC' );
+ $result->run( $this );
+ }
+
+ date_default_timezone_set( 'Pacific/Fiji' ); // UTC+12.
+ $result->run( $this );
+
+ date_default_timezone_set( 'Pacific/Tahiti' ); // UTC-10: it's a magical place.
+ $result->run( $this );
+
+ date_default_timezone_set( $this->existing_timezone );
+
+ return $result;
+ }
+}
diff --git a/libraries/action-scheduler/tests/README.md b/libraries/action-scheduler/tests/README.md
new file mode 100644
index 0000000..79873d9
--- /dev/null
+++ b/libraries/action-scheduler/tests/README.md
@@ -0,0 +1,20 @@
+# Action Scheduler tests
+
+To run unit tests:
+
+1. Make sure that PHPUnit is installed by running:
+ ```
+ $ composer install
+ ```
+
+2. Install WordPress and the WP Unit Test lib using the `install.sh` script:
+ ```
+ $ tests/bin/install.sh [db-host] [wp-version] [skip-database-creation]
+ ```
+
+You may need to quote strings with backslashes to prevent them from being processed by the shell or other programs.
+
+Then, to run the tests:
+ ```
+ $ composer run test
+ ```
diff --git a/libraries/action-scheduler/tests/bin/install.sh b/libraries/action-scheduler/tests/bin/install.sh
new file mode 100755
index 0000000..ee05775
--- /dev/null
+++ b/libraries/action-scheduler/tests/bin/install.sh
@@ -0,0 +1,181 @@
+#!/usr/bin/env bash
+
+if [ $# -lt 3 ]; then
+ echo "usage: $0 [db-host] [wp-version] [skip-database-creation]"
+ exit 1
+fi
+
+DB_NAME=$1
+DB_USER=$2
+DB_PASS=$3
+DB_HOST=${4-localhost}
+WP_VERSION=${5-latest}
+SKIP_DB_CREATE=${6-false}
+
+TMPDIR=${TMPDIR-/tmp}
+TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
+WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
+WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress}
+
+download() {
+ if [ `which curl` ]; then
+ curl -s "$1" > "$2";
+ elif [ `which wget` ]; then
+ wget -nv -O "$2" "$1"
+ fi
+}
+
+if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
+ WP_BRANCH=${WP_VERSION%\-*}
+ WP_TESTS_TAG="branches/$WP_BRANCH"
+
+elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
+ WP_TESTS_TAG="branches/$WP_VERSION"
+elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
+ if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
+ # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
+ WP_TESTS_TAG="tags/${WP_VERSION%??}"
+ else
+ WP_TESTS_TAG="tags/$WP_VERSION"
+ fi
+elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
+ WP_TESTS_TAG="trunk"
+else
+ # http serves a single offer, whereas https serves multiple. we only want one
+ download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
+ grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
+ LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
+ if [[ -z "$LATEST_VERSION" ]]; then
+ echo "Latest WordPress version could not be found"
+ exit 1
+ fi
+ WP_TESTS_TAG="tags/$LATEST_VERSION"
+fi
+set -ex
+
+install_wp() {
+
+ if [ -d $WP_CORE_DIR ]; then
+ return;
+ fi
+
+ mkdir -p $WP_CORE_DIR
+
+ if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
+ mkdir -p $TMPDIR/wordpress-trunk
+ rm -rf $TMPDIR/wordpress-trunk/*
+ svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress
+ mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR
+ else
+ if [ $WP_VERSION == 'latest' ]; then
+ local ARCHIVE_NAME='latest'
+ elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
+ # https serves multiple offers, whereas http serves single.
+ download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json
+ if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
+ # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
+ LATEST_VERSION=${WP_VERSION%??}
+ else
+ # otherwise, scan the releases and get the most up to date minor version of the major release
+ local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'`
+ LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1)
+ fi
+ if [[ -z "$LATEST_VERSION" ]]; then
+ local ARCHIVE_NAME="wordpress-$WP_VERSION"
+ else
+ local ARCHIVE_NAME="wordpress-$LATEST_VERSION"
+ fi
+ else
+ local ARCHIVE_NAME="wordpress-$WP_VERSION"
+ fi
+ download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
+ tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
+ fi
+
+ download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
+}
+
+install_test_suite() {
+ # portable in-place argument for both GNU sed and Mac OSX sed
+ if [[ $(uname -s) == 'Darwin' ]]; then
+ local ioption='-i.bak'
+ else
+ local ioption='-i'
+ fi
+
+ # set up testing suite if it doesn't yet exist
+ if [ ! -d $WP_TESTS_DIR ]; then
+ # set up testing suite
+ mkdir -p $WP_TESTS_DIR
+ rm -rf $WP_TESTS_DIR/{includes,data}
+ svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
+ svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
+ fi
+
+ if [ ! -f wp-tests-config.php ]; then
+ download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
+ # remove all forward slashes in the end
+ WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
+ sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
+ fi
+
+}
+
+recreate_db() {
+ shopt -s nocasematch
+ if [[ $1 =~ ^(y|yes)$ ]]
+ then
+ mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA
+ create_db
+ echo "Recreated the database ($DB_NAME)."
+ else
+ echo "Leaving the existing database ($DB_NAME) in place."
+ fi
+ shopt -u nocasematch
+}
+
+create_db() {
+ mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
+}
+
+install_db() {
+
+ if [ ${SKIP_DB_CREATE} = "true" ]; then
+ return 0
+ fi
+
+ # parse DB_HOST for port or socket references
+ local PARTS=(${DB_HOST//\:/ })
+ local DB_HOSTNAME=${PARTS[0]};
+ local DB_SOCK_OR_PORT=${PARTS[1]};
+ local EXTRA=""
+
+ if ! [ -z $DB_HOSTNAME ] ; then
+ if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
+ EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
+ elif ! [ -z $DB_SOCK_OR_PORT ] ; then
+ EXTRA=" --socket=$DB_SOCK_OR_PORT"
+ elif ! [ -z $DB_HOSTNAME ] ; then
+ EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
+ fi
+ fi
+
+ # create database
+ if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ]
+ then
+ echo "Reinstalling will delete the existing test database ($DB_NAME)"
+ read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB
+ recreate_db $DELETE_EXISTING_DB
+ else
+ create_db
+ fi
+}
+
+install_wp
+install_test_suite
+install_db
diff --git a/libraries/action-scheduler/tests/bootstrap.php b/libraries/action-scheduler/tests/bootstrap.php
new file mode 100644
index 0000000..ba6c31b
--- /dev/null
+++ b/libraries/action-scheduler/tests/bootstrap.php
@@ -0,0 +1,37 @@
+
+
+
+
+
+ ./phpunit/migration
+
+
+ ./phpunit/jobstore/ActionScheduler_DBStoreMigrator_Test.php
+ ./phpunit/jobstore/ActionScheduler_DBStore_Test.php
+ ./phpunit/jobstore/ActionScheduler_HybridStore_Test.php
+ ./phpunit/logging/ActionScheduler_DBLogger_Test.php
+
+
+ ./phpunit/helpers
+ ./phpunit/jobs
+ ./phpunit/lock
+ ./phpunit/procedural_api
+ ./phpunit/runner
+ ./phpunit/schedules
+ ./phpunit/versioning
+ ./phpunit/logging/ActionScheduler_wpCommentLogger_Test.php
+ ./phpunit/jobstore/ActionScheduler_wpPostStore_Test.php
+
+
+
+
+ ignore
+
+
+
+
+ ..
+
+ .
+
+
+
+
diff --git a/libraries/action-scheduler/tests/phpunit/ActionScheduler_Mock_Async_Request_QueueRunner.php b/libraries/action-scheduler/tests/phpunit/ActionScheduler_Mock_Async_Request_QueueRunner.php
new file mode 100644
index 0000000..35d286c
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/ActionScheduler_Mock_Async_Request_QueueRunner.php
@@ -0,0 +1,16 @@
+createResult();
+ }
+
+ $this->existing_timezone = date_default_timezone_get();
+
+ if ( 'UTC' !== $this->existing_timezone ) {
+ date_default_timezone_set( 'UTC' );
+ $result->run( $this );
+ }
+
+ date_default_timezone_set( 'Pacific/Fiji' ); // UTC+12.
+ $result->run( $this );
+
+ date_default_timezone_set( 'Pacific/Tahiti' ); // UTC-10: it's a magical place.
+ $result->run( $this );
+
+ date_default_timezone_set( $this->existing_timezone );
+
+ return $result;
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_Callbacks.php b/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_Callbacks.php
new file mode 100644
index 0000000..fe56531
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_Callbacks.php
@@ -0,0 +1,34 @@
+assertEquals(
+ '0',
+ ini_get( 'max_execution_time' ),
+ 'If the max_execution_time was already zero (unlimited), then it will not be changed.'
+ );
+
+ ini_set( 'max_execution_time', 60 );
+ ActionScheduler_Compatibility::raise_time_limit( 30 );
+ $this->assertEquals(
+ '60',
+ ini_get( 'max_execution_time' ),
+ 'If the max_execution_time was already a higher value than we specify, then it will not be changed.'
+ );
+
+ ActionScheduler_Compatibility::raise_time_limit( 200 );
+ $this->assertEquals(
+ '200',
+ ini_get( 'max_execution_time' ),
+ 'If the max_execution_time was a lower value than we specify, but was above zero, then it will be updated to the new value.'
+ );
+
+ ActionScheduler_Compatibility::raise_time_limit( 0 );
+ $this->assertEquals(
+ '0',
+ ini_get( 'max_execution_time' ),
+ 'If the max_execution_time was a positive, non-zero value and we then specify zero (unlimited) as the new value, then it will be updated.'
+ );
+
+ // Cleanup.
+ ini_set( 'max_execution_time', $default_max_execution_time );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_TimezoneHelper_Test.php b/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_TimezoneHelper_Test.php
new file mode 100644
index 0000000..4c46a5e
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_TimezoneHelper_Test.php
@@ -0,0 +1,110 @@
+getTimezone();
+ $this->assertInstanceOf( 'DateTimeZone', $timezone );
+ $this->assertEquals( $timezone_string, $timezone->getName() );
+
+ remove_filter( 'option_timezone_string', $timezone_filter );
+ }
+
+ /**
+ * Get timezone strings.
+ *
+ * @return array[]
+ */
+ public function local_timezone_provider() {
+ return array(
+ array( 'America/New_York' ),
+ array( 'Australia/Melbourne' ),
+ array( 'UTC' ),
+ );
+ }
+
+ /**
+ * Ensure that most GMT offsets don't return UTC as the timezone.
+ *
+ * @dataProvider local_timezone_offsets_provider
+ *
+ * @param string $gmt_offset GMT offset.
+ */
+ public function test_local_timezone_offsets( $gmt_offset ) {
+ $gmt_filter = function ( $gmt ) use ( $gmt_offset ) {
+ return $gmt_offset;
+ };
+
+ $date = new ActionScheduler_DateTime();
+
+ add_filter( 'option_gmt_offset', $gmt_filter );
+ ActionScheduler_TimezoneHelper::set_local_timezone( $date );
+ remove_filter( 'option_gmt_offset', $gmt_filter );
+
+ $offset_in_seconds = $gmt_offset * HOUR_IN_SECONDS;
+
+ $this->assertEquals( $offset_in_seconds, $date->getOffset() );
+ $this->assertEquals( $offset_in_seconds, $date->getOffsetTimestamp() - $date->getTimestamp() );
+ }
+
+ /**
+ * Get timezone offsets.
+ *
+ * @return array[]
+ */
+ public function local_timezone_offsets_provider() {
+ return array(
+ array( '-11' ),
+ array( '-10.5' ),
+ array( '-10' ),
+ array( '-9' ),
+ array( '-8' ),
+ array( '-7' ),
+ array( '-6' ),
+ array( '-5' ),
+ array( '-4.5' ),
+ array( '-4' ),
+ array( '-3.5' ),
+ array( '-3' ),
+ array( '-2' ),
+ array( '-1' ),
+ array( '1' ),
+ array( '1.5' ),
+ array( '2' ),
+ array( '3' ),
+ array( '4' ),
+ array( '5' ),
+ array( '5.5' ),
+ array( '5.75' ),
+ array( '6' ),
+ array( '7' ),
+ array( '8' ),
+ array( '8.5' ),
+ array( '9' ),
+ array( '9.5' ),
+ array( '10' ),
+ array( '10.5' ),
+ array( '11' ),
+ array( '11.5' ),
+ array( '12' ),
+ array( '13' ),
+ );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_Action_Test.php b/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_Action_Test.php
new file mode 100644
index 0000000..2966525
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_Action_Test.php
@@ -0,0 +1,54 @@
+assertEquals( $schedule, $action->get_schedule() );
+ }
+
+ public function test_null_schedule() {
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK );
+ $this->assertInstanceOf( 'ActionScheduler_NullSchedule', $action->get_schedule() );
+ }
+
+ public function test_set_hook() {
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK );
+ $this->assertEquals( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, $action->get_hook() );
+ }
+
+ public function test_args() {
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK );
+ $this->assertEmpty( $action->get_args() );
+
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 5, 10, 15 ) );
+ $this->assertEqualSets( array( 5, 10, 15 ), $action->get_args() );
+ }
+
+ public function test_set_group() {
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), null, 'my_group' );
+ $this->assertEquals( 'my_group', $action->get_group() );
+ }
+
+ public function test_execute() {
+ $mock = new MockAction();
+
+ $random = md5( wp_rand() );
+ add_action( $random, array( $mock, 'action' ) );
+
+ $action = new ActionScheduler_Action( $random, array( $random ) );
+ $action->execute();
+
+ remove_action( $random, array( $mock, 'action' ) );
+
+ $this->assertEquals( 1, $mock->get_call_count() );
+ $events = $mock->get_events();
+ $event = reset( $events );
+ $this->assertEquals( $random, reset( $event['args'] ) );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_NullAction_Test.php b/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_NullAction_Test.php
new file mode 100644
index 0000000..9e630eb
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_NullAction_Test.php
@@ -0,0 +1,15 @@
+assertEmpty( $action->get_hook() );
+ $this->assertEmpty( $action->get_args() );
+ $this->assertNull( $action->get_schedule()->get_date() );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/jobstore/AbstractStoreTest.php b/libraries/action-scheduler/tests/phpunit/jobstore/AbstractStoreTest.php
new file mode 100644
index 0000000..fb16ba7
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/jobstore/AbstractStoreTest.php
@@ -0,0 +1,163 @@
+get_store();
+ $action_id = $store->save_action( $action );
+
+ $this->assertEquals( ActionScheduler_Store::STATUS_PENDING, $store->get_status( $action_id ) );
+
+ $store->mark_complete( $action_id );
+ $this->assertEquals( ActionScheduler_Store::STATUS_COMPLETE, $store->get_status( $action_id ) );
+
+ $store->mark_failure( $action_id );
+ $this->assertEquals( ActionScheduler_Store::STATUS_FAILED, $store->get_status( $action_id ) );
+ }
+
+ // Start tests for \ActionScheduler_Store::query_actions().
+
+ // phpcs:ignore Squiz.Commenting.FunctionComment.WrongStyle
+ public function test_query_actions_query_type_arg_invalid_option() {
+ $this->expectException( InvalidArgumentException::class );
+ $this->get_store()->query_actions( array( 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ), 'invalid' );
+ }
+
+ public function test_query_actions_query_type_arg_valid_options() {
+ $store = $this->get_store();
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) );
+
+ $action_id_1 = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule ) );
+ $action_id_2 = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule ) );
+
+ $this->assertEquals( array( $action_id_1, $action_id_2 ), $store->query_actions( array( 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ) ) );
+ $this->assertEquals( 2, $store->query_actions( array( 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ), 'count' ) );
+ }
+
+ public function test_query_actions_by_single_status() {
+ $store = $this->get_store();
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) );
+
+ $this->assertEquals( 0, $store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_PENDING ), 'count' ) );
+
+ $action_id_1 = $store->save_action( new ActionScheduler_Action( 'my_hook_1', array( 1 ), $schedule ) );
+ $action_id_2 = $store->save_action( new ActionScheduler_Action( 'my_hook_2', array( 1 ), $schedule ) );
+ $action_id_3 = $store->save_action( new ActionScheduler_Action( 'my_hook_3', array( 1 ), $schedule ) );
+ $store->mark_complete( $action_id_3 );
+
+ $this->assertEquals( 2, $store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_PENDING ), 'count' ) );
+ $this->assertEquals( 1, $store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_COMPLETE ), 'count' ) );
+ }
+
+ public function test_query_actions_by_array_status() {
+ $store = $this->get_store();
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) );
+
+ $this->assertEquals(
+ 0,
+ $store->query_actions(
+ array(
+ 'status' => array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_RUNNING ),
+ ),
+ 'count'
+ )
+ );
+
+ $action_id_1 = $store->save_action( new ActionScheduler_Action( 'my_hook_1', array( 1 ), $schedule ) );
+ $action_id_2 = $store->save_action( new ActionScheduler_Action( 'my_hook_2', array( 1 ), $schedule ) );
+ $action_id_3 = $store->save_action( new ActionScheduler_Action( 'my_hook_3', array( 1 ), $schedule ) );
+ $store->mark_failure( $action_id_3 );
+
+ $this->assertEquals(
+ 3,
+ $store->query_actions(
+ array(
+ 'status' => array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_FAILED ),
+ ),
+ 'count'
+ )
+ );
+ $this->assertEquals(
+ 2,
+ $store->query_actions(
+ array(
+ 'status' => array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_COMPLETE ),
+ ),
+ 'count'
+ )
+ );
+ }
+
+ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
+ // End tests for \ActionScheduler_Store::query_actions().
+
+ /**
+ * The `has_pending_actions_due` method should return a boolean value depending on whether there are
+ * pending actions.
+ *
+ * @return void
+ */
+ public function test_has_pending_actions_due() {
+ $store = $this->get_store();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+
+ for ( $i = - 3; $i <= 3; $i ++ ) {
+ // Some past actions, some future actions.
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
+
+ $store->save_action( $action );
+ }
+ $this->assertTrue( $store->has_pending_actions_due() );
+
+ $runner->run();
+ $this->assertFalse( $store->has_pending_actions_due() );
+ }
+
+ /**
+ * The `has_pending_actions_due` method should return false when all pending actions are in the future.
+ *
+ * @return void
+ */
+ public function test_has_pending_actions_due_only_future_actions() {
+ $store = $this->get_store();
+
+ for ( $i = 1; $i <= 3; $i ++ ) {
+ // Only future actions.
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
+
+ $store->save_action( $action );
+ }
+ $this->assertFalse( $store->has_pending_actions_due() );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStoreMigrator_Test.php b/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStoreMigrator_Test.php
new file mode 100644
index 0000000..2cee74e
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStoreMigrator_Test.php
@@ -0,0 +1,26 @@
+save_action( $action, null, $last_attempt_date );
+ $action_date = $store->get_date( $action_id );
+
+ $this->assertEquals( $last_attempt_date->format( 'U' ), $action_date->format( 'U' ) );
+
+ $action_id = $store->save_action( $action, $scheduled_date, $last_attempt_date );
+ $action_date = $store->get_date( $action_id );
+
+ $this->assertEquals( $last_attempt_date->format( 'U' ), $action_date->format( 'U' ) );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStore_Test.php b/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStore_Test.php
new file mode 100644
index 0000000..c8394b2
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStore_Test.php
@@ -0,0 +1,722 @@
+query( "DELETE FROM {$wpdb->actionscheduler_actions}" );
+
+ parent::setUp();
+ }
+
+ /**
+ * Get data store for tests.
+ *
+ * @return ActionScheduler_DBStore
+ */
+ protected function get_store() {
+ return new ActionScheduler_DBStore();
+ }
+
+ public function test_create_action() {
+ $time = as_get_datetime_object();
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $store = new ActionScheduler_DBStore();
+ $action_id = $store->save_action( $action );
+
+ $this->assertNotEmpty( $action_id );
+ }
+
+ public function test_create_action_with_scheduled_date() {
+ $time = as_get_datetime_object( strtotime( '-1 week' ) );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), new ActionScheduler_SimpleSchedule( $time ) );
+ $store = new ActionScheduler_DBStore();
+ $action_id = $store->save_action( $action, $time );
+ $action_date = $store->get_date( $action_id );
+
+ $this->assertEquals( $time->format( 'U' ), $action_date->format( 'U' ) );
+ }
+
+ public function test_retrieve_action() {
+ $time = as_get_datetime_object();
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
+ $store = new ActionScheduler_DBStore();
+ $action_id = $store->save_action( $action );
+
+ $retrieved = $store->fetch_action( $action_id );
+ $this->assertEquals( $action->get_hook(), $retrieved->get_hook() );
+ $this->assertEqualSets( $action->get_args(), $retrieved->get_args() );
+ $this->assertEquals( $action->get_schedule()->get_date()->format( 'U' ), $retrieved->get_schedule()->get_date()->format( 'U' ) );
+ $this->assertEquals( $action->get_group(), $retrieved->get_group() );
+ }
+
+ public function test_cancel_action() {
+ $time = as_get_datetime_object();
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
+ $store = new ActionScheduler_DBStore();
+ $action_id = $store->save_action( $action );
+ $store->cancel_action( $action_id );
+
+ $fetched = $store->fetch_action( $action_id );
+ $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
+ }
+
+ public function test_cancel_actions_by_hook() {
+ $store = new ActionScheduler_DBStore();
+ $actions = array();
+ $hook = 'by_hook_test';
+ for ( $day = 1; $day <= 3; $day++ ) {
+ $delta = sprintf( '+%d day', $day );
+ $time = as_get_datetime_object( $delta );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( $hook, array(), $schedule, 'my_group' );
+ $actions[] = $store->save_action( $action );
+ }
+ $store->cancel_actions_by_hook( $hook );
+
+ foreach ( $actions as $action_id ) {
+ $fetched = $store->fetch_action( $action_id );
+ $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
+ }
+ }
+
+ public function test_cancel_actions_by_group() {
+ $store = new ActionScheduler_DBStore();
+ $actions = array();
+ $group = 'by_group_test';
+ for ( $day = 1; $day <= 3; $day++ ) {
+ $delta = sprintf( '+%d day', $day );
+ $time = as_get_datetime_object( $delta );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, $group );
+ $actions[] = $store->save_action( $action );
+ }
+ $store->cancel_actions_by_group( $group );
+
+ foreach ( $actions as $action_id ) {
+ $fetched = $store->fetch_action( $action_id );
+ $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
+ }
+ }
+
+ public function test_claim_actions() {
+ $created_actions = array();
+ $store = new ActionScheduler_DBStore();
+ for ( $i = 3; $i > - 3; $i -- ) {
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
+
+ $created_actions[] = $store->save_action( $action );
+ }
+
+ $claim = $store->stake_claim();
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+
+ $this->assertCount( 3, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions, 3, 3 ), $claim->get_actions() );
+ }
+
+ public function test_claim_actions_order() {
+
+ $store = new ActionScheduler_DBStore();
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
+ $created_actions = array(
+ $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ),
+ $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ),
+ );
+
+ $claim = $store->stake_claim();
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+
+ // Verify uniqueness of action IDs.
+ $this->assertEquals( 2, count( array_unique( $created_actions ) ) );
+
+ // Verify the count and order of the actions.
+ $claimed_actions = $claim->get_actions();
+ $this->assertCount( 2, $claimed_actions );
+ $this->assertEquals( $created_actions, $claimed_actions );
+
+ // Verify the reversed order doesn't pass.
+ $reversed_actions = array_reverse( $created_actions );
+ $this->assertNotEquals( $reversed_actions, $claimed_actions );
+ }
+
+ public function test_claim_actions_by_hooks() {
+ $created_actions_by_hook = array();
+ $created_actions = array();
+
+ $store = new ActionScheduler_DBStore();
+ $unique_hook_one = 'my_unique_hook_one';
+ $unique_hook_two = 'my_unique_hook_two';
+ $unique_hooks = array(
+ $unique_hook_one,
+ $unique_hook_two,
+ );
+
+ for ( $i = 3; $i > - 3; $i -- ) {
+ foreach ( $unique_hooks as $unique_hook ) {
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( $unique_hook, array( $i ), $schedule, 'my_group' );
+ $action_id = $store->save_action( $action );
+
+ $created_actions[] = $action_id;
+ $created_actions_by_hook[ $unique_hook ][] = $action_id;
+ }
+ }
+
+ $claim = $store->stake_claim( 10, null, $unique_hooks );
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+ $this->assertCount( 6, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions, 6, 6 ), $claim->get_actions() );
+
+ $store->release_claim( $claim );
+
+ $claim = $store->stake_claim( 10, null, array( $unique_hook_one ) );
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+ $this->assertCount( 3, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ], 3, 3 ), $claim->get_actions() );
+
+ $store->release_claim( $claim );
+
+ $claim = $store->stake_claim( 10, null, array( $unique_hook_two ) );
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+ $this->assertCount( 3, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ], 3, 3 ), $claim->get_actions() );
+ }
+
+ public function test_claim_actions_by_group() {
+ $created_actions = array();
+ $store = new ActionScheduler_DBStore();
+ $unique_group_one = 'my_unique_group_one';
+ $unique_group_two = 'my_unique_group_two';
+ $unique_groups = array(
+ $unique_group_one,
+ $unique_group_two,
+ );
+
+ for ( $i = 3; $i > - 3; $i -- ) {
+ foreach ( $unique_groups as $unique_group ) {
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, $unique_group );
+
+ $created_actions[ $unique_group ][] = $store->save_action( $action );
+ }
+ }
+
+ $claim = $store->stake_claim( 10, null, array(), $unique_group_one );
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+ $this->assertCount( 3, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions[ $unique_group_one ], 3, 3 ), $claim->get_actions() );
+
+ $store->release_claim( $claim );
+
+ $claim = $store->stake_claim( 10, null, array(), $unique_group_two );
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+ $this->assertCount( 3, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions[ $unique_group_two ], 3, 3 ), $claim->get_actions() );
+ }
+
+ /**
+ * The DBStore allows one or more groups to be excluded from a claim.
+ */
+ public function test_claim_actions_with_group_exclusions() {
+ $created_actions = array();
+ $store = new ActionScheduler_DBStore();
+ $groups = array( 'foo', 'bar', 'baz' );
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
+
+ // Create 6 actions (with 2 in each test group).
+ foreach ( $groups as $group_slug ) {
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, $group_slug );
+
+ $created_actions[ $group_slug ] = array(
+ $store->save_action( $action ),
+ $store->save_action( $action ),
+ );
+ }
+
+ // If we exclude group 'foo' (representing 2 actions) the remaining 4 actions from groups 'bar' and 'baz' should still be claimed.
+ $store->set_claim_filter( 'exclude-groups', 'foo' );
+ $claim = $store->stake_claim();
+ $this->assertEquals(
+ array_merge( $created_actions['bar'], $created_actions['baz'] ),
+ $claim->get_actions(),
+ 'A single group can successfully be excluded from claims.'
+ );
+ $store->release_claim( $claim );
+
+ // If we exclude groups 'bar' and 'baz' (representing 4 actions) the remaining 2 actions from group 'foo' should still be claimed.
+ $store->set_claim_filter( 'exclude-groups', array( 'bar', 'baz' ) );
+ $claim = $store->stake_claim();
+ $this->assertEquals(
+ $created_actions['foo'],
+ $claim->get_actions(),
+ 'Multiple groups can successfully be excluded from claims.'
+ );
+ $store->release_claim( $claim );
+
+ // If we include group 'foo' (representing 2 actions) after excluding all groups, the inclusion should 'win'.
+ $store->set_claim_filter( 'exclude-groups', array( 'foo', 'bar', 'baz' ) );
+ $claim = $store->stake_claim( 10, null, array(), 'foo' );
+ $this->assertEquals(
+ $created_actions['foo'],
+ $claim->get_actions(),
+ 'Including a specific group takes precedence over group exclusions.'
+ );
+ $store->release_claim( $claim );
+ }
+
+ public function test_claim_actions_by_hook_and_group() {
+ $created_actions_by_hook = array();
+ $created_actions = array();
+ $store = new ActionScheduler_DBStore();
+
+ $unique_hook_one = 'my_other_unique_hook_one';
+ $unique_hook_two = 'my_other_unique_hook_two';
+ $unique_hooks = array(
+ $unique_hook_one,
+ $unique_hook_two,
+ );
+
+ $unique_group_one = 'my_other_other_unique_group_one';
+ $unique_group_two = 'my_other_unique_group_two';
+ $unique_groups = array(
+ $unique_group_one,
+ $unique_group_two,
+ );
+
+ for ( $i = 3; $i > - 3; $i -- ) {
+ foreach ( $unique_hooks as $unique_hook ) {
+ foreach ( $unique_groups as $unique_group ) {
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( $unique_hook, array( $i ), $schedule, $unique_group );
+ $action_id = $store->save_action( $action );
+
+ $created_actions[ $unique_group ][] = $action_id;
+ $created_actions_by_hook[ $unique_hook ][ $unique_group ][] = $action_id;
+ }
+ }
+ }
+
+ /** Test Both Hooks with Each Group */
+
+ $claim = $store->stake_claim( 10, null, $unique_hooks, $unique_group_one );
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+ $this->assertCount( 6, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions[ $unique_group_one ], 6, 6 ), $claim->get_actions() );
+
+ $store->release_claim( $claim );
+
+ $claim = $store->stake_claim( 10, null, $unique_hooks, $unique_group_two );
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+ $this->assertCount( 6, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions[ $unique_group_two ], 6, 6 ), $claim->get_actions() );
+
+ $store->release_claim( $claim );
+
+ /** Test Just One Hook with Group One */
+
+ $claim = $store->stake_claim( 10, null, array( $unique_hook_one ), $unique_group_one );
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+ $this->assertCount( 3, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ][ $unique_group_one ], 3, 3 ), $claim->get_actions() );
+
+ $store->release_claim( $claim );
+
+ $claim = $store->stake_claim( 24, null, array( $unique_hook_two ), $unique_group_one );
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+ $this->assertCount( 3, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ][ $unique_group_one ], 3, 3 ), $claim->get_actions() );
+
+ $store->release_claim( $claim );
+
+ /** Test Just One Hook with Group Two */
+
+ $claim = $store->stake_claim( 10, null, array( $unique_hook_one ), $unique_group_two );
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+ $this->assertCount( 3, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ][ $unique_group_two ], 3, 3 ), $claim->get_actions() );
+
+ $store->release_claim( $claim );
+
+ $claim = $store->stake_claim( 24, null, array( $unique_hook_two ), $unique_group_two );
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+ $this->assertCount( 3, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ][ $unique_group_two ], 3, 3 ), $claim->get_actions() );
+ }
+
+ /**
+ * Confirm that priorities are respected when claiming actions.
+ *
+ * @return void
+ */
+ public function test_claim_actions_respecting_priority() {
+ $store = new ActionScheduler_DBStore();
+
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-2 hours' ) );
+ $routine_action_1 = $store->save_action( new ActionScheduler_Action( 'routine_past_due', array(), $schedule, '' ) );
+
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
+ $action = new ActionScheduler_Action( 'high_priority_past_due', array(), $schedule, '' );
+ $action->set_priority( 5 );
+ $priority_action = $store->save_action( $action );
+
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-4 hours' ) );
+ $routine_action_2 = $store->save_action( new ActionScheduler_Action( 'routine_past_due', array(), $schedule, '' ) );
+
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '+1 hour' ) );
+ $action = new ActionScheduler_Action( 'high_priority_future', array(), $schedule, '' );
+ $action->set_priority( 2 );
+ $priority_future_action = $store->save_action( $action );
+
+ $claim = $store->stake_claim();
+ $this->assertEquals(
+ array( $priority_action, $routine_action_2, $routine_action_1 ),
+ $claim->get_actions(),
+ 'High priority actions take precedence over older but lower priority actions.'
+ );
+ }
+
+ /**
+ * The query used to claim actions explicitly ignores future pending actions, but it
+ * is still possible under unusual conditions (such as if MySQL runs out of temporary
+ * storage space) for such actions to be returned.
+ *
+ * When this happens, we still expect the store to filter them out, otherwise there is
+ * a risk that actions will be unexpectedly processed ahead of time.
+ *
+ * @see https://github.com/woocommerce/action-scheduler/issues/634
+ */
+ public function test_claim_filters_out_unexpected_future_actions() {
+ $group = __METHOD__;
+ $store = new ActionScheduler_DBStore();
+
+ // Create 4 actions: 2 that are already due (-3hrs and -1hrs) and 2 that are not yet due (+1hr and +3hrs).
+ for ( $i = -3; $i <= 3; $i += 2 ) {
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( $i . ' hours' ) );
+ $action_ids[] = $store->save_action( new ActionScheduler_Action( 'test_' . $i, array(), $schedule, $group ) );
+ }
+
+ // This callback is used to simulate the unusual conditions whereby MySQL might unexpectedly return future
+ // actions, contrary to the conditions used by the store object when staking its claim.
+ $simulate_unexpected_db_behavior = function( $sql ) use ( $action_ids ) {
+ global $wpdb;
+
+ // Look out for the claim update query, ignore all others.
+ if (
+ 0 !== strpos( $sql, "UPDATE $wpdb->actionscheduler_actions" )
+ || ! preg_match( "/claim_id = 0 AND scheduled_date_gmt <= '([0-9:\-\s]{19})'/", $sql, $matches )
+ || count( $matches ) !== 2
+ ) {
+ return $sql;
+ }
+
+ // Now modify the query, forcing it to also return the future actions we created.
+ return str_replace( $matches[1], as_get_datetime_object( '+4 hours' )->format( 'Y-m-d H:i:s' ), $sql );
+ };
+
+ add_filter( 'query', $simulate_unexpected_db_behavior );
+ $claim = $store->stake_claim( 10, null, array(), $group );
+ $claimed_actions = $claim->get_actions();
+ $this->assertCount( 2, $claimed_actions );
+
+ // Cleanup.
+ remove_filter( 'query', $simulate_unexpected_db_behavior );
+ $store->release_claim( $claim );
+ }
+
+ public function test_duplicate_claim() {
+ $created_actions = array();
+ $store = new ActionScheduler_DBStore();
+ for ( $i = 0; $i > - 3; $i -- ) {
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
+
+ $created_actions[] = $store->save_action( $action );
+ }
+
+ $claim1 = $store->stake_claim();
+ $claim2 = $store->stake_claim();
+ $this->assertCount( 3, $claim1->get_actions() );
+ $this->assertCount( 0, $claim2->get_actions() );
+ }
+
+ public function test_release_claim() {
+ $created_actions = array();
+ $store = new ActionScheduler_DBStore();
+ for ( $i = 0; $i > - 3; $i -- ) {
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
+
+ $created_actions[] = $store->save_action( $action );
+ }
+
+ $claim1 = $store->stake_claim();
+
+ $store->release_claim( $claim1 );
+ $this->assertCount( 0, $store->find_actions_by_claim_id( $claim1->get_id() ) );
+
+ $claim2 = $store->stake_claim();
+ $this->assertCount( 3, $claim2->get_actions() );
+ $store->release_claim( $claim2 );
+ $this->assertCount( 0, $store->find_actions_by_claim_id( $claim1->get_id() ) );
+
+ }
+
+ public function test_search() {
+ $created_actions = array();
+ $store = new ActionScheduler_DBStore();
+ for ( $i = - 3; $i <= 3; $i ++ ) {
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
+
+ $created_actions[] = $store->save_action( $action );
+ }
+
+ $next_no_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK );
+ $this->assertEquals( $created_actions[0], $next_no_args );
+
+ $next_with_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 1 ) ) );
+ $this->assertEquals( $created_actions[4], $next_with_args );
+
+ $non_existent = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 17 ) ) );
+ $this->assertNull( $non_existent );
+ }
+
+ public function test_search_by_group() {
+ $store = new ActionScheduler_DBStore();
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) );
+
+ $abc = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'abc' ) );
+ $def = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'def' ) );
+ $ghi = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'ghi' ) );
+
+ $this->assertEquals( $abc, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'abc' ) ) );
+ $this->assertEquals( $def, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'def' ) ) );
+ $this->assertEquals( $ghi, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'ghi' ) ) );
+ }
+
+ public function test_get_run_date() {
+ $time = as_get_datetime_object( '-10 minutes' );
+ $schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $store = new ActionScheduler_DBStore();
+ $action_id = $store->save_action( $action );
+
+ $this->assertEquals( $time->format( 'U' ), $store->get_date( $action_id )->format( 'U' ) );
+
+ $action = $store->fetch_action( $action_id );
+ $action->execute();
+ $now = as_get_datetime_object();
+ $store->mark_complete( $action_id );
+
+ $this->assertEquals( $now->format( 'U' ), $store->get_date( $action_id )->format( 'U' ) );
+
+ $next = $action->get_schedule()->get_next( $now );
+ $new_action_id = $store->save_action( $action, $next );
+
+ $this->assertEquals( (int) ( $now->format( 'U' ) ) + HOUR_IN_SECONDS, $store->get_date( $new_action_id )->format( 'U' ) );
+ }
+
+ /**
+ * Test creating a unique action.
+ */
+ public function test_create_action_unique() {
+ $time = as_get_datetime_object();
+ $hook = md5( wp_rand() );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $store = new ActionScheduler_DBStore();
+ $action = new ActionScheduler_Action( $hook, array(), $schedule );
+
+ $action_id = $store->save_action( $action );
+ $this->assertNotEquals( 0, $action_id );
+ $action_from_db = $store->fetch_action( $action_id );
+ $this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) );
+
+ $action = new ActionScheduler_Action( $hook, array(), $schedule );
+ $action_id_duplicate = $store->save_unique_action( $action );
+ $this->assertEquals( 0, $action_id_duplicate );
+ }
+
+ /**
+ * Test saving unique actions across different groups. Different groups should be saved, same groups shouldn't.
+ */
+ public function test_create_action_unique_with_different_groups() {
+ $time = as_get_datetime_object();
+ $hook = md5( wp_rand() );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $store = new ActionScheduler_DBStore();
+ $action = new ActionScheduler_Action( $hook, array(), $schedule, 'group1' );
+
+ $action_id = $store->save_action( $action );
+ $action_from_db = $store->fetch_action( $action_id );
+ $this->assertNotEquals( 0, $action_id );
+ $this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) );
+
+ $action2 = new ActionScheduler_Action( $hook, array(), $schedule, 'group2' );
+ $action_id_group2 = $store->save_unique_action( $action2 );
+ $this->assertNotEquals( 0, $action_id_group2 );
+ $action_2_from_db = $store->fetch_action( $action_id_group2 );
+ $this->assertTrue( is_a( $action_2_from_db, ActionScheduler_Action::class ) );
+
+ $action3 = new ActionScheduler_Action( $hook, array(), $schedule, 'group2' );
+ $action_id_group2_double = $store->save_unique_action( $action3 );
+ $this->assertEquals( 0, $action_id_group2_double );
+ }
+
+ /**
+ * Test saving a unique action first, and then successfully scheduling a non-unique action.
+ */
+ public function test_create_action_unique_and_then_non_unique() {
+ $time = as_get_datetime_object();
+ $hook = md5( wp_rand() );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $store = new ActionScheduler_DBStore();
+ $action = new ActionScheduler_Action( $hook, array(), $schedule );
+
+ $action_id = $store->save_unique_action( $action );
+ $this->assertNotEquals( 0, $action_id );
+ $action_from_db = $store->fetch_action( $action_id );
+ $this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) );
+
+ // Non unique action is scheduled even if the previous one was unique.
+ $action = new ActionScheduler_Action( $hook, array(), $schedule );
+ $action_id_duplicate = $store->save_action( $action );
+ $this->assertNotEquals( 0, $action_id_duplicate );
+ $action_from_db_duplicate = $store->fetch_action( $action_id_duplicate );
+ $this->assertTrue( is_a( $action_from_db_duplicate, ActionScheduler_Action::class ) );
+ }
+
+ /**
+ * Test asserting that action when an action is created with empty args, it matches with actions created with args for uniqueness.
+ */
+ public function test_create_action_unique_with_empty_array() {
+ $time = as_get_datetime_object();
+ $hook = md5( wp_rand() );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $store = new ActionScheduler_DBStore();
+ $action = new ActionScheduler_Action( $hook, array( 'foo' => 'bar' ), $schedule );
+
+ $action_id = $store->save_unique_action( $action );
+ $this->assertNotEquals( 0, $action_id );
+ $action_from_db = $store->fetch_action( $action_id );
+ $this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) );
+
+ $action_with_empty_args = new ActionScheduler_Action( $hook, array(), $schedule );
+ $action_id_duplicate = $store->save_unique_action( $action_with_empty_args );
+ $this->assertEquals( 0, $action_id_duplicate );
+ }
+
+ /**
+ * Uniqueness does not check for args, so actions with different args can't be scheduled when unique is true.
+ */
+ public function test_create_action_unique_with_different_args_still_fail() {
+ $time = as_get_datetime_object();
+ $hook = md5( wp_rand() );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $store = new ActionScheduler_DBStore();
+ $action = new ActionScheduler_Action( $hook, array( 'foo' => 'bar' ), $schedule );
+
+ $action_id = $store->save_unique_action( $action );
+ $this->assertNotEquals( 0, $action_id );
+ $action_from_db = $store->fetch_action( $action_id );
+ $this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) );
+
+ $action_with_diff_args = new ActionScheduler_Action( $hook, array( 'foo' => 'bazz' ), $schedule );
+ $action_id_duplicate = $store->save_unique_action( $action_with_diff_args );
+ $this->assertEquals( 0, $action_id_duplicate );
+ }
+
+ /**
+ * When a set of claimed actions are processed, they should be executed in the expected order (by priority,
+ * then by least number of attempts, then by scheduled date, then finally by action ID).
+ *
+ * @return void
+ */
+ public function test_actions_are_processed_in_correct_order() {
+ global $wpdb;
+
+ $now = time();
+ $actual_order = array();
+
+ // When `foo` actions are processed, record the sequence number they supply.
+ $watcher = function ( $number ) use ( &$actual_order ) {
+ $actual_order[] = $number;
+ };
+
+ as_schedule_single_action( $now - 10, 'foo', array( 4 ), '', false, 10 );
+ as_schedule_single_action( $now - 20, 'foo', array( 3 ), '', false, 10 );
+ as_schedule_single_action( $now - 5, 'foo', array( 2 ), '', false, 5 );
+ as_schedule_single_action( $now - 20, 'foo', array( 1 ), '', false, 5 );
+ $reattempted = as_schedule_single_action( $now - 40, 'foo', array( 7 ), '', false, 20 );
+ as_schedule_single_action( $now - 40, 'foo', array( 5 ), '', false, 20 );
+ as_schedule_single_action( $now - 40, 'foo', array( 6 ), '', false, 20 );
+
+ // Modify the `attempt` count on one of our test actions, to change expectations about its execution order.
+ $wpdb->update(
+ $wpdb->actionscheduler_actions,
+ array( 'attempts' => 5 ),
+ array( 'action_id' => $reattempted )
+ );
+
+ add_action( 'foo', $watcher );
+ ActionScheduler_Mocker::get_queue_runner( ActionScheduler::store() )->run();
+ remove_action( 'foo', $watcher );
+
+ $this->assertEquals( range( 1, 7 ), $actual_order, 'When a claim is processed, individual actions execute in the expected order.' );
+ }
+
+ /**
+ * When a set of claimed actions are processed, they should be executed in the expected order (by priority,
+ * then by least number of attempts, then by scheduled date, then finally by action ID). This should be true
+ * even if actions are scheduled from within other scheduled actions.
+ *
+ * This test is a variation of `test_actions_are_processed_in_correct_order`, see discussion in
+ * https://github.com/woocommerce/action-scheduler/issues/951 to see why this specific nuance is tested.
+ *
+ * @return void
+ */
+ public function test_child_actions_are_processed_in_correct_order() {
+ $time = time() - 10;
+ $actual_order = array();
+ $watcher = function ( $number ) use ( &$actual_order ) {
+ $actual_order[] = $number;
+ };
+ $parent_action = function () use ( $time ) {
+ // We generate 20 test actions because this is optimal for reproducing the conditions in the
+ // linked bug report. With fewer actions, the error condition is less likely to surface.
+ for ( $i = 1; $i <= 20; $i++ ) {
+ as_schedule_single_action( $time, 'foo', array( $i ) );
+ }
+ };
+
+ add_action( 'foo', $watcher );
+ add_action( 'parent', $parent_action );
+
+ as_schedule_single_action( $time, 'parent' );
+ ActionScheduler_Mocker::get_queue_runner( ActionScheduler::store() )->run();
+
+ remove_action( 'foo', $watcher );
+ add_action( 'parent', $parent_action );
+
+ $this->assertEquals( range( 1, 20 ), $actual_order, 'Once claimed, scheduled actions are executed in the expected order, including if "child actions" are scheduled from within another action.' );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_HybridStore_Test.php b/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_HybridStore_Test.php
new file mode 100644
index 0000000..f1cb65e
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_HybridStore_Test.php
@@ -0,0 +1,277 @@
+init();
+ }
+ update_option( ActionScheduler_HybridStore::DEMARKATION_OPTION, $this->demarkation_id );
+ $hybrid = new ActionScheduler_HybridStore();
+ $hybrid->set_autoincrement( '', ActionScheduler_StoreSchema::ACTIONS_TABLE );
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+
+ // reset the autoincrement index.
+ /** @var \wpdb $wpdb */
+ global $wpdb;
+ $wpdb->query( "TRUNCATE TABLE {$wpdb->actionscheduler_actions}" );
+ $wpdb->query( "TRUNCATE TABLE {$wpdb->actionscheduler_logs}" );
+ delete_option( ActionScheduler_HybridStore::DEMARKATION_OPTION );
+ }
+
+ public function test_actions_are_migrated_on_find() {
+ $source_store = new PostStore();
+ $destination_store = new ActionScheduler_DBStore();
+ $source_logger = new CommentLogger();
+ $destination_logger = new ActionScheduler_DBLogger();
+
+ $config = new Config();
+ $config->set_source_store( $source_store );
+ $config->set_source_logger( $source_logger );
+ $config->set_destination_store( $destination_store );
+ $config->set_destination_logger( $destination_logger );
+
+ $hybrid_store = new ActionScheduler_HybridStore( $config );
+
+ $time = as_get_datetime_object( '10 minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
+ $source_id = $source_store->save_action( $action );
+
+ $found = $hybrid_store->find_action( __FUNCTION__, array() );
+
+ $this->assertNotEquals( $source_id, $found );
+ $this->assertGreaterThanOrEqual( $this->demarkation_id, $found );
+
+ $found_in_source = $source_store->fetch_action( $source_id );
+ $this->assertInstanceOf( NullAction::class, $found_in_source );
+ }
+
+ public function test_actions_are_migrated_on_query() {
+ $source_store = new PostStore();
+ $destination_store = new ActionScheduler_DBStore();
+ $source_logger = new CommentLogger();
+ $destination_logger = new ActionScheduler_DBLogger();
+
+ $config = new Config();
+ $config->set_source_store( $source_store );
+ $config->set_source_logger( $source_logger );
+ $config->set_destination_store( $destination_store );
+ $config->set_destination_logger( $destination_logger );
+
+ $hybrid_store = new ActionScheduler_HybridStore( $config );
+
+ $source_actions = array();
+ $destination_actions = array();
+
+ for ( $i = 0; $i < 10; $i++ ) {
+ // create in instance in the source store.
+ $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
+
+ $source_actions[] = $source_store->save_action( $action );
+
+ // create an instance in the destination store.
+ $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
+
+ $destination_actions[] = $destination_store->save_action( $action );
+ }
+
+ $found = $hybrid_store->query_actions(
+ array(
+ 'hook' => __FUNCTION__,
+ 'per_page' => 6,
+ )
+ );
+
+ $this->assertCount( 6, $found );
+ foreach ( $found as $key => $action_id ) {
+ $this->assertNotContains( $action_id, $source_actions );
+ $this->assertGreaterThanOrEqual( $this->demarkation_id, $action_id );
+ if ( 0 === $key % 2 ) { // it should have been in the source store.
+ $this->assertNotContains( $action_id, $destination_actions );
+ } else { // it should have already been in the destination store.
+ $this->assertContains( $action_id, $destination_actions );
+ }
+ }
+
+ // six of the original 10 should have migrated to the new store,
+ // even though only three were retrieve in the final query.
+ $found_in_source = $source_store->query_actions(
+ array(
+ 'hook' => __FUNCTION__,
+ 'per_page' => 10,
+ )
+ );
+ $this->assertCount( 4, $found_in_source );
+ }
+
+
+ public function test_actions_are_migrated_on_claim() {
+ $source_store = new PostStore();
+ $destination_store = new ActionScheduler_DBStore();
+ $source_logger = new CommentLogger();
+ $destination_logger = new ActionScheduler_DBLogger();
+
+ $config = new Config();
+ $config->set_source_store( $source_store );
+ $config->set_source_logger( $source_logger );
+ $config->set_destination_store( $destination_store );
+ $config->set_destination_logger( $destination_logger );
+
+ $hybrid_store = new ActionScheduler_HybridStore( $config );
+
+ $source_actions = array();
+ $destination_actions = array();
+
+ for ( $i = 0; $i < 10; $i++ ) {
+ // create in instance in the source store.
+ $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
+
+ $source_actions[] = $source_store->save_action( $action );
+
+ // create an instance in the destination store.
+ $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
+
+ $destination_actions[] = $destination_store->save_action( $action );
+ }
+
+ $claim = $hybrid_store->stake_claim( 6 );
+
+ $claimed_actions = $claim->get_actions();
+ $this->assertCount( 6, $claimed_actions );
+ $this->assertCount( 3, array_intersect( $destination_actions, $claimed_actions ) );
+
+ // six of the original 10 should have migrated to the new store,
+ // even though only three were retrieve in the final claim.
+ $found_in_source = $source_store->query_actions(
+ array(
+ 'hook' => __FUNCTION__,
+ 'per_page' => 10,
+ )
+ );
+ $this->assertCount( 4, $found_in_source );
+
+ $this->assertEquals( 0, $source_store->get_claim_count() );
+ $this->assertEquals( 1, $destination_store->get_claim_count() );
+ $this->assertEquals( 1, $hybrid_store->get_claim_count() );
+
+ }
+
+ public function test_fetch_respects_demarkation() {
+ $source_store = new PostStore();
+ $destination_store = new ActionScheduler_DBStore();
+ $source_logger = new CommentLogger();
+ $destination_logger = new ActionScheduler_DBLogger();
+
+ $config = new Config();
+ $config->set_source_store( $source_store );
+ $config->set_source_logger( $source_logger );
+ $config->set_destination_store( $destination_store );
+ $config->set_destination_logger( $destination_logger );
+
+ $hybrid_store = new ActionScheduler_HybridStore( $config );
+
+ $source_actions = array();
+ $destination_actions = array();
+
+ for ( $i = 0; $i < 2; $i++ ) {
+ // create in instance in the source store.
+ $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
+
+ $source_actions[] = $source_store->save_action( $action );
+
+ // create an instance in the destination store.
+ $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
+
+ $destination_actions[] = $destination_store->save_action( $action );
+ }
+
+ foreach ( $source_actions as $action_id ) {
+ $action = $hybrid_store->fetch_action( $action_id );
+ $this->assertInstanceOf( ActionScheduler_Action::class, $action );
+ $this->assertNotInstanceOf( NullAction::class, $action );
+ }
+
+ foreach ( $destination_actions as $action_id ) {
+ $action = $hybrid_store->fetch_action( $action_id );
+ $this->assertInstanceOf( ActionScheduler_Action::class, $action );
+ $this->assertNotInstanceOf( NullAction::class, $action );
+ }
+ }
+
+ public function test_mark_complete_respects_demarkation() {
+ $source_store = new PostStore();
+ $destination_store = new ActionScheduler_DBStore();
+ $source_logger = new CommentLogger();
+ $destination_logger = new ActionScheduler_DBLogger();
+
+ $config = new Config();
+ $config->set_source_store( $source_store );
+ $config->set_source_logger( $source_logger );
+ $config->set_destination_store( $destination_store );
+ $config->set_destination_logger( $destination_logger );
+
+ $hybrid_store = new ActionScheduler_HybridStore( $config );
+
+ $source_actions = array();
+ $destination_actions = array();
+
+ for ( $i = 0; $i < 2; $i++ ) {
+ // create in instance in the source store.
+ $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
+
+ $source_actions[] = $source_store->save_action( $action );
+
+ // create an instance in the destination store.
+ $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule );
+
+ $destination_actions[] = $destination_store->save_action( $action );
+ }
+
+ foreach ( $source_actions as $action_id ) {
+ $hybrid_store->mark_complete( $action_id );
+ $action = $hybrid_store->fetch_action( $action_id );
+ $this->assertInstanceOf( ActionScheduler_FinishedAction::class, $action );
+ }
+
+ foreach ( $destination_actions as $action_id ) {
+ $hybrid_store->mark_complete( $action_id );
+ $action = $hybrid_store->fetch_action( $action_id );
+ $this->assertInstanceOf( ActionScheduler_FinishedAction::class, $action );
+ }
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_wpPostStore_Test.php b/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_wpPostStore_Test.php
new file mode 100644
index 0000000..e11fe94
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_wpPostStore_Test.php
@@ -0,0 +1,474 @@
+save_action( $action );
+
+ $this->assertNotEmpty( $action_id );
+ }
+
+ public function test_create_action_with_scheduled_date() {
+ $time = as_get_datetime_object( strtotime( '-1 week' ) );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), new ActionScheduler_SimpleSchedule( $time ) );
+ $store = new ActionScheduler_wpPostStore();
+
+ $action_id = $store->save_action( $action, $time );
+ $action_date = $store->get_date( $action_id );
+
+ $this->assertEquals( $time->getTimestamp(), $action_date->getTimestamp() );
+ }
+
+ public function test_retrieve_action() {
+ $time = as_get_datetime_object();
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
+ $store = new ActionScheduler_wpPostStore();
+ $action_id = $store->save_action( $action );
+ $retrieved = $store->fetch_action( $action_id );
+
+ $this->assertEquals( $action->get_hook(), $retrieved->get_hook() );
+ $this->assertEqualSets( $action->get_args(), $retrieved->get_args() );
+ $this->assertEquals( $action->get_schedule()->get_date()->getTimestamp(), $retrieved->get_schedule()->get_date()->getTimestamp() );
+ $this->assertEquals( $action->get_group(), $retrieved->get_group() );
+ }
+
+ /**
+ * @dataProvider provide_bad_args
+ *
+ * @param string $content Post content.
+ */
+ public function test_action_bad_args( $content ) {
+ $store = new ActionScheduler_wpPostStore();
+ $post_id = wp_insert_post(
+ array(
+ 'post_type' => ActionScheduler_wpPostStore::POST_TYPE,
+ 'post_status' => ActionScheduler_Store::STATUS_PENDING,
+ 'post_content' => $content,
+ )
+ );
+
+ $fetched = $store->fetch_action( $post_id );
+ $this->assertInstanceOf( 'ActionScheduler_NullSchedule', $fetched->get_schedule() );
+ }
+
+ public function provide_bad_args() {
+ return array(
+ array( '{"bad_json":true}}' ),
+ );
+ }
+
+ public function test_cancel_action() {
+ $time = as_get_datetime_object();
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
+ $store = new ActionScheduler_wpPostStore();
+ $action_id = $store->save_action( $action );
+ $store->cancel_action( $action_id );
+
+ $fetched = $store->fetch_action( $action_id );
+ $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
+ }
+
+ public function test_cancel_actions_by_hook() {
+ $store = new ActionScheduler_wpPostStore();
+ $actions = array();
+ $hook = 'by_hook_test';
+ for ( $day = 1; $day <= 3; $day++ ) {
+ $delta = sprintf( '+%d day', $day );
+ $time = as_get_datetime_object( $delta );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( $hook, array(), $schedule, 'my_group' );
+ $actions[] = $store->save_action( $action );
+ }
+ $store->cancel_actions_by_hook( $hook );
+
+ foreach ( $actions as $action_id ) {
+ $fetched = $store->fetch_action( $action_id );
+ $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
+ }
+ }
+
+ public function test_cancel_actions_by_group() {
+ $store = new ActionScheduler_wpPostStore();
+ $actions = array();
+ $group = 'by_group_test';
+
+ for ( $day = 1; $day <= 3; $day++ ) {
+ $delta = sprintf( '+%d day', $day );
+ $time = as_get_datetime_object( $delta );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, $group );
+ $actions[] = $store->save_action( $action );
+ }
+ $store->cancel_actions_by_group( $group );
+
+ foreach ( $actions as $action_id ) {
+ $fetched = $store->fetch_action( $action_id );
+ $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched );
+ }
+ }
+
+ public function test_claim_actions() {
+ $created_actions = array();
+ $store = new ActionScheduler_wpPostStore();
+
+ for ( $i = 3; $i > -3; $i-- ) {
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
+
+ $created_actions[] = $store->save_action( $action );
+ }
+
+ $claim = $store->stake_claim();
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+
+ $this->assertCount( 3, $claim->get_actions() );
+ $this->assertEqualSets( array_slice( $created_actions, 3, 3 ), $claim->get_actions() );
+ }
+
+ public function test_claim_actions_order() {
+ $store = new ActionScheduler_wpPostStore();
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
+ $created_actions = array(
+ $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ),
+ $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ),
+ );
+
+ $claim = $store->stake_claim();
+ $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim );
+
+ // Verify uniqueness of action IDs.
+ $this->assertEquals( 2, count( array_unique( $created_actions ) ) );
+
+ // Verify the count and order of the actions.
+ $claimed_actions = $claim->get_actions();
+ $this->assertCount( 2, $claimed_actions );
+ $this->assertEquals( $created_actions, $claimed_actions );
+
+ // Verify the reversed order doesn't pass.
+ $reversed_actions = array_reverse( $created_actions );
+ $this->assertNotEquals( $reversed_actions, $claimed_actions );
+ }
+
+ public function test_duplicate_claim() {
+ $created_actions = array();
+ $store = new ActionScheduler_wpPostStore();
+
+ for ( $i = 0; $i > -3; $i-- ) {
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
+
+ $created_actions[] = $store->save_action( $action );
+ }
+
+ $claim1 = $store->stake_claim();
+ $claim2 = $store->stake_claim();
+ $this->assertCount( 3, $claim1->get_actions() );
+ $this->assertCount( 0, $claim2->get_actions() );
+ }
+
+ public function test_release_claim() {
+ $created_actions = array();
+ $store = new ActionScheduler_wpPostStore();
+
+ for ( $i = 0; $i > -3; $i-- ) {
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
+
+ $created_actions[] = $store->save_action( $action );
+ }
+
+ $claim1 = $store->stake_claim();
+
+ $store->release_claim( $claim1 );
+
+ $claim2 = $store->stake_claim();
+ $this->assertCount( 3, $claim2->get_actions() );
+ }
+
+ public function test_search() {
+ $created_actions = array();
+ $store = new ActionScheduler_wpPostStore();
+
+ for ( $i = -3; $i <= 3; $i++ ) {
+ $time = as_get_datetime_object( $i . ' hours' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' );
+
+ $created_actions[] = $store->save_action( $action );
+ }
+
+ $next_no_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK );
+ $this->assertEquals( $created_actions[0], $next_no_args );
+
+ $next_with_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 1 ) ) );
+ $this->assertEquals( $created_actions[4], $next_with_args );
+
+ $non_existent = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 17 ) ) );
+ $this->assertNull( $non_existent );
+ }
+
+ public function test_search_by_group() {
+ $store = new ActionScheduler_wpPostStore();
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) );
+
+ $abc = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'abc' ) );
+ $def = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'def' ) );
+ $ghi = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'ghi' ) );
+
+ $this->assertEquals( $abc, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'abc' ) ) );
+ $this->assertEquals( $def, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'def' ) ) );
+ $this->assertEquals( $ghi, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'ghi' ) ) );
+ }
+
+ public function test_post_author() {
+ $current_user = get_current_user_id();
+
+ $time = as_get_datetime_object();
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $store = new ActionScheduler_wpPostStore();
+ $action_id = $store->save_action( $action );
+
+ $post = get_post( $action_id );
+ $this->assertEquals( 0, $post->post_author );
+
+ $new_user = $this->factory->user->create_object(
+ array(
+ 'user_login' => __FUNCTION__,
+ 'user_pass' => md5( wp_rand() ),
+ )
+ );
+
+ wp_set_current_user( $new_user );
+
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $action_id = $store->save_action( $action );
+ $post = get_post( $action_id );
+ $this->assertEquals( 0, $post->post_author );
+
+ wp_set_current_user( $current_user );
+ }
+
+ /**
+ * @issue 13
+ */
+ public function test_post_status_for_recurring_action() {
+ $time = as_get_datetime_object( '10 minutes' );
+ $schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $store = new ActionScheduler_wpPostStore();
+ $action_id = $store->save_action( $action );
+
+ $action = $store->fetch_action( $action_id );
+ $action->execute();
+ $store->mark_complete( $action_id );
+
+ $next = $action->get_schedule()->get_next( as_get_datetime_object() );
+ $new_action_id = $store->save_action( $action, $next );
+
+ $this->assertEquals( 'publish', get_post_status( $action_id ) );
+ $this->assertEquals( 'pending', get_post_status( $new_action_id ) );
+ }
+
+ public function test_get_run_date() {
+ $time = as_get_datetime_object( '-10 minutes' );
+ $schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $store = new ActionScheduler_wpPostStore();
+ $action_id = $store->save_action( $action );
+
+ $this->assertEquals( $store->get_date( $action_id )->getTimestamp(), $time->getTimestamp() );
+
+ $action = $store->fetch_action( $action_id );
+ $action->execute();
+ $now = as_get_datetime_object();
+ $store->mark_complete( $action_id );
+
+ $this->assertEquals( $store->get_date( $action_id )->getTimestamp(), $now->getTimestamp(), '', 1 ); // allow timestamp to be 1 second off for older versions of PHP.
+
+ $next = $action->get_schedule()->get_next( $now );
+ $new_action_id = $store->save_action( $action, $next );
+
+ $this->assertEquals( (int) ( $now->getTimestamp() ) + HOUR_IN_SECONDS, $store->get_date( $new_action_id )->getTimestamp() );
+ }
+
+ public function test_claim_actions_by_hooks() {
+ $hook1 = __FUNCTION__ . '_hook_1';
+ $hook2 = __FUNCTION__ . '_hook_2';
+ $store = new ActionScheduler_wpPostStore();
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
+
+ $action1 = $store->save_action( new ActionScheduler_Action( $hook1, array(), $schedule ) );
+ $action2 = $store->save_action( new ActionScheduler_Action( $hook2, array(), $schedule ) );
+
+ // Claiming no hooks should include all actions.
+ $claim = $store->stake_claim( 10 );
+ $this->assertEquals( 2, count( $claim->get_actions() ) );
+ $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
+ $this->assertTrue( in_array( $action2, $claim->get_actions(), true ) );
+ $store->release_claim( $claim );
+
+ // Claiming a hook should claim only actions with that hook.
+ $claim = $store->stake_claim( 10, null, array( $hook1 ) );
+ $this->assertEquals( 1, count( $claim->get_actions() ) );
+ $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
+ $store->release_claim( $claim );
+
+ // Claiming two hooks should claim actions with either of those hooks.
+ $claim = $store->stake_claim( 10, null, array( $hook1, $hook2 ) );
+ $this->assertEquals( 2, count( $claim->get_actions() ) );
+ $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
+ $this->assertTrue( in_array( $action2, $claim->get_actions(), true ) );
+ $store->release_claim( $claim );
+
+ // Claiming two hooks should claim actions with either of those hooks.
+ $claim = $store->stake_claim( 10, null, array( __METHOD__ . '_hook_3' ) );
+ $this->assertEquals( 0, count( $claim->get_actions() ) );
+ $this->assertFalse( in_array( $action1, $claim->get_actions(), true ) );
+ $this->assertFalse( in_array( $action2, $claim->get_actions(), true ) );
+ $store->release_claim( $claim );
+ }
+
+ /**
+ * @issue 121
+ */
+ public function test_claim_actions_by_group() {
+ $group1 = md5( wp_rand() );
+ $store = new ActionScheduler_wpPostStore();
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
+
+ $action1 = $store->save_action( new ActionScheduler_Action( __METHOD__, array(), $schedule, $group1 ) );
+ $action2 = $store->save_action( new ActionScheduler_Action( __METHOD__, array(), $schedule ) );
+
+ // Claiming no group should include all actions.
+ $claim = $store->stake_claim( 10 );
+ $this->assertEquals( 2, count( $claim->get_actions() ) );
+ $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
+ $this->assertTrue( in_array( $action2, $claim->get_actions(), true ) );
+ $store->release_claim( $claim );
+
+ // Claiming a group should claim only actions in that group.
+ $claim = $store->stake_claim( 10, null, array(), $group1 );
+ $this->assertEquals( 1, count( $claim->get_actions() ) );
+ $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
+ $store->release_claim( $claim );
+ }
+
+ public function test_claim_actions_by_hook_and_group() {
+ $hook1 = __FUNCTION__ . '_hook_1';
+ $hook2 = __FUNCTION__ . '_hook_2';
+ $hook3 = __FUNCTION__ . '_hook_3';
+ $group1 = 'group_' . md5( wp_rand() );
+ $group2 = 'group_' . md5( wp_rand() );
+ $store = new ActionScheduler_wpPostStore();
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) );
+
+ $action1 = $store->save_action( new ActionScheduler_Action( $hook1, array(), $schedule, $group1 ) );
+ $action2 = $store->save_action( new ActionScheduler_Action( $hook2, array(), $schedule ) );
+ $action3 = $store->save_action( new ActionScheduler_Action( $hook3, array(), $schedule, $group2 ) );
+
+ // Claiming no hooks or group should include all actions.
+ $claim = $store->stake_claim( 10 );
+ $this->assertEquals( 3, count( $claim->get_actions() ) );
+ $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
+ $this->assertTrue( in_array( $action2, $claim->get_actions(), true ) );
+ $store->release_claim( $claim );
+
+ // Claiming a group and hook should claim only actions in that group.
+ $claim = $store->stake_claim( 10, null, array( $hook1 ), $group1 );
+ $this->assertEquals( 1, count( $claim->get_actions() ) );
+ $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) );
+ $store->release_claim( $claim );
+
+ // Claiming a group and hook should claim only actions with that hook in that group.
+ $claim = $store->stake_claim( 10, null, array( $hook2 ), $group1 );
+ $this->assertEquals( 0, count( $claim->get_actions() ) );
+ $this->assertFalse( in_array( $action1, $claim->get_actions(), true ) );
+ $this->assertFalse( in_array( $action2, $claim->get_actions(), true ) );
+ $store->release_claim( $claim );
+
+ // Claiming a group and hook should claim only actions with that hook in that group.
+ $claim = $store->stake_claim( 10, null, array( $hook1, $hook2 ), $group2 );
+ $this->assertEquals( 0, count( $claim->get_actions() ) );
+ $this->assertFalse( in_array( $action1, $claim->get_actions(), true ) );
+ $this->assertFalse( in_array( $action2, $claim->get_actions(), true ) );
+ $store->release_claim( $claim );
+ }
+
+ /**
+ * The query used to claim actions explicitly ignores future pending actions, but it
+ * is still possible under unusual conditions (such as if MySQL runs out of temporary
+ * storage space) for such actions to be returned.
+ *
+ * When this happens, we still expect the store to filter them out, otherwise there is
+ * a risk that actions will be unexpectedly processed ahead of time.
+ *
+ * @see https://github.com/woocommerce/action-scheduler/issues/634
+ */
+ public function test_claim_filters_out_unexpected_future_actions() {
+ $group = __METHOD__;
+ $store = new ActionScheduler_wpPostStore();
+
+ // Create 4 actions: 2 that are already due (-3hrs and -1hrs) and 2 that are not yet due (+1hr and +3hrs).
+ for ( $i = -3; $i <= 3; $i += 2 ) {
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( $i . ' hours' ) );
+ $action_ids[] = $store->save_action( new ActionScheduler_Action( 'test_' . $i, array(), $schedule, $group ) );
+ }
+
+ // This callback is used to simulate the unusual conditions whereby MySQL might unexpectedly return future
+ // actions, contrary to the conditions used by the store object when staking its claim.
+ $simulate_unexpected_db_behavior = function( $sql ) use ( $action_ids ) {
+ global $wpdb;
+
+ $post_type = ActionScheduler_wpPostStore::POST_TYPE;
+ $pending = ActionScheduler_wpPostStore::STATUS_PENDING;
+
+ // Look out for the claim update query, ignore all others.
+ if (
+ 0 !== strpos( $sql, "UPDATE $wpdb->posts" )
+ || 0 !== strpos( $sql, "WHERE post_type = '$post_type' AND post_status = '$pending' AND post_password = ''" )
+ || ! preg_match( "/AND post_date_gmt <= '([0-9:\-\s]{19})'/", $sql, $matches )
+ || count( $matches ) !== 2
+ ) {
+ return $sql;
+ }
+
+ // Now modify the query, forcing it to also return the future actions we created.
+ return str_replace( $matches[1], as_get_datetime_object( '+4 hours' )->format( 'Y-m-d H:i:s' ), $sql );
+ };
+
+ add_filter( 'query', $simulate_unexpected_db_behavior );
+ $claim = $store->stake_claim( 10, null, array(), $group );
+ $claimed_actions = $claim->get_actions();
+ $this->assertCount( 2, $claimed_actions );
+
+ // Cleanup.
+ remove_filter( 'query', $simulate_unexpected_db_behavior );
+ $store->release_claim( $claim );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/lock/ActionScheduler_OptionLock_Test.php b/libraries/action-scheduler/tests/phpunit/lock/ActionScheduler_OptionLock_Test.php
new file mode 100644
index 0000000..63afa31
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/lock/ActionScheduler_OptionLock_Test.php
@@ -0,0 +1,77 @@
+assertInstanceOf( 'ActionScheduler_Lock', $lock );
+ $this->assertInstanceOf( 'ActionScheduler_OptionLock', $lock );
+ }
+
+ public function test_is_locked() {
+ $lock = ActionScheduler::lock();
+ $lock_type = md5( wp_rand() );
+
+ $this->assertFalse( $lock->is_locked( $lock_type ) );
+
+ $lock->set( $lock_type );
+ $this->assertTrue( $lock->is_locked( $lock_type ) );
+ }
+
+ public function test_set() {
+ $lock = ActionScheduler::lock();
+ $lock_type = md5( wp_rand() );
+
+ $lock->set( $lock_type );
+ $this->assertTrue( $lock->is_locked( $lock_type ) );
+ }
+
+ public function test_get_expiration() {
+ $lock = ActionScheduler::lock();
+ $lock_type = md5( wp_rand() );
+
+ $lock->set( $lock_type );
+
+ $expiration = $lock->get_expiration( $lock_type );
+ $current_time = time();
+
+ $this->assertGreaterThanOrEqual( 0, $expiration );
+ $this->assertGreaterThan( $current_time, $expiration );
+ $this->assertLessThan( $current_time + MINUTE_IN_SECONDS + 1, $expiration );
+ }
+
+ /**
+ * A call to `ActionScheduler::lock()->set()` should fail if the lock is already held (ie, by another process).
+ *
+ * @return void
+ */
+ public function test_lock_resists_race_conditions() {
+ global $wpdb;
+
+ $lock = ActionScheduler::lock();
+ $type = md5( wp_rand() );
+
+ // Approximate conditions in which a concurrently executing request manages to set (and obtain) the lock
+ // immediately before the current request can do so.
+ $simulate_concurrent_claim = function ( $query ) use ( $lock, $type ) {
+ static $executed = false;
+
+ if ( ! $executed && false !== strpos( $query, 'action_scheduler_lock_' ) && 0 === strpos( $query, 'INSERT INTO' ) ) {
+ $executed = true;
+ $lock->set( $type );
+ }
+
+ return $query;
+ };
+
+ add_filter( 'query', $simulate_concurrent_claim );
+ $wpdb->suppress_errors( true );
+ $this->assertFalse( $lock->is_locked( $type ), 'Initially, the lock is not held' );
+ $this->assertFalse( $lock->set( $type ), 'The lock was not obtained, because another process already claimed it.' );
+ $wpdb->suppress_errors( false );
+ remove_filter( 'query', $simulate_concurrent_claim );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_DBLogger_Test.php b/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_DBLogger_Test.php
new file mode 100644
index 0000000..27f9b4c
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_DBLogger_Test.php
@@ -0,0 +1,134 @@
+assertInstanceOf( 'ActionScheduler_Logger', $logger );
+ $this->assertInstanceOf( ActionScheduler_DBLogger::class, $logger );
+ }
+
+ public function test_add_log_entry() {
+ $action_id = as_schedule_single_action( time(), __METHOD__ );
+ $logger = ActionScheduler::logger();
+ $message = 'Logging that something happened';
+ $log_id = $logger->log( $action_id, $message );
+ $entry = $logger->get_entry( $log_id );
+
+ $this->assertEquals( $action_id, $entry->get_action_id() );
+ $this->assertEquals( $message, $entry->get_message() );
+ }
+
+ public function test_storage_logs() {
+ $action_id = as_schedule_single_action( time(), __METHOD__ );
+ $logger = ActionScheduler::logger();
+ $logs = $logger->get_logs( $action_id );
+ $expected = new ActionScheduler_LogEntry( $action_id, 'action created' );
+ $this->assertCount( 1, $logs );
+ $this->assertEquals( $expected->get_action_id(), $logs[0]->get_action_id() );
+ $this->assertEquals( $expected->get_message(), $logs[0]->get_message() );
+ }
+
+ public function test_execution_logs() {
+ $action_id = as_schedule_single_action( time(), ActionScheduler_Callbacks::HOOK_WITH_CALLBACK );
+ $logger = ActionScheduler::logger();
+ $started = new ActionScheduler_LogEntry( $action_id, 'action started via Unit Tests' );
+ $finished = new ActionScheduler_LogEntry( $action_id, 'action complete via Unit Tests' );
+
+ $runner = ActionScheduler_Mocker::get_queue_runner();
+ $runner->run( 'Unit Tests' );
+
+ // Expect 3 logs with the correct action ID.
+ $logs = $logger->get_logs( $action_id );
+ $this->assertCount( 3, $logs );
+ foreach ( $logs as $log ) {
+ $this->assertEquals( $action_id, $log->get_action_id() );
+ }
+
+ // Expect created, then started, then completed.
+ $this->assertEquals( 'action created', $logs[0]->get_message() );
+ $this->assertEquals( $started->get_message(), $logs[1]->get_message() );
+ $this->assertEquals( $finished->get_message(), $logs[2]->get_message() );
+ }
+
+ public function test_failed_execution_logs() {
+ $hook = __METHOD__;
+ add_action( $hook, array( $this, 'a_hook_callback_that_throws_an_exception' ) );
+ $action_id = as_schedule_single_action( time(), $hook );
+ $logger = ActionScheduler::logger();
+ $started = new ActionScheduler_LogEntry( $action_id, 'action started via Unit Tests' );
+ $finished = new ActionScheduler_LogEntry( $action_id, 'action complete via Unit Tests' );
+ $failed = new ActionScheduler_LogEntry( $action_id, 'action failed via Unit Tests: Execution failed' );
+
+ $runner = ActionScheduler_Mocker::get_queue_runner();
+ $runner->run( 'Unit Tests' );
+
+ // Expect 3 logs with the correct action ID.
+ $logs = $logger->get_logs( $action_id );
+ $this->assertCount( 3, $logs );
+ foreach ( $logs as $log ) {
+ $this->assertEquals( $action_id, $log->get_action_id() );
+ $this->assertNotEquals( $finished->get_message(), $log->get_message() );
+ }
+
+ // Expect created, then started, then failed.
+ $this->assertEquals( 'action created', $logs[0]->get_message() );
+ $this->assertEquals( $started->get_message(), $logs[1]->get_message() );
+ $this->assertEquals( $failed->get_message(), $logs[2]->get_message() );
+ }
+
+ public function test_fatal_error_log() {
+ $action_id = as_schedule_single_action( time(), __METHOD__ );
+ $logger = ActionScheduler::logger();
+ $args = array(
+ 'type' => E_ERROR,
+ 'message' => 'Test error',
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ );
+
+ do_action( 'action_scheduler_unexpected_shutdown', $action_id, $args );
+
+ $logs = $logger->get_logs( $action_id );
+ $found_log = false;
+ foreach ( $logs as $l ) {
+ if ( strpos( $l->get_message(), 'unexpected shutdown' ) === 0 ) {
+ $found_log = true;
+ }
+ }
+ $this->assertTrue( $found_log, 'Unexpected shutdown log not found' );
+ }
+
+ public function test_canceled_action_log() {
+ $action_id = as_schedule_single_action( time(), __METHOD__ );
+ as_unschedule_action( __METHOD__ );
+ $logger = ActionScheduler::logger();
+ $logs = $logger->get_logs( $action_id );
+ $expected = new ActionScheduler_LogEntry( $action_id, 'action canceled' );
+ $this->assertEquals( $expected->get_message(), end( $logs )->get_message() );
+ }
+
+ public function test_deleted_action_cleanup() {
+ $time = as_get_datetime_object( '-10 minutes' );
+ $schedule = new \ActionScheduler_SimpleSchedule( $time );
+ $action = new \ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $store = new ActionScheduler_DBStore();
+ $action_id = $store->save_action( $action );
+
+ $logger = new ActionScheduler_DBLogger();
+ $logs = $logger->get_logs( $action_id );
+ $this->assertNotEmpty( $logs );
+
+ $store->delete_action( $action_id );
+ $logs = $logger->get_logs( $action_id );
+ $this->assertEmpty( $logs );
+ }
+
+ public function a_hook_callback_that_throws_an_exception() {
+ throw new \RuntimeException( 'Execution failed' );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_wpCommentLogger_Test.php b/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_wpCommentLogger_Test.php
new file mode 100644
index 0000000..b5d41db
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_wpCommentLogger_Test.php
@@ -0,0 +1,241 @@
+assertInstanceOf( 'ActionScheduler_Logger', $logger );
+ if ( $this->using_comment_logger() ) {
+ $this->assertInstanceOf( 'ActionScheduler_wpCommentLogger', $logger );
+ } else {
+ $this->assertNotInstanceOf( 'ActionScheduler_wpCommentLogger', $logger );
+ }
+ }
+
+ public function test_add_log_entry() {
+ $action_id = as_schedule_single_action( time(), 'a hook' );
+ $logger = ActionScheduler::logger();
+ $message = 'Logging that something happened';
+ $log_id = $logger->log( $action_id, $message );
+ $entry = $logger->get_entry( $log_id );
+
+ $this->assertEquals( $action_id, $entry->get_action_id() );
+ $this->assertEquals( $message, $entry->get_message() );
+ }
+
+ public function test_add_log_datetime() {
+ $action_id = as_schedule_single_action( time(), 'a hook' );
+ $logger = ActionScheduler::logger();
+ $message = 'Logging that something happened';
+ $date = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
+ $log_id = $logger->log( $action_id, $message, $date );
+ $entry = $logger->get_entry( $log_id );
+
+ $this->assertEquals( $action_id, $entry->get_action_id() );
+ $this->assertEquals( $message, $entry->get_message() );
+
+ $date = new ActionScheduler_DateTime( 'now', new DateTimeZone( 'UTC' ) );
+ $log_id = $logger->log( $action_id, $message, $date );
+ $entry = $logger->get_entry( $log_id );
+
+ $this->assertEquals( $action_id, $entry->get_action_id() );
+ $this->assertEquals( $message, $entry->get_message() );
+ }
+
+ public function test_erroneous_entry_id() {
+ $comment = wp_insert_comment(
+ array(
+ 'comment_post_ID' => 1,
+ 'comment_author' => 'test',
+ 'comment_content' => 'this is not a log entry',
+ )
+ );
+
+ $logger = ActionScheduler::logger();
+ $entry = $logger->get_entry( $comment );
+
+ $this->assertEquals( '', $entry->get_action_id() );
+ $this->assertEquals( '', $entry->get_message() );
+ }
+
+ public function test_storage_comments() {
+ $action_id = as_schedule_single_action( time(), 'a hook' );
+ $logger = ActionScheduler::logger();
+ $logs = $logger->get_logs( $action_id );
+ $expected = new ActionScheduler_LogEntry( $action_id, 'action created' );
+
+ // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
+ $this->assertTrue( in_array( $this->log_entry_to_array( $expected ), $this->log_entry_to_array( $logs ) ) );
+ }
+
+ protected function log_entry_to_array( $logs ) {
+ if ( $logs instanceof ActionScheduler_LogEntry ) {
+ return array(
+ 'action_id' => $logs->get_action_id(),
+ 'message' => $logs->get_message(),
+ );
+ }
+
+ foreach ( $logs as $id => $log ) {
+ $logs[ $id ] = array(
+ 'action_id' => $log->get_action_id(),
+ 'message' => $log->get_message(),
+ );
+ }
+
+ return $logs;
+ }
+
+ public function test_execution_comments() {
+ $action_id = as_schedule_single_action( time(), ActionScheduler_Callbacks::HOOK_WITH_CALLBACK );
+ $logger = ActionScheduler::logger();
+ $started = new ActionScheduler_LogEntry( $action_id, 'action started via Unit Tests' );
+ $finished = new ActionScheduler_LogEntry( $action_id, 'action complete via Unit Tests' );
+
+ $runner = ActionScheduler_Mocker::get_queue_runner();
+ $runner->run( 'Unit Tests' );
+
+ $logs = $logger->get_logs( $action_id );
+
+ // phpcs:disable WordPress.PHP.StrictInArray.MissingTrueStrict
+ $this->assertTrue( in_array( $this->log_entry_to_array( $started ), $this->log_entry_to_array( $logs ) ) );
+ $this->assertTrue( in_array( $this->log_entry_to_array( $finished ), $this->log_entry_to_array( $logs ) ) );
+ // phpcs:enable
+ }
+
+ public function test_failed_execution_comments() {
+ $hook = md5( wp_rand() );
+ add_action( $hook, array( $this, 'a_hook_callback_that_throws_an_exception' ) );
+
+ $action_id = as_schedule_single_action( time(), $hook );
+ $logger = ActionScheduler::logger();
+ $started = new ActionScheduler_LogEntry( $action_id, 'action started via Unit Tests' );
+ $finished = new ActionScheduler_LogEntry( $action_id, 'action complete via Unit Tests' );
+ $failed = new ActionScheduler_LogEntry( $action_id, 'action failed via Unit Tests: Execution failed' );
+
+ $runner = ActionScheduler_Mocker::get_queue_runner();
+ $runner->run( 'Unit Tests' );
+
+ $logs = $logger->get_logs( $action_id );
+
+ // phpcs:disable WordPress.PHP.StrictInArray.MissingTrueStrict
+ $this->assertTrue( in_array( $this->log_entry_to_array( $started ), $this->log_entry_to_array( $logs ) ) );
+ $this->assertFalse( in_array( $this->log_entry_to_array( $finished ), $this->log_entry_to_array( $logs ) ) );
+ $this->assertTrue( in_array( $this->log_entry_to_array( $failed ), $this->log_entry_to_array( $logs ) ) );
+ // phpcs:enable
+ }
+
+ public function test_failed_schedule_next_instance_comments() {
+ $action_id = wp_rand();
+ $logger = ActionScheduler::logger();
+ $log_entry = new ActionScheduler_LogEntry( $action_id, 'There was a failure scheduling the next instance of this action: Execution failed' );
+
+ try {
+ $this->a_hook_callback_that_throws_an_exception();
+ } catch ( Exception $e ) {
+ do_action( 'action_scheduler_failed_to_schedule_next_instance', $action_id, $e, new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ) );
+ }
+
+ $logs = $logger->get_logs( $action_id );
+
+ // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
+ $this->assertTrue( in_array( $this->log_entry_to_array( $log_entry ), $this->log_entry_to_array( $logs ) ) );
+ }
+
+ public function test_fatal_error_comments() {
+ $hook = md5( wp_rand() );
+ $action_id = as_schedule_single_action( time(), $hook );
+ $logger = ActionScheduler::logger();
+ $args = array(
+ 'type' => E_ERROR,
+ 'message' => 'Test error',
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ );
+
+ do_action( 'action_scheduler_unexpected_shutdown', $action_id, $args );
+
+ $logs = $logger->get_logs( $action_id );
+ $found_log = false;
+ foreach ( $logs as $l ) {
+ if ( strpos( $l->get_message(), 'unexpected shutdown' ) === 0 ) {
+ $found_log = true;
+ }
+ }
+ $this->assertTrue( $found_log, 'Unexpected shutdown log not found' );
+ }
+
+ public function test_canceled_action_comments() {
+ $action_id = as_schedule_single_action( time(), 'a hook' );
+ as_unschedule_action( 'a hook' );
+
+ $logger = ActionScheduler::logger();
+ $logs = $logger->get_logs( $action_id );
+ $expected = new ActionScheduler_LogEntry( $action_id, 'action canceled' );
+
+ // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
+ $this->assertTrue( in_array( $this->log_entry_to_array( $expected ), $this->log_entry_to_array( $logs ) ) );
+ }
+
+ public function a_hook_callback_that_throws_an_exception() {
+ throw new RuntimeException( 'Execution failed' );
+ }
+
+ public function test_filtering_of_get_comments() {
+ if ( ! $this->using_comment_logger() ) {
+ $this->assertTrue( true );
+ return;
+ }
+
+ $post_id = $this->factory->post->create_object( array( 'post_title' => __FUNCTION__ ) );
+ $comment_id = $this->factory->comment->create_object(
+ array(
+ 'comment_post_ID' => $post_id,
+ 'comment_author' => __CLASS__,
+ 'comment_content' => __FUNCTION__,
+ )
+ );
+
+ // Verify that we're getting the expected comment before we add logging comments.
+ $comments = get_comments();
+ $this->assertCount( 1, $comments );
+ $this->assertEquals( $comment_id, $comments[0]->comment_ID );
+
+ $action_id = as_schedule_single_action( time(), 'a hook' );
+ $logger = ActionScheduler::logger();
+ $message = 'Logging that something happened';
+ $log_id = $logger->log( $action_id, $message );
+
+ // Verify that logging comments are excluded from general comment queries.
+ $comments = get_comments();
+ $this->assertCount( 1, $comments );
+ $this->assertEquals( $comment_id, $comments[0]->comment_ID );
+
+ // Verify that logging comments are returned when asking for them specifically.
+ $comments = get_comments(
+ array(
+ 'type' => ActionScheduler_wpCommentLogger::TYPE,
+ )
+ );
+
+ // Expecting two: one when the action is created, another when we added our custom log.
+ $this->assertCount( 2, $comments );
+ $this->assertContains( $log_id, wp_list_pluck( $comments, 'comment_ID' ) );
+ }
+
+ private function using_comment_logger() {
+ if ( is_null( $this->use_comment_logger ) ) {
+ $this->use_comment_logger = ! ActionScheduler_DataController::dependencies_met();
+ }
+
+ return $this->use_comment_logger;
+ }
+}
+
diff --git a/libraries/action-scheduler/tests/phpunit/migration/ActionMigrator_Test.php b/libraries/action-scheduler/tests/phpunit/migration/ActionMigrator_Test.php
new file mode 100644
index 0000000..a4d8285
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/migration/ActionMigrator_Test.php
@@ -0,0 +1,144 @@
+init();
+ }
+ }
+
+ public function test_migrate_from_wpPost_to_db() {
+ $source = new ActionScheduler_wpPostStore();
+ $destination = new ActionScheduler_DBStore();
+ $migrator = new ActionMigrator( $source, $destination, $this->get_log_migrator() );
+
+ $time = as_get_datetime_object();
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
+ $action_id = $source->save_action( $action );
+
+ $new_id = $migrator->migrate( $action_id );
+
+ // ensure we get the same record out of the new store as we stored in the old.
+ $retrieved = $destination->fetch_action( $new_id );
+ $this->assertEquals( $action->get_hook(), $retrieved->get_hook() );
+ $this->assertEqualSets( $action->get_args(), $retrieved->get_args() );
+ $this->assertEquals( $action->get_schedule()->get_date()->format( 'U' ), $retrieved->get_schedule()->get_date()->format( 'U' ) );
+ $this->assertEquals( $action->get_group(), $retrieved->get_group() );
+ $this->assertEquals( \ActionScheduler_Store::STATUS_PENDING, $destination->get_status( $new_id ) );
+
+ // ensure that the record in the old store does not exist.
+ $old_action = $source->fetch_action( $action_id );
+ $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action );
+ }
+
+ public function test_does_not_migrate_missing_action_from_wpPost_to_db() {
+ $source = new ActionScheduler_wpPostStore();
+ $destination = new ActionScheduler_DBStore();
+ $migrator = new ActionMigrator( $source, $destination, $this->get_log_migrator() );
+
+ $action_id = wp_rand( 100, 100000 );
+
+ $new_id = $migrator->migrate( $action_id );
+ $this->assertEquals( 0, $new_id );
+
+ // ensure we get the same record out of the new store as we stored in the old.
+ $retrieved = $destination->fetch_action( $new_id );
+ $this->assertInstanceOf( 'ActionScheduler_NullAction', $retrieved );
+ }
+
+ public function test_migrate_completed_action_from_wpPost_to_db() {
+ $source = new ActionScheduler_wpPostStore();
+ $destination = new ActionScheduler_DBStore();
+ $migrator = new ActionMigrator( $source, $destination, $this->get_log_migrator() );
+
+ $time = as_get_datetime_object();
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
+ $action_id = $source->save_action( $action );
+ $source->mark_complete( $action_id );
+
+ $new_id = $migrator->migrate( $action_id );
+
+ // ensure we get the same record out of the new store as we stored in the old.
+ $retrieved = $destination->fetch_action( $new_id );
+ $this->assertEquals( $action->get_hook(), $retrieved->get_hook() );
+ $this->assertEqualSets( $action->get_args(), $retrieved->get_args() );
+ $this->assertEquals( $action->get_schedule()->get_date()->format( 'U' ), $retrieved->get_schedule()->get_date()->format( 'U' ) );
+ $this->assertEquals( $action->get_group(), $retrieved->get_group() );
+ $this->assertTrue( $retrieved->is_finished() );
+ $this->assertEquals( \ActionScheduler_Store::STATUS_COMPLETE, $destination->get_status( $new_id ) );
+
+ // ensure that the record in the old store does not exist.
+ $old_action = $source->fetch_action( $action_id );
+ $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action );
+ }
+
+ public function test_migrate_failed_action_from_wpPost_to_db() {
+ $source = new ActionScheduler_wpPostStore();
+ $destination = new ActionScheduler_DBStore();
+ $migrator = new ActionMigrator( $source, $destination, $this->get_log_migrator() );
+
+ $time = as_get_datetime_object();
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
+ $action_id = $source->save_action( $action );
+ $source->mark_failure( $action_id );
+
+ $new_id = $migrator->migrate( $action_id );
+
+ // ensure we get the same record out of the new store as we stored in the old.
+ $retrieved = $destination->fetch_action( $new_id );
+ $this->assertEquals( $action->get_hook(), $retrieved->get_hook() );
+ $this->assertEqualSets( $action->get_args(), $retrieved->get_args() );
+ $this->assertEquals( $action->get_schedule()->get_date()->format( 'U' ), $retrieved->get_schedule()->get_date()->format( 'U' ) );
+ $this->assertEquals( $action->get_group(), $retrieved->get_group() );
+ $this->assertTrue( $retrieved->is_finished() );
+ $this->assertEquals( \ActionScheduler_Store::STATUS_FAILED, $destination->get_status( $new_id ) );
+
+ // ensure that the record in the old store does not exist.
+ $old_action = $source->fetch_action( $action_id );
+ $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action );
+ }
+
+ public function test_migrate_canceled_action_from_wpPost_to_db() {
+ $source = new ActionScheduler_wpPostStore();
+ $destination = new ActionScheduler_DBStore();
+ $migrator = new ActionMigrator( $source, $destination, $this->get_log_migrator() );
+
+ $time = as_get_datetime_object();
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' );
+ $action_id = $source->save_action( $action );
+ $source->cancel_action( $action_id );
+
+ $new_id = $migrator->migrate( $action_id );
+
+ // ensure we get the same record out of the new store as we stored in the old.
+ $retrieved = $destination->fetch_action( $new_id );
+ $this->assertEquals( $action->get_hook(), $retrieved->get_hook() );
+ $this->assertEqualSets( $action->get_args(), $retrieved->get_args() );
+ $this->assertEquals( $action->get_schedule()->get_date()->format( 'U' ), $retrieved->get_schedule()->get_date()->format( 'U' ) );
+ $this->assertEquals( $action->get_group(), $retrieved->get_group() );
+ $this->assertTrue( $retrieved->is_finished() );
+ $this->assertEquals( \ActionScheduler_Store::STATUS_CANCELED, $destination->get_status( $new_id ) );
+
+ // ensure that the record in the old store does not exist.
+ $old_action = $source->fetch_action( $action_id );
+ $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action );
+ }
+
+ private function get_log_migrator() {
+ return new LogMigrator( \ActionScheduler::logger(), new ActionScheduler_DBLogger() );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/migration/BatchFetcher_Test.php b/libraries/action-scheduler/tests/phpunit/migration/BatchFetcher_Test.php
new file mode 100644
index 0000000..affbd31
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/migration/BatchFetcher_Test.php
@@ -0,0 +1,75 @@
+init();
+ }
+ }
+
+ public function test_nothing_to_migrate() {
+ $store = new PostStore();
+ $batch_fetcher = new BatchFetcher( $store );
+
+ $actions = $batch_fetcher->fetch();
+ $this->assertEmpty( $actions );
+ }
+
+ public function test_get_due_before_future() {
+ $store = new PostStore();
+ $due = array();
+ $future = array();
+
+ for ( $i = 0; $i < 5; $i ++ ) {
+ $time = as_get_datetime_object( $i + 1 . ' minutes' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $future[] = $store->save_action( $action );
+
+ $time = as_get_datetime_object( $i + 1 . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $due[] = $store->save_action( $action );
+ }
+
+ $batch_fetcher = new BatchFetcher( $store );
+
+ $actions = $batch_fetcher->fetch();
+
+ $this->assertEqualSets( $due, $actions );
+ }
+
+ public function test_get_future_before_complete() {
+ $store = new PostStore();
+ $future = array();
+ $complete = array();
+
+ for ( $i = 0; $i < 5; $i ++ ) {
+ $time = as_get_datetime_object( $i + 1 . ' minutes' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $future[] = $store->save_action( $action );
+
+ $time = as_get_datetime_object( $i + 1 . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_FinishedAction( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $complete[] = $store->save_action( $action );
+ }
+
+ $batch_fetcher = new BatchFetcher( $store );
+
+ $actions = $batch_fetcher->fetch();
+
+ $this->assertEqualSets( $future, $actions );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/migration/Config_Test.php b/libraries/action-scheduler/tests/phpunit/migration/Config_Test.php
new file mode 100644
index 0000000..6ca1e74
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/migration/Config_Test.php
@@ -0,0 +1,33 @@
+expectException( \RuntimeException::class );
+ $config->get_source_store();
+ }
+
+ public function test_source_logger_required() {
+ $config = new Config();
+ $this->expectException( \RuntimeException::class );
+ $config->get_source_logger();
+ }
+
+ public function test_destination_store_required() {
+ $config = new Config();
+ $this->expectException( \RuntimeException::class );
+ $config->get_destination_store();
+ }
+
+ public function test_destination_logger_required() {
+ $config = new Config();
+ $this->expectException( \RuntimeException::class );
+ $config->get_destination_logger();
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/migration/Controller_Test.php b/libraries/action-scheduler/tests/phpunit/migration/Controller_Test.php
new file mode 100644
index 0000000..b35001c
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/migration/Controller_Test.php
@@ -0,0 +1,76 @@
+schedule_migration();
+
+ $this->assertTrue(
+ as_next_scheduled_action( Scheduler::HOOK ) > 0,
+ 'Confirm that the Migration Controller scheduled the migration.'
+ );
+
+ as_unschedule_action( Scheduler::HOOK );
+ }
+
+ /**
+ * Test to ensure that if an essential table is missing, the Migration
+ * Controller will not schedule a migration.
+ *
+ * @see https://github.com/woocommerce/action-scheduler/issues/653
+ */
+ public function test_migration_not_scheduled_if_tables_are_missing() {
+ as_unschedule_action( Scheduler::HOOK );
+ $this->rename_claims_table();
+ Controller::instance()->schedule_migration();
+
+ $this->assertFalse(
+ as_next_scheduled_action( Scheduler::HOOK ),
+ 'When required tables are missing, the migration will not be scheduled.'
+ );
+
+ $this->restore_claims_table_name();
+ }
+
+ /**
+ * Rename the claims table, so that it cannot be used by the library.
+ */
+ private function rename_claims_table() {
+ global $wpdb;
+ $normal_table_name = $wpdb->prefix . Schema::CLAIMS_TABLE;
+ $modified_table_name = $normal_table_name . 'x';
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $wpdb->query( "RENAME TABLE {$normal_table_name} TO {$modified_table_name}" );
+ }
+
+ /**
+ * Restore the expected name of the claims table, so that it can be used by the library
+ * and any further tests.
+ */
+ private function restore_claims_table_name() {
+ global $wpdb;
+ $normal_table_name = $wpdb->prefix . Schema::CLAIMS_TABLE;
+ $modified_table_name = $normal_table_name . 'x';
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $wpdb->query( "RENAME TABLE {$modified_table_name} TO {$normal_table_name}" );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/migration/LogMigrator_Test.php b/libraries/action-scheduler/tests/phpunit/migration/LogMigrator_Test.php
new file mode 100644
index 0000000..4913e80
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/migration/LogMigrator_Test.php
@@ -0,0 +1,52 @@
+init();
+ }
+ }
+
+ public function test_migrate_from_wpComment_to_db() {
+ $source = new ActionScheduler_wpCommentLogger();
+ $destination = new ActionScheduler_DBLogger();
+ $migrator = new LogMigrator( $source, $destination );
+ $source_action_id = wp_rand( 10, 10000 );
+ $destination_action_id = wp_rand( 10, 10000 );
+
+ $logs = array();
+ for ( $i = 0; $i < 3; $i++ ) {
+ for ( $j = 0; $j < 5; $j++ ) {
+ $logs[ $i ][ $j ] = md5( wp_rand() );
+ if ( 1 === $i ) {
+ $source->log( $source_action_id, $logs[ $i ][ $j ] );
+ }
+ }
+ }
+
+ $migrator->migrate( $source_action_id, $destination_action_id );
+
+ $migrated = $destination->get_logs( $destination_action_id );
+ $this->assertEqualSets(
+ $logs[1],
+ array_map(
+ function( $log ) {
+ return $log->get_message();
+ },
+ $migrated
+ )
+ );
+
+ // no API for deleting logs, so we leave them for manual cleanup later.
+ $this->assertCount( 5, $source->get_logs( $source_action_id ) );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/migration/Runner_Test.php b/libraries/action-scheduler/tests/phpunit/migration/Runner_Test.php
new file mode 100644
index 0000000..1067ef8
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/migration/Runner_Test.php
@@ -0,0 +1,95 @@
+init();
+ }
+ }
+
+ public function test_migrate_batches() {
+ $source_store = new PostStore();
+ $destination_store = new ActionScheduler_DBStore();
+ $source_logger = new CommentLogger();
+ $destination_logger = new ActionScheduler_DBLogger();
+
+ $config = new Config();
+ $config->set_source_store( $source_store );
+ $config->set_source_logger( $source_logger );
+ $config->set_destination_store( $destination_store );
+ $config->set_destination_logger( $destination_logger );
+
+ $runner = new Runner( $config );
+
+ $due = array();
+ $future = array();
+ $complete = array();
+
+ for ( $i = 0; $i < 5; $i ++ ) {
+ $time = as_get_datetime_object( $i + 1 . ' minutes' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $future[] = $source_store->save_action( $action );
+
+ $time = as_get_datetime_object( $i + 1 . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $due[] = $source_store->save_action( $action );
+
+ $time = as_get_datetime_object( $i + 1 . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_FinishedAction( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $complete[] = $source_store->save_action( $action );
+ }
+
+ $created = $source_store->query_actions( array( 'per_page' => 0 ) );
+ $this->assertCount( 15, $created );
+
+ $runner->run( 10 );
+
+ $args = array(
+ 'per_page' => 0,
+ 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK,
+ );
+
+ // due actions should migrate in the first batch.
+ $migrated = $destination_store->query_actions( $args );
+ $this->assertCount( 5, $migrated );
+
+ $remaining = $source_store->query_actions( $args );
+ $this->assertCount( 10, $remaining );
+
+ $runner->run( 10 );
+
+ // pending actions should migrate in the second batch.
+ $migrated = $destination_store->query_actions( $args );
+ $this->assertCount( 10, $migrated );
+
+ $remaining = $source_store->query_actions( $args );
+ $this->assertCount( 5, $remaining );
+
+ $runner->run( 10 );
+
+ // completed actions should migrate in the third batch.
+ $migrated = $destination_store->query_actions( $args );
+ $this->assertCount( 15, $migrated );
+
+ $remaining = $source_store->query_actions( $args );
+ $this->assertCount( 0, $remaining );
+
+ }
+
+}
diff --git a/libraries/action-scheduler/tests/phpunit/migration/Scheduler_Test.php b/libraries/action-scheduler/tests/phpunit/migration/Scheduler_Test.php
new file mode 100644
index 0000000..c6b14f4
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/migration/Scheduler_Test.php
@@ -0,0 +1,138 @@
+init();
+ }
+ }
+
+ public function test_migration_is_complete() {
+ ActionScheduler_DataController::mark_migration_complete();
+ $this->assertTrue( ActionScheduler_DataController::is_migration_complete() );
+ }
+
+ public function test_migration_is_not_complete() {
+ $this->assertFalse( ActionScheduler_DataController::is_migration_complete() );
+ update_option( ActionScheduler_DataController::STATUS_FLAG, 'something_random' );
+ $this->assertFalse( ActionScheduler_DataController::is_migration_complete() );
+ }
+
+ public function test_migration_is_scheduled() {
+ // Clear the any existing migration hooks that have already been setup.
+ as_unschedule_all_actions( Scheduler::HOOK );
+
+ $scheduler = new Scheduler();
+ $this->assertFalse(
+ $scheduler->is_migration_scheduled(),
+ 'Migration is not automatically scheduled when a new ' . Scheduler::class . ' instance is created.'
+ );
+
+ $scheduler->schedule_migration();
+ $this->assertTrue(
+ $scheduler->is_migration_scheduled(),
+ 'Migration is scheduled only after schedule_migration() has been called.'
+ );
+ }
+
+ public function test_scheduler_runs_migration() {
+ $source_store = new PostStore();
+ $destination_store = new ActionScheduler_DBStore();
+
+ $return_5 = function () {
+ return 5;
+ };
+ add_filter( 'action_scheduler/migration_batch_size', $return_5 );
+
+ // Make sure successive migration actions are delayed so all actions aren't migrated at once on separate hooks.
+ $return_60 = function () {
+ return 60;
+ };
+ add_filter( 'action_scheduler/migration_interval', $return_60 );
+
+ for ( $i = 0; $i < 10; $i++ ) {
+ $time = as_get_datetime_object( $i + 1 . ' minutes' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $future[] = $source_store->save_action( $action );
+
+ $time = as_get_datetime_object( $i + 1 . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $due[] = $source_store->save_action( $action );
+ }
+
+ $this->assertCount( 20, $source_store->query_actions( array( 'per_page' => 0 ) ) );
+
+ $scheduler = new Scheduler();
+ $scheduler->unschedule_migration();
+ $scheduler->schedule_migration( time() - 1 );
+
+ $queue_runner = ActionScheduler_Mocker::get_queue_runner( $destination_store );
+ $queue_runner->run();
+
+ // 5 actions should have moved from the source store when the queue runner triggered the migration action.
+ $args = array(
+ 'per_page' => 0,
+ 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK,
+ );
+ $this->assertCount( 15, $source_store->query_actions( $args ) );
+
+ remove_filter( 'action_scheduler/migration_batch_size', $return_5 );
+ remove_filter( 'action_scheduler/migration_interval', $return_60 );
+ }
+
+ public function test_scheduler_marks_itself_complete() {
+ $source_store = new PostStore();
+ $destination_store = new ActionScheduler_DBStore();
+
+ for ( $i = 0; $i < 5; $i ++ ) {
+ $time = as_get_datetime_object( $i + 1 . ' minutes ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule );
+ $due[] = $source_store->save_action( $action );
+ }
+
+ $this->assertCount( 5, $source_store->query_actions( array( 'per_page' => 0 ) ) );
+
+ $scheduler = new Scheduler();
+ $scheduler->unschedule_migration();
+ $scheduler->schedule_migration( time() - 1 );
+
+ $queue_runner = ActionScheduler_Mocker::get_queue_runner( $destination_store );
+ $queue_runner->run();
+
+ // All actions should have moved from the source store when the queue runner triggered the migration action.
+ $args = array(
+ 'per_page' => 0,
+ 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK,
+ );
+ $this->assertCount( 0, $source_store->query_actions( $args ) );
+
+ // schedule another so we can get it to run immediately.
+ $scheduler->unschedule_migration();
+ $scheduler->schedule_migration( time() - 1 );
+
+ // run again so it knows that there's nothing left to process.
+ $queue_runner->run();
+
+ $scheduler->unhook();
+
+ // ensure the flag is set marking migration as complete.
+ $this->assertTrue( ActionScheduler_DataController::is_migration_complete() );
+
+ // ensure that another instance has not been scheduled.
+ $this->assertFalse( $scheduler->is_migration_scheduled() );
+
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/procedural_api/procedural_api_Test.php b/libraries/action-scheduler/tests/phpunit/procedural_api/procedural_api_Test.php
new file mode 100644
index 0000000..eb9a1c9
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/procedural_api/procedural_api_Test.php
@@ -0,0 +1,484 @@
+fetch_action( $action_id );
+ $this->assertEquals( $time, $action->get_schedule()->get_date()->getTimestamp() );
+ $this->assertEquals( $hook, $action->get_hook() );
+ }
+
+ public function test_recurring_action() {
+ $time = time();
+ $hook = md5( wp_rand() );
+ $action_id = as_schedule_recurring_action( $time, HOUR_IN_SECONDS, $hook );
+
+ $store = ActionScheduler::store();
+ $action = $store->fetch_action( $action_id );
+ $this->assertEquals( $time, $action->get_schedule()->get_date()->getTimestamp() );
+ $this->assertEquals( $time + HOUR_IN_SECONDS + 2, $action->get_schedule()->get_next( as_get_datetime_object( $time + 2 ) )->getTimestamp() );
+ $this->assertEquals( $hook, $action->get_hook() );
+ }
+
+ /**
+ * Test that we reject attempts to register a recurring action with an invalid interval. This guards against
+ * 'runaway' recurring actions that are created accidentally and treated as having a zero-second interval.
+ *
+ * @return void
+ */
+ public function test_recurring_actions_reject_invalid_intervals() {
+ $this->assertGreaterThan(
+ 0,
+ as_schedule_recurring_action( time(), 1, 'foo' ),
+ 'When an integer is provided as the interval, a recurring action is successfully created.'
+ );
+
+ $this->assertGreaterThan(
+ 0,
+ as_schedule_recurring_action( time(), '10', 'foo' ),
+ 'When an integer-like string is provided as the interval, a recurring action is successfully created.'
+ );
+
+ $this->assertGreaterThan(
+ 0,
+ as_schedule_recurring_action( time(), 100.0, 'foo' ),
+ 'When an integer-value as a double is provided as the interval, a recurring action is successfully created.'
+ );
+
+ $this->setExpectedIncorrectUsage( 'as_schedule_recurring_action' );
+ $this->assertEquals(
+ 0,
+ as_schedule_recurring_action( time(), 'nonsense', 'foo' ),
+ 'When a non-numeric string is provided as the interval, a recurring action is not created and a doing-it-wrong notice is emitted.'
+ );
+
+ $this->setExpectedIncorrectUsage( 'as_schedule_recurring_action' );
+ $this->assertEquals(
+ 0,
+ as_schedule_recurring_action( time(), 123.456, 'foo' ),
+ 'When a non-integer double is provided as the interval, a recurring action is not created and a doing-it-wrong notice is emitted.'
+ );
+ }
+
+ public function test_cron_schedule() {
+ $time = as_get_datetime_object( '2014-01-01' );
+ $hook = md5( wp_rand() );
+ $action_id = as_schedule_cron_action( $time->getTimestamp(), '0 0 10 10 *', $hook );
+
+ $store = ActionScheduler::store();
+ $action = $store->fetch_action( $action_id );
+ $expected_date = as_get_datetime_object( '2014-10-10' );
+ $this->assertEquals( $expected_date->getTimestamp(), $action->get_schedule()->get_date()->getTimestamp() );
+ $this->assertEquals( $hook, $action->get_hook() );
+
+ $expected_date = as_get_datetime_object( '2015-10-10' );
+ $this->assertEquals( $expected_date->getTimestamp(), $action->get_schedule()->get_next( as_get_datetime_object( '2015-01-02' ) )->getTimestamp() );
+ }
+
+ public function test_get_next() {
+ $time = as_get_datetime_object( 'tomorrow' );
+ $hook = md5( wp_rand() );
+ as_schedule_recurring_action( $time->getTimestamp(), HOUR_IN_SECONDS, $hook );
+
+ $next = as_next_scheduled_action( $hook );
+
+ $this->assertEquals( $time->getTimestamp(), $next );
+ }
+
+ public function test_get_next_async() {
+ $hook = md5( wp_rand() );
+ $action_id = as_enqueue_async_action( $hook );
+
+ $next = as_next_scheduled_action( $hook );
+
+ $this->assertTrue( $next );
+
+ $store = ActionScheduler::store();
+
+ // Completed async actions should still return false.
+ $store->mark_complete( $action_id );
+ $next = as_next_scheduled_action( $hook );
+ $this->assertFalse( $next );
+
+ // Failed async actions should still return false.
+ $store->mark_failure( $action_id );
+ $next = as_next_scheduled_action( $hook );
+ $this->assertFalse( $next );
+
+ // Cancelled async actions should still return false.
+ $store->cancel_action( $action_id );
+ $next = as_next_scheduled_action( $hook );
+ $this->assertFalse( $next );
+ }
+
+ public function provider_time_hook_args_group() {
+ $time = time() + 60 * 2;
+ $hook = md5( wp_rand() );
+ $args = array( wp_rand(), wp_rand() );
+ $group = 'test_group';
+
+ return array(
+
+ // Test with no args or group.
+ array(
+ 'time' => $time,
+ 'hook' => $hook,
+ 'args' => array(),
+ 'group' => '',
+ ),
+
+ // Test with args but no group.
+ array(
+ 'time' => $time,
+ 'hook' => $hook,
+ 'args' => $args,
+ 'group' => '',
+ ),
+
+ // Test with group but no args.
+ array(
+ 'time' => $time,
+ 'hook' => $hook,
+ 'args' => array(),
+ 'group' => $group,
+ ),
+
+ // Test with args & group.
+ array(
+ 'time' => $time,
+ 'hook' => $hook,
+ 'args' => $args,
+ 'group' => $group,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provider_time_hook_args_group
+ */
+ public function test_unschedule( $time, $hook, $args, $group ) {
+
+ $action_id_unscheduled = as_schedule_single_action( $time, $hook, $args, $group );
+ $action_scheduled_time = $time + 1;
+ $action_id_scheduled = as_schedule_single_action( $action_scheduled_time, $hook, $args, $group );
+
+ as_unschedule_action( $hook, $args, $group );
+
+ $next = as_next_scheduled_action( $hook, $args, $group );
+ $this->assertEquals( $action_scheduled_time, $next );
+
+ $store = ActionScheduler::store();
+ $unscheduled_action = $store->fetch_action( $action_id_unscheduled );
+
+ // Make sure the next scheduled action is unscheduled.
+ $this->assertEquals( $hook, $unscheduled_action->get_hook() );
+ $this->assertEquals( as_get_datetime_object( $time ), $unscheduled_action->get_schedule()->get_date() );
+ $this->assertEquals( ActionScheduler_Store::STATUS_CANCELED, $store->get_status( $action_id_unscheduled ) );
+ $this->assertNull( $unscheduled_action->get_schedule()->get_next( as_get_datetime_object() ) );
+
+ // Make sure other scheduled actions are not unscheduled.
+ $this->assertEquals( ActionScheduler_Store::STATUS_PENDING, $store->get_status( $action_id_scheduled ) );
+ $scheduled_action = $store->fetch_action( $action_id_scheduled );
+
+ $this->assertEquals( $hook, $scheduled_action->get_hook() );
+ $this->assertEquals( $action_scheduled_time, $scheduled_action->get_schedule()->get_date()->getTimestamp() );
+ }
+
+ /**
+ * @dataProvider provider_time_hook_args_group
+ */
+ public function test_unschedule_all( $time, $hook, $args, $group ) {
+
+ $hook = md5( $hook );
+ $action_ids = array();
+
+ for ( $i = 0; $i < 3; $i++ ) {
+ $action_ids[] = as_schedule_single_action( $time, $hook, $args, $group );
+ }
+
+ as_unschedule_all_actions( $hook, $args, $group );
+
+ $next = as_next_scheduled_action( $hook );
+ $this->assertFalse( $next );
+
+ $after = as_get_datetime_object( $time );
+ $after->modify( '+1 minute' );
+
+ $store = ActionScheduler::store();
+
+ foreach ( $action_ids as $action_id ) {
+ $action = $store->fetch_action( $action_id );
+
+ $this->assertEquals( $hook, $action->get_hook() );
+ $this->assertEquals( as_get_datetime_object( $time ), $action->get_schedule()->get_date() );
+ $this->assertEquals( ActionScheduler_Store::STATUS_CANCELED, $store->get_status( $action_id ) );
+ $this->assertNull( $action->get_schedule()->get_next( $after ) );
+ }
+ }
+
+ public function test_as_get_datetime_object_default() {
+
+ $utc_now = new ActionScheduler_DateTime( 'now', new DateTimeZone( 'UTC' ) );
+ $as_now = as_get_datetime_object();
+
+ // Don't want to use 'U' as timestamps will always be in UTC.
+ $this->assertEquals( $utc_now->format( 'Y-m-d H:i:s' ), $as_now->format( 'Y-m-d H:i:s' ) );
+ }
+
+ public function test_as_get_datetime_object_relative() {
+
+ $utc_tomorrow = new ActionScheduler_DateTime( 'tomorrow', new DateTimeZone( 'UTC' ) );
+ $as_tomorrow = as_get_datetime_object( 'tomorrow' );
+
+ $this->assertEquals( $utc_tomorrow->format( 'Y-m-d H:i:s' ), $as_tomorrow->format( 'Y-m-d H:i:s' ) );
+
+ $utc_tomorrow = new ActionScheduler_DateTime( 'yesterday', new DateTimeZone( 'UTC' ) );
+ $as_tomorrow = as_get_datetime_object( 'yesterday' );
+
+ $this->assertEquals( $utc_tomorrow->format( 'Y-m-d H:i:s' ), $as_tomorrow->format( 'Y-m-d H:i:s' ) );
+ }
+
+ public function test_as_get_datetime_object_fixed() {
+
+ $utc_tomorrow = new ActionScheduler_DateTime( '29 February 2016', new DateTimeZone( 'UTC' ) );
+ $as_tomorrow = as_get_datetime_object( '29 February 2016' );
+
+ $this->assertEquals( $utc_tomorrow->format( 'Y-m-d H:i:s' ), $as_tomorrow->format( 'Y-m-d H:i:s' ) );
+
+ $utc_tomorrow = new ActionScheduler_DateTime( '1st January 2024', new DateTimeZone( 'UTC' ) );
+ $as_tomorrow = as_get_datetime_object( '1st January 2024' );
+
+ $this->assertEquals( $utc_tomorrow->format( 'Y-m-d H:i:s' ), $as_tomorrow->format( 'Y-m-d H:i:s' ) );
+ }
+
+ public function test_as_get_datetime_object_timezone() {
+
+ $timezone_au = 'Australia/Brisbane';
+ $timezone_default = date_default_timezone_get();
+
+ // phpcs:ignore
+ date_default_timezone_set( $timezone_au );
+
+ $au_now = new ActionScheduler_DateTime( 'now' );
+ $as_now = as_get_datetime_object();
+
+ // Make sure they're for the same time.
+ $this->assertEquals( $au_now->getTimestamp(), $as_now->getTimestamp() );
+
+ // But not in the same timezone, as $as_now should be using UTC.
+ $this->assertNotEquals( $au_now->format( 'Y-m-d H:i:s' ), $as_now->format( 'Y-m-d H:i:s' ) );
+
+ $au_now = new ActionScheduler_DateTime( 'now' );
+ $as_au_now = as_get_datetime_object();
+
+ $this->assertEquals( $au_now->getTimestamp(), $as_now->getTimestamp(), '', 2 );
+
+ // But not in the same timezone, as $as_now should be using UTC.
+ $this->assertNotEquals( $au_now->format( 'Y-m-d H:i:s' ), $as_now->format( 'Y-m-d H:i:s' ) );
+
+ // phpcs:ignore
+ date_default_timezone_set( $timezone_default );
+ }
+
+ public function test_as_get_datetime_object_type() {
+ $f = 'Y-m-d H:i:s';
+ $now = as_get_datetime_object();
+ $this->assertInstanceOf( 'ActionScheduler_DateTime', $now );
+
+ $datetime = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
+ $as_datetime = as_get_datetime_object( $datetime );
+ $this->assertEquals( $datetime->format( $f ), $as_datetime->format( $f ) );
+ }
+
+ public function test_as_has_scheduled_action() {
+ $store = ActionScheduler::store();
+
+ $time = as_get_datetime_object( 'tomorrow' );
+ $action_id = as_schedule_single_action( $time->getTimestamp(), 'hook_1' );
+
+ $this->assertTrue( as_has_scheduled_action( 'hook_1' ) );
+ $this->assertFalse( as_has_scheduled_action( 'hook_2' ) );
+
+ // Go to in-progress.
+ $store->log_execution( $action_id );
+ $this->assertTrue( as_has_scheduled_action( 'hook_1' ) );
+
+ // Go to complete.
+ $store->mark_complete( $action_id );
+ $this->assertFalse( as_has_scheduled_action( 'hook_1' ) );
+
+ // Go to failed.
+ $store->mark_failure( $action_id );
+ $this->assertFalse( as_has_scheduled_action( 'hook_1' ) );
+ }
+
+ public function test_as_has_scheduled_action_with_args() {
+ as_schedule_single_action( time(), 'hook_1', array( 'a' ) );
+
+ $this->assertTrue( as_has_scheduled_action( 'hook_1' ) );
+ $this->assertFalse( as_has_scheduled_action( 'hook_1', array( 'b' ) ) );
+
+ // Test for any args.
+ $this->assertTrue( as_has_scheduled_action( 'hook_1', array( 'a' ) ) );
+ }
+
+ public function test_as_has_scheduled_action_with_group() {
+ as_schedule_single_action( time(), 'hook_1', array(), 'group_1' );
+
+ $this->assertTrue( as_has_scheduled_action( 'hook_1', null, 'group_1' ) );
+ $this->assertTrue( as_has_scheduled_action( 'hook_1', array(), 'group_1' ) );
+ }
+ // phpcs:enable
+
+ /**
+ * Test as_enqueue_async_action with unique param.
+ */
+ public function test_as_enqueue_async_action_unique() {
+ $this->set_action_scheduler_store( new ActionScheduler_DBStore() );
+
+ $action_id = as_enqueue_async_action( 'hook_1', array( 'a' ), 'dummy', true );
+ $this->assertValidAction( $action_id );
+
+ $action_id_duplicate = as_enqueue_async_action( 'hook_1', array( 'a' ), 'dummy', true );
+ $this->assertEquals( 0, $action_id_duplicate );
+ }
+
+ /**
+ * Test enqueuing a unique action using the hybrid store.
+ * This is using a best-effort approach, so it's possible that the action will be enqueued even if it's not unique.
+ */
+ public function test_as_enqueue_async_action_unique_hybrid_best_effort() {
+ $this->set_action_scheduler_store( new ActionScheduler_HybridStore() );
+
+ $action_id = as_enqueue_async_action( 'hook_1', array( 'a' ), 'dummy', true );
+ $this->assertValidAction( $action_id );
+
+ $action_id_duplicate = as_enqueue_async_action( 'hook_1', array( 'a' ), 'dummy', true );
+ $this->assertEquals( 0, $action_id_duplicate );
+ }
+
+ /**
+ * Test as_schedule_single_action with unique param.
+ */
+ public function test_as_schedule_single_action_unique() {
+ $this->set_action_scheduler_store( new ActionScheduler_DBStore() );
+
+ $action_id = as_schedule_single_action( time(), 'hook_1', array( 'a' ), 'dummy', true );
+ $this->assertValidAction( $action_id );
+
+ $action_id_duplicate = as_schedule_single_action( time(), 'hook_1', array( 'a' ), 'dummy', true );
+ $this->assertEquals( 0, $action_id_duplicate );
+ }
+
+ /**
+ * Test as_schedule_recurring_action with unique param.
+ */
+ public function test_as_schedule_recurring_action_unique() {
+ $this->set_action_scheduler_store( new ActionScheduler_DBStore() );
+
+ $action_id = as_schedule_recurring_action( time(), MINUTE_IN_SECONDS, 'hook_1', array( 'a' ), 'dummy', true );
+ $this->assertValidAction( $action_id );
+
+ $action_id_duplicate = as_schedule_recurring_action( time(), MINUTE_IN_SECONDS, 'hook_1', array( 'a' ), 'dummy', true );
+ $this->assertEquals( 0, $action_id_duplicate );
+ }
+
+ /**
+ * Test as_schedule_cron with unique param.
+ */
+ public function test_as_schedule_cron_action() {
+ $this->set_action_scheduler_store( new ActionScheduler_DBStore() );
+
+ $action_id = as_schedule_cron_action( time(), '0 0 * * *', 'hook_1', array( 'a' ), 'dummy', true );
+ $this->assertValidAction( $action_id );
+
+ $action_id_duplicate = as_schedule_cron_action( time(), '0 0 * * *', 'hook_1', array( 'a' ), 'dummy', true );
+ $this->assertEquals( 0, $action_id_duplicate );
+ }
+
+ /**
+ * Test recovering from an incorrect database schema when scheduling a single action.
+ */
+ public function test_as_recover_from_incorrect_schema() {
+ // custom error reporting so we can test for errors sent to error_log.
+ global $wpdb;
+ $wpdb->suppress_errors( true );
+ $error_capture = tmpfile();
+ $actual_error_log = ini_set( 'error_log', stream_get_meta_data( $error_capture )['uri'] ); // phpcs:ignore WordPress.PHP.IniSet.Risky
+
+ // we need a hybrid store so that dropping the priority column will cause an exception.
+ $this->set_action_scheduler_store( new ActionScheduler_HybridStore() );
+ $this->assertEquals( 'ActionScheduler_HybridStore', get_class( ActionScheduler::store() ) );
+
+ // drop the priority column from the actions table.
+ $wpdb->query( "ALTER TABLE {$wpdb->actionscheduler_actions} DROP COLUMN priority" );
+
+ // try to schedule a single action.
+ $action_id = as_schedule_single_action( time(), 'hook_17', array( 'a', 'b' ), 'dummytest', true );
+
+ // ensure that no exception was thrown and zero was returned.
+ $this->assertEquals( 0, $action_id );
+
+ // try to schedule an async action.
+ $action_id = as_enqueue_async_action( 'hook_18', array( 'a', 'b' ), 'dummytest', true );
+ // ensure that no exception was thrown and zero was returned.
+ $this->assertEquals( 0, $action_id );
+
+ // try to schedule a recurring action.
+ $action_id = as_schedule_recurring_action( time(), MINUTE_IN_SECONDS, 'hook_19', array( 'a', 'b' ), 'dummytest', true );
+ // ensure that no exception was thrown and zero was returned.
+ $this->assertEquals( 0, $action_id );
+
+ // try to schedule a cron action.
+ $action_id = as_schedule_cron_action( time(), '0 0 * * *', 'hook_20', array( 'a', 'b' ), 'dummytest', true );
+ // ensure that no exception was thrown and zero was returned.
+ $this->assertEquals( 0, $action_id );
+
+ // ensure that all four errors were logged to error_log.
+ $logged_errors = stream_get_contents( $error_capture );
+ $this->assertContains( 'Caught exception while enqueuing action "hook_17": Error saving action', $logged_errors );
+ $this->assertContains( 'Caught exception while enqueuing action "hook_18": Error saving action', $logged_errors );
+ $this->assertContains( 'Caught exception while enqueuing action "hook_19": Error saving action', $logged_errors );
+ $this->assertContains( 'Caught exception while enqueuing action "hook_20": Error saving action', $logged_errors );
+ $this->assertContains( "Unknown column 'priority' in 'field list'", $logged_errors );
+
+ // recreate the priority column.
+ $wpdb->query( "ALTER TABLE {$wpdb->actionscheduler_actions} ADD COLUMN priority tinyint(10) UNSIGNED NOT NULL DEFAULT 10" );
+ // restore error logging.
+ $wpdb->suppress_errors( false );
+ ini_set( 'error_log', $actual_error_log ); // phpcs:ignore WordPress.PHP.IniSet.Risky
+ }
+
+ /**
+ * Helper method to set actions scheduler store.
+ *
+ * @param ActionScheduler_Store $store Store instance to set.
+ */
+ private function set_action_scheduler_store( $store ) {
+ $store_factory_setter = function() use ( $store ) {
+ self::$store = $store;
+ };
+ $binded_store_factory_setter = Closure::bind( $store_factory_setter, null, ActionScheduler_Store::class );
+ $binded_store_factory_setter();
+ }
+
+ /**
+ * Helper method to assert valid action.
+ *
+ * @param int $action_id Action ID to assert.
+ */
+ private function assertValidAction( $action_id ) {
+ $this->assertNotEquals( 0, $action_id );
+ $action = ActionScheduler::store()->fetch_action( $action_id );
+ $this->assertInstanceOf( 'ActionScheduler_Action', $action );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/procedural_api/wc_get_scheduled_actions_Test.php b/libraries/action-scheduler/tests/phpunit/procedural_api/wc_get_scheduled_actions_Test.php
new file mode 100644
index 0000000..a8a2913
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/procedural_api/wc_get_scheduled_actions_Test.php
@@ -0,0 +1,131 @@
+ */
+ private $hooks = array();
+
+ /** @var array */
+ private $args = array();
+
+ /** @var array */
+ private $groups = array();
+
+ public function setUp() {
+ parent::setUp();
+
+ $store = ActionScheduler::store();
+
+ for ( $i = 0; $i < 10; $i++ ) {
+ $this->hooks[ $i ] = md5( wp_rand() );
+ $this->args[ $i ] = md5( wp_rand() );
+ $this->groups[ $i ] = md5( wp_rand() );
+ }
+
+ for ( $i = 0; $i < 10; $i++ ) {
+ for ( $j = 0; $j < 10; $j++ ) {
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( $j - 3 . 'days' ) );
+ $group = $this->groups[ ( $i + $j ) % 10 ];
+ $action = new ActionScheduler_Action( $this->hooks[ $i ], array( $this->args[ $j ] ), $schedule, $group );
+ $store->save_action( $action );
+ }
+ }
+ }
+
+ public function test_date_queries() {
+ $actions = as_get_scheduled_actions(
+ array(
+ 'date' => as_get_datetime_object( gmdate( 'Y-m-d 00:00:00' ) ),
+ 'per_page' => -1,
+ ),
+ 'ids'
+ );
+ $this->assertCount( 30, $actions );
+
+ $actions = as_get_scheduled_actions(
+ array(
+ 'date' => as_get_datetime_object( gmdate( 'Y-m-d 00:00:00' ) ),
+ 'date_compare' => '>=',
+ 'per_page' => -1,
+ ),
+ 'ids'
+ );
+ $this->assertCount( 70, $actions );
+ }
+
+ public function test_hook_queries() {
+ $actions = as_get_scheduled_actions(
+ array(
+ 'hook' => $this->hooks[2],
+ 'per_page' => -1,
+ ),
+ 'ids'
+ );
+ $this->assertCount( 10, $actions );
+
+ $actions = as_get_scheduled_actions(
+ array(
+ 'hook' => $this->hooks[2],
+ 'date' => as_get_datetime_object( gmdate( 'Y-m-d 00:00:00' ) ),
+ 'per_page' => -1,
+ ),
+ 'ids'
+ );
+ $this->assertCount( 3, $actions );
+ }
+
+ public function test_args_queries() {
+ $actions = as_get_scheduled_actions(
+ array(
+ 'args' => array( $this->args[5] ),
+ 'per_page' => -1,
+ ),
+ 'ids'
+ );
+ $this->assertCount( 10, $actions );
+
+ $actions = as_get_scheduled_actions(
+ array(
+ 'args' => array( $this->args[5] ),
+ 'hook' => $this->hooks[3],
+ 'per_page' => -1,
+ ),
+ 'ids'
+ );
+ $this->assertCount( 1, $actions );
+
+ $actions = as_get_scheduled_actions(
+ array(
+ 'args' => array( $this->args[5] ),
+ 'hook' => $this->hooks[3],
+ 'date' => as_get_datetime_object( gmdate( 'Y-m-d 00:00:00' ) ),
+ 'per_page' => -1,
+ ),
+ 'ids'
+ );
+ $this->assertCount( 0, $actions );
+ }
+
+ public function test_group_queries() {
+ $actions = as_get_scheduled_actions(
+ array(
+ 'group' => $this->groups[1],
+ 'per_page' => -1,
+ ),
+ 'ids'
+ );
+ $this->assertCount( 10, $actions );
+
+ $actions = as_get_scheduled_actions(
+ array(
+ 'group' => $this->groups[1],
+ 'hook' => $this->hooks[9],
+ 'per_page' => -1,
+ ),
+ 'ids'
+ );
+ $this->assertCount( 1, $actions );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueCleaner_Test.php b/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueCleaner_Test.php
new file mode 100644
index 0000000..e137200
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueCleaner_Test.php
@@ -0,0 +1,173 @@
+save_action( $action );
+ }
+
+ $runner->run();
+
+ add_filter( 'action_scheduler_retention_period', '__return_zero' ); // delete any finished job.
+ $cleaner = new ActionScheduler_QueueCleaner( $store );
+ $cleaned = $cleaner->delete_old_actions();
+ remove_filter( 'action_scheduler_retention_period', '__return_zero' );
+
+ $this->assertIsArray( $cleaned, 'ActionScheduler_QueueCleaner::delete_old_actions() returns an array.' );
+ $this->assertCount( 5, $cleaned, 'ActionScheduler_QueueCleaner::delete_old_actions() deleted the expected number of actions.' );
+
+ foreach ( $created_actions as $action_id ) {
+ $action = $store->fetch_action( $action_id );
+ $this->assertFalse( $action->is_finished() ); // it's a NullAction.
+ }
+ }
+
+ public function test_invalid_retention_period_filter_hook() {
+ // Supplying a non-integer such as null would break under 3.5.4 and earlier.
+ add_filter( 'action_scheduler_retention_period', '__return_null' );
+ $cleaner = new ActionScheduler_QueueCleaner( ActionScheduler::store() );
+
+ $this->setExpectedIncorrectUsage( 'ActionScheduler_QueueCleaner::delete_old_actions' );
+ $result = $cleaner->delete_old_actions();
+ remove_filter( 'action_scheduler_retention_period', '__return_zero' );
+
+ $this->assertIsArray(
+ $result,
+ 'ActionScheduler_QueueCleaner::delete_old_actions() can be invoked without a fatal error, even if the retention period was invalid.'
+ );
+
+ $this->assertCount(
+ 0,
+ $result,
+ 'ActionScheduler_QueueCleaner::delete_old_actions() will not delete any actions if the retention period was invalid.'
+ );
+ }
+
+ public function test_delete_canceled_actions() {
+ $store = ActionScheduler::store();
+
+ $random = md5( wp_rand() );
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) );
+
+ $created_actions = array();
+ for ( $i = 0; $i < 5; $i++ ) {
+ $action = new ActionScheduler_Action( $random, array( $random ), $schedule );
+ $action_id = $store->save_action( $action );
+ $store->cancel_action( $action_id );
+ $created_actions[] = $action_id;
+ }
+
+ // track the actions that are deleted.
+ $mock_action = new MockAction();
+ add_action( 'action_scheduler_deleted_action', array( $mock_action, 'action' ), 10, 1 );
+ add_filter( 'action_scheduler_retention_period', '__return_zero' ); // delete any finished job.
+
+ $cleaner = new ActionScheduler_QueueCleaner( $store );
+ $cleaner->delete_old_actions();
+
+ remove_filter( 'action_scheduler_retention_period', '__return_zero' );
+ remove_action( 'action_scheduler_deleted_action', array( $mock_action, 'action' ), 10 );
+
+ $deleted_actions = array();
+ foreach ( $mock_action->get_args() as $action ) {
+ $deleted_actions[] = reset( $action );
+ }
+
+ $this->assertEqualSets( $created_actions, $deleted_actions );
+ }
+
+ public function test_do_not_delete_recent_actions() {
+ $store = ActionScheduler::store();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+ $random = md5( wp_rand() );
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) );
+
+ $created_actions = array();
+ for ( $i = 0; $i < 5; $i++ ) {
+ $action = new ActionScheduler_Action( $random, array( $random ), $schedule );
+ $created_actions[] = $store->save_action( $action );
+ }
+
+ $runner->run();
+
+ $cleaner = new ActionScheduler_QueueCleaner( $store );
+ $cleaner->delete_old_actions();
+
+ foreach ( $created_actions as $action_id ) {
+ $action = $store->fetch_action( $action_id );
+ $this->assertTrue( $action->is_finished() ); // It's a FinishedAction.
+ }
+ }
+
+ public function test_reset_unrun_actions() {
+ $store = ActionScheduler::store();
+
+ $random = md5( wp_rand() );
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) );
+
+ $created_actions = array();
+ for ( $i = 0; $i < 5; $i++ ) {
+ $action = new ActionScheduler_Action( $random, array( $random ), $schedule );
+ $created_actions[] = $store->save_action( $action );
+ }
+
+ $store->stake_claim( 10 );
+
+ // don't actually process the jobs, to simulate a request that timed out.
+
+ add_filter( 'action_scheduler_timeout_period', '__return_zero' ); // delete any finished job.
+ $cleaner = new ActionScheduler_QueueCleaner( $store );
+ $cleaner->reset_timeouts();
+
+ remove_filter( 'action_scheduler_timeout_period', '__return_zero' );
+
+ $claim = $store->stake_claim( 10 );
+ $this->assertEqualSets( $created_actions, $claim->get_actions() );
+ }
+
+ public function test_do_not_reset_failed_action() {
+ $store = ActionScheduler::store();
+ $random = md5( wp_rand() );
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) );
+
+ $created_actions = array();
+ for ( $i = 0; $i < 5; $i++ ) {
+ $action = new ActionScheduler_Action( $random, array( $random ), $schedule );
+ $created_actions[] = $store->save_action( $action );
+ }
+
+ $claim = $store->stake_claim( 10 );
+ foreach ( $claim->get_actions() as $action_id ) {
+ // simulate the first action interrupted by an uncatchable fatal error.
+ $store->log_execution( $action_id );
+ break;
+ }
+
+ add_filter( 'action_scheduler_timeout_period', '__return_zero' ); // delete any finished job.
+ $cleaner = new ActionScheduler_QueueCleaner( $store );
+ $cleaner->reset_timeouts();
+ remove_filter( 'action_scheduler_timeout_period', '__return_zero' );
+
+ $new_claim = $store->stake_claim( 10 );
+ $this->assertCount( 4, $new_claim->get_actions() );
+
+ add_filter( 'action_scheduler_failure_period', '__return_zero' );
+ $cleaner->mark_failures();
+ remove_filter( 'action_scheduler_failure_period', '__return_zero' );
+
+ $failed = $store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_FAILED ) );
+ $this->assertEquals( $created_actions[0], $failed[0] );
+ $this->assertCount( 1, $failed );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueRunner_Test.php b/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueRunner_Test.php
new file mode 100644
index 0000000..46e850c
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueRunner_Test.php
@@ -0,0 +1,613 @@
+run();
+
+ $this->assertEquals( 0, $actions_run );
+ }
+
+ public function test_run() {
+ $store = ActionScheduler::store();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+ $mock = new MockAction();
+ $random = md5( wp_rand() );
+
+ add_action( $random, array( $mock, 'action' ) );
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) );
+
+ for ( $i = 0; $i < 5; $i++ ) {
+ $action = new ActionScheduler_Action( $random, array( $random ), $schedule );
+ $store->save_action( $action );
+ }
+
+ $actions_run = $runner->run();
+
+ remove_action( $random, array( $mock, 'action' ) );
+
+ $this->assertEquals( 5, $mock->get_call_count() );
+ $this->assertEquals( 5, $actions_run );
+ }
+
+ public function test_run_with_future_actions() {
+ $store = ActionScheduler::store();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+ $mock = new MockAction();
+ $random = md5( wp_rand() );
+
+ add_action( $random, array( $mock, 'action' ) );
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) );
+
+ for ( $i = 0; $i < 3; $i++ ) {
+ $action = new ActionScheduler_Action( $random, array( $random ), $schedule );
+ $store->save_action( $action );
+ }
+
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) );
+ for ( $i = 0; $i < 3; $i++ ) {
+ $action = new ActionScheduler_Action( $random, array( $random ), $schedule );
+ $store->save_action( $action );
+ }
+
+ $actions_run = $runner->run();
+
+ remove_action( $random, array( $mock, 'action' ) );
+
+ $this->assertEquals( 3, $mock->get_call_count() );
+ $this->assertEquals( 3, $actions_run );
+ }
+
+ /**
+ * When an action is processed, it is set to "in-progress" (running) status immediately before the
+ * callback is invoked. If this fails (which could be because it is already in progress) then the
+ * action should be skipped.
+ *
+ * @return void
+ */
+ public function test_run_with_action_that_is_already_in_progress() {
+ $store = ActionScheduler::store();
+ $hook = uniqid();
+ $callback = function () {};
+ $count = 0;
+ $actions = array();
+ $completed = array();
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) );
+
+ for ( $i = 0; $i < 3; $i++ ) {
+ $actions[] = $store->save_action( new ActionScheduler_Action( $hook, array( $hook ), $schedule ) );
+ }
+
+ /**
+ * This function "sabotages" the next action by prematurely setting its status to "in-progress", simulating
+ * an edge case where a concurrent process runs the action.
+ */
+ $saboteur = function () use ( &$count, $store, $actions ) {
+ if ( 0 === $count++ ) {
+ $store->log_execution( $actions[1] );
+ }
+ };
+
+ /**
+ * @param int $action_id The ID of the recently completed action.
+ *
+ * @return void
+ */
+ $spy = function ( $action_id ) use ( &$completed ) {
+ $completed[] = $action_id;
+ };
+
+ add_action( 'action_scheduler_begin_execute', $saboteur );
+ add_action( 'action_scheduler_completed_action', $spy );
+ add_action( $hook, $callback );
+
+ $actions_attempted = ActionScheduler_Mocker::get_queue_runner( $store )->run();
+
+ remove_action( 'action_scheduler_begin_execute', $saboteur );
+ remove_action( 'action_scheduler_completed_action', $spy );
+ remove_action( $hook, $callback );
+
+ $this->assertEquals( 3, $actions_attempted, 'The queue runner attempted to process all 3 actions.' );
+ $this->assertEquals( array( $actions[0], $actions[2] ), $completed, 'Only two of the three actions were completed (one was skipped, because it was processed by a concurrent request).' );
+ }
+
+ public function test_completed_action_status() {
+ $store = ActionScheduler::store();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+ $random = md5( wp_rand() );
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '12 hours ago' ) );
+ $action = new ActionScheduler_Action( $random, array(), $schedule );
+ $action_id = $store->save_action( $action );
+
+ $runner->run();
+
+ $finished_action = $store->fetch_action( $action_id );
+
+ $this->assertTrue( $finished_action->is_finished() );
+ }
+
+ public function test_next_instance_of_cron_action() {
+ // Create an action with daily Cron expression (i.e. midnight each day).
+ $random = md5( wp_rand() );
+ $action_id = ActionScheduler::factory()->cron( $random, array(), null, '0 0 * * *' );
+ $store = ActionScheduler::store();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+
+ // Make sure the 1st instance of the action is scheduled to occur tomorrow.
+ $date = as_get_datetime_object( 'tomorrow' );
+ $date->modify( '-1 minute' );
+ $claim = $store->stake_claim( 10, $date );
+ $this->assertCount( 0, $claim->get_actions() );
+
+ $store->release_claim( $claim );
+
+ $date->modify( '+1 minute' );
+
+ $claim = $store->stake_claim( 10, $date );
+ $actions = $claim->get_actions();
+ $this->assertCount( 1, $actions );
+
+ $fetched_action_id = reset( $actions );
+ $fetched_action = $store->fetch_action( $fetched_action_id );
+
+ $this->assertEquals( $fetched_action_id, $action_id );
+ $this->assertEquals( $random, $fetched_action->get_hook() );
+ $this->assertEquals( $date->getTimestamp(), $fetched_action->get_schedule()->get_date()->getTimestamp(), '', 1 );
+
+ $store->release_claim( $claim );
+
+ // Make sure the 2nd instance of the cron action is scheduled to occur tomorrow still.
+ $runner->process_action( $action_id );
+
+ $claim = $store->stake_claim( 10, $date );
+ $actions = $claim->get_actions();
+ $this->assertCount( 1, $actions );
+
+ $fetched_action_id = reset( $actions );
+ $fetched_action = $store->fetch_action( $fetched_action_id );
+
+ $this->assertNotEquals( $fetched_action_id, $action_id );
+ $this->assertEquals( $random, $fetched_action->get_hook() );
+ $this->assertEquals( $date->getTimestamp(), $fetched_action->get_schedule()->get_date()->getTimestamp(), '', 1 );
+ }
+
+ public function test_next_instance_of_interval_action() {
+ $random = md5( wp_rand() );
+ $date = as_get_datetime_object( '12 hours ago' );
+ $store = ActionScheduler::store();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+
+ // Create an action to recur every 24 hours, with the first instance scheduled to run 12 hours ago.
+ $action_id = ActionScheduler::factory()->create(
+ array(
+ 'type' => 'recurring',
+ 'hook' => $random,
+ 'when' => $date->getTimestamp(),
+ 'pattern' => DAY_IN_SECONDS,
+ 'priority' => 2,
+ )
+ );
+
+ // Make sure the 1st instance of the action is scheduled to occur 12 hours ago.
+ $claim = $store->stake_claim( 10, $date );
+ $actions = $claim->get_actions();
+ $this->assertCount( 1, $actions );
+
+ $fetched_action_id = reset( $actions );
+ $fetched_action = $store->fetch_action( $fetched_action_id );
+
+ $this->assertEquals( $fetched_action_id, $action_id );
+ $this->assertEquals( $random, $fetched_action->get_hook() );
+ $this->assertEquals( $date->getTimestamp(), $fetched_action->get_schedule()->get_date()->getTimestamp(), '', 1 );
+
+ $store->release_claim( $claim );
+
+ // Make sure after the queue is run, the 2nd instance of the action is scheduled to occur in 24 hours.
+ $runner->run();
+
+ $date = as_get_datetime_object( '+1 day' );
+ $claim = $store->stake_claim( 10, $date );
+ $actions = $claim->get_actions();
+ $this->assertCount( 1, $actions );
+
+ $fetched_action_id = reset( $actions );
+ $fetched_action = $store->fetch_action( $fetched_action_id );
+
+ $this->assertNotEquals( $fetched_action_id, $action_id );
+ $this->assertEquals( $random, $fetched_action->get_hook() );
+ $this->assertEquals( $date->getTimestamp(), $fetched_action->get_schedule()->get_date()->getTimestamp(), '', 1 );
+ $this->assertEquals( 2, $fetched_action->get_priority(), 'The replacement action should inherit the same priority as the original action.' );
+ $store->release_claim( $claim );
+
+ // Make sure the 3rd instance of the cron action is scheduled for 24 hours from now, as the action was run early, ahead of schedule.
+ $runner->process_action( $fetched_action_id );
+ $date = as_get_datetime_object( '+1 day' );
+
+ $claim = $store->stake_claim( 10, $date );
+ $actions = $claim->get_actions();
+ $this->assertCount( 1, $actions );
+
+ $fetched_action_id = reset( $actions );
+ $fetched_action = $store->fetch_action( $fetched_action_id );
+
+ $this->assertNotEquals( $fetched_action_id, $action_id );
+ $this->assertEquals( $random, $fetched_action->get_hook() );
+ $this->assertEquals( $date->getTimestamp(), $fetched_action->get_schedule()->get_date()->getTimestamp(), '', 1 );
+ }
+
+ /**
+ * As soon as one recurring action has been executed its replacement will be scheduled.
+ *
+ * This is true even if the current action fails. This makes sense, since a failure may be temporary in nature.
+ * However, if the same recurring action consistently fails then it is likely that there is a problem and we should
+ * stop creating new instances. This test outlines the expected behavior in this regard.
+ *
+ * @return void
+ */
+ public function test_failing_recurring_actions_are_not_rescheduled_when_threshold_met() {
+ $store = ActionScheduler_Store::instance();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+ $created_actions = array();
+
+ // Create 4 failed actions (one below the threshold of what counts as 'consistently failing').
+ for ( $i = 0; $i < 3; $i++ ) {
+ // We give each action a unique set of args, this illustrates that in the context of determining consistent
+ // failure we care only about the hook and not other properties of the action.
+ $args = array( 'unique-' . $i => hash( 'md5', $i ) );
+ $hook = 'will-fail';
+ $date = as_get_datetime_object( 12 - $i . ' hours ago' );
+ $action_id = as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, $hook, $args );
+ $store->mark_failure( $action_id );
+ $created_actions[] = $action_id;
+ }
+
+ // Now create a further action using the same hook, that is also destined to fail.
+ $date = as_get_datetime_object( '6 hours ago' );
+ $pending_action_id = as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, $hook, $args );
+ $created_actions[] = $pending_action_id;
+
+ // Process the queue!
+ $runner->run();
+
+ $pending_actions = $store->query_actions(
+ array(
+ 'hook' => $hook,
+ 'args' => $args,
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ )
+ );
+
+ $new_pending_action_id = current( $pending_actions );
+
+ // We now have 5 instances of the same recurring action. 4 have already failed, one is pending.
+ $this->assertCount( 1, $pending_actions, 'If the threshold for consistent failure has not been met, a replacement action should have been scheduled.' );
+ $this->assertNotContains( $new_pending_action_id, $created_actions, 'Confirm that the replacement action is new, and not one of those we created manually earlier in the test.' );
+
+ // Process the pending action (we do this directly instead of via `$runner->run()` because it won't actually
+ // become due for another hour).
+ $runner->process_action( $new_pending_action_id );
+ $pending_actions = $store->query_actions(
+ array(
+ 'hook' => $hook,
+ 'args' => $args,
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ )
+ );
+
+ // Now 5 instances of the same recurring action have all failed, therefore the threshold for consistent failure
+ // has been met and, this time, a new action should *not* have been scheduled.
+ $this->assertCount( 0, $pending_actions, 'The failure threshold (5 consecutive fails for recurring actions with the same signature) having been met, no further actions were scheduled.' );
+ }
+
+ /**
+ * If a recurring action continually fails, it will not be re-scheduled. However, a hook makes it possible to
+ * exempt specific actions from this behavior (without impacting other unrelated recurring actions).
+ *
+ * @see self::test_failing_recurring_actions_are_not_rescheduled_when_threshold_met()
+ * @return void
+ */
+ public function test_exceptions_can_be_made_for_failing_recurring_actions() {
+ $store = ActionScheduler_Store::instance();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+ $observed = 0;
+
+ // Create 2 sets of 5 actions that have already past and have already failed (five being the threshold of what
+ // counts as 'consistently failing').
+ for ( $i = 0; $i < 4; $i++ ) {
+ $date = as_get_datetime_object( 12 - $i . ' hours ago' );
+ $store->mark_failure( as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, 'foo' ) );
+ $store->mark_failure( as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, 'bar' ) );
+ }
+
+ // Add one more action (pending and past-due) to each set.
+ $date = as_get_datetime_object( '6 hours ago' );
+ as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, 'foo' );
+ as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, 'bar' );
+
+ // Define a filter function that allows scheduled actions for hook 'foo' to still be rescheduled, despite its
+ // history of consistent failure.
+ $filter = function( $is_failing, $action ) use ( &$observed ) {
+ $observed++;
+ return 'foo' === $action->get_hook() ? false : $is_failing;
+ };
+
+ // Process the queue with our consistent-failure filter function in place.
+ add_filter( 'action_scheduler_recurring_action_is_consistently_failing', $filter, 10, 2 );
+ $runner->run();
+
+ // Check how many (if any) of our test actions were re-scheduled.
+ $pending_foo_actions = $store->query_actions(
+ array(
+ 'hook' => 'foo',
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ )
+ );
+ $pending_bar_actions = $store->query_actions(
+ array(
+ 'hook' => 'bar',
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ )
+ );
+
+ // Expectations...
+ $this->assertCount( 1, $pending_foo_actions, 'We expect a new instance of action "foo" will have been scheduled.' );
+ $this->assertCount( 0, $pending_bar_actions, 'We expect no further instances of action "bar" will have been scheduled.' );
+ $this->assertEquals( 2, $observed, 'We expect our callback to have been invoked twice, once in relation to each test action.' );
+
+ // Clean-up...
+ remove_filter( 'action_scheduler_recurring_action_is_consistently_failing', $filter, 10, 2 );
+ }
+
+ public function test_hooked_into_wp_cron() {
+ $next = wp_next_scheduled( ActionScheduler_QueueRunner::WP_CRON_HOOK, array( 'WP Cron' ) );
+ $this->assertNotEmpty( $next );
+ }
+
+ public function test_batch_count_limit() {
+ $store = ActionScheduler::store();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+ $mock = new MockAction();
+ $random = md5( wp_rand() );
+
+ add_action( $random, array( $mock, 'action' ) );
+ $schedule = new ActionScheduler_SimpleSchedule( new ActionScheduler_DateTime( '1 day ago' ) );
+
+ for ( $i = 0; $i < 2; $i++ ) {
+ $action = new ActionScheduler_Action( $random, array( $random ), $schedule );
+ $store->save_action( $action );
+ }
+
+ $claim = $store->stake_claim();
+
+ $actions_run = $runner->run();
+
+ $this->assertEquals( 0, $mock->get_call_count() );
+ $this->assertEquals( 0, $actions_run );
+
+ $store->release_claim( $claim );
+
+ $actions_run = $runner->run();
+
+ $this->assertEquals( 2, $mock->get_call_count() );
+ $this->assertEquals( 2, $actions_run );
+
+ remove_action( $random, array( $mock, 'action' ) );
+ }
+
+ public function test_changing_batch_count_limit() {
+ $store = ActionScheduler::store();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+ $random = md5( wp_rand() );
+ $schedule = new ActionScheduler_SimpleSchedule( new ActionScheduler_DateTime( '1 day ago' ) );
+
+ for ( $i = 0; $i < 30; $i++ ) {
+ $action = new ActionScheduler_Action( $random, array( $random ), $schedule );
+ $store->save_action( $action );
+ }
+
+ $claims = array();
+
+ for ( $i = 0; $i < 5; $i++ ) {
+ $claims[] = $store->stake_claim( 5 );
+ }
+
+ $mock1 = new MockAction();
+ add_action( $random, array( $mock1, 'action' ) );
+ $actions_run = $runner->run();
+ remove_action( $random, array( $mock1, 'action' ) );
+
+ $this->assertEquals( 0, $mock1->get_call_count() );
+ $this->assertEquals( 0, $actions_run );
+
+ add_filter( 'action_scheduler_queue_runner_concurrent_batches', array( $this, 'return_6' ) );
+
+ $mock2 = new MockAction();
+ add_action( $random, array( $mock2, 'action' ) );
+ $actions_run = $runner->run();
+ remove_action( $random, array( $mock2, 'action' ) );
+
+ $this->assertEquals( 5, $mock2->get_call_count() );
+ $this->assertEquals( 5, $actions_run );
+
+ remove_filter( 'action_scheduler_queue_runner_concurrent_batches', array( $this, 'return_6' ) );
+
+ for ( $i = 0; $i < 5; $i++ ) { // to make up for the actions we just processed.
+ $action = new ActionScheduler_Action( $random, array( $random ), $schedule );
+ $store->save_action( $action );
+ }
+
+ $mock3 = new MockAction();
+ add_action( $random, array( $mock3, 'action' ) );
+ $actions_run = $runner->run();
+ remove_action( $random, array( $mock3, 'action' ) );
+
+ $this->assertEquals( 0, $mock3->get_call_count() );
+ $this->assertEquals( 0, $actions_run );
+
+ remove_filter( 'action_scheduler_queue_runner_concurrent_batches', array( $this, 'return_6' ) );
+ }
+
+ public function return_6() {
+ return 6;
+ }
+
+ public function test_store_fetch_action_failure_schedule_next_instance() {
+ $random = md5( wp_rand() );
+ $schedule = new ActionScheduler_IntervalSchedule( as_get_datetime_object( '12 hours ago' ), DAY_IN_SECONDS );
+ $action = new ActionScheduler_Action( $random, array(), $schedule );
+ $action_id = ActionScheduler::store()->save_action( $action );
+
+ // Set up a mock store that will throw an exception when fetching actions.
+ $store = $this
+ ->getMockBuilder( 'ActionScheduler_wpPostStore' )
+ ->setMethods( array( 'fetch_action' ) )
+ ->getMock();
+ $store
+ ->method( 'fetch_action' )
+ ->with( array( $action_id ) )
+ ->will( $this->throwException( new Exception() ) );
+
+ // Set up a mock queue runner to verify that schedule_next_instance()
+ // isn't called for an undefined $action.
+ $runner = $this
+ ->getMockBuilder( 'ActionScheduler_QueueRunner' )
+ ->setConstructorArgs( array( $store ) )
+ ->setMethods( array( 'schedule_next_instance' ) )
+ ->getMock();
+ $runner
+ ->expects( $this->never() )
+ ->method( 'schedule_next_instance' );
+
+ $runner->run();
+
+ // Set up a mock store that will throw an exception when fetching actions.
+ $store2 = $this
+ ->getMockBuilder( 'ActionScheduler_wpPostStore' )
+ ->setMethods( array( 'fetch_action' ) )
+ ->getMock();
+ $store2
+ ->method( 'fetch_action' )
+ ->with( array( $action_id ) )
+ ->willReturn( null );
+
+ // Set up a mock queue runner to verify that schedule_next_instance()
+ // isn't called for an undefined $action.
+ $runner2 = $this
+ ->getMockBuilder( 'ActionScheduler_QueueRunner' )
+ ->setConstructorArgs( array( $store ) )
+ ->setMethods( array( 'schedule_next_instance' ) )
+ ->getMock();
+ $runner2
+ ->expects( $this->never() )
+ ->method( 'schedule_next_instance' );
+
+ $runner2->run();
+ }
+
+ /**
+ * Checks that actions are processed in the correct order. Specifically, that past-due actions are not
+ * penalized in favor of newer async actions.
+ *
+ * @return void
+ */
+ public function test_order_in_which_actions_are_processed() {
+ /** @var ActionScheduler_Store $store */
+ $store = ActionScheduler::store();
+ $runner = ActionScheduler_Mocker::get_queue_runner( $store );
+ $execution_order = array();
+ $past_due_action = as_schedule_single_action( time() - HOUR_IN_SECONDS, __METHOD__, array( 'execute' => 'first' ) );
+ $async_action = as_enqueue_async_action( __METHOD__, array( 'execute' => 'second' ) );
+
+ $monitor = function ( $order ) use ( &$execution_order ) {
+ $execution_order[] = $order;
+ };
+
+ add_action( __METHOD__, $monitor );
+ $runner->run();
+ remove_action( __METHOD__, $monitor );
+
+ $this->assertEquals(
+ array(
+ 'first',
+ 'second',
+ ),
+ $execution_order
+ );
+ }
+
+ /**
+ * Tests the ability of the queue runner to accommodate a range of error conditions (raised recoverable errors
+ * under PHP 5.6, thrown errors under PHP 7.0 upwards, and exceptions under all supported versions).
+ *
+ * @return void
+ */
+ public function test_recoverable_errors_do_not_break_queue_runner() {
+ $executed = 0;
+ as_enqueue_async_action( 'foo' );
+ as_enqueue_async_action( 'bar' );
+ as_enqueue_async_action( 'baz' );
+ as_enqueue_async_action( 'foobar' );
+
+ /**
+ * Trigger a custom user error.
+ *
+ * @return void
+ */
+ $foo = function () use ( &$executed ) {
+ $executed++;
+ trigger_error( 'Trouble.', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
+ };
+
+ /**
+ * Throw an exception.
+ *
+ * @throws Exception Intentionally raised for testing purposes.
+ *
+ * @return void
+ */
+ $bar = function () use ( &$executed ) {
+ $executed++;
+ throw new Exception( 'More trouble.' );
+ };
+
+ /**
+ * Trigger a recoverable fatal error. Under PHP 5.6 the error will be raised, and under PHP 7.0 and higher the
+ * error will be thrown (different mechanisms are needed to support this difference).
+ *
+ * @throws Throwable Intentionally raised for testing purposes.
+ *
+ * @return void
+ */
+ $baz = function () use ( &$executed ) {
+ $executed++;
+ (string) (object) array();
+ };
+
+ /**
+ * A problem-free callback.
+ *
+ * @return void
+ */
+ $foobar = function () use ( &$executed ) {
+ $executed++;
+ };
+
+ add_action( 'foo', $foo );
+ add_action( 'bar', $bar );
+ add_action( 'baz', $baz );
+ add_action( 'foobar', $foobar );
+
+ ActionScheduler_Mocker::get_queue_runner( ActionScheduler::store() )->run();
+ $this->assertEquals( 4, $executed, 'All enqueued actions ran as expected despite errors and exceptions being raised by the first actions in the set.' );
+
+ remove_action( 'foo', $foo );
+ remove_action( 'bar', $bar );
+ remove_action( 'baz', $baz );
+ remove_action( 'foobar', $foobar );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_CronSchedule_Test.php b/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_CronSchedule_Test.php
new file mode 100644
index 0000000..0900514
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_CronSchedule_Test.php
@@ -0,0 +1,75 @@
+modify( '-1 hour' );
+ $schedule = new ActionScheduler_CronSchedule( $start, $cron );
+ $this->assertEquals( $time, $schedule->get_date() );
+ $this->assertEquals( $start, $schedule->get_first_date() );
+
+ // Test delaying for a future start date.
+ $start->modify( '+1 week' );
+ $time->modify( '+1 week' );
+
+ $schedule = new ActionScheduler_CronSchedule( $start, $cron );
+ $this->assertEquals( $time, $schedule->get_date() );
+ $this->assertEquals( $start, $schedule->get_first_date() );
+ }
+
+ public function test_creation_with_first_date() {
+ $time = as_get_datetime_object( 'tomorrow' );
+ $cron = CronExpression::factory( '@daily' );
+ $start = clone $time;
+ $start->modify( '-1 hour' );
+ $schedule = new ActionScheduler_CronSchedule( $start, $cron );
+ $this->assertEquals( $time, $schedule->get_date() );
+ $this->assertEquals( $start, $schedule->get_first_date() );
+
+ // Test delaying for a future start date.
+ $first = clone $time;
+ $first->modify( '-1 day' );
+ $start->modify( '+1 week' );
+ $time->modify( '+1 week' );
+
+ $schedule = new ActionScheduler_CronSchedule( $start, $cron, $first );
+ $this->assertEquals( $time, $schedule->get_date() );
+ $this->assertEquals( $first, $schedule->get_first_date() );
+ }
+
+ public function test_next() {
+ $time = as_get_datetime_object( '2013-06-14' );
+ $cron = CronExpression::factory( '@daily' );
+ $schedule = new ActionScheduler_CronSchedule( $time, $cron );
+ $this->assertEquals( as_get_datetime_object( 'tomorrow' ), $schedule->get_next( as_get_datetime_object() ) );
+ }
+
+ public function test_is_recurring() {
+ $schedule = new ActionScheduler_CronSchedule( as_get_datetime_object( '2013-06-14' ), CronExpression::factory( '@daily' ) );
+ $this->assertTrue( $schedule->is_recurring() );
+ }
+
+ public function test_cron_format() {
+ $time = as_get_datetime_object( '2014-01-01' );
+ $cron = CronExpression::factory( '0 0 10 10 *' );
+ $schedule = new ActionScheduler_CronSchedule( $time, $cron );
+ $this->assertEquals( as_get_datetime_object( '2014-10-10' ), $schedule->get_date() );
+
+ $cron = CronExpression::factory( '0 0 L 1/2 *' );
+ $schedule = new ActionScheduler_CronSchedule( $time, $cron );
+ $this->assertEquals( as_get_datetime_object( '2014-01-31' ), $schedule->get_date() );
+ $this->assertEquals( as_get_datetime_object( '2014-07-31' ), $schedule->get_next( as_get_datetime_object( '2014-06-01' ) ) );
+ $this->assertEquals( as_get_datetime_object( '2028-11-30' ), $schedule->get_next( as_get_datetime_object( '2028-11-01' ) ) );
+
+ $cron = CronExpression::factory( '30 14 * * MON#3 *' );
+ $schedule = new ActionScheduler_CronSchedule( $time, $cron );
+ $this->assertEquals( as_get_datetime_object( '2014-01-20 14:30:00' ), $schedule->get_date() );
+ $this->assertEquals( as_get_datetime_object( '2014-05-19 14:30:00' ), $schedule->get_next( as_get_datetime_object( '2014-05-01' ) ) );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_IntervalSchedule_Test.php b/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_IntervalSchedule_Test.php
new file mode 100644
index 0000000..a40052a
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_IntervalSchedule_Test.php
@@ -0,0 +1,37 @@
+assertEquals( $time, $schedule->get_date() );
+ $this->assertEquals( $time, $schedule->get_first_date() );
+ }
+
+ public function test_creation_with_first_date() {
+ $first = as_get_datetime_object();
+ $time = as_get_datetime_object( '+12 hours' );
+ $schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS, $first );
+ $this->assertEquals( $time, $schedule->get_date() );
+ $this->assertEquals( $first, $schedule->get_first_date() );
+ }
+
+ public function test_next() {
+ $now = time();
+ $start = $now - 30;
+ $schedule = new ActionScheduler_IntervalSchedule( as_get_datetime_object( "@$start" ), MINUTE_IN_SECONDS );
+ $this->assertEquals( $start, $schedule->get_date()->getTimestamp() );
+ $this->assertEquals( $now + MINUTE_IN_SECONDS, $schedule->get_next( as_get_datetime_object() )->getTimestamp() );
+ $this->assertEquals( $start, $schedule->get_next( as_get_datetime_object( "@$start" ) )->getTimestamp() );
+ }
+
+ public function test_is_recurring() {
+ $start = time() - 30;
+ $schedule = new ActionScheduler_IntervalSchedule( as_get_datetime_object( "@$start" ), MINUTE_IN_SECONDS );
+ $this->assertTrue( $schedule->is_recurring() );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_NullSchedule_Test.php b/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_NullSchedule_Test.php
new file mode 100644
index 0000000..4e10da2
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_NullSchedule_Test.php
@@ -0,0 +1,17 @@
+assertNull( $schedule->get_date() );
+ }
+
+ public function test_is_recurring() {
+ $schedule = new ActionScheduler_NullSchedule();
+ $this->assertFalse( $schedule->is_recurring() );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_SimpleSchedule_Test.php b/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_SimpleSchedule_Test.php
new file mode 100644
index 0000000..d017673
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_SimpleSchedule_Test.php
@@ -0,0 +1,36 @@
+assertEquals( $time, $schedule->get_date() );
+ }
+
+ public function test_past_date() {
+ $time = as_get_datetime_object( '-1 day' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $this->assertEquals( $time, $schedule->get_date() );
+ }
+
+ public function test_future_date() {
+ $time = as_get_datetime_object( '+1 day' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $this->assertEquals( $time, $schedule->get_date() );
+ }
+
+ public function test_grace_period_for_next() {
+ $time = as_get_datetime_object( '3 seconds ago' );
+ $schedule = new ActionScheduler_SimpleSchedule( $time );
+ $this->assertEquals( $time, $schedule->get_date() );
+ }
+
+ public function test_is_recurring() {
+ $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '+1 day' ) );
+ $this->assertFalse( $schedule->is_recurring() );
+ }
+}
diff --git a/libraries/action-scheduler/tests/phpunit/versioning/ActionScheduler_Versions_Test.php b/libraries/action-scheduler/tests/phpunit/versioning/ActionScheduler_Versions_Test.php
new file mode 100644
index 0000000..7eeee35
--- /dev/null
+++ b/libraries/action-scheduler/tests/phpunit/versioning/ActionScheduler_Versions_Test.php
@@ -0,0 +1,42 @@
+register( '1.0-dev', 'callback_1_dot_0_dev' );
+ $versions->register( '1.0', 'callback_1_dot_0' );
+
+ $registered = $versions->get_versions();
+
+ $this->assertArrayHasKey( '1.0-dev', $registered );
+ $this->assertArrayHasKey( '1.0', $registered );
+ $this->assertCount( 2, $registered );
+
+ $this->assertEquals( 'callback_1_dot_0_dev', $registered['1.0-dev'] );
+ }
+
+ public function test_duplicate_version() {
+ $versions = new ActionScheduler_Versions();
+ $versions->register( '1.0', 'callback_1_dot_0_a' );
+ $versions->register( '1.0', 'callback_1_dot_0_b' );
+
+ $registered = $versions->get_versions();
+
+ $this->assertArrayHasKey( '1.0', $registered );
+ $this->assertCount( 1, $registered );
+ }
+
+ public function test_latest_version() {
+ $versions = new ActionScheduler_Versions();
+ $this->assertEquals( '__return_null', $versions->latest_version_callback() );
+ $versions->register( '1.2', 'callback_1_dot_2' );
+ $versions->register( '1.3', 'callback_1_dot_3' );
+ $versions->register( '1.0', 'callback_1_dot_0' );
+
+ $this->assertEquals( '1.3', $versions->latest_version() );
+ $this->assertEquals( 'callback_1_dot_3', $versions->latest_version_callback() );
+ }
+}
diff --git a/mailrelay.php b/mailrelay.php
index 1766bb5..200c814 100644
--- a/mailrelay.php
+++ b/mailrelay.php
@@ -19,6 +19,8 @@
define( 'MAILRELAY_BASE_DOMAIN', 'ipzmarketing.com' );
}
+require_once __DIR__ . '/libraries/action-scheduler/action-scheduler.php';
+
require_once __DIR__ . '/inc/common.php';
require_once __DIR__ . '/inc/block-widgets.php';
require_once __DIR__ . '/inc/global-hooks.php';
diff --git a/phpcs.xml b/phpcs.xml
index 10458cc..b211bdd 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -26,6 +26,7 @@
+ */libraries/**/node_modules/**/vendor/*
\ No newline at end of file