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 +359 -13 3.11.1 → 3.13.3 View file →
@@ -25,8 +25,44 @@
25 25 */
26 26 class Wpfnl_Activator
27 27 {
28 28
29 + /**
30 + * Flags that the one-time canvas layout pin has already run.
31 + *
32 + * @since 3.12.13
33 + */
34 + const BUILDER_MODE_PINNED_OPTION = 'wpfunnels_builder_mode_pinned';
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 +
29 65 private static $db_updates = array(
30 66 '3.5.0' => array(
31 67 'wpf_create_350_stat_table',
32 68 'wpf_create_350_optin_entries_table'
@@ -33,8 +69,11 @@
33 69 ),
34 70 '3.11.0' => array(
35 71 'wpf_create_3110_checkout_visits_table',
36 72 ),
73 + '3.12.12' => array(
74 + 'wpf_add_31212_stats_funnel_status_index',
75 + ),
37 76 );
38 77
39 78
40 79 /**
@@ -42,10 +81,14 @@
42 81 *
43 82 * @return void
44 83 */
45 84 public static function init() {
85 + add_action( 'init', array( __CLASS__, 'maybe_pin_legacy_builder_mode' ), 1 );
46 86 add_action( 'init', array( __CLASS__, 'update' ), 20 );
47 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' ) );
48 91 }
49 92
50 93 /**
51 94 * Push all db updates to ActionScheduler
@@ -120,34 +163,212 @@
120 163
121 164 /**
122 165 * Triggered when a callback has ran.
123 166 *
124 - * @param $callback
125 - * @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.
126 180 */
127 181 protected static function run_update_callback_end( $callback, $result ) {
128 - if ( $result ) {
129 - as_schedule_single_action(
130 - time(),
131 - 'wpfunnels_run_update_callback',
132 - array(
133 - 'update_callback' => $callback,
134 - ),
135 - 'wpfunnels-db-updates'
136 - );
137 - } else {
138 - update_option('wpfunnels_'.$callback.'_update', 'completed');
182 + if ( ! $result ) {
183 + update_option( 'wpfunnels_' . $callback . '_update', 'completed' );
184 + return;
139 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 + );
140 199 }
141 200
142 201
143 202
144 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 + /**
145 364 * Initiate the activation process
146 365 *
147 366 * @since 1.0.0
148 367 */
149 368 public static function activate(){
369 + $is_new_install = self::is_new_install(); // capture before update_wpfunnles_version() sets the flag
370 +
150 371 set_transient( 'wpfunnels_just_activated', true, 60 );
151 372 self::set_wpfunnels_activation_transients();
152 373
153 374 // added from version 3.1.7
@@ -156,8 +377,13 @@
156 377 self::update_wpfunnles_version();
157 378 self::update_wpfunnels_db_version();
158 379 self::update_installed_time();
159 380
381 + if ( $is_new_install ) {
382 + self::seed_default_builder_mode();
383 + self::seed_default_notification_settings();
384 + }
385 +
160 386 // add funnel type meta
161 387 Wpfnl_functions::add_type_meta();
162 388 }
163 389
@@ -198,8 +424,121 @@
198 424 return is_null(get_site_option('wpfunnels_version', null));
199 425 }
200 426
201 427 /**
428 + * Seed funnel_builder_mode for brand-new installs.
429 + *
430 + * Writes the current default explicitly so the value in the database always
431 + * reflects a decision, and marks the legacy pin as handled — a fresh install
432 + * has no old default to preserve.
433 + *
434 + * @since 3.11.0
435 + */
436 + private static function seed_default_builder_mode()
437 + {
438 + $general_settings = get_option( '_wpfunnels_general_settings', array() );
439 + if ( ! isset( $general_settings['funnel_builder_mode'] ) ) {
440 + $general_settings['funnel_builder_mode'] = Wpfnl_functions::DEFAULT_FUNNEL_BUILDER_MODE;
441 + update_option( '_wpfunnels_general_settings', $general_settings );
442 + }
443 +
444 + update_option( self::BUILDER_MODE_PINNED_OPTION, 'yes' );
445 + }
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 + }
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 +
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 + /**
508 + * Keep pre-existing sites on the horizontal canvas.
509 + *
510 + * The default canvas layout became vertical in 3.12.13. A site that has been
511 + * running without a stored funnel_builder_mode has been on the old horizontal
512 + * default all along, so flipping the fallback would silently rearrange every
513 + * canvas its users know. This writes horizontal in explicitly, once, leaving
514 + * the new default to apply only to installs created after the change. Users
515 + * opt in to vertical from Settings or the canvas view switcher.
516 + *
517 + * Runs synchronously on `init` rather than through the Action Scheduler db
518 + * updates, because a queued job could land after the user has already opened
519 + * the builder and seen the wrong layout.
520 + *
521 + * @since 3.12.13
522 + */
523 + public static function maybe_pin_legacy_builder_mode()
524 + {
525 + if ( 'yes' === get_option( self::BUILDER_MODE_PINNED_OPTION ) ) {
526 + return;
527 + }
528 +
529 + $general_settings = get_option( '_wpfunnels_general_settings', array() );
530 + $general_settings = is_array( $general_settings ) ? $general_settings : array();
531 +
532 + if ( ! isset( $general_settings['funnel_builder_mode'] ) ) {
533 + $general_settings['funnel_builder_mode'] = 'horizontal';
534 + update_option( '_wpfunnels_general_settings', $general_settings );
535 + }
536 +
537 + update_option( self::BUILDER_MODE_PINNED_OPTION, 'yes' );
538 + }
539 +
540 + /**
202 541 * Update db version to current
203 542 *
204 543 * @param null $version
205 544 *
@@ -305,8 +644,15 @@
305 644 date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
306 645 PRIMARY KEY (id)
307 646 ) $charset_collate;";
308 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();
309 655 }
310 656
311 657
312 658 /**