first commit

This commit is contained in:
User A0264400
2026-04-01 23:20:16 +03:00
commit a766acdc90
23071 changed files with 4933189 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
<?php
namespace WPForms\Tasks\Actions;
use WPForms\Tasks\Task;
use WPForms\Tasks\Meta;
/**
* Class AsyncRequestTask is responsible to send information in the background.
*
* @since 1.7.5
*/
class AsyncRequestTask extends Task {
/**
* Action name for this task.
*
* @since 1.7.5
*/
const ACTION = 'wpforms_process_async_request';
/**
* Class constructor.
*
* @since 1.7.5
*/
public function __construct() {
// Task functionality is needed on cron request only.
if ( ! ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
return;
}
parent::__construct( self::ACTION );
$this->hooks();
}
/**
* Add hooks.
*
* @since 1.7.5
*/
private function hooks() {
// Register the migrate action.
add_action( self::ACTION, [ $this, 'process' ] );
}
/**
* Send usage tracking to the server.
*
* @since 1.7.5
*
* @param int $meta_id Action meta id.
*/
public static function process( $meta_id ) {
$params = ( new Meta() )->get( $meta_id );
if ( ! $params ) {
return;
}
list( $url, $args ) = $params->data;
wp_safe_remote_get( $url, $args );
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace WPForms\Tasks\Actions;
use WPForms\Tasks\Task;
use WPForms\Integrations\Stripe\Api\DomainManager;
use WPForms\Integrations\Stripe\Helpers;
/**
* Class DomainAutoRegistrationTask.
*
* @since 1.8.6
*/
class DomainAutoRegistrationTask extends Task {
/**
* Action name.
*
* @since 1.8.6
*/
const ACTION = 'wpforms_process_domain_auto_registration';
/**
* Status option name.
*
* @since 1.8.6
*/
const STATUS = 'wpforms_process_domain_auto_registration_status';
/**
* Start status.
*
* @since 1.8.6
*/
const START = 'start';
/**
* In progress status.
*
* @since 1.8.6
*/
const IN_PROGRESS = 'in_progress';
/**
* Completed status.
*
* @since 1.8.6
*/
const COMPLETED = 'completed';
/**
* Domain manager.
*
* @since 1.8.6
*
* @var DomainManager
*/
private $domain_manager;
/**
* Log title.
*
* @since 1.9.1
*
* @var string
*/
protected $log_title = 'Migration';
/**
* Constructor.
*
* @since 1.8.6
*/
public function __construct() {
parent::__construct( self::ACTION );
$this->domain_manager = new DomainManager();
}
/**
* Process the task.
*
* @since 1.8.6
*/
public function init() {
// Get a task status.
$status = get_option( self::STATUS );
// This task is run in \WPForms\Migrations\Upgrade186::run(),
// and started in \WPForms\Migrations\UpgradeBase::run_async().
// Bail out if a task is not started or completed.
if ( ! $status || $status === self::COMPLETED ) {
return;
}
// Mark that the task is in progress.
update_option( self::STATUS, self::IN_PROGRESS );
// Register hooks.
$this->hooks();
$tasks = wpforms()->obj( 'tasks' );
// Add new if none exists.
if ( $tasks->is_scheduled( self::ACTION ) !== false ) {
return;
}
$tasks->create( self::ACTION )->async()->register();
}
/**
* Register hooks.
*
* @since 1.8.6
*/
private function hooks() {
add_action( self::ACTION, [ $this, 'process' ] );
}
/**
* Process the task.
*
* @since 1.8.6
*/
public function process() {
// If the Stripe account is connected, then try to register domain.
if ( Helpers::has_stripe_keys() && $this->domain_manager->validate() ) {
$this->log( 'Stripe Payments: Stripe domain auto registration during migration to WPForms 1.8.6.' );
}
// Mark that the task is completed.
update_option( self::STATUS, self::COMPLETED );
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace WPForms\Tasks\Actions;
use WPForms\Tasks\Task;
use WPForms\Tasks\Meta;
/**
* Class EntryEmailsMetaCleanupTask.
*
* @since 1.5.9
*/
class EntryEmailsMetaCleanupTask extends Task {
/**
* Action name for this task.
*
* @since 1.5.9
*/
const ACTION = 'wpforms_process_entry_emails_meta_cleanup';
/**
* Class constructor.
*
* @since 1.5.9
*/
public function __construct() {
parent::__construct( self::ACTION );
$this->init();
}
/**
* Initialize the task with all the proper checks.
*
* @since 1.5.9
*/
public function init() {
// Register the action handler.
$this->hooks();
$tasks = wpforms()->obj( 'tasks' );
$email_async = wpforms_setting( 'email-async' );
// Add new if none exists.
if ( $tasks->is_scheduled( self::ACTION ) !== false ) {
// Cancel scheduled action if email async option is not set.
if ( ! $email_async ) {
$this->cancel();
}
return;
}
// Do not schedule action if email async option is not set.
if ( ! $email_async ) {
return;
}
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
/**
* Filters the email cleanup task interval.
*
* @since 1.5.9
*
* @param int $interval Interval in seconds.
*/
$interval = (int) apply_filters( 'wpforms_tasks_entry_emails_meta_cleanup_interval', DAY_IN_SECONDS );
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
$this->recurring( strtotime( 'tomorrow' ), $interval )
->params( $interval )
->register();
}
/**
* Add hooks.
*
* @since 1.7.3
*/
private function hooks() {
add_action( self::ACTION, [ $this, 'process' ] );
}
/**
* Perform the cleanup action: remove outdated meta for entry emails task.
*
* @since 1.5.9
*
* @param int $meta_id ID for meta information for a task.
*/
public function process( $meta_id ) {
$task_meta = new Meta();
$meta = $task_meta->get( (int) $meta_id );
// We should actually receive something.
if ( empty( $meta ) || empty( $meta->data ) ) {
return;
}
list( $interval ) = $meta->data;
$task_meta->clean_by( EntryEmailsTask::ACTION, (int) $interval );
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace WPForms\Tasks\Actions;
use WPForms\Tasks\Task;
use WPForms\Tasks\Meta;
/**
* Class EntryEmailsTask is responsible for defining how to send emails,
* when the form was submitted.
*
* @since 1.5.9
*/
class EntryEmailsTask extends Task {
/**
* Action name for this task.
*
* @since 1.5.9
*/
const ACTION = 'wpforms_process_entry_emails';
/**
* Class constructor.
*
* @since 1.5.9
*/
public function __construct() {
parent::__construct( self::ACTION );
$this->async();
}
/**
* Get the data from Tasks meta table, check/unpack it and
* send the email straight away.
*
* @since 1.5.9
* @since 1.5.9.3 Send immediately instead of calling \WPForms_Process::entry_email() method.
*
* @param int $meta_id ID for meta information for a task.
*/
public static function process( $meta_id ) {
$task_meta = new Meta();
$meta = $task_meta->get( (int) $meta_id );
// We should actually receive something.
if ( empty( $meta ) || empty( $meta->data ) ) {
return;
}
// We expect a certain number of params.
if ( count( $meta->data ) !== 5 ) {
return;
}
// We expect a certain meta data structure for this task.
list( $to, $subject, $message, $headers, $attachments ) = $meta->data;
// Let's do this NOW, finally.
wp_mail( $to, $subject, $message, $headers, $attachments );
}
}

View File

@@ -0,0 +1,575 @@
<?php
// phpcs:disable Generic.Commenting.DocComment.MissingShort
/** @noinspection PhpUnnecessaryCurlyVarSyntaxInspection */
/** @noinspection SqlResolve */
// phpcs:enable Generic.Commenting.DocComment.MissingShort
namespace WPForms\Tasks\Actions;
use WP_Post;
use WP_Query;
use WP_Screen;
use WPForms\Forms\Locator;
use WPForms\Tasks\Meta;
use WPForms\Tasks\Task;
use WPForms\Tasks\Tasks;
/**
* Class FormLocatorScanTask.
*
* @since 1.7.4
*/
class FormsLocatorScanTask extends Task {
/**
* Scan action name for this task.
*
* @since 1.7.4
*/
const SCAN_ACTION = 'wpforms_process_forms_locator_scan';
/**
* Re-scan action name for this task.
*
* @since 1.7.4
*/
const RESCAN_ACTION = 'wpforms_process_forms_locator_rescan';
/**
* Save action name for this task.
*
* @since 1.7.4
*/
const SAVE_ACTION = 'wpforms_process_forms_locator_save';
/**
* Delete action name for this task.
*
* @since 1.7.4
*/
const DELETE_ACTION = 'wpforms_process_forms_locator_delete';
/**
* Scan status option name.
*
* @since 1.7.4
*/
const SCAN_STATUS = 'wpforms_process_forms_locator_status';
/**
* Scan status "In Progress".
*
* @since 1.7.4
*/
const SCAN_STATUS_IN_PROGRESS = 'in progress';
/**
* Scan status "Completed".
*
* @since 1.7.4
*/
const SCAN_STATUS_COMPLETED = 'completed';
/**
* Locations query arg.
*
* @since 1.7.4
*/
const LOCATIONS_QUERY_ARG = 'locations';
/**
* Chunk size to use in get_form_locations().
* Specifies how many posts to load for scanning in one db request.
* Affects memory usage.
*
* @since 1.7.4
*/
const CHUNK_SIZE = 50;
/**
* Locator class instance.
*
* @since 1.7.4
*
* @var Locator
*/
private $locator;
/**
* Tasks class instance.
*
* @since 1.7.4
*
* @var Tasks
*/
private $tasks;
/**
* Task recurring interval in seconds.
*
* @since 1.7.4
*
* @var int
*/
private $interval;
/**
* Log title.
*
* @since 1.9.1
*
* @var string
*/
protected $log_title = 'Forms Locator';
/**
* Class constructor.
*
* @since 1.7.4
*/
public function __construct() {
parent::__construct( self::SCAN_ACTION );
$this->init();
}
/**
* Initialize the task with all the proper checks.
*
* @since 1.7.4
*/
public function init() {
$this->locator = wpforms()->obj( 'locator' );
/**
* Allow developers to modify the task interval.
*
* @since 1.7.4
*
* @param int $interval The task recurring interval in seconds. If <= 0, the task will be cancelled.
*/
$this->interval = (int) apply_filters( 'wpforms_tasks_actions_forms_locator_scan_task_interval', DAY_IN_SECONDS );
$this->hooks();
$this->tasks = wpforms()->obj( 'tasks' );
// Do not add a new one if scheduled.
if ( $this->tasks->is_scheduled( self::SCAN_ACTION ) !== false ) {
if ( $this->interval <= 0 ) {
$this->cancel();
}
return;
}
$this->add_scan_task();
}
/**
* Add scan task.
*
* @since 1.7.4
*/
private function add_scan_task() {
if ( $this->interval <= 0 ) {
return;
}
// Add a new task if none exists.
$this->recurring( time(), $this->interval )
->params()
->register();
}
/**
* Add hooks.
*
* @since 1.7.4
*/
private function hooks() {
// Register hidden action for testing and support.
add_action( 'current_screen', [ $this, 'maybe_run_actions_in_admin' ] );
// Register Action Scheduler actions.
add_action( self::SCAN_ACTION, [ $this, 'scan' ] );
add_action( self::RESCAN_ACTION, [ $this, 'rescan' ] );
add_action( self::SAVE_ACTION, [ $this, 'save' ] );
add_action( self::DELETE_ACTION, [ $this, 'delete' ] );
add_action( 'action_scheduler_after_process_queue', [ $this, 'after_process_queue' ] );
}
/**
* Maybe rescan or delete locations.
* Hidden undocumented actions for tests and support.
*
* @since 1.7.4
*
* @param WP_Screen $current_screen Current WP_Screen object.
*/
public function maybe_run_actions_in_admin( $current_screen ) {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if (
! $current_screen ||
$current_screen->id !== 'toplevel_page_wpforms-overview' ||
! isset( $_GET[ self::LOCATIONS_QUERY_ARG ] ) ||
! wpforms_debug()
) {
return;
}
if ( $_GET[ self::LOCATIONS_QUERY_ARG ] === 'delete' ) {
$this->delete();
}
if ( $_GET[ self::LOCATIONS_QUERY_ARG ] === 'scan' ) {
$this->rescan();
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
wp_safe_redirect( remove_query_arg( [ self::LOCATIONS_QUERY_ARG ] ) );
exit;
}
/**
* Run scan task.
*
* @since 1.7.4
*/
public function scan() {
if ( ! $this->tasks ) {
return;
}
// Bail out if the scan is already in progress.
if ( self::SCAN_STATUS_IN_PROGRESS === (string) get_option( self::SCAN_STATUS ) ) {
return;
}
// Mark that scan is in progress.
update_option( self::SCAN_STATUS, self::SCAN_STATUS_IN_PROGRESS );
$this->log( 'Forms Locator scan action started.' );
// This part of the scan shouldn't take more than 1 second even on big sites.
$post_ids = $this->search_in_posts();
$post_locations = $this->get_form_locations( $post_ids );
$widget_locations = $this->locator->search_in_widgets();
$standalone_locations = $this->search_in_standalone_forms();
$locations = array_merge( $post_locations, $widget_locations, $standalone_locations );
$form_location_metas = $this->get_form_location_metas( $locations );
/**
* This part of the scan can take a while.
* Saving hundreds of metas with a potentially very high number of locations could be time and memory consuming.
* That is why we perform save via Action Scheduler.
*/
$meta_chunks = array_chunk( $form_location_metas, self::CHUNK_SIZE, true );
$count = count( $meta_chunks );
foreach ( $meta_chunks as $index => $meta_chunk ) {
$this->tasks->create( self::SAVE_ACTION )->async()->params( $meta_chunk, $index, $count )->register();
}
$this->log( 'Save tasks created.' );
}
/**
* Run immediate scan.
*
* @since 1.7.4
*/
public function rescan() {
$this->cancel();
$this->add_scan_task();
}
/**
* Save form locations.
*
* @since 1.7.4
*
* @param int $meta_id Action meta id.
*/
public function save( $meta_id ) {
$params = ( new Meta() )->get( $meta_id );
if ( ! $params ) {
return;
}
list( $meta_chunk, $index, $count ) = $params->data;
foreach ( $meta_chunk as $form_id => $meta ) {
update_post_meta( $form_id, Locator::LOCATIONS_META, $meta );
}
$this->log(
sprintf(
'Forms Locator save action %1$d/%2$d completed.',
$index + 1,
$count
)
);
}
/**
* Delete form locations.
*
* @since 1.7.4
*/
public function delete() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
$wpdb->prepare(
"DELETE FROM $wpdb->postmeta WHERE meta_key = %s",
Locator::LOCATIONS_META
)
);
delete_option( self::SCAN_STATUS );
wp_cache_flush();
}
/**
* After process queue action.
* Delete transient to indicate that scanning is completed.
*
* @since 1.7.4
*/
public function after_process_queue() {
if ( $this->tasks->is_scheduled( self::SAVE_ACTION ) ) {
return;
}
// Mark that scan is finished.
if ( (string) get_option( self::SCAN_STATUS ) === self::SCAN_STATUS_IN_PROGRESS ) {
update_option( self::SCAN_STATUS, self::SCAN_STATUS_COMPLETED );
$this->log( 'Forms Locator scan action completed.' );
}
}
/**
* Search form in posts.
*
* @since 1.7.4
*
* @return int[]
*/
private function search_in_posts() {
global $wpdb;
$post_statuses = wpforms_wpdb_prepare_in( $this->locator->get_post_statuses() );
$post_types = wpforms_wpdb_prepare_in( $this->locator->get_post_types() );
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$ids = $wpdb->get_col(
"SELECT p.ID
FROM (SELECT ID
FROM $wpdb->posts
WHERE post_status IN ( $post_statuses ) AND post_type IN ( $post_types ) ) AS ids
INNER JOIN $wpdb->posts as p ON ids.ID = p.ID
WHERE p.post_content REGEXP '\\\[wpforms|wpforms/form-selector'"
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return array_map( 'intval', $ids );
}
/**
* Filters the SELECT clause of the query.
* Get a minimal set of fields from the post record.
*
* @since 1.7.4
*
* @param string $fields The SELECT clause of the query.
* @param WP_Query $query The WP_Query instance (passed by reference).
*
* @return string
*
* @noinspection PhpUnusedParameterInspection
*/
public function posts_fields_filter( $fields, $query ) {
global $wpdb;
$fields_arr = [ 'ID', 'post_title', 'post_status', 'post_type', 'post_content', 'post_name' ];
$fields_arr = array_map(
static function ( $field ) use ( $wpdb ) {
return "$wpdb->posts." . $field;
},
$fields_arr
);
return implode( ', ', $fields_arr );
}
/**
* Get form locations.
*
* @since 1.7.4
*
* @param int[] $post_ids Post IDs.
*
* @return array
*/
private function get_form_locations( $post_ids ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
/**
* Block caching here, as caching produces unneeded db requests in
* update_object_term_cache() and update_postmeta_cache().
*/
$query_args = [
'post_type' => $this->locator->get_post_types(),
'post_status' => $this->locator->get_post_statuses(),
'post__in' => $post_ids,
'no_found_rows' => true,
'posts_per_page' => - 1,
'cache_results' => false,
];
// Get form locations by chunks to prevent out of memory issue.
$post_id_chunks = array_chunk( $post_ids, self::CHUNK_SIZE );
$locations = [];
add_filter( 'posts_fields', [ $this, 'posts_fields_filter' ], 10, 2 );
foreach ( $post_id_chunks as $post_id_chunk ) {
$query_args['post__in'] = $post_id_chunk;
$query = new WP_Query( $query_args );
$locations = $this->get_form_locations_from_posts( $query->posts, $locations );
}
remove_filter( 'posts_fields', [ $this, 'posts_fields_filter' ] );
return $locations;
}
/**
* Get locations from posts.
*
* @since 1.7.4
*
* @param WP_Post[] $posts Posts.
* @param array $locations Locations.
*
* @return array
*/
private function get_form_locations_from_posts( $posts, $locations = [] ) {
$home_url = home_url();
foreach ( $posts as $post ) {
$form_ids = $this->locator->get_form_ids( $post->post_content );
if ( ! $form_ids ) {
continue;
}
$url = get_permalink( $post );
$url = ( $url === false || is_wp_error( $url ) ) ? '' : $url;
$url = str_replace( $home_url, '', $url );
foreach ( $form_ids as $form_id ) {
$locations[] = [
'type' => $post->post_type,
'title' => $post->post_title,
'form_id' => $form_id,
'id' => $post->ID,
'status' => $post->post_status,
'url' => $url,
];
}
}
return $locations;
}
/**
* Search in standalone forms.
*
* @since 1.8.7
*
* @return array
*/
private function search_in_standalone_forms(): array {
global $wpdb;
$location_types = [];
foreach ( Locator::STANDALONE_LOCATION_TYPES as $location_type ) {
$location_types[] = '"' . $location_type . '_enable":"1"';
}
$regexp = implode( '|', $location_types );
$post_statuses = wpforms_wpdb_prepare_in( $this->locator->get_post_statuses() );
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$standalone_forms = $wpdb->get_results(
"SELECT ID, post_content, post_status
FROM $wpdb->posts
WHERE post_status IN ( $post_statuses ) AND
post_type = 'wpforms' AND
post_content REGEXP '$regexp';"
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$locations = [];
foreach ( $standalone_forms as $standalone_form ) {
$form_data = json_decode( $standalone_form->post_content, true );
$locations[] = $this->locator->build_standalone_location(
(int) $standalone_form->ID,
$form_data,
$standalone_form->post_status
);
}
return $locations;
}
/**
* Get form location metas.
*
* @param array $locations Locations.
*
* @since 1.7.4
*
* @return array
*/
private function get_form_location_metas( $locations ) {
$metas = [];
foreach ( $locations as $location ) {
if ( empty( $location['form_id'] ) ) {
continue;
}
$metas[ $location['form_id'] ][] = $location;
}
return $metas;
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace WPForms\Tasks\Actions;
use WPForms\Tasks\Task;
/**
* Class Font Awesome Upgrade task.
*
* @since 1.8.3
*/
class IconChoicesFontAwesomeUpgradeTask extends Task {
/**
* Action name for this task.
*
* @since 1.8.3
*/
const ACTION = 'wpforms_process_font_awesome_upgrade';
/**
* Status option name.
*
* @since 1.8.3
*/
const STATUS = 'wpforms_process_font_awesome_upgrade_status';
/**
* Start status.
*
* @since 1.8.3
*/
const START = 'start';
/**
* In progress status.
*
* @since 1.8.3
*/
const IN_PROGRESS = 'in_progress';
/**
* Completed status.
*
* @since 1.8.3
*/
const COMPLETED = 'completed';
/**
* Log title.
*
* @since 1.9.1
*
* @var string
*/
protected $log_title = 'Migration';
/**
* Constructor.
*
* @since 1.8.3
*/
public function __construct() {
parent::__construct( self::ACTION );
}
/**
* Process the task.
*
* @since 1.8.3
*/
public function init() {
// Bail out if migration is not started or completed.
$status = get_option( self::STATUS );
// This task is run in \WPForms\Pro\Migrations\Upgrade183::run(),
// and started in \WPForms\Migrations\UpgradeBase::run_async().
// Bail out if a task is not started or completed.
if ( ! $status || $status === self::COMPLETED ) {
return;
}
// Mark that migration is in progress.
update_option( self::STATUS, self::IN_PROGRESS );
$this->hooks();
$tasks = wpforms()->obj( 'tasks' );
// Add new if none exists.
if ( $tasks->is_scheduled( self::ACTION ) !== false ) {
return;
}
$tasks->create( self::ACTION )->async()->register();
}
/**
* Hooks.
*
* @since 1.8.3
*/
private function hooks() {
add_action( self::ACTION, [ $this, 'upgrade' ] );
}
/**
* Upgrade.
*
* @since 1.8.3
*/
public function upgrade() {
$upload_dir = wpforms_upload_dir();
$tmp_base_path = $upload_dir['path'] . '/icon-choices-tmp';
$cache_base_path = $upload_dir['path'] . '/icon-choices';
$icons_data_file = $cache_base_path . '/icons.json';
if ( ! file_exists( $icons_data_file ) ) {
$this->log( 'Font Awesome Upgrade: Font Awesome Upgrade: Library is not present, nothing to upgrade.' );
update_option( self::STATUS, self::COMPLETED );
return;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();
global $wp_filesystem;
$wp_filesystem->rmdir( $tmp_base_path, true );
wpforms()->obj( 'icon_choices' )->run_install( $tmp_base_path );
if ( is_dir( $tmp_base_path ) ) {
// Remove old cache.
$this->log( 'Font Awesome Upgrade: Removing existing instance of the library.' );
$wp_filesystem->rmdir( $cache_base_path, true );
// Rename temporary directory.
$this->log( 'Font Awesome Upgrade: Renaming temporary directory.' );
$wp_filesystem->move( $tmp_base_path, $cache_base_path );
// Mark that migration is finished.
$this->log( 'Font Awesome Upgrade: Finished upgrading.' );
update_option( self::STATUS, self::COMPLETED );
return;
}
$this->log( 'Font Awesome Upgrade: Something went wrong, library was not upgraded.' );
}
}

View File

@@ -0,0 +1,268 @@
<?php
namespace WPForms\Tasks\Actions;
use WPForms\Tasks\Meta;
use WPForms\Tasks\Task;
use WPForms\Tasks\Tasks;
use WPForms_Entry_Fields_Handler;
use WPForms_Entry_Handler;
/**
* Class Migration173Task.
*
* @since 1.7.3
*/
class Migration173Task extends Task {
/**
* Action name for this task.
*
* @since 1.7.3
*/
const ACTION = 'wpforms_process_migration_173';
/**
* Status option name.
*
* @since 1.7.3
*/
const STATUS = 'wpforms_process_migration_173_status';
/**
* Start status.
*
* @since 1.7.3
*/
const START = 'start';
/**
* In progress status.
*
* @since 1.7.3
*/
const IN_PROGRESS = 'in progress';
/**
* Completed status.
*
* @since 1.7.3
*/
const COMPLETED = 'completed';
/**
* Chunk size to use.
* Specifies how many entries to load for scanning in one db request.
* Affects memory usage.
*
* @since 1.7.3
*/
const CHUNK_SIZE = 50;
/**
* Entry handler.
*
* @since 1.7.3
*
* @var WPForms_Entry_Handler
*/
private $entry_handler;
/**
* Entry fields handler.
*
* @since 1.7.3
*
* @var WPForms_Entry_Fields_Handler
*/
private $entry_fields_handler;
/**
* Class constructor.
*
* @since 1.7.3
*/
public function __construct() {
parent::__construct( self::ACTION );
}
/**
* Initialize the task with all the proper checks.
*
* @since 1.7.3
*/
public function init() {
$this->entry_handler = wpforms()->obj( 'entry' );
$this->entry_fields_handler = wpforms()->obj( 'entry_fields' );
if ( ! $this->entry_handler || ! $this->entry_fields_handler ) {
return;
}
// Bail out if migration is not started or completed.
$status = get_option( self::STATUS );
if ( ! $status || $status === self::COMPLETED ) {
return;
}
// Mark that migration is in progress.
update_option( self::STATUS, self::IN_PROGRESS );
$this->hooks();
$tasks = wpforms()->obj( 'tasks' );
// Add new if none exists.
if ( $tasks->is_scheduled( self::ACTION ) !== false ) {
return;
}
// Init migration.
$this->init_migration( $tasks );
}
/**
* Add hooks.
*
* @since 1.7.3
*/
private function hooks() {
// Register the migrate action.
add_action( self::ACTION, [ $this, 'migrate' ] );
// Register after process queue action.
add_action( 'action_scheduler_after_process_queue', [ $this, 'after_process_queue' ] );
}
/**
* Migrate an entry.
*
* @since 1.7.3
*
* @param int $meta_id Action meta id.
*/
public function migrate( $meta_id ) {
$params = ( new Meta() )->get( $meta_id );
if ( ! $params ) {
return;
}
list( $entry_id_chunk ) = $params->data;
foreach ( $entry_id_chunk as $entry_id ) {
$this->save_entry( $entry_id );
}
}
/**
* After process queue action.
* Set status as completed.
*
* @since 1.7.3
*/
public function after_process_queue() {
if ( as_has_scheduled_action( self::ACTION ) ) {
return;
}
// Mark that migration is finished.
update_option( self::STATUS, self::COMPLETED );
}
/**
* Init migration.
*
* @since 1.7.3
*
* @param Tasks $tasks Tasks class instance.
*/
private function init_migration( $tasks ) {
// This part of the migration shouldn't take more than 1 second even on big sites.
$entry_ids = $this->get_legacy_entry_ids();
if ( ! $entry_ids ) {
// Mark that migration is completed.
update_option( self::STATUS, self::COMPLETED );
return;
}
/**
* This part of the migration can take a while.
* Saving hundreds of entries with a potentially very high number of entry fields could be time and memory consuming.
* That is why we perform save via Action Scheduler.
*/
$entry_id_chunks = array_chunk( $entry_ids, self::CHUNK_SIZE, true );
foreach ( $entry_id_chunks as $entry_id_chunk ) {
$tasks->create( self::ACTION )->async()->params( $entry_id_chunk )->register();
}
}
/**
* Get entry ids which do not have relevant entry field records.
*
* @since 1.7.3
*
* @return int[]
*/
private function get_legacy_entry_ids() {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
$entries = $wpdb->get_results(
"
SELECT e.entry_id FROM {$this->entry_handler->table_name} e
LEFT JOIN {$this->entry_fields_handler->table_name} ef
ON e.entry_id=ef.entry_id
WHERE
e.status IN( 'partial', 'abandoned' ) AND
ef.entry_id IS NULL"
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
if ( ! $entries || ! is_array( $entries ) ) {
return [];
}
return array_map( 'intval', wp_list_pluck( $entries, 'entry_id' ) );
}
/**
* Save entry properly.
*
* @since 1.7.3
*
* @param int $entry_id Entry id.
*/
private function save_entry( $entry_id ) {
$entry = $this->entry_handler->get( $entry_id );
if ( ! $entry || ! isset( $entry->form_id, $entry->fields, $entry->date_modified ) ) {
return;
}
$fields = json_decode( $entry->fields, true );
if ( ! is_array( $fields ) ) {
return;
}
$form_data = [
'id' => (int) $entry->form_id,
'date' => $entry->date_modified,
];
$this->entry_fields_handler->save( $fields, $form_data, $entry_id, true );
}
}

View File

@@ -0,0 +1,454 @@
<?php
namespace WPForms\Tasks\Actions;
use WPForms\Tasks\Task;
use WPForms\Tasks\Tasks;
use WPForms_Entry_Handler;
use WPForms_Entry_Meta_Handler;
/**
* Class Migration175Task.
*
* @since 1.7.5
*/
class Migration175Task extends Task {
/**
* Action name for this task.
*
* @since 1.7.5
*/
const ACTION = 'wpforms_process_migration_175';
/**
* Status option name.
*
* @since 1.7.5
*/
const STATUS = 'wpforms_process_migration_175_status';
/**
* Start status.
*
* @since 1.7.5
*/
const START = 'start';
/**
* In progress status.
*
* @since 1.7.5
*/
const IN_PROGRESS = 'in progress';
/**
* Completed status.
*
* @since 1.7.5
*/
const COMPLETED = 'completed';
/**
* Chunk size to use.
* Specifies how many entries to convert in one db request.
*
* @since 1.7.5
*/
const CHUNK_SIZE = 5000;
/**
* Chunk size of the migration task.
* Specifies how many entry ids to load at once for further conversion.
*
* @since 1.7.5
*/
const TASK_CHUNK_SIZE = self::CHUNK_SIZE * 10;
/**
* Entry handler.
*
* @since 1.7.5
*
* @var WPForms_Entry_Handler
*/
private $entry_handler;
/**
* Entry meta handler.
*
* @since 1.7.5
*
* @var WPForms_Entry_Meta_Handler
*/
private $entry_meta_handler;
/**
* Temporary table name.
*
* @since 1.7.5
*
* @var string
*/
private $temp_table_name;
/**
* Class constructor.
*
* @since 1.7.5
*/
public function __construct() {
parent::__construct( self::ACTION );
}
/**
* Initialize the task with all the proper checks.
*
* @since 1.7.5
*/
public function init() {
global $wpdb;
$this->entry_handler = wpforms()->obj( 'entry' );
$this->entry_meta_handler = wpforms()->obj( 'entry_meta' );
$this->temp_table_name = "{$wpdb->prefix}wpforms_temp_entry_ids";
if ( ! $this->entry_handler || ! $this->entry_meta_handler ) {
return;
}
// Bail out if migration is not started or completed.
$status = get_option( self::STATUS );
if ( ! $status || $status === self::COMPLETED ) {
return;
}
$this->hooks();
if ( $status === self::START ) {
// Mark that migration is in progress.
update_option( self::STATUS, self::IN_PROGRESS );
// Alter entry meta table.
$this->alter_entry_meta_table();
// Init migration.
$this->init_migration();
}
}
/**
* Modify field in the entry meta table.
*
* @since 1.7.5
*/
private function alter_entry_meta_table() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query( "ALTER TABLE {$this->entry_meta_handler->table_name} MODIFY type VARCHAR(255)" );
}
/**
* Add index to a table.
*
* @since 1.7.5
*
* @param string $table_name Table.
* @param string $index_name Index name.
* @param string $key_part Key part.
*
* @return void
*/
private function add_index( $table_name, $index_name, $key_part ) {
global $wpdb;
// Check id index already exists.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
$result = $wpdb->get_var(
"SELECT COUNT(1) IndexIsThere
FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = '$table_name'
AND index_name = '$index_name'"
);
if ( $result === '1' ) {
return;
}
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
// Change the column length for the wp_wpforms_entry_meta.type column to 255 and add an index.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( "CREATE INDEX $index_name ON $table_name ( $key_part )" );
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
}
/**
* Add hooks.
*
* @since 1.7.5
*/
private function hooks() {
// Register the migrate action.
add_action( self::ACTION, [ $this, 'migrate' ] );
// Register after process queue action.
add_action( 'action_scheduler_after_process_queue', [ $this, 'after_process_queue' ] );
}
/**
* Migrate an entry.
*
* @param int $action_index Action index.
*
* @since 1.7.5
*/
public function migrate( $action_index ) {
global $wpdb;
$db_indexes = [
- 3 => [
'table_name' => $this->entry_meta_handler->table_name,
'index_name' => 'form_id',
'key_part' => 'form_id',
],
- 2 => [
'table_name' => $this->entry_meta_handler->table_name,
'index_name' => 'type',
'key_part' => 'type',
],
- 1 => [
'table_name' => $this->entry_meta_handler->table_name,
'index_name' => 'data',
'key_part' => 'data(32)',
],
];
// We create indexes in the background as it could take significant time on a big database.
if ( array_key_exists( $action_index, $db_indexes ) ) {
$this->add_index(
$db_indexes[ $action_index ]['table_name'],
$db_indexes[ $action_index ]['index_name'],
$db_indexes[ $action_index ]['key_part']
);
return;
}
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
// The query length in migrate_payment_data() is about 500 chars for 1 entry (7 metas).
// The length of the query is defined by MAX_ALLOWED_PACKET variable, which defaults to 4 MB on MySQL 5.7.
// We increase MAX_ALLOWED_PACKET variable to fit the number of entries specified in self::CHUNK_SIZE.
$new_max_allowed_packet = 500 * self::CHUNK_SIZE;
$max_allowed_packet = (int) $wpdb->get_var( "SHOW VARIABLES LIKE 'MAX_ALLOWED_PACKET'", 1 );
if ( $new_max_allowed_packet > $max_allowed_packet ) {
$wpdb->query( "SET MAX_ALLOWED_PACKET = $new_max_allowed_packet" );
}
// Using OFFSET makes a way longer request, as MySQL has to access all rows before OFFSET.
// We follow very fast way with indexed column (id > $action_index).
$entry_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT entry_id FROM $this->temp_table_name
WHERE id > %d LIMIT %d",
$action_index,
self::TASK_CHUNK_SIZE
)
);
$i = 0;
$entry_ids_count = count( $entry_ids );
// This cycle is twice less memory consuming than array_chunk( $entry_ids ).
while ( $i < $entry_ids_count ) {
$entry_ids_chunk = array_slice( $entry_ids, $i, self::CHUNK_SIZE );
$this->migrate_payment_data( implode( ',', $entry_ids_chunk ) );
$i += self::CHUNK_SIZE;
}
if ( $new_max_allowed_packet > $max_allowed_packet ) {
$wpdb->query( "SET MAX_ALLOWED_PACKET = $max_allowed_packet" );
}
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
}
/**
* After process queue action.
* Set status as completed.
*
* @since 1.7.5
*/
public function after_process_queue() {
$tasks = wpforms()->obj( 'tasks' );
if ( ! $tasks || $tasks->is_scheduled( self::ACTION ) ) {
return;
}
$this->drop_temp_table();
// Mark that migration is finished.
update_option( self::STATUS, self::COMPLETED );
}
/**
* Init migration.
*
* @since 1.7.5
* @noinspection PhpUndefinedFunctionInspection
*/
private function init_migration() {
// Get all payment entries.
$count = $this->get_unprocessed_payment_entry_ids();
if ( ! $count ) {
$this->drop_temp_table();
}
// We need 3 preliminary steps to create indexes.
$index = - 3;
while ( $index < $count ) {
// We do not use Task class here as we do not need meta. So, we reduce the number of DB requests.
as_enqueue_async_action(
self::ACTION,
[ $index ],
Tasks::GROUP
);
$index = $index < 0 ? $index + 1 : $index + self::CHUNK_SIZE;
}
}
/**
* Migrate payment data to the correct table.
*
* @param string $entry_ids_list List of entry ids.
*
* @since 1.7.5
*/
private function migrate_payment_data( $entry_ids_list ) {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
"SELECT entry_id, form_id, user_id, status, meta, date
FROM {$this->entry_handler->table_name}
WHERE entry_id IN ( $entry_ids_list )"
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
$values = [];
foreach ( $wpdb->last_result as $entry ) {
$meta = json_decode( $entry->meta, true );
if ( ! is_array( $meta ) ) {
continue;
}
foreach ( $meta as $meta_key => $meta_value ) {
// If meta_key doesn't begin with `payment_`, prefix it.
$meta_key = strpos( $meta_key, 'payment_' ) === 0 ? $meta_key : "payment_$meta_key";
// We do not use $wpdb->prepare here, as it is 5 times slower.
// Prepare takes 1.3 sec to prepare 1000 entries (6000 meta records).
// It is incomparable with the two queries here.
// With sprintf, the total processing time of this method is 0.15 sec for 1000 entries.
$values[] = sprintf(
"( %d, %d, %d, '%s', '%s', '%s', '%s' )",
$entry->entry_id,
$entry->form_id,
$entry->user_id,
$entry->status,
$meta_key,
$meta_value,
$entry->date
);
}
}
// Bail out if there is no found payment meta.
if ( empty( $values ) ) {
return;
}
$values = implode( ', ', $values );
// The following query length is about 500 chars for 1 entry (7 metas).
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
"INSERT INTO {$this->entry_meta_handler->table_name}
( entry_id, form_id, user_id, status, type, data, date )
VALUES $values"
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
}
/**
* Get entry ids which do not have relevant entry field records.
* Store them in a temporary table.
*
* @since 1.7.5
*
* @return int
*/
private function get_unprocessed_payment_entry_ids() {
global $wpdb;
$this->drop_temp_table();
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query(
"CREATE TABLE $this->temp_table_name
(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
entry_id BIGINT NOT NULL
)"
);
$wpdb->query(
"INSERT INTO $this->temp_table_name (entry_id)
SELECT entry_id
FROM {$this->entry_handler->table_name}
WHERE type = 'payment'
AND entry_id NOT IN
(SELECT entry_id FROM {$this->entry_meta_handler->table_name} WHERE type LIKE 'payment_%')"
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $wpdb->rows_affected;
}
/**
* Drop a temporary table.
*
* @since 1.7.5
*/
private function drop_temp_table() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange
$wpdb->query( "DROP TABLE IF EXISTS $this->temp_table_name" );
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace WPForms\Tasks\Actions;
use WPForms\Tasks\Task;
/**
* Class PurgeSpamTask.
*
* @since 1.9.1
*/
class PurgeSpamTask extends Task {
/**
* Action name for this task.
*
* @since 1.9.1
*/
const ACTION = 'wpforms_process_purge_spam';
/**
* Interval in seconds.
*
* @since 1.9.1
*
* @var int
*/
private $interval;
/**
* Tasks class instance.
*
* @since 1.9.1
*
* @var Tasks
*/
private $tasks;
/**
* Log title.
*
* @since 1.9.1
*
* @var string
*/
protected $log_title = 'Purge Spam';
/**
* Class constructor.
*
* @since 1.9.1
*/
public function __construct() {
parent::__construct( self::ACTION );
$this->init();
$this->hooks();
}
/**
* Init.
*
* @since 1.9.1
*/
public function init() {
/**
* Filter the interval for the purge spam task, in seconds.
*
* @since 1.9.1
*
* @param int $interval Interval in seconds.
*
* @return int
*/
$this->interval = (int) apply_filters( 'wpforms_tasks_actions_purge_spam_task_interval', DAY_IN_SECONDS );
$this->tasks = wpforms()->obj( 'tasks' );
// Do not add a new one if scheduled.
if ( $this->tasks->is_scheduled( self::ACTION ) !== false ) {
if ( $this->interval <= 0 ) {
$this->cancel();
}
return;
}
$this->add_scan_task();
}
/**
* Add hooks.
*
* @since 1.9.1
*/
public function hooks() {
add_action( self::ACTION, [ $this, 'process' ] );
}
/**
* Add a new task.
*
* @since 1.9.1
*/
private function add_scan_task() {
if ( $this->interval <= 0 ) {
return;
}
$this->tasks->create( self::ACTION )
->recurring( time(), $this->interval )
->params()
->register();
}
/**
* Purge spam action.
*
* @since 1.9.1
*/
public function process() {
$entry_obj = wpforms()->obj( 'entry' );
if ( ! $entry_obj ) {
return;
}
$entry_obj->purge_spam();
$this->log( 'Purge spam completed.' );
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace WPForms\Tasks\Actions;
use WPForms\Integrations\Square\Api\Api;
use WPForms\Integrations\Square\Connection;
use WPForms\Tasks\Task;
use WPForms\Tasks\Meta;
/**
* Class SquareSubscriptionTransactionIDTask.
*
* @since 1.9.5
*/
class SquareSubscriptionTransactionIDTask extends Task {
/**
* Action name.
*
* @since 1.9.5
*/
private const ACTION = 'wpforms_process_square_subscription_transaction_id';
/**
* Constructor.
*
* @since 1.9.5
*/
public function __construct() {
parent::__construct( self::ACTION );
$this->init();
}
/**
* Initialize.
*
* @since 1.9.5
*/
private function init() {
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.9.5
*/
private function hooks() {
add_action( 'wpforms_process_payment_saved', [ $this, 'add_task' ], 999, 3 );
add_action( self::ACTION, [ $this, 'process' ] );
}
/**
* Add task to the queue.
*
* @since 1.9.5
*
* @param string $payment_id Payment ID.
* @param array $fields Final/sanitized submitted field data.
* @param array $form_data Form data and settings.
*/
public function add_task( $payment_id, array $fields, array $form_data ) {
$payment_obj = wpforms()->obj( 'payment' );
if ( ! $payment_obj ) {
return;
}
$payment = $payment_obj->get( (int) $payment_id );
if ( ! $payment ) {
return;
}
// Bail early if not Square subscription.
if ( $payment->gateway !== 'square' || $payment->type !== 'subscription' ) {
return;
}
// Bail early if transaction_id is already set via webhooks.
if ( ! empty( $payment->transaction_id ) ) {
return;
}
// Add task to the queue.
wpforms()->obj( 'tasks' )
->create( self::ACTION )
->once( time() + MINUTE_IN_SECONDS )
->params( (int) $payment_id )
->register();
}
/**
* Process the task.
*
* @since 1.9.5
*
* @param int $meta_id Meta ID.
*/
public function process( $meta_id ) {
$task_meta = new Meta();
$meta = $task_meta->get( (int) $meta_id );
if ( empty( $meta ) || empty( $meta->data ) ) {
return;
}
[ $payment_id ] = $meta->data;
$payment = wpforms()->obj( 'payment' )->get( (int) $payment_id );
// Bail early if transaction_id is already set via webhooks.
if ( ! empty( $payment->transaction_id ) ) {
return;
}
if ( ! Connection::get() ) {
return;
}
$api = new Api( Connection::get() );
$subscription = $api->retrieve_subscription( $payment->subscription_id );
if ( $subscription === null ) {
return;
}
$invoice = $api->get_latest_subscription_invoice( $subscription );
if ( $invoice === null ) {
return;
}
$transaction_id = $api->get_latest_invoice_transaction_id( $invoice );
// Set transaction_id for the subscription in case it not received earlier.
wpforms()->obj( 'payment' )->update(
$payment_id,
[ 'transaction_id' => $transaction_id ],
'',
'',
[ 'cap' => false ]
);
// Log.
wpforms()->obj( 'payment_meta' )->add_log(
$payment_id,
sprintf(
'Square subscription was created. (Invoice ID: %s)',
$invoice->getId()
)
);
}
}

View File

@@ -0,0 +1,310 @@
<?php
namespace WPForms\Tasks\Actions;
use WPForms\Integrations\Stripe\Api\PaymentIntents;
use WPForms\Tasks\Task;
use WPForms\Integrations\Stripe\Helpers;
/**
* Class StripeLinkSubscriptionsTask.
*
* @since 1.8.7
*/
class StripeLinkSubscriptionsTask extends Task {
/**
* Action name for this task.
*
* @since 1.8.7
*/
const ACTION = 'wpforms_process_stripe_link_subscriptions';
/**
* Status option name.
*
* @since 1.8.7
*/
const STATUS = 'wpforms_process_stripe_link_subscriptions_status';
/**
* Start status.
*
* @since 1.8.7
*/
const START = 'start';
/**
* In progress status.
*
* @since 1.8.7
*/
const IN_PROGRESS = 'in_progress';
/**
* Completed status.
*
* @since 1.8.7
*/
const COMPLETED = 'completed';
/**
* Latest processed payment id.
*
* @since 1.8.7
*/
const LATEST_PROCESSED_OPTION = 'wpforms_stripe_link_subscriptions_latest_processed';
/**
* Stripe PaymentIntents API.
*
* @since 1.8.7
*
* @var PaymentIntents
*/
private $api;
/**
* Log title.
*
* @since 1.9.1
*
* @var string
*/
protected $log_title = 'Migration';
/**
* Class constructor.
*
* @since 1.8.7
*/
public function __construct() {
parent::__construct( self::ACTION );
}
/**
* Initialize the task.
*
* @since 1.8.7
*/
public function init() {
// Get a task status.
$status = get_option( self::STATUS );
// This task is run in \WPForms\Migrations\Upgrade187::run(),
// and started in \WPForms\Migrations\UpgradeBase::run_async().
// Bail out if a task is not started or completed.
if ( ! $status || $status === self::COMPLETED ) {
return;
}
// Mark that the task is in progress.
if ( $status === self::START ) {
update_option( self::STATUS, self::IN_PROGRESS );
}
// Register hooks.
$this->hooks();
$tasks = wpforms()->obj( 'tasks' );
// Add new if none exists.
if ( $tasks->is_scheduled( self::ACTION ) !== false ) {
return;
}
// Add a new task if none exists.
$tasks->create( self::ACTION )
->async()
->register();
}
/**
* Register hooks.
*
* @since 1.8.7
*/
private function hooks() {
// Register the migrate action.
add_action( self::ACTION, [ $this, 'run' ] );
}
/**
* Run a process task.
*
* @since 1.8.7
*/
public function run() {
// Bail if no Stripe account is connected.
if ( ! Helpers::has_stripe_keys() ) {
$this->complete();
return;
}
$link_subscriptions = $this->get_link_subscriptions();
// Bail if all subscription were processed.
if ( empty( $link_subscriptions ) ) {
$this->complete();
return;
}
$this->api = new PaymentIntents();
$this->process( $link_subscriptions );
}
/**
* Process subscriptions.
*
* @since 1.8.7
*
* @param array $subscriptions Array of subscriptions.
*/
private function process( array $subscriptions ) {
foreach ( $subscriptions as $subscription ) {
$this->update_latest_processed( $subscription->id );
// Use subscription mode to cover all cases (e.g. mode might be switched to test while upgrading).
$payment = $this->api->retrieve_payment_intent( $subscription->transaction_id, [ 'mode' => $subscription->mode ] );
// Bail if original payment was unsuccessful.
if ( is_null( $payment ) || empty( $payment->status ) || $payment->status !== 'succeeded' ) {
continue;
}
$setup_intent_data = $this->prepare_setup_intent_data( $payment, $subscription );
// Bail if subscription has already had correct mandate.
if ( ! $setup_intent_data ) {
continue;
}
$intent = $this->api->create_setup_intent( $setup_intent_data, [ 'mode' => $subscription->mode ] );
// Log failed subscription payment id.
if ( empty( $intent ) ) {
$this->log( 'Stripe Link Subscriptions: Failed ' . $subscription->id );
}
}
}
/**
* Update latest processed id.
*
* @since 1.8.7
*
* @param int $id Subscription ID.
*/
private function update_latest_processed( int $id ) {
update_option( self::LATEST_PROCESSED_OPTION, $id );
}
/**
* Get all Stripe subscriptions charged through Link.
*
* @since 1.8.7
*
* @return array
*/
private function get_link_subscriptions(): array {
global $wpdb;
$latest_payment = (int) get_option( self::LATEST_PROCESSED_OPTION, 0 );
$payments_table = wpforms()->obj( 'payment' )->table_name;
$paymentmeta_table = wpforms()->obj( 'payment_meta' )->table_name;
$query[] = "SELECT p.* FROM {$payments_table} as p";
$query[] = "INNER JOIN {$paymentmeta_table} as pm ON p.id = pm.payment_id";
$query[] = "WHERE p.id > %d AND p.gateway = 'stripe' AND p.type = 'subscription' AND pm.meta_key = 'method_type' AND pm.meta_value = 'link'";
// Stripe API allows up to 100 read operations per second and 100 write operations per second in live mode,
// and 25 operations per second for each in test mode.
$query[] = 'ORDER BY p.id LIMIT 20';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
return $wpdb->get_results( $wpdb->prepare( implode( ' ', $query ), $latest_payment ), OBJECT_K );
}
/**
* Prepare Setup Intent data.
*
* @since 1.8.7
*
* @param object $payment Stripe payment object.
* @param object $subscription Subscription object.
*
* @return array
*/
private function prepare_setup_intent_data( $payment, $subscription ): array {
if ( ! empty( $payment->mandate ) ) {
$mandate = $this->api->retrieve_mandate( $payment->mandate, [ 'mode' => $subscription->mode ] );
}
$data = [
'payment_method_types' => [ 'link' ],
'customer' => $payment->customer,
'payment_method' => $payment->payment_method,
'usage' => 'off_session',
'confirm' => true,
];
// Prepare default data in case mandate is not available.
if ( empty( $mandate ) ) {
$subscription_meta = wpforms()->obj( 'payment_meta' )->get_all( $subscription->id );
$data['mandate_data'] = [
'customer_acceptance' => [
'type' => 'online',
'online' => [
'ip_address' => $subscription_meta['ip_address']->value,
'user_agent' => $subscription_meta['user_agent']->value,
],
],
];
return $data;
}
// Mandate is correct so no actions needed.
if ( $mandate->type !== 'single_use' ) {
return [];
}
$data['mandate_data'] = [
'customer_acceptance' => [
'type' => 'online',
'online' => [
'ip_address' => $mandate->customer_acceptance->online->ip_address,
'user_agent' => $mandate->customer_acceptance->online->user_agent,
],
],
];
return $data;
}
/**
* Mark that the task is completed.
*
* @since 1.8.7
*/
public function complete() {
$this->log( 'Stripe Link Subscriptions: Completed' );
update_option( self::STATUS, self::COMPLETED );
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace WPForms\Tasks\Actions;
use WPForms\Tasks\Task;
use WPForms\Integrations\Stripe\Api\WebhooksManager;
use WPForms\Integrations\Stripe\Helpers;
/**
* Class WebhooksAutoConfigurationTask.
*
* @since 1.8.4
*/
class WebhooksAutoConfigurationTask extends Task {
/**
* Action name.
*
* @since 1.8.4
*/
const ACTION = 'wpforms_process_webhooks_auto_configuration';
/**
* Status option name.
*
* @since 1.8.4
*/
const STATUS = 'wpforms_process_webhooks_auto_configuration_status';
/**
* Start status.
*
* @since 1.8.4
*/
const START = 'start';
/**
* In progress status.
*
* @since 1.8.4
*/
const IN_PROGRESS = 'in_progress';
/**
* Completed status.
*
* @since 1.8.4
*/
const COMPLETED = 'completed';
/**
* Webhooks manager.
*
* @since 1.8.4
*
* @var WebhooksManager
*/
private $webhooks_manager;
/**
* Log title.
*
* @since 1.9.1
*
* @var string
*/
protected $log_title = 'Migration';
/**
* Constructor.
*
* @since 1.8.4
*/
public function __construct() {
parent::__construct( self::ACTION );
$this->webhooks_manager = new WebhooksManager();
}
/**
* Process the task.
*
* @since 1.8.4
*/
public function init() {
// Get a task status.
$status = get_option( self::STATUS );
// This task is run in \WPForms\Migrations\Upgrade184::run(),
// and started in \WPForms\Migrations\UpgradeBase::run_async().
// Bail out if a task is not started or completed.
if ( ! $status || $status === self::COMPLETED ) {
return;
}
// Mark that the task is in progress.
update_option( self::STATUS, self::IN_PROGRESS );
// Register hooks.
$this->hooks();
$tasks = wpforms()->obj( 'tasks' );
// Add new if none exists.
if ( $tasks->is_scheduled( self::ACTION ) !== false ) {
return;
}
$tasks->create( self::ACTION )->async()->register();
}
/**
* Register hooks.
*
* @since 1.8.4
*/
private function hooks() {
add_action( self::ACTION, [ $this, 'process' ] );
}
/**
* Process the task.
*
* @since 1.8.4
*/
public function process() {
// If the Stripe account is connected, then try to configure webhooks.
if ( Helpers::has_stripe_keys() && $this->webhooks_manager->connect() ) {
$this->log( 'Stripe Payments: Webhooks configured during migration to WPForms 1.8.4.' );
}
// Mark that the task is completed.
update_option( self::STATUS, self::COMPLETED );
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace WPForms\Tasks;
use WPForms_DB;
/**
* Class Meta helps to manage the tasks meta information
* between Action Scheduler and WPForms hooks arguments.
* We can't pass arguments longer than >191 chars in JSON to AS,
* so we need to store them somewhere (and clean from time to time).
*
* @since 1.5.9
*/
class Meta extends WPForms_DB {
/**
* Primary key (unique field) for the database table.
*
* @since 1.5.9
*
* @var string
*/
public $primary_key = 'id';
/**
* Database type identifier.
*
* @since 1.5.9
*
* @var string
*/
public $type = 'tasks_meta';
/**
* Primary class constructor.
*
* @since 1.5.9
*/
public function __construct() {
parent::__construct();
$this->table_name = self::get_table_name();
}
/**
* Get the DB table name.
*
* @since 1.5.9
*
* @return string
*/
public static function get_table_name() {
global $wpdb;
return $wpdb->prefix . 'wpforms_tasks_meta';
}
/**
* Get table columns.
*
* @since 1.5.9
*/
public function get_columns() {
return [
'id' => '%d',
'action' => '%s',
'data' => '%s',
'date' => '%s',
];
}
/**
* Default column values.
*
* @since 1.5.9
*
* @return array
*/
public function get_column_defaults() {
return [
'action' => '',
'data' => '',
'date' => gmdate( 'Y-m-d H:i:s' ),
];
}
/**
* Create custom entry meta database table.
* Used in migration and on plugin activation.
*
* @since 1.5.9
*
* @noinspection UnusedFunctionResultInspection
*/
public function create_table() {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $this->table_name (
id bigint(20) NOT NULL AUTO_INCREMENT,
action varchar(255) NOT NULL,
data longtext NOT NULL,
date datetime NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
dbDelta( $sql );
}
/**
* Remove queue records for a defined period of time in the past.
* Calling this method will remove queue records that are older than $period seconds.
*
* @since 1.5.9
*
* @param string $action Action that should be cleaned up.
* @param int $interval Number of seconds from now.
*
* @return int Number of removed tasks meta records.
*/
public function clean_by( $action, $interval ) {
global $wpdb;
if ( empty( $action ) || empty( $interval ) ) {
return 0;
}
$table = self::get_table_name();
$action = sanitize_key( $action );
$date = gmdate( 'Y-m-d H:i:s', time() - (int) $interval );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
return (int) $wpdb->query(
$wpdb->prepare(
"DELETE FROM $table WHERE action = %s AND date < %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$action,
$date
)
);
}
/**
* Inserts a new record into the database.
*
* @since 1.5.9
*
* @param array $data Column data.
* @param string $type Optional. Data type context.
*
* @return int ID for the newly inserted record. Zero otherwise.
*/
public function add( $data, $type = '' ) {
if ( empty( $data['action'] ) || ! is_string( $data['action'] ) ) {
return 0;
}
$data['action'] = sanitize_key( $data['action'] );
if ( isset( $data['data'] ) ) {
$data['data'] = $this->prepare_data( $data['data'] );
}
if ( empty( $type ) ) {
$type = $this->type;
}
return parent::add( $data, $type );
}
/**
* Prepare data.
*
* @since 1.7.0
*
* @param array $data Meta data.
*
* @return string
*/
private function prepare_data( $data ) {
$string = wp_json_encode( $data );
if ( $string === false ) {
$string = '';
}
/*
* We are encoding the string representation of all the data to make sure that nothing can harm the database.
* This is not an encryption, and we need this data later "as is",
* so we are using one of the fastest ways to do that.
* This data is removed from DB daily.
*/
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
return base64_encode( $string );
}
/**
* Retrieve a row from the database based on a given row ID.
*
* @since 1.5.9
*
* @param int $meta_id Meta ID.
*
* @return null|object
* @noinspection PhpParameterNameChangedDuringInheritanceInspection
*/
public function get( $meta_id ) {
$meta = parent::get( $meta_id );
if ( empty( $meta ) || empty( $meta->data ) ) {
return $meta;
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$decoded = base64_decode( $meta->data );
if ( $decoded === false || ! is_string( $decoded ) ) {
$meta->data = '';
} else {
$meta->data = json_decode( $decoded, true );
}
return $meta;
}
/**
* Get meta ID by action name and params.
*
* @since 1.7.0
*
* @param string $action Action name.
* @param array $params Action params.
*
* @return int
*/
public function get_meta_id( $action, $params ) {
global $wpdb;
$table = self::get_table_name();
$action = sanitize_key( $action );
$data = $this->prepare_data( array_values( $params ) );
return absint(
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->get_var(
$wpdb->prepare(
"SELECT id FROM $table WHERE action = %s AND data = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$action,
$data
)
)
);
}
}

View File

@@ -0,0 +1,377 @@
<?php
namespace WPForms\Tasks;
use InvalidArgumentException;
use UnexpectedValueException;
/**
* Class Task.
*
* @since 1.5.9
*/
class Task {
/**
* This task is async (runs asap).
*
* @since 1.5.9
*/
const TYPE_ASYNC = 'async';
/**
* This task is a recurring.
*
* @since 1.5.9
*/
const TYPE_RECURRING = 'scheduled';
/**
* This task is run once.
*
* @since 1.5.9
*/
const TYPE_ONCE = 'once';
/**
* Type of the task.
*
* @since 1.5.9
*
* @var string
*/
private $type;
/**
* Action that will be used as a hook.
*
* @since 1.5.9
*
* @var string
*/
private $action;
/**
* Task meta ID.
*
* @since 1.5.9
*
* @var int
*/
private $meta_id;
/**
* All the params that should be passed to the hook.
*
* @since 1.5.9
*
* @var array
*/
private $params;
/**
* When the first instance of the job will run.
* Used for ONCE ane RECURRING tasks.
*
* @since 1.5.9
*
* @var int
*/
private $timestamp;
/**
* How long to wait between runs.
* Used for RECURRING tasks.
*
* @since 1.5.9
*
* @var int
*/
private $interval;
/**
* Task meta.
*
* @since 1.7.0
*
* @var Meta
*/
private $meta;
/**
* Log title.
*
* @since 1.9.1
*
* @var string
*/
protected $log_title = 'Task';
/**
* Task constructor.
*
* @since 1.5.9
*
* @param string $action Action of the task.
*
* @throws InvalidArgumentException When action is not a string.
* @throws UnexpectedValueException When action is empty.
*/
public function __construct( $action ) {
if ( ! is_string( $action ) ) {
throw new InvalidArgumentException( 'Task action should be a string.' );
}
$this->action = sanitize_key( $action );
$this->meta = new Meta();
if ( empty( $this->action ) ) {
throw new UnexpectedValueException( 'Task action cannot be empty.' );
}
}
/**
* Define the type of the task as async.
*
* @since 1.5.9
*
* @return Task
*/
public function async() {
$this->type = self::TYPE_ASYNC;
return $this;
}
/**
* Define the type of the task as recurring.
*
* @since 1.5.9
*
* @param int $timestamp When the first instance of the job will run.
* @param int $interval How long to wait between runs.
*
* @return Task
*/
public function recurring( $timestamp, $interval ) {
$this->type = self::TYPE_RECURRING;
$this->timestamp = (int) $timestamp;
$this->interval = (int) $interval;
return $this;
}
/**
* Define the type of the task as one-time.
*
* @since 1.5.9
*
* @param int $timestamp When the first instance of the job will run.
*
* @return Task
*/
public function once( $timestamp ) {
$this->type = self::TYPE_ONCE;
$this->timestamp = (int) $timestamp;
return $this;
}
/**
* Pass any number of params that should be saved to Meta table.
*
* @since 1.5.9
*
* @return Task
*/
public function params() {
$this->params = func_get_args();
return $this;
}
/**
* Register the action.
* Should be the final call in a chain.
*
* @since 1.5.9
*
* @return null|string Action ID.
*/
public function register() {
$action_id = null;
// No processing if ActionScheduler is not usable.
if ( ! wpforms()->obj( 'tasks' )->is_usable() ) {
return $action_id;
}
// Save data to tasks meta table.
if ( $this->params !== null ) {
$this->meta_id = $this->meta->add(
[
'action' => $this->action,
'data' => $this->params,
]
);
if ( empty( $this->meta_id ) ) {
return $action_id;
}
}
// Prevent 500 errors when Action Scheduler tables don't exist.
try {
switch ( $this->type ) {
case self::TYPE_ASYNC:
$action_id = $this->register_async();
break;
case self::TYPE_RECURRING:
$action_id = $this->register_recurring();
break;
case self::TYPE_ONCE:
$action_id = $this->register_once();
break;
}
} catch ( \RuntimeException $exception ) {
$action_id = null;
}
return $action_id;
}
/**
* Register the async task.
*
* @since 1.5.9
*
* @return null|string Action ID.
* @noinspection PhpUndefinedFunctionInspection
*/
protected function register_async() {
if ( ! function_exists( 'as_enqueue_async_action' ) ) {
return null;
}
return as_enqueue_async_action(
$this->action,
/**
* Filter arguments passed to the async task.
*
* @since 1.9.4
*
* @param array $args Arguments passed to the async task.
*
* @return array
*/
(array) apply_filters(
'wpforms_tasks_task_register_async_args',
[
'tasks_meta_id' => $this->meta_id,
]
),
Tasks::GROUP
);
}
/**
* Register the recurring task.
*
* @since 1.5.9
*
* @return null|string Action ID.
* @noinspection PhpUndefinedFunctionInspection
*/
protected function register_recurring() {
if ( ! function_exists( 'as_schedule_recurring_action' ) ) {
return null;
}
return as_schedule_recurring_action(
$this->timestamp,
$this->interval,
$this->action,
[ 'tasks_meta_id' => $this->meta_id ],
Tasks::GROUP
);
}
/**
* Register the one-time task.
*
* @since 1.5.9
*
* @return null|string Action ID.
* @noinspection PhpUndefinedFunctionInspection
*/
protected function register_once() {
if ( ! function_exists( 'as_schedule_single_action' ) ) {
return null;
}
return as_schedule_single_action(
$this->timestamp,
$this->action,
[ 'tasks_meta_id' => $this->meta_id ],
Tasks::GROUP
);
}
/**
* Cancel all occurrences of this task.
*
* @since 1.6.1
*
* @return null|bool|string Null if no matching action found,
* false if AS library is missing,
* true if scheduled task has no params,
* string of the scheduled action ID if a scheduled action was found and unscheduled.
* @noinspection PhpUndefinedFunctionInspection
*/
public function cancel() {
if ( ! function_exists( 'as_unschedule_all_actions' ) ) {
return false;
}
if ( $this->params === null ) {
as_unschedule_all_actions( $this->action );
return true;
}
$this->meta_id = $this->meta->get_meta_id( $this->action, $this->params );
if ( $this->meta_id === null ) {
return null;
}
return as_unschedule_action( $this->action, [ 'tasks_meta_id' => $this->meta_id ], Tasks::GROUP );
}
/**
* Log message to WPForms logger and standard debug.log file.
*
* @since 1.9.1
*
* @param string $message The error message that should be logged.
*/
protected function log( $message ) {
wpforms_log( $this->log_title, $message, [ 'type' => 'log' ] );
}
}

View File

@@ -0,0 +1,447 @@
<?php
// phpcs:ignore Generic.Commenting.DocComment.MissingShort
/** @noinspection PhpUndefinedClassInspection */
namespace WPForms\Tasks;
use ActionScheduler;
use ActionScheduler_Action;
use ActionScheduler_DataController;
use ActionScheduler_DBStore;
use WPForms\Helpers\Transient;
use WPForms\Tasks\Actions\EntryEmailsMetaCleanupTask;
use WPForms\Tasks\Actions\EntryEmailsTask;
use WPForms\Tasks\Actions\FormsLocatorScanTask;
use WPForms\Tasks\Actions\AsyncRequestTask;
use WPForms\Tasks\Actions\PurgeSpamTask;
/**
* Class Tasks manages the tasks queue and provides API to work with it.
*
* @since 1.5.9
*/
class Tasks {
/**
* Group that will be assigned to all actions.
*
* @since 1.5.9
*/
const GROUP = 'wpforms';
/**
* Actions setting name.
*
* @since 1.7.3
*/
const ACTIONS = 'actions';
/**
* WPForms pending or in-progress actions.
*
* @since 1.7.3
*
* @var array
*/
private $active_actions;
/**
* Determine if WPForms task is executing.
*
* @since 1.9.4
*
* @var bool
*/
private static $task_executing = false;
/**
* Perform certain things on class init.
*
* @since 1.5.9
*/
public function init() {
// Get WPForms pending or in-progress actions.
$this->active_actions = $this->get_active_actions();
// Register WPForms tasks.
foreach ( $this->get_tasks() as $task ) {
if ( ! is_subclass_of( $task, Task::class ) ) {
continue;
}
new $task();
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.7.5
*/
public function hooks() {
add_action( 'delete_expired_transients', [ Transient::class, 'delete_all_expired' ], 11 );
add_action( 'admin_menu', [ $this, 'admin_hide_as_menu' ], PHP_INT_MAX );
/*
* By default we send emails in the same process as the form submission is done.
* That means that when many emails are set in form Notifications -
* the form submission can take a while because of all those emails that are sending in the background.
* Since WPForms 1.6.0 users can enable a new option in Settings > Emails,
* called "Optimize Email Sending", to send email in async way.
* This feature was enabled for WPForms 1.5.9, but some users were not happy.
*/
if ( ! (bool) wpforms_setting( 'email-async', false ) ) {
add_filter( 'wpforms_tasks_entry_emails_trigger_send_same_process', '__return_true' );
}
add_action( EntryEmailsTask::ACTION, [ EntryEmailsTask::class, 'process' ] );
add_action( 'action_scheduler_after_execute', [ $this, 'clear_action_meta' ], PHP_INT_MAX, 2 );
add_action( 'action_scheduler_begin_execute', [ $this, 'start_executing' ], 1 );
add_action( 'action_scheduler_after_execute', [ $this, 'stop_executing' ], 1, 2 );
}
/**
* Public interface to check if WPForms task is executing.
*
* @since 1.9.4
*
* @return bool
*/
public static function is_executing(): bool {
return self::$task_executing;
}
/**
* Set a flag to indicate that WPForms task is executing.
*
* @since 1.9.4
*
* @param int $action_id The action ID to process.
*/
public function start_executing( $action_id ) {
$action_id = (int) $action_id;
if ( ! class_exists( 'ActionScheduler' ) ) {
return;
}
$store = ActionScheduler::store();
if ( ! $store ) {
return;
}
$action = $store->fetch_action( $action_id );
if ( ! $action || $action->get_group() !== self::GROUP ) {
return;
}
self::$task_executing = true;
/**
* Fires before WPForms task is executing.
*
* @since 1.9.4
*
* @param int $action_id The action ID to process.
* @param ActionScheduler_Action $action Action Scheduler action object.
*/
do_action( 'wpforms_tasks_start_executing', $action_id, $action );
}
/**
* Set a flag to indicate that WPForms task is executing.
*
* @since 1.9.4
*
* @param int $action_id The action ID to process.
* @param ActionScheduler_Action $action Action Scheduler action object.
*/
public function stop_executing( $action_id, $action ) {
if ( ! $action || ! method_exists( $action, 'get_group' ) || $action->get_group() !== self::GROUP ) {
return;
}
self::$task_executing = false;
/**
* Fires after WPForms task is executed.
*
* @since 1.9.4
*
* @param int $action_id The action ID to process.
* @param ActionScheduler_Action $action Action Scheduler action object.
*/
do_action( 'wpforms_tasks_stop_executing', $action_id, $action );
}
/**
* Get the list of WPForms default scheduled tasks.
* Tasks, that are fired under certain specific circumstances
* (like sending form submission email notifications)
* are not listed here.
*
* @since 1.5.9
*
* @return Task[] List of tasks classes.
*/
public function get_tasks() {
if ( ! $this->is_usable() ) {
return [];
}
$tasks = [
EntryEmailsMetaCleanupTask::class,
FormsLocatorScanTask::class,
AsyncRequestTask::class,
PurgeSpamTask::class,
];
/**
* Filters the task class list to initialize.
*
* @since 1.5.9
*
* @param array $tasks Task class list.
*/
return apply_filters( 'wpforms_tasks_get_tasks', $tasks );
}
/**
* Hide Action Scheduler admin area when not in debug mode.
*
* @since 1.5.9
* @since 1.9.4 Does not hide the menu when some popular plugins are active.
*/
public function admin_hide_as_menu(): void {
$plugin_exceptions = [
'action-scheduler/action-scheduler.php',
'woocommerce/woocommerce.php',
'wp-rocket/wp-rocket.php',
];
/**
* Filters the list of plugins for which
* the Action Scheduler Tools -> Scheduled Actions menu item
* should remain visible.
*
* @since 1.9.4
*
* @param array $plugin_exceptions List of plugin exceptions.
*/
$plugin_exceptions = apply_filters( 'wpforms_tasks_action_scheduler_tools_plugin_exceptions', $plugin_exceptions );
$show_as_menu =
( defined( 'WPFORMS_SHOW_ACTION_SCHEDULER_MENU' ) && constant( 'WPFORMS_SHOW_ACTION_SCHEDULER_MENU' ) ) ||
wpforms_debug() ||
! empty( array_filter( $plugin_exceptions, 'is_plugin_active' ) );
$hide_as_menu = ! $show_as_menu;
/**
* Filter to redefine that WPForms hides Tools > Action Scheduler menu item.
*
* @since 1.5.9
*
* @param bool $hide_as_menu Hide Tools > Action Scheduler menu item.
*/
if ( apply_filters( 'wpforms_tasks_admin_hide_as_menu', $hide_as_menu ) ) {
remove_submenu_page( 'tools.php', 'action-scheduler' );
}
}
/**
* Create a new task.
* Used for "inline" tasks, that require additional information
* from the plugin runtime before they can be scheduled.
*
* Example:
* wpforms()->obj( 'tasks' )
* ->create( 'i_am_the_dude' )
* ->async()
* ->params( 'The Big Lebowski', 1998 )
* ->register();
*
* This `i_am_the_dude` action will be later processed as:
* add_action( 'i_am_the_dude', 'thats_what_you_call_me' );
*
* Function `thats_what_you_call_me()` will receive `$meta_id` param,
* and you will be able to receive all params from the action like this:
* $params = ( new Meta() )->get( (int) $meta_id );
* list( $name, $year ) = $params->data;
*
* @since 1.5.9
*
* @param string $action Action that will be used as a hook.
*
* @return Task
*/
public function create( $action ) {
return new Task( $action );
}
/**
* Cancel all the AS actions for a group.
*
* @since 1.5.9
*
* @param string $group Group to cancel all actions for.
*/
public function cancel_all( $group = '' ) {
if ( empty( $group ) ) {
$group = self::GROUP;
} else {
$group = sanitize_key( $group );
}
if ( class_exists( 'ActionScheduler_DBStore' ) ) {
ActionScheduler_DBStore::instance()->cancel_actions_by_group( $group );
$this->active_actions = $this->get_active_actions();
}
}
/**
* Whether ActionScheduler thinks that it has migrated or not.
*
* @since 1.5.9.3
*
* @return bool
*/
public function is_usable() {
// No tasks if ActionScheduler wasn't loaded.
if ( ! class_exists( 'ActionScheduler_DataController' ) ) {
return false;
}
return ActionScheduler_DataController::is_migration_complete();
}
/**
* Whether task has been scheduled and is pending or in-progress.
*
* @since 1.6.0
*
* @param string $hook Hook to check for.
*
* @return bool|null
* @noinspection PhpUndefinedFunctionInspection
*/
public function is_scheduled( $hook ) {
if ( ! function_exists( 'as_has_scheduled_action' ) ) {
return null;
}
if ( in_array( $hook, $this->active_actions, true ) ) {
return true;
}
// Action is not in the array, so it is not scheduled or belongs to another group.
return as_has_scheduled_action( $hook );
}
/**
* Get all WPForms pending or in-progress actions.
*
* @since 1.7.3
*/
private function get_active_actions() {
global $wpdb;
$group = self::GROUP;
$sql = "SELECT a.hook FROM {$wpdb->prefix}actionscheduler_actions a
JOIN {$wpdb->prefix}actionscheduler_groups g ON g.group_id = a.group_id
WHERE g.slug = '$group' AND a.status IN ( 'in-progress', 'pending' )";
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
$results = $wpdb->get_results( $sql, 'ARRAY_N' );
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
return $results ? array_merge( ...$results ) : [];
}
/**
* Delete a task by its ID.
*
* @since 1.9.6.1
*
* @param int $action_id Action ID.
*/
public function delete_action( $action_id ): void {
global $wpdb;
$sql = "DELETE FROM {$wpdb->prefix}actionscheduler_actions WHERE action_id = %d";
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( $wpdb->prepare( $sql, (int) $action_id ) );
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Fetch action by ID.
*
* @since 1.9.6.1
*
* @param int $action_id Action ID.
*
* @return null|ActionScheduler_Action
*/
public function fetch_action( $action_id ): ?ActionScheduler_Action {
if ( ! class_exists( 'ActionScheduler' ) ) {
return null;
}
return ActionScheduler::store()->fetch_action( $action_id );
}
/**
* Clear the meta after action complete.
* Fired before an action is marked as completed.
*
* @since 1.7.5
*
* @param integer $action_id Action ID.
* @param ActionScheduler_Action $action Action name.
*/
public function clear_action_meta( $action_id, $action ) {
$action_schedule = $action->get_schedule();
if ( $action_schedule === null || $action_schedule->is_recurring() ) {
return;
}
$hook_name = $action->get_hook();
if ( ! $this->is_scheduled( $hook_name ) ) {
return;
}
$hook_args = $action->get_args();
if ( ! isset( $hook_args['tasks_meta_id'] ) ) {
return;
}
$meta = new Meta();
$meta->delete( $hook_args['tasks_meta_id'] );
}
}