PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.1
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.1
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 2.8.17 2.8.18 All 259 releases
wpfunnels / includes / utils / class-wpfnl-activator.php

class-wpfnl-activator.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.1, at includes/utils/class-wpfnl-activator.php

700 lines 20.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Fired during plugin activation
5 *
6 * @link https://rextheme.com
7 * @since 1.0.0
8 *
9 * @package Wpfnl
10 * @subpackage Wpfnl/includes
11 */
12
13 use WPFunnels\Wpfnl;
14 use WPFunnels\Wpfnl_functions;
15
16 /**
17 * Fired during plugin activation.
18 *
19 * This class defines all code necessary to run during the plugin's activation.
20 *
21 * @since 1.0.0
22 * @package Wpfnl
23 * @subpackage Wpfnl/includes
24 * @author RexTheme <[email protected]>
25 */
26 class Wpfnl_Activator
27 {
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
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 );
77
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 }
92
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;
102
103 $db_update_callbacks= self::get_db_update_callbacks();
104
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 }
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 }
133
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();
567 update_site_option( 'wpfunnels_installed_time', $installed_time );
568 }
569 return $installed_time;
570 }
571
572
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 }
699 }
700