PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.3
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
← All changes | includes/utils/class-wpfnl-activator.php +288 -13 3.12.12 → 3.13.3 View file →
@@ -32,8 +32,37 @@
32 32 * @since 3.12.13
33 33 */
34 34 const BUILDER_MODE_PINNED_OPTION = 'wpfunnels_builder_mode_pinned';
35 35
36 + /**
37 + * Flags that a fresh install still owes the revenue report its schedule.
38 + *
39 + * Set while seeding on activation and cleared once Action Scheduler is
40 + * loaded and the recurring action is queued.
41 + *
42 + * @since 3.14.0
43 + */
44 + const REVENUE_REPORT_PENDING_OPTION = 'wpfunnels_revenue_report_pending_schedule';
45 +
46 + /**
47 + * Seconds to wait before re-queuing a DB update callback that reported
48 + * more work to do. Throttles a misbehaving callback instead of letting it
49 + * saturate the ActionScheduler queue runner.
50 + *
51 + * @since 3.12.13
52 + */
53 + const REQUEUE_DELAY = MINUTE_IN_SECONDS;
54 +
55 + /**
56 + * One-time cleanup of the ActionScheduler rows left behind by the runaway
57 + * update-callback loop that shipped in 3.12.12.
58 + *
59 + * @since 3.12.13
60 + */
61 + const RUNAWAY_PURGE_HOOK = 'wpfunnels_purge_runaway_update_actions';
62 + const RUNAWAY_PURGE_OPTION = 'wpfunnels_runaway_update_actions_purged';
63 + const RUNAWAY_PURGE_BATCH = 5000;
64 +
36 65 private static $db_updates = array(
37 66 '3.5.0' => array(
38 67 'wpf_create_350_stat_table',
39 68 'wpf_create_350_optin_entries_table'
@@ -55,8 +84,11 @@
55 84 public static function init() {
56 85 add_action( 'init', array( __CLASS__, 'maybe_pin_legacy_builder_mode' ), 1 );
57 86 add_action( 'init', array( __CLASS__, 'update' ), 20 );
58 87 add_action('wpfunnels_run_update_callback', array( __CLASS__, 'run_update_callback' ) );
88 + add_action( 'init', array( __CLASS__, 'maybe_schedule_runaway_purge' ), 21 );
89 + add_action( 'init', array( __CLASS__, 'maybe_schedule_seeded_revenue_report' ), 21 );
90 + add_action( self::RUNAWAY_PURGE_HOOK, array( __CLASS__, 'purge_runaway_update_actions' ) );
59 91 }
60 92
61 93 /**
62 94 * Push all db updates to ActionScheduler
@@ -131,29 +163,205 @@
131 163
132 164 /**
133 165 * Triggered when a callback has ran.
134 166 *
135 - * @param $callback
136 - * @param $result
167 + * A truthy $result means the callback processed one batch and has more work
168 + * left, so it gets queued again; a falsy $result means it finished. Callbacks
169 + * that do all their work in one pass MUST return false — returning true from a
170 + * single-pass callback is what produced the runaway queue fixed in 3.12.13.
171 + *
172 + * Two guards keep a callback that gets this wrong from flooding the queue:
173 + * the re-queue is skipped when an identical action is already pending, and it
174 + * is spaced out by REQUEUE_DELAY rather than being due the instant it lands.
175 + * A genuine batched migration loses nothing from the delay; a buggy one is
176 + * capped at one row per interval instead of saturating the queue runner.
177 + *
178 + * @param string $callback Callback name.
179 + * @param bool $result Whether the callback has more work queued up.
137 180 */
138 181 protected static function run_update_callback_end( $callback, $result ) {
139 - if ( $result ) {
140 - as_schedule_single_action(
141 - time(),
142 - 'wpfunnels_run_update_callback',
143 - array(
144 - 'update_callback' => $callback,
145 - ),
146 - 'wpfunnels-db-updates'
147 - );
148 - } else {
149 - update_option('wpfunnels_'.$callback.'_update', 'completed');
182 + if ( ! $result ) {
183 + update_option( 'wpfunnels_' . $callback . '_update', 'completed' );
184 + return;
150 185 }
186 +
187 + $args = array( 'update_callback' => $callback );
188 +
189 + if ( as_next_scheduled_action( 'wpfunnels_run_update_callback', $args, 'wpfunnels-db-updates' ) ) {
190 + return;
191 + }
192 +
193 + as_schedule_single_action(
194 + time() + self::REQUEUE_DELAY,
195 + 'wpfunnels_run_update_callback',
196 + $args,
197 + 'wpfunnels-db-updates'
198 + );
151 199 }
152 200
153 201
154 202
155 203 /**
204 + * Queue the one-time purge of runaway update-callback rows.
205 + *
206 + * 3.12.12 shipped a callback that reported "more work to do" on every run,
207 + * so ActionScheduler re-queued it without pause and affected sites ended up
208 + * with millions of rows in actionscheduler_actions. The return-value fix
209 + * stops the loop, but it cannot remove what already accumulated, and the
210 + * volume is far past what the Scheduled Actions screen can delete by hand.
211 + *
212 + * @since 3.12.13
213 + */
214 + public static function maybe_schedule_runaway_purge() {
215 + if ( 'completed' === get_option( self::RUNAWAY_PURGE_OPTION ) ) {
216 + return;
217 + }
218 +
219 + // Front-end requests are the bulk of a site's traffic and there is no
220 + // reason to spend a queue lookup on each one; admin and cron hits are
221 + // frequent enough to get the chain started and to restart it if it
222 + // ever breaks part-way through.
223 + if ( ! is_admin() && ! wp_doing_cron() ) {
224 + return;
225 + }
226 +
227 + if ( ! function_exists( 'as_schedule_single_action' ) || ! function_exists( 'as_next_scheduled_action' ) ) {
228 + return;
229 + }
230 +
231 + // Covers in-progress as well as pending, so a batch that is mid-flight
232 + // does not get a duplicate queued alongside it.
233 + if ( as_next_scheduled_action( self::RUNAWAY_PURGE_HOOK, array(), 'wpfunnels-db-updates' ) ) {
234 + return;
235 + }
236 +
237 + self::queue_purge_batch( time() );
238 + }
239 +
240 +
241 + /**
242 + * Delete one batch of finished update-callback rows, chaining the next
243 + * batch until nothing is left.
244 + *
245 + * Only rows in a terminal state are touched. Pending and in-progress rows
246 + * are left alone so a site part-way through a legitimate migration does not
247 + * lose queued work.
248 + *
249 + * @since 3.12.13
250 + */
251 + public static function purge_runaway_update_actions() {
252 + global $wpdb;
253 +
254 + $actions_table = $wpdb->prefix . 'actionscheduler_actions';
255 +
256 + if ( ! self::table_exists( $actions_table ) ) {
257 + self::finish_runaway_purge();
258 + return;
259 + }
260 +
261 + $ids = $wpdb->get_col( $wpdb->prepare(
262 + "SELECT action_id FROM {$actions_table} WHERE hook = %s AND status IN ( %s, %s, %s ) LIMIT %d",
263 + 'wpfunnels_run_update_callback',
264 + 'complete',
265 + 'canceled',
266 + 'failed',
267 + self::RUNAWAY_PURGE_BATCH
268 + ) ); // phpcs:ignore
269 +
270 + if ( empty( $ids ) ) {
271 + self::finish_runaway_purge();
272 + return;
273 + }
274 +
275 + $ids = array_map( 'absint', $ids );
276 + $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
277 + $logs_table = $wpdb->prefix . 'actionscheduler_logs';
278 +
279 + // Logs first: dropping the actions before their logs would strand the
280 + // log rows with no id left to match them on.
281 + if ( self::table_exists( $logs_table ) ) {
282 + $wpdb->query( $wpdb->prepare( "DELETE FROM {$logs_table} WHERE action_id IN ({$placeholders})", $ids ) ); // phpcs:ignore
283 + }
284 +
285 + $wpdb->query( $wpdb->prepare( "DELETE FROM {$actions_table} WHERE action_id IN ({$placeholders})", $ids ) ); // phpcs:ignore
286 +
287 + // Chain the next batch by hand rather than using a recurring action.
288 + // ActionScheduler re-creates a recurring action after the callback
289 + // returns, from the action object it read before execution, so a
290 + // recurring purge could not switch itself off from in here — it would
291 + // keep queueing a fresh row every interval long after the work ran out.
292 + self::queue_purge_batch( time() + MINUTE_IN_SECONDS );
293 + }
294 +
295 +
296 + /**
297 + * Queue a single purge batch, unless one is already waiting.
298 + *
299 + * The pending check is deliberately narrower than as_next_scheduled_action(),
300 + * which reports true for the in-progress action as well. Called from inside
301 + * a running batch, that would always short-circuit and the chain would stop
302 + * after its first pass.
303 + *
304 + * @param int $timestamp When the batch should run.
305 + *
306 + * @since 3.12.13
307 + */
308 + protected static function queue_purge_batch( $timestamp ) {
309 + if ( ! function_exists( 'as_schedule_single_action' ) || ! function_exists( 'as_get_scheduled_actions' ) ) {
310 + return;
311 + }
312 +
313 + $pending = as_get_scheduled_actions(
314 + array(
315 + 'hook' => self::RUNAWAY_PURGE_HOOK,
316 + 'status' => ActionScheduler_Store::STATUS_PENDING,
317 + 'group' => 'wpfunnels-db-updates',
318 + 'per_page' => 1,
319 + ),
320 + 'ids'
321 + );
322 +
323 + if ( ! empty( $pending ) ) {
324 + return;
325 + }
326 +
327 + as_schedule_single_action( $timestamp, self::RUNAWAY_PURGE_HOOK, array(), 'wpfunnels-db-updates' );
328 + }
329 +
330 +
331 + /**
332 + * Mark the purge done and make sure no further batch is queued.
333 + *
334 + * @since 3.12.13
335 + */
336 + protected static function finish_runaway_purge() {
337 + update_option( self::RUNAWAY_PURGE_OPTION, 'completed' );
338 +
339 + // Nothing should be queued at this point, since a batch only chains a
340 + // successor when it actually deleted something. Swept anyway so a row
341 + // left over from an interrupted run cannot keep the chain alive.
342 + if ( function_exists( 'as_unschedule_all_actions' ) ) {
343 + as_unschedule_all_actions( self::RUNAWAY_PURGE_HOOK, array(), 'wpfunnels-db-updates' );
344 + }
345 + }
346 +
347 +
348 + /**
349 + * Whether a database table exists.
350 + *
351 + * @param string $table Fully prefixed table name.
352 + * @return bool
353 + *
354 + * @since 3.12.13
355 + */
356 + protected static function table_exists( $table ) {
357 + global $wpdb;
358 +
359 + return (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ); // phpcs:ignore
360 + }
361 +
362 +
363 + /**
156 364 * Initiate the activation process
157 365 *
158 366 * @since 1.0.0
159 367 */
@@ -171,8 +379,9 @@
171 379 self::update_installed_time();
172 380
173 381 if ( $is_new_install ) {
174 382 self::seed_default_builder_mode();
383 + self::seed_default_notification_settings();
175 384 }
176 385
177 386 // add funnel type meta
178 387 Wpfnl_functions::add_type_meta();
@@ -234,10 +443,69 @@
234 443
235 444 update_option( self::BUILDER_MODE_PINNED_OPTION, 'yes' );
236 445 }
237 446
447 + /**
448 + * Seed the notification settings for brand-new installs.
449 + *
450 + * The store revenue report is on out of the box from 3.14.0, but only for
451 + * sites created from that version onward: the choice is written into the
452 + * database here rather than flipped in the fallback that
453 + * Wpfnl_functions::get_notification_settings() applies, so an existing site
454 + * — which has either its own saved settings or no row at all — is left
455 + * exactly as it was and never starts mailing after an update.
456 + *
457 + * A site that somehow already holds notification settings is skipped, so
458 + * this can never overwrite a stored preference.
459 + *
460 + * @since 3.14.0
461 + */
462 + private static function seed_default_notification_settings()
463 + {
464 + $stored = get_option( '_wpfunnels_notification_settings', array() );
465 + if ( ! empty( $stored ) ) {
466 + return;
467 + }
238 468
469 + update_option( '_wpfunnels_notification_settings', Wpfnl_functions::get_new_install_notification_settings() );
470 +
471 + // Action Scheduler is not dependable during activation, so the actual
472 + // queueing waits for a normal request on `init`.
473 + update_option( self::REVENUE_REPORT_PENDING_OPTION, 'yes' );
474 + }
475 +
476 +
239 477 /**
478 + * Queue the seeded revenue report once Action Scheduler is available.
479 + *
480 + * Only runs for an install that was just seeded — the flag is set nowhere
481 + * else — so no existing site's schedule is touched. If the admin saved the
482 + * settings before this ran, that save has already queued the action and this
483 + * only clears the flag.
484 + *
485 + * @since 3.14.0
486 + */
487 + public static function maybe_schedule_seeded_revenue_report()
488 + {
489 + if ( 'yes' !== get_option( self::REVENUE_REPORT_PENDING_OPTION ) ) {
490 + return;
491 + }
492 +
493 + if ( ! function_exists( 'as_schedule_recurring_action' ) || ! function_exists( 'as_next_scheduled_action' ) ) {
494 + return;
495 + }
496 +
497 + $settings = Wpfnl_functions::get_notification_settings();
498 +
499 + if ( 'yes' === $settings['enable_revenue_report'] && ! Wpfnl_functions::is_revenue_report_scheduled() ) {
500 + Wpfnl_functions::schedule_revenue_report_email( $settings );
501 + }
502 +
503 + delete_option( self::REVENUE_REPORT_PENDING_OPTION );
504 + }
505 +
506 +
507 + /**
240 508 * Keep pre-existing sites on the horizontal canvas.
241 509 *
242 510 * The default canvas layout became vertical in 3.12.13. A site that has been
243 511 * running without a stored funnel_builder_mode has been on the old horizontal
@@ -376,8 +644,15 @@
376 644 date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
377 645 PRIMARY KEY (id)
378 646 ) $charset_collate;";
379 647 dbDelta( $sql );
648 +
649 + /**
650 + * Create the AI copilot conversation and message tables.
651 + *
652 + * @since 3.13.0
653 + */
654 + \WPFunnels\AI\ConversationStore::createTables();
380 655 }
381 656
382 657
383 658 /**