PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.0
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.0
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
wpfunnels / includes / utils / class-wpfnl-activator.php

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

629 lines 17.6 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 * Seconds to wait before re-queuing a DB update callback that reported
38 * more work to do. Throttles a misbehaving callback instead of letting it
39 * saturate the ActionScheduler queue runner.
40 *
41 * @since 3.12.13
42 */
43 const REQUEUE_DELAY = MINUTE_IN_SECONDS;
44
45 /**
46 * One-time cleanup of the ActionScheduler rows left behind by the runaway
47 * update-callback loop that shipped in 3.12.12.
48 *
49 * @since 3.12.13
50 */
51 const RUNAWAY_PURGE_HOOK = 'wpfunnels_purge_runaway_update_actions';
52 const RUNAWAY_PURGE_OPTION = 'wpfunnels_runaway_update_actions_purged';
53 const RUNAWAY_PURGE_BATCH = 5000;
54
55 private static $db_updates = array(
56 '3.5.0' => array(
57 'wpf_create_350_stat_table',
58 'wpf_create_350_optin_entries_table'
59 ),
60 '3.11.0' => array(
61 'wpf_create_3110_checkout_visits_table',
62 ),
63 '3.12.12' => array(
64 'wpf_add_31212_stats_funnel_status_index',
65 ),
66 );
67
68
69 /**
70 * Init hook
71 *
72 * @return void
73 */
74 public static function init() {
75 add_action( 'init', array( __CLASS__, 'maybe_pin_legacy_builder_mode' ), 1 );
76 add_action( 'init', array( __CLASS__, 'update' ), 20 );
77 add_action('wpfunnels_run_update_callback', array( __CLASS__, 'run_update_callback' ) );
78 add_action( 'init', array( __CLASS__, 'maybe_schedule_runaway_purge' ), 21 );
79 add_action( self::RUNAWAY_PURGE_HOOK, array( __CLASS__, 'purge_runaway_update_actions' ) );
80 }
81
82 /**
83 * Push all db updates to ActionScheduler
84 *
85 * @return void
86 * @since 3.5.0
87 */
88 public static function update() {
89 $current_db_version = get_option( 'wpfunnels_db_version' );
90 $loop = 0;
91
92 $db_update_callbacks= self::get_db_update_callbacks();
93
94 foreach ( $db_update_callbacks as $version => $update_callbacks ) {
95 if ( version_compare( $current_db_version, $version, '<' ) ) {
96 foreach ( $update_callbacks as $update_callback ) {
97 if (!as_next_scheduled_action('wpfunnels_run_update_callback', array(
98 'update_callback' => $update_callback,
99 ),
100 'wpfunnels-db-updates'
101 )) {
102 as_schedule_single_action(
103 time() + $loop,
104 'wpfunnels_run_update_callback',
105 array(
106 'update_callback' => $update_callback,
107 ),
108 'wpfunnels-db-updates'
109 );
110 ++$loop;
111 }
112 }
113 }
114 }
115
116 // After the callbacks finish, update the db version to the current WPFunnel version.
117 $current_wpfnl_version = wpfnl()->get_version();
118 if ( version_compare( $current_db_version, $current_wpfnl_version, '<' ) ) {
119 update_option( 'wpfunnels_db_version', $current_wpfnl_version );
120 }
121 }
122
123
124 /**
125 * Run an update callback when triggered by ActionScheduler.
126 *
127 * @param $update_callback
128 * @since 3.2.0
129 */
130 public static function run_update_callback( $update_callback ) {
131 include_once WPFNL_DIR . '/includes/wpf-update-functions.php';
132 if ( is_callable( $update_callback ) ) {
133 self::run_update_callback_start( $update_callback );
134 $result = (bool) call_user_func( $update_callback );
135 self::run_update_callback_end( $update_callback, $result );
136 }
137 }
138
139
140 /**
141 * Triggered when a callback will run.
142 *
143 * @since 3.2.0
144 * @param string $callback Callback name.
145 */
146 protected static function run_update_callback_start( $callback ) {
147 if ( ! defined( 'WPF_UPDATING' ) ) {
148 define( 'WPF_UPDATING', true );
149 }
150 }
151
152
153 /**
154 * Triggered when a callback has ran.
155 *
156 * A truthy $result means the callback processed one batch and has more work
157 * left, so it gets queued again; a falsy $result means it finished. Callbacks
158 * that do all their work in one pass MUST return false — returning true from a
159 * single-pass callback is what produced the runaway queue fixed in 3.12.13.
160 *
161 * Two guards keep a callback that gets this wrong from flooding the queue:
162 * the re-queue is skipped when an identical action is already pending, and it
163 * is spaced out by REQUEUE_DELAY rather than being due the instant it lands.
164 * A genuine batched migration loses nothing from the delay; a buggy one is
165 * capped at one row per interval instead of saturating the queue runner.
166 *
167 * @param string $callback Callback name.
168 * @param bool $result Whether the callback has more work queued up.
169 */
170 protected static function run_update_callback_end( $callback, $result ) {
171 if ( ! $result ) {
172 update_option( 'wpfunnels_' . $callback . '_update', 'completed' );
173 return;
174 }
175
176 $args = array( 'update_callback' => $callback );
177
178 if ( as_next_scheduled_action( 'wpfunnels_run_update_callback', $args, 'wpfunnels-db-updates' ) ) {
179 return;
180 }
181
182 as_schedule_single_action(
183 time() + self::REQUEUE_DELAY,
184 'wpfunnels_run_update_callback',
185 $args,
186 'wpfunnels-db-updates'
187 );
188 }
189
190
191
192 /**
193 * Queue the one-time purge of runaway update-callback rows.
194 *
195 * 3.12.12 shipped a callback that reported "more work to do" on every run,
196 * so ActionScheduler re-queued it without pause and affected sites ended up
197 * with millions of rows in actionscheduler_actions. The return-value fix
198 * stops the loop, but it cannot remove what already accumulated, and the
199 * volume is far past what the Scheduled Actions screen can delete by hand.
200 *
201 * @since 3.12.13
202 */
203 public static function maybe_schedule_runaway_purge() {
204 if ( 'completed' === get_option( self::RUNAWAY_PURGE_OPTION ) ) {
205 return;
206 }
207
208 // Front-end requests are the bulk of a site's traffic and there is no
209 // reason to spend a queue lookup on each one; admin and cron hits are
210 // frequent enough to get the chain started and to restart it if it
211 // ever breaks part-way through.
212 if ( ! is_admin() && ! wp_doing_cron() ) {
213 return;
214 }
215
216 if ( ! function_exists( 'as_schedule_single_action' ) || ! function_exists( 'as_next_scheduled_action' ) ) {
217 return;
218 }
219
220 // Covers in-progress as well as pending, so a batch that is mid-flight
221 // does not get a duplicate queued alongside it.
222 if ( as_next_scheduled_action( self::RUNAWAY_PURGE_HOOK, array(), 'wpfunnels-db-updates' ) ) {
223 return;
224 }
225
226 self::queue_purge_batch( time() );
227 }
228
229
230 /**
231 * Delete one batch of finished update-callback rows, chaining the next
232 * batch until nothing is left.
233 *
234 * Only rows in a terminal state are touched. Pending and in-progress rows
235 * are left alone so a site part-way through a legitimate migration does not
236 * lose queued work.
237 *
238 * @since 3.12.13
239 */
240 public static function purge_runaway_update_actions() {
241 global $wpdb;
242
243 $actions_table = $wpdb->prefix . 'actionscheduler_actions';
244
245 if ( ! self::table_exists( $actions_table ) ) {
246 self::finish_runaway_purge();
247 return;
248 }
249
250 $ids = $wpdb->get_col( $wpdb->prepare(
251 "SELECT action_id FROM {$actions_table} WHERE hook = %s AND status IN ( %s, %s, %s ) LIMIT %d",
252 'wpfunnels_run_update_callback',
253 'complete',
254 'canceled',
255 'failed',
256 self::RUNAWAY_PURGE_BATCH
257 ) ); // phpcs:ignore
258
259 if ( empty( $ids ) ) {
260 self::finish_runaway_purge();
261 return;
262 }
263
264 $ids = array_map( 'absint', $ids );
265 $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
266 $logs_table = $wpdb->prefix . 'actionscheduler_logs';
267
268 // Logs first: dropping the actions before their logs would strand the
269 // log rows with no id left to match them on.
270 if ( self::table_exists( $logs_table ) ) {
271 $wpdb->query( $wpdb->prepare( "DELETE FROM {$logs_table} WHERE action_id IN ({$placeholders})", $ids ) ); // phpcs:ignore
272 }
273
274 $wpdb->query( $wpdb->prepare( "DELETE FROM {$actions_table} WHERE action_id IN ({$placeholders})", $ids ) ); // phpcs:ignore
275
276 // Chain the next batch by hand rather than using a recurring action.
277 // ActionScheduler re-creates a recurring action after the callback
278 // returns, from the action object it read before execution, so a
279 // recurring purge could not switch itself off from in here — it would
280 // keep queueing a fresh row every interval long after the work ran out.
281 self::queue_purge_batch( time() + MINUTE_IN_SECONDS );
282 }
283
284
285 /**
286 * Queue a single purge batch, unless one is already waiting.
287 *
288 * The pending check is deliberately narrower than as_next_scheduled_action(),
289 * which reports true for the in-progress action as well. Called from inside
290 * a running batch, that would always short-circuit and the chain would stop
291 * after its first pass.
292 *
293 * @param int $timestamp When the batch should run.
294 *
295 * @since 3.12.13
296 */
297 protected static function queue_purge_batch( $timestamp ) {
298 if ( ! function_exists( 'as_schedule_single_action' ) || ! function_exists( 'as_get_scheduled_actions' ) ) {
299 return;
300 }
301
302 $pending = as_get_scheduled_actions(
303 array(
304 'hook' => self::RUNAWAY_PURGE_HOOK,
305 'status' => ActionScheduler_Store::STATUS_PENDING,
306 'group' => 'wpfunnels-db-updates',
307 'per_page' => 1,
308 ),
309 'ids'
310 );
311
312 if ( ! empty( $pending ) ) {
313 return;
314 }
315
316 as_schedule_single_action( $timestamp, self::RUNAWAY_PURGE_HOOK, array(), 'wpfunnels-db-updates' );
317 }
318
319
320 /**
321 * Mark the purge done and make sure no further batch is queued.
322 *
323 * @since 3.12.13
324 */
325 protected static function finish_runaway_purge() {
326 update_option( self::RUNAWAY_PURGE_OPTION, 'completed' );
327
328 // Nothing should be queued at this point, since a batch only chains a
329 // successor when it actually deleted something. Swept anyway so a row
330 // left over from an interrupted run cannot keep the chain alive.
331 if ( function_exists( 'as_unschedule_all_actions' ) ) {
332 as_unschedule_all_actions( self::RUNAWAY_PURGE_HOOK, array(), 'wpfunnels-db-updates' );
333 }
334 }
335
336
337 /**
338 * Whether a database table exists.
339 *
340 * @param string $table Fully prefixed table name.
341 * @return bool
342 *
343 * @since 3.12.13
344 */
345 protected static function table_exists( $table ) {
346 global $wpdb;
347
348 return (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ); // phpcs:ignore
349 }
350
351
352 /**
353 * Initiate the activation process
354 *
355 * @since 1.0.0
356 */
357 public static function activate(){
358 $is_new_install = self::is_new_install(); // capture before update_wpfunnles_version() sets the flag
359
360 set_transient( 'wpfunnels_just_activated', true, 60 );
361 self::set_wpfunnels_activation_transients();
362
363 // added from version 3.1.7
364 self::create_tables();
365
366 self::update_wpfunnles_version();
367 self::update_wpfunnels_db_version();
368 self::update_installed_time();
369
370 if ( $is_new_install ) {
371 self::seed_default_builder_mode();
372 }
373
374 // add funnel type meta
375 Wpfnl_functions::add_type_meta();
376 }
377
378
379
380
381 /**
382 * Update WP Funnels version to current.
383 *
384 * @since 1.0.0
385 */
386 private static function update_wpfunnles_version()
387 {
388 update_site_option('wpfunnels_version', Wpfnl::get_instance()->get_version());
389 }
390
391
392 /**
393 * See if we need to redirect the admin to setup wizard or not.
394 *
395 * @since 1.0.0
396 */
397 private static function set_wpfunnels_activation_transients()
398 {
399 if (self::is_new_install()) {
400 set_transient('_wpfunnels_activation_redirect', 1, 30);
401 }
402 }
403
404 /**
405 * Brand new install of wpfunnels
406 *
407 * @return bool
408 * @since 1.0.0
409 */
410 public static function is_new_install()
411 {
412 return is_null(get_site_option('wpfunnels_version', null));
413 }
414
415 /**
416 * Seed funnel_builder_mode for brand-new installs.
417 *
418 * Writes the current default explicitly so the value in the database always
419 * reflects a decision, and marks the legacy pin as handled — a fresh install
420 * has no old default to preserve.
421 *
422 * @since 3.11.0
423 */
424 private static function seed_default_builder_mode()
425 {
426 $general_settings = get_option( '_wpfunnels_general_settings', array() );
427 if ( ! isset( $general_settings['funnel_builder_mode'] ) ) {
428 $general_settings['funnel_builder_mode'] = Wpfnl_functions::DEFAULT_FUNNEL_BUILDER_MODE;
429 update_option( '_wpfunnels_general_settings', $general_settings );
430 }
431
432 update_option( self::BUILDER_MODE_PINNED_OPTION, 'yes' );
433 }
434
435
436 /**
437 * Keep pre-existing sites on the horizontal canvas.
438 *
439 * The default canvas layout became vertical in 3.12.13. A site that has been
440 * running without a stored funnel_builder_mode has been on the old horizontal
441 * default all along, so flipping the fallback would silently rearrange every
442 * canvas its users know. This writes horizontal in explicitly, once, leaving
443 * the new default to apply only to installs created after the change. Users
444 * opt in to vertical from Settings or the canvas view switcher.
445 *
446 * Runs synchronously on `init` rather than through the Action Scheduler db
447 * updates, because a queued job could land after the user has already opened
448 * the builder and seen the wrong layout.
449 *
450 * @since 3.12.13
451 */
452 public static function maybe_pin_legacy_builder_mode()
453 {
454 if ( 'yes' === get_option( self::BUILDER_MODE_PINNED_OPTION ) ) {
455 return;
456 }
457
458 $general_settings = get_option( '_wpfunnels_general_settings', array() );
459 $general_settings = is_array( $general_settings ) ? $general_settings : array();
460
461 if ( ! isset( $general_settings['funnel_builder_mode'] ) ) {
462 $general_settings['funnel_builder_mode'] = 'horizontal';
463 update_option( '_wpfunnels_general_settings', $general_settings );
464 }
465
466 update_option( self::BUILDER_MODE_PINNED_OPTION, 'yes' );
467 }
468
469 /**
470 * Update db version to current
471 *
472 * @param null $version
473 *
474 * @since 1.0.0
475 */
476 private static function update_wpfunnels_db_version($version = null)
477 {
478 if ( self::needs_db_update() ) {
479 self::update();
480 } else {
481 update_site_option('wpfunnels_db_version', is_null($version) ? Wpfnl::get_instance()->get_version() : $version);
482 }
483 }
484
485
486 /**
487 * Retrieve the time when funnel is installed
488 *
489 * @return int|mixed|void
490 * @since 2.0.0
491 */
492 public static function get_installed_time() {
493 $installed_time = get_option( 'wpfunnels_installed_time' );
494 if ( ! $installed_time ) {
495 $installed_time = time();
496 update_site_option( 'wpfunnels_installed_time', $installed_time );
497 }
498 return $installed_time;
499 }
500
501
502 public static function update_installed_time() {
503 self::get_installed_time();
504 }
505
506
507 /**
508 * Create necessary databases on plugin installation process
509 *
510 * @since 3.1.7
511 */
512 public static function create_tables() {
513
514 if ( !self::should_create_table() ) {
515 return;
516 }
517
518 global $wpdb;
519 $wpdb->hide_errors();
520 $charset_collate = $wpdb->get_charset_collate();
521 $table_name = $wpdb->prefix . 'wpfnl_stats';
522
523 $sql = "CREATE TABLE IF NOT EXISTS $table_name (
524 id mediumint(9) NOT NULL AUTO_INCREMENT,
525 order_id bigint(20) unsigned NOT NULL,
526 funnel_id bigint(20) unsigned NOT NULL,
527 parent_id bigint(20) unsigned NOT NULL,
528 customer_id bigint(20) unsigned NOT NULL,
529 total_sales double DEFAULT 0 NOT NULL,
530 orderbump_sales double DEFAULT 0 NOT NULL,
531 upsell_sales double DEFAULT 0 NOT NULL,
532 downsell_sales double DEFAULT 0 NOT NULL,
533 gateway double DEFAULT 0 NOT NULL,
534 status varchar(20) NOT NULL,
535 paid_date datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
536 date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
537 date_created_gmt datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
538 PRIMARY KEY (id)
539 ) $charset_collate;";
540
541 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
542 dbDelta( $sql );
543
544
545 /**
546 * Create checkout visits table for conversion rate tracking
547 */
548 $table_name = $wpdb->prefix . 'wpfnl_checkout_visits';
549 $sql = "CREATE TABLE IF NOT EXISTS $table_name (
550 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
551 type varchar(10) NOT NULL DEFAULT 'store',
552 funnel_id bigint(20) unsigned NOT NULL DEFAULT 0,
553 session_hash varchar(32) NOT NULL,
554 visit_date date NOT NULL,
555 date_created datetime NOT NULL,
556 PRIMARY KEY (id),
557 UNIQUE KEY unique_visit (session_hash, type, funnel_id, visit_date)
558 ) $charset_collate;";
559 dbDelta( $sql );
560
561 /**
562 * Create table for optin entries
563 */
564 $table_name = $wpdb->prefix . 'wpfnl_optin_entries';
565 $sql = "CREATE TABLE IF NOT EXISTS $table_name (
566 id mediumint(9) NOT NULL AUTO_INCREMENT,
567 funnel_id bigint(20) unsigned NOT NULL,
568 step_id bigint(20) unsigned NOT NULL,
569 user_id bigint(20) unsigned NOT NULL,
570 email varchar(100) NOT NULL,
571 hash varchar(100) NOT NULL,
572 data LONGTEXT NULL DEFAULT NULL,
573 date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
574 PRIMARY KEY (id)
575 ) $charset_collate;";
576 dbDelta( $sql );
577
578 /**
579 * Create the AI copilot conversation and message tables.
580 *
581 * @since 3.13.0
582 */
583 \WPFunnels\AI\ConversationStore::createTables();
584 }
585
586
587 /**
588 * Check if we need to create new tables
589 *
590 * @return bool
591 * @since 3.1.7
592 */
593 public static function should_create_table() {
594 $db_version = get_option( 'wpfunnels_db_version', '3.2.0' );
595
596 if ( version_compare('3.2.1', $db_version, '>') ) {
597 return true;
598 }
599 return false;
600 }
601
602
603 /**
604 * Get list of DB update callbacks.
605 *
606 * @return array
607 * @since 3.5.0
608 */
609 public static function get_db_update_callbacks() {
610 return self::$db_updates;
611 }
612
613
614 /**
615 * Is a DB update needed?
616 *
617 * @return bool
618 * @since 3.5.0
619 */
620 public static function needs_db_update() {
621 $current_db_version = get_option( 'wpfunnels_db_version', null );
622 $updates = self::get_db_update_callbacks();
623 $update_versions = array_keys( $updates );
624 usort( $update_versions, 'version_compare' );
625
626 return ! is_null( $current_db_version ) && version_compare( $current_db_version, end( $update_versions ), '<' );
627 }
628 }
629