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.4 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 All 262 releases
← All changes | includes/utils/class-wpfnl-activator.php +378 -13 3.10.6 → 3.13.3 View file →
@@ -25,13 +25,55 @@
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 69 ),
70 + '3.11.0' => array(
71 + 'wpf_create_3110_checkout_visits_table',
72 + ),
73 + '3.12.12' => array(
74 + 'wpf_add_31212_stats_funnel_status_index',
75 + ),
34 76 );
35 77
36 78
37 79 /**
@@ -39,10 +81,14 @@
39 81 *
40 82 * @return void
41 83 */
42 84 public static function init() {
85 + add_action( 'init', array( __CLASS__, 'maybe_pin_legacy_builder_mode' ), 1 );
43 86 add_action( 'init', array( __CLASS__, 'update' ), 20 );
44 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' ) );
45 91 }
46 92
47 93 /**
48 94 * Push all db updates to ActionScheduler
@@ -117,34 +163,212 @@
117 163
118 164 /**
119 165 * Triggered when a callback has ran.
120 166 *
121 - * @param $callback
122 - * @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.
123 180 */
124 181 protected static function run_update_callback_end( $callback, $result ) {
125 - if ( $result ) {
126 - as_schedule_single_action(
127 - time(),
128 - 'wpfunnels_run_update_callback',
129 - array(
130 - 'update_callback' => $callback,
131 - ),
132 - 'wpfunnels-db-updates'
133 - );
134 - } else {
135 - update_option('wpfunnels_'.$callback.'_update', 'completed');
182 + if ( ! $result ) {
183 + update_option( 'wpfunnels_' . $callback . '_update', 'completed' );
184 + return;
136 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 + );
137 199 }
138 200
139 201
140 202
141 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 + /**
142 364 * Initiate the activation process
143 365 *
144 366 * @since 1.0.0
145 367 */
146 368 public static function activate(){
369 + $is_new_install = self::is_new_install(); // capture before update_wpfunnles_version() sets the flag
370 +
147 371 set_transient( 'wpfunnels_just_activated', true, 60 );
148 372 self::set_wpfunnels_activation_transients();
149 373
150 374 // added from version 3.1.7
@@ -153,8 +377,13 @@
153 377 self::update_wpfunnles_version();
154 378 self::update_wpfunnels_db_version();
155 379 self::update_installed_time();
156 380
381 + if ( $is_new_install ) {
382 + self::seed_default_builder_mode();
383 + self::seed_default_notification_settings();
384 + }
385 +
157 386 // add funnel type meta
158 387 Wpfnl_functions::add_type_meta();
159 388 }
160 389
@@ -195,8 +424,121 @@
195 424 return is_null(get_site_option('wpfunnels_version', null));
196 425 }
197 426
198 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 + /**
199 541 * Update db version to current
200 542 *
201 543 * @param null $version
202 544 *
@@ -271,8 +613,24 @@
271 613 dbDelta( $sql );
272 614
273 615
274 616 /**
617 + * Create checkout visits table for conversion rate tracking
618 + */
619 + $table_name = $wpdb->prefix . 'wpfnl_checkout_visits';
620 + $sql = "CREATE TABLE IF NOT EXISTS $table_name (
621 + id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
622 + type varchar(10) NOT NULL DEFAULT 'store',
623 + funnel_id bigint(20) unsigned NOT NULL DEFAULT 0,
624 + session_hash varchar(32) NOT NULL,
625 + visit_date date NOT NULL,
626 + date_created datetime NOT NULL,
627 + PRIMARY KEY (id),
628 + UNIQUE KEY unique_visit (session_hash, type, funnel_id, visit_date)
629 + ) $charset_collate;";
630 + dbDelta( $sql );
631 +
632 + /**
275 633 * Create table for optin entries
276 634 */
277 635 $table_name = $wpdb->prefix . 'wpfnl_optin_entries';
278 636 $sql = "CREATE TABLE IF NOT EXISTS $table_name (
@@ -286,8 +644,15 @@
286 644 date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
287 645 PRIMARY KEY (id)
288 646 ) $charset_collate;";
289 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();
290 655 }
291 656
292 657
293 658 /**