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 +656 -76 2.8.16 → 3.13.3 View file →
@@ -25,95 +25,675 @@
25 25 */
26 26 class Wpfnl_Activator
27 27 {
28 28
29 - /**
30 - * Short Description. (use period)
31 - *
32 - * Long Description.
33 - *
34 - * @since 1.0.0
35 - */
36 - public static function activate()
37 - {
38 -
39 - self::set_wpfunnels_activation_transients();
40 - self::update_wpfunnles_version();
41 - self::update_wpfunnels_db_version();
42 - self::update_installed_time();
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';
43 35
44 - // add funnel type meta
45 - Wpfnl_functions::add_type_meta();
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';
46 45
47 -
48 - }
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;
49 54
50 -
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;
51 64
65 + private static $db_updates = array(
66 + '3.5.0' => array(
67 + 'wpf_create_350_stat_table',
68 + 'wpf_create_350_optin_entries_table'
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 + ),
76 + );
52 77
53 - /**
54 - * Update WP Funnels version to current.
55 - *
56 - * @since 1.0.0
57 - */
58 - private static function update_wpfunnles_version()
59 - {
60 - update_site_option('wpfunnels_version', Wpfnl::get_instance()->get_version());
61 - }
62 78
79 + /**
80 + * Init hook
81 + *
82 + * @return void
83 + */
84 + public static function init() {
85 + add_action( 'init', array( __CLASS__, 'maybe_pin_legacy_builder_mode' ), 1 );
86 + add_action( 'init', array( __CLASS__, 'update' ), 20 );
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' ) );
91 + }
63 92
64 - /**
65 - * See if we need to redirect the admin to setup wizard or not.
66 - *
67 - * @since 1.0.0
68 - */
69 - private static function set_wpfunnels_activation_transients()
70 - {
71 - if (self::is_new_install()) {
72 - set_transient('_wpfunnels_activation_redirect', 1, 30);
73 - }
74 - }
93 + /**
94 + * Push all db updates to ActionScheduler
95 + *
96 + * @return void
97 + * @since 3.5.0
98 + */
99 + public static function update() {
100 + $current_db_version = get_option( 'wpfunnels_db_version' );
101 + $loop = 0;
75 102
76 - /**
77 - * Brand new install of wpfunnels
78 - *
79 - * @return bool
80 - * @since 1.0.0
81 - */
82 - public static function is_new_install()
83 - {
84 - return is_null(get_site_option('wpfunnels_version', null));
85 - }
103 + $db_update_callbacks= self::get_db_update_callbacks();
86 104
87 - /**
88 - * Update db version to current
89 - *
90 - * @param null $version
91 - *
92 - * @since 1.0.0
93 - */
94 - private static function update_wpfunnels_db_version($version = null)
95 - {
96 - update_site_option('wpfunnels_db_version', is_null($version) ? Wpfnl::get_instance()->get_version() : $version);
97 - }
105 + foreach ( $db_update_callbacks as $version => $update_callbacks ) {
106 + if ( version_compare( $current_db_version, $version, '<' ) ) {
107 + foreach ( $update_callbacks as $update_callback ) {
108 + if (!as_next_scheduled_action('wpfunnels_run_update_callback', array(
109 + 'update_callback' => $update_callback,
110 + ),
111 + 'wpfunnels-db-updates'
112 + )) {
113 + as_schedule_single_action(
114 + time() + $loop,
115 + 'wpfunnels_run_update_callback',
116 + array(
117 + 'update_callback' => $update_callback,
118 + ),
119 + 'wpfunnels-db-updates'
120 + );
121 + ++$loop;
122 + }
123 + }
124 + }
125 + }
98 126
127 + // After the callbacks finish, update the db version to the current WPFunnel version.
128 + $current_wpfnl_version = wpfnl()->get_version();
129 + if ( version_compare( $current_db_version, $current_wpfnl_version, '<' ) ) {
130 + update_option( 'wpfunnels_db_version', $current_wpfnl_version );
131 + }
132 + }
99 133
100 - /**
101 - * Retrieve the time when funnel is installed
102 - *
103 - * @return int|mixed|void
104 - * @since 2.0.0
105 - */
106 - public static function get_installed_time() {
107 - $installed_time = get_option( 'wpfunnels_installed_time' );
108 - if ( ! $installed_time ) {
109 - $installed_time = time();
134 +
135 + /**
136 + * Run an update callback when triggered by ActionScheduler.
137 + *
138 + * @param $update_callback
139 + * @since 3.2.0
140 + */
141 + public static function run_update_callback( $update_callback ) {
142 + include_once WPFNL_DIR . '/includes/wpf-update-functions.php';
143 + if ( is_callable( $update_callback ) ) {
144 + self::run_update_callback_start( $update_callback );
145 + $result = (bool) call_user_func( $update_callback );
146 + self::run_update_callback_end( $update_callback, $result );
147 + }
148 + }
149 +
150 +
151 + /**
152 + * Triggered when a callback will run.
153 + *
154 + * @since 3.2.0
155 + * @param string $callback Callback name.
156 + */
157 + protected static function run_update_callback_start( $callback ) {
158 + if ( ! defined( 'WPF_UPDATING' ) ) {
159 + define( 'WPF_UPDATING', true );
160 + }
161 + }
162 +
163 +
164 + /**
165 + * Triggered when a callback has ran.
166 + *
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.
180 + */
181 + protected static function run_update_callback_end( $callback, $result ) {
182 + if ( ! $result ) {
183 + update_option( 'wpfunnels_' . $callback . '_update', 'completed' );
184 + return;
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 + );
199 + }
200 +
201 +
202 +
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 + /**
364 + * Initiate the activation process
365 + *
366 + * @since 1.0.0
367 + */
368 + public static function activate(){
369 + $is_new_install = self::is_new_install(); // capture before update_wpfunnles_version() sets the flag
370 +
371 + set_transient( 'wpfunnels_just_activated', true, 60 );
372 + self::set_wpfunnels_activation_transients();
373 +
374 + // added from version 3.1.7
375 + self::create_tables();
376 +
377 + self::update_wpfunnles_version();
378 + self::update_wpfunnels_db_version();
379 + self::update_installed_time();
380 +
381 + if ( $is_new_install ) {
382 + self::seed_default_builder_mode();
383 + self::seed_default_notification_settings();
384 + }
385 +
386 + // add funnel type meta
387 + Wpfnl_functions::add_type_meta();
388 + }
389 +
390 +
391 +
392 +
393 + /**
394 + * Update WP Funnels version to current.
395 + *
396 + * @since 1.0.0
397 + */
398 + private static function update_wpfunnles_version()
399 + {
400 + update_site_option('wpfunnels_version', Wpfnl::get_instance()->get_version());
401 + }
402 +
403 +
404 + /**
405 + * See if we need to redirect the admin to setup wizard or not.
406 + *
407 + * @since 1.0.0
408 + */
409 + private static function set_wpfunnels_activation_transients()
410 + {
411 + if (self::is_new_install()) {
412 + set_transient('_wpfunnels_activation_redirect', 1, 30);
413 + }
414 + }
415 +
416 + /**
417 + * Brand new install of wpfunnels
418 + *
419 + * @return bool
420 + * @since 1.0.0
421 + */
422 + public static function is_new_install()
423 + {
424 + return is_null(get_site_option('wpfunnels_version', null));
425 + }
426 +
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 + /**
541 + * Update db version to current
542 + *
543 + * @param null $version
544 + *
545 + * @since 1.0.0
546 + */
547 + private static function update_wpfunnels_db_version($version = null)
548 + {
549 + if ( self::needs_db_update() ) {
550 + self::update();
551 + } else {
552 + update_site_option('wpfunnels_db_version', is_null($version) ? Wpfnl::get_instance()->get_version() : $version);
553 + }
554 + }
555 +
556 +
557 + /**
558 + * Retrieve the time when funnel is installed
559 + *
560 + * @return int|mixed|void
561 + * @since 2.0.0
562 + */
563 + public static function get_installed_time() {
564 + $installed_time = get_option( 'wpfunnels_installed_time' );
565 + if ( ! $installed_time ) {
566 + $installed_time = time();
110 567 update_site_option( 'wpfunnels_installed_time', $installed_time );
111 - }
112 - return $installed_time;
113 - }
568 + }
569 + return $installed_time;
570 + }
114 571
115 572
116 - public static function update_installed_time() {
117 - self::get_installed_time();
118 - }
573 + public static function update_installed_time() {
574 + self::get_installed_time();
575 + }
576 +
577 +
578 + /**
579 + * Create necessary databases on plugin installation process
580 + *
581 + * @since 3.1.7
582 + */
583 + public static function create_tables() {
584 +
585 + if ( !self::should_create_table() ) {
586 + return;
587 + }
588 +
589 + global $wpdb;
590 + $wpdb->hide_errors();
591 + $charset_collate = $wpdb->get_charset_collate();
592 + $table_name = $wpdb->prefix . 'wpfnl_stats';
593 +
594 + $sql = "CREATE TABLE IF NOT EXISTS $table_name (
595 + id mediumint(9) NOT NULL AUTO_INCREMENT,
596 + order_id bigint(20) unsigned NOT NULL,
597 + funnel_id bigint(20) unsigned NOT NULL,
598 + parent_id bigint(20) unsigned NOT NULL,
599 + customer_id bigint(20) unsigned NOT NULL,
600 + total_sales double DEFAULT 0 NOT NULL,
601 + orderbump_sales double DEFAULT 0 NOT NULL,
602 + upsell_sales double DEFAULT 0 NOT NULL,
603 + downsell_sales double DEFAULT 0 NOT NULL,
604 + gateway double DEFAULT 0 NOT NULL,
605 + status varchar(20) NOT NULL,
606 + paid_date datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
607 + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
608 + date_created_gmt datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
609 + PRIMARY KEY (id)
610 + ) $charset_collate;";
611 +
612 + require_once ABSPATH . 'wp-admin/includes/upgrade.php';
613 + dbDelta( $sql );
614 +
615 +
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 + /**
633 + * Create table for optin entries
634 + */
635 + $table_name = $wpdb->prefix . 'wpfnl_optin_entries';
636 + $sql = "CREATE TABLE IF NOT EXISTS $table_name (
637 + id mediumint(9) NOT NULL AUTO_INCREMENT,
638 + funnel_id bigint(20) unsigned NOT NULL,
639 + step_id bigint(20) unsigned NOT NULL,
640 + user_id bigint(20) unsigned NOT NULL,
641 + email varchar(100) NOT NULL,
642 + hash varchar(100) NOT NULL,
643 + data LONGTEXT NULL DEFAULT NULL,
644 + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
645 + PRIMARY KEY (id)
646 + ) $charset_collate;";
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();
655 + }
656 +
657 +
658 + /**
659 + * Check if we need to create new tables
660 + *
661 + * @return bool
662 + * @since 3.1.7
663 + */
664 + public static function should_create_table() {
665 + $db_version = get_option( 'wpfunnels_db_version', '3.2.0' );
666 +
667 + if ( version_compare('3.2.1', $db_version, '>') ) {
668 + return true;
669 + }
670 + return false;
671 + }
672 +
673 +
674 + /**
675 + * Get list of DB update callbacks.
676 + *
677 + * @return array
678 + * @since 3.5.0
679 + */
680 + public static function get_db_update_callbacks() {
681 + return self::$db_updates;
682 + }
683 +
684 +
685 + /**
686 + * Is a DB update needed?
687 + *
688 + * @return bool
689 + * @since 3.5.0
690 + */
691 + public static function needs_db_update() {
692 + $current_db_version = get_option( 'wpfunnels_db_version', null );
693 + $updates = self::get_db_update_callbacks();
694 + $update_versions = array_keys( $updates );
695 + usort( $update_versions, 'version_compare' );
696 +
697 + return ! is_null( $current_db_version ) && version_compare( $current_db_version, end( $update_versions ), '<' );
698 + }
119 699 }