Tasks.php000066600000010241152142116110006336 0ustar00get_tasks() as $task ) { if ( ! is_subclass_of( $task, Task::class ) ) { continue; } new $task(); } 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' ] ); } /** * 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 = [ Actions\EntryEmailsMetaCleanupTask::class, ]; return apply_filters( 'wpforms_tasks_get_tasks', $tasks ); } /** * Hide Action Scheduler admin area when not in debug mode. * * @since 1.5.9 */ public function admin_hide_as_menu() { // Filter to redefine that WPForms hides Tools > Action Scheduler menu item. if ( apply_filters( 'wpforms_tasks_admin_hide_as_menu', ! wpforms_debug() ) ) { 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()->get( '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 ) = $meta->data; * * @since 1.5.9 * * @param string $action Action that will be used as a hook. * * @return \WPForms\Tasks\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 ); } } /** * 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. * * @since 1.6.0 * * @param string $hook Hook to check for. * * @return bool */ public function is_scheduled( $hook ) { if ( ! function_exists( 'as_next_scheduled_action' ) ) { return false; } return as_next_scheduled_action( $hook ); } } Task.php000066600000011234152142116110006156 0ustar00action = sanitize_key( $action ); 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 \WPForms\Tasks\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 \WPForms\Tasks\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 \WPForms\Tasks\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 \WPForms\Tasks\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()->get( 'tasks' )->is_usable() ) { return $action_id; } // Save data to tasks meta table. $task_meta = new Meta(); $this->meta_id = $task_meta->add( [ 'action' => $this->action, 'data' => $this->params, ] ); if ( empty( $this->meta_id ) ) { return $action_id; } 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; } return $action_id; } /** * Register the async task. * * @since 1.5.9 * * @return null|string Action ID. */ protected function register_async() { if ( ! function_exists( 'as_enqueue_async_action' ) ) { return null; } return as_enqueue_async_action( $this->action, [ 'tasks_meta_id' => $this->meta_id ], Tasks::GROUP ); } /** * Register the recurring task. * * @since 1.5.9 * * @return null|string Action ID. */ 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. */ 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 ); } } Actions/EntryEmailsMetaCleanupTask.php000066600000002535152142116110014056 0ustar00init(); } /** * Initialize the task with all the proper checks. * * @since 1.5.9 */ public function init() { // Register the action handler. add_action( self::ACTION, [ $this, 'process' ] ); if ( ! function_exists( 'as_next_scheduled_action' ) ) { return; } // Add new if none exists. if ( as_next_scheduled_action( self::ACTION ) !== false ) { return; } $interval = (int) apply_filters( 'wpforms_tasks_entry_emails_meta_cleanup_interval', DAY_IN_SECONDS ); $this->recurring( strtotime( 'tomorrow' ), $interval ) ->params( $interval ) ->register(); } /** * Perform the cleanup action: remove outdated meta for entry emails task. * * @since 1.5.9 * * @param int $interval Data older than this interval will be removed. */ public function process( $interval ) { ( new Meta() )->clean_by( EntryEmailsTask::ACTION, (int) $interval ); } } Actions/EntryEmailsTask.php000066600000002532152142116110011734 0ustar00async(); } /** * 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 ); } } Meta.php000066600000011060152142116110006137 0ustar00191 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() { $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 array( 'id' => '%d', 'action' => '%s', 'data' => '%s', 'date' => '%s', ); } /** * Default column values. * * @since 1.5.9 * * @return array */ public function get_column_defaults() { return array( '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 */ public function create_table() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $charset_collate = ''; if ( ! empty( $wpdb->charset ) ) { $charset_collate .= "DEFAULT CHARACTER SET {$wpdb->charset}"; } if ( ! empty( $wpdb->collate ) ) { $charset_collate .= " COLLATE {$wpdb->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.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. 0 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'] ) ) { $string = wp_json_encode( $data['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 way to do that. * This data is removed from DB on a daily basis. */ // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $data['data'] = base64_encode( $string ); } if ( empty( $type ) ) { $type = $this->type; } return parent::add( $data, $type ); } /** * 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 */ 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; } }