PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.3.0
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.3.0
3.3.0 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.1 trunk 1.0.4 1.1.0 1.1.0.1 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 All 162 releases
cartflows / classes / class-cartflows-update.php

class-cartflows-update.php in CartFlows – Funnel Builder & Checkout Plugin for WooCommerce 3.3.0, at classes/class-cartflows-update.php

653 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Update Compatibility
4 *
5 * @package CartFlows
6 */
7
8 // Exit if accessed directly.
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12 if ( ! class_exists( 'Cartflows_Update' ) ) :
13
14 /**
15 * CartFlows Update initial setup
16 *
17 * @since 1.0.0
18 */
19 class Cartflows_Update {
20
21 /**
22 * Class instance.
23 *
24 * @access private
25 * @var Cartflows_Update|null
26 */
27 private static $instance;
28
29 /**
30 * Initiator
31 *
32 * @return Cartflows_Update
33 */
34 public static function get_instance() {
35 if ( ! isset( self::$instance ) ) {
36 self::$instance = new self();
37 }
38 return self::$instance;
39 }
40
41 /**
42 * Constructor
43 */
44 public function __construct() {
45 add_action( 'admin_init', array( $this, 'init' ) );
46 }
47
48 /**
49 * Init
50 *
51 * @since 1.0.0
52 * @return void
53 */
54 public function init() {
55
56 do_action( 'cartflows_update_before' );
57
58 // Get auto saved version number.
59 $saved_version = get_option( 'cartflows-version', false );
60
61 // Update auto saved version number.
62 if ( ! $saved_version ) {
63 update_option( 'cartflows-version', CARTFLOWS_VER );
64 return;
65 }
66
67 // If equals then return.
68 if ( version_compare( $saved_version, CARTFLOWS_VER, '=' ) ) {
69 return;
70 }
71
72 $this->logger_files();
73
74 if ( version_compare( $saved_version, '1.1.22', '<' ) ) {
75 update_option( 'wcf_setup_skipped', true );
76 }
77
78 if ( version_compare( $saved_version, '1.2.0', '<' ) ) {
79
80 $this->changed_wp_templates();
81 }
82
83 /* Add legacy admin option */
84 if ( version_compare( $saved_version, '1.6.0', '<' ) ) {
85 update_option( 'cartflows-old-ui-user', true );
86 update_option( 'cartflows-legacy-admin', true );
87 update_option( 'cartflows-legacy-meta-show-design-options', true );
88 }
89
90 /* Updating meta for global checkout migration & Permalinks */
91 if ( version_compare( $saved_version, '1.10.0', '<' ) ) {
92
93 $global_checkout = \Cartflows_Helper::get_common_setting( 'global_checkout' );
94
95 if ( $global_checkout ) {
96 $flow_id = wcf()->utils->get_flow_id_from_step_id( $global_checkout );
97 if ( $flow_id ) {
98 update_option( '_cartflows_store_checkout', $flow_id );
99 update_option( '_cartflows_old_global_checkout', $global_checkout );
100 delete_post_meta( $global_checkout, 'wcf-checkout-products' );
101 }
102 }
103
104 update_option( 'cartflows_show_weekly_report_email_notice', 'yes' );
105
106 $permalink_settings = Cartflows_Helper::get_admin_settings_option( '_cartflows_permalink', false, false );
107
108 if ( ! $permalink_settings ) {
109
110 $default_settings = array(
111 'permalink' => CARTFLOWS_STEP_POST_TYPE,
112 'permalink_flow_base' => CARTFLOWS_FLOW_POST_TYPE,
113 'permalink_structure' => '',
114
115 );
116
117 Cartflows_Helper::update_admin_settings_option( '_cartflows_permalink', $default_settings );
118
119 }
120 }
121 // Update required license key to lowercase. Can be removed after 3 major update.
122 if ( version_compare( $saved_version, '1.11.1', '<' ) && _is_cartflows_pro() ) {
123
124 $api_key = get_option( 'wc_am_client_CartFlows_api_key' );
125 $api_key_data = get_option( 'wc_am_client_CartFlows' );
126
127 // Take backup of options.
128 update_option(
129 'cartflows_license_backup_data',
130 array(
131 'wc_am_client_cartflows_api_key' => $api_key,
132 'wc_am_client_cartflows' => $api_key_data,
133 )
134 );
135
136 delete_option( 'wc_am_client_CartFlows_api_key' );
137 delete_option( 'wc_am_client_CartFlows' );
138
139 // Delete the cached value for old user ( before v1.11.0 and after v1.11.0 ) so WP can add new value.
140 wp_cache_flush();
141
142 if ( $api_key_data ) {
143
144 $new_data = array(
145 'wc_am_client_cartflows_api_key' => isset( $api_key_data['wc_am_client_CartFlows_api_key'] ) ? $api_key_data['wc_am_client_CartFlows_api_key'] : $api_key_data['wc_am_client_cartflows_api_key'],
146 );
147
148 update_option( 'wc_am_client_cartflows', $new_data );
149 }
150
151 if ( $api_key ) {
152 update_option( 'wc_am_client_cartflows_api_key', $api_key );
153 }
154 }
155
156 // Set default page builder to "Other" for those who have set it as "DIVI".
157 if ( version_compare( $saved_version, '2.0.0', '<' ) ) {
158 $common_settings = get_option( '_cartflows_common', array() );
159
160 if ( is_array( $common_settings ) && 'divi' === $common_settings['default_page_builder'] ) {
161
162 $common_settings['default_page_builder'] = 'elementor';
163 update_option( '_cartflows_common', $common_settings );
164 }
165 }
166
167 // Migrate old combined custom script meta to separate JS and CSS meta fields.
168 if ( version_compare( $saved_version, '2.2.2', '<' ) ) {
169 $this->migrate_custom_scripts();
170 }
171
172 // 3.0 ships a redesigned admin UI. Existing users (saved_version < 3.0.0) keep the
173 // legacy admin until they opt into the new UI from the in-app notice or the
174 // Advanced-tab toggle. Saved as 'enable' to match the toggle's backComp format
175 // so the field round-trips correctly. Fresh installs short-circuit at the
176 // `! $saved_version` early return above and default to the new UI.
177 if ( is_string( $saved_version ) && version_compare( $saved_version, '3.0.0', '<' ) ) {
178 update_option( 'cartflows-legacy-admin', 'enable' );
179 }
180
181 // Knowledge Base docs were stored autoloaded, adding hundreds of KB to every request.
182 if ( is_string( $saved_version ) && version_compare( $saved_version, '3.2.1', '<' ) ) {
183 $this->migrate_knowledge_base_docs_storage();
184 }
185
186 // Update auto saved version number.
187 update_option( 'cartflows-version', CARTFLOWS_VER );
188
189 // Update cartflows asset version to regenerate the dynamic css. We are using the time() function to add the random number.
190 update_option( 'cartflows-assets-version', time() );
191
192 do_action( 'cartflows_update_after' );
193 }
194
195 /**
196 * Loading logger files.
197 *
198 * @since 1.0.0
199 * @return void
200 */
201 public function logger_files() {
202
203 if ( ! defined( 'CARTFLOWS_LOG_DIR' ) ) {
204
205 $upload_dir = wp_upload_dir( null, false );
206
207 define( 'CARTFLOWS_LOG_DIR', $upload_dir['basedir'] . '/cartflows-logs/' );
208 }
209
210 wcf()->create_files();
211 }
212
213 /**
214 * Set script migration status.
215 *
216 * @param string $status Migration status value.
217 * @since 2.2.2
218 * @return void
219 */
220 public function set_script_migration_status( $status ) {
221 update_option( 'cartflows_script_migration_status', $status );
222 }
223
224 /**
225 * Determine if the migration notice should be shown.
226 *
227 * Returns false if migration is completed or accepted.
228 * Returns false if status is 'skipped' and 7 days have not yet passed.
229 * Returns false if no old script values exist (auto-sets status to 'completed').
230 *
231 * @since 2.2.2
232 * @return bool
233 */
234 public function should_show_migration_notice() {
235 $status = \CartFlows_Helper::get_script_migration_status();
236
237 if ( 'completed' === $status || 'accepted' === $status ) {
238 return false;
239 }
240
241 if ( 'skipped' === $status ) {
242 $skip_timestamp = (int) get_option( 'cartflows_script_migration_skip_timestamp', 0 );
243
244 // Show again only after 7 days have passed since last skip.
245 if ( ( time() - $skip_timestamp ) < WEEK_IN_SECONDS ) {
246 return false;
247 }
248 }
249
250 // Check cached result first to avoid running the meta query on every page load.
251 $has_old_scripts = get_option( 'cartflows_has_old_custom_scripts', '' );
252
253 if ( '' === $has_old_scripts ) {
254 // First time check — run the query once and cache the result.
255 $has_old_scripts = $this->has_old_custom_scripts() ? 'yes' : 'no';
256 update_option( 'cartflows_has_old_custom_scripts', $has_old_scripts, true );
257 }
258
259 // If no old script values exist, auto-complete migration and enable the code editor.
260 if ( 'yes' !== $has_old_scripts ) {
261 $this->set_script_migration_status( 'completed' );
262 return false;
263 }
264
265 return true;
266 }
267
268 /**
269 * Check if any posts have old combined custom script meta values.
270 *
271 * Queries the database for any cartflows_step or cartflows_flow posts
272 * that have non-empty 'wcf-custom-script' or 'wcf-flow-custom-script' meta.
273 *
274 * @since 2.2.3
275 * @return bool True if old script data exists, false otherwise.
276 */
277 public function has_old_custom_scripts() {
278
279 global $wpdb;
280
281 $result = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
282 $wpdb->prepare(
283 "SELECT 1 FROM {$wpdb->postmeta} pm
284 INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
285 WHERE p.post_type IN (%s, %s)
286 AND pm.meta_key IN (%s, %s)
287 AND pm.meta_value != ''
288 LIMIT 1",
289 'cartflows_step',
290 'cartflows_flow',
291 'wcf-custom-script',
292 'wcf-flow-custom-script'
293 )
294 );
295
296 return ( null !== $result );
297 }
298
299 /**
300 * Get current migration skip count.
301 *
302 * @since 2.2.2
303 * @return int
304 */
305 public function get_migration_skip_count() {
306 return (int) get_option( 'cartflows_script_migration_skip_count', 0 );
307 }
308
309 /**
310 * Increment migration skip count and record the skip timestamp.
311 *
312 * @since 2.2.2
313 * @return void
314 */
315 public function increment_migration_skip_count() {
316 $count = $this->get_migration_skip_count();
317 update_option( 'cartflows_script_migration_skip_count', $count + 1 );
318 update_option( 'cartflows_script_migration_skip_timestamp', time() );
319 }
320
321 /**
322 * Run on-demand migration of custom scripts triggered by user action.
323 *
324 * Unlike migrate_custom_scripts() which runs during version updates,
325 * this method is called only when the user explicitly clicks "Migrate Data"
326 * in the admin notice. It returns a count of migrated posts and updates
327 * the migration status to 'completed'.
328 *
329 * @since 2.2.2
330 * @return int Number of posts that had data migrated.
331 */
332 public function migrate_custom_scripts_on_demand() {
333
334 $migrated_count = 0;
335
336 // Migrate step-level custom scripts.
337 $migrated_count += $this->migrate_post_type_scripts_counted(
338 'cartflows_step',
339 'wcf-custom-script',
340 'wcf-step-custom-js',
341 'wcf-step-custom-css'
342 );
343
344 // Migrate flow-level custom scripts.
345 $migrated_count += $this->migrate_post_type_scripts_counted(
346 'cartflows_flow',
347 'wcf-flow-custom-script',
348 'wcf-flow-custom-js',
349 'wcf-flow-custom-css'
350 );
351
352 $this->set_script_migration_status( 'completed' );
353
354 return $migrated_count;
355 }
356
357 /**
358 * Migrate custom scripts for a specific post type and return the count of migrated posts.
359 *
360 * @param string $post_type The post type to query.
361 * @param string $old_meta_key The old combined script meta key.
362 * @param string $new_js_meta_key The new JS-only meta key.
363 * @param string $new_css_meta_key The new CSS-only meta key.
364 *
365 * @since 2.2.2
366 * @return int Number of posts migrated.
367 */
368 public function migrate_post_type_scripts_counted( $post_type, $old_meta_key, $new_js_meta_key, $new_css_meta_key ) {
369
370 global $wpdb;
371
372 $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
373 $wpdb->prepare(
374 "SELECT p.ID, pm.meta_value FROM {$wpdb->posts} p
375 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
376 WHERE p.post_type = %s AND pm.meta_key = %s AND pm.meta_value != ''
377 LIMIT 500",
378 $post_type,
379 $old_meta_key
380 )
381 );
382
383 if ( empty( $results ) || ! is_array( $results ) ) {
384 return 0;
385 }
386
387 $migrated = 0;
388
389 foreach ( $results as $row ) {
390 $post_id = $row->ID;
391 $old_value = $row->meta_value;
392
393 // Skip if the new meta fields already have values (don't overwrite manual entries).
394 $existing_js = get_post_meta( $post_id, $new_js_meta_key, true );
395 $existing_css = get_post_meta( $post_id, $new_css_meta_key, true );
396
397 if ( ! empty( $existing_js ) || ! empty( $existing_css ) ) {
398 continue;
399 }
400
401 // Decode HTML entities first since the old value was stored encoded.
402 $decoded = html_entity_decode( $old_value, ENT_QUOTES, 'UTF-8' );
403
404 // Extract CSS and JS from the combined script.
405 $parsed = $this->parse_combined_script( $decoded );
406
407 $did_migrate = false;
408
409 if ( ! empty( $parsed['css'] ) ) {
410 update_post_meta( $post_id, $new_css_meta_key, htmlentities( $parsed['css'] ) );
411 $did_migrate = true;
412 }
413
414 if ( ! empty( $parsed['js'] ) ) {
415 update_post_meta( $post_id, $new_js_meta_key, htmlentities( $parsed['js'] ) );
416 $did_migrate = true;
417 }
418
419 if ( $did_migrate ) {
420 ++$migrated;
421 }
422 }
423
424 return $migrated;
425 }
426
427 /**
428 * Stop the Knowledge Base docs option from being autoloaded on every request.
429 * Only the legacy admin renders the screen that reads it, so the new UI drops it entirely.
430 *
431 * @since x.x.x
432 * @return void
433 */
434 public function migrate_knowledge_base_docs_storage() {
435
436 // Earlier blocks in init() rewrite this option, so CARTFLOWS_LEGACY_ADMIN is stale here — see class-cartflows-loader.php:158 for the allowlist.
437 $legacy_admin_saved = get_option( 'cartflows-legacy-admin', false );
438 $is_legacy_admin = in_array( $legacy_admin_saved, array( true, 1, '1', 'enable', 'yes' ), true );
439
440 if ( ! $is_legacy_admin ) {
441 delete_option( 'cartflows_docs_data' );
442
443 if ( function_exists( 'as_unschedule_all_actions' ) ) {
444 as_unschedule_all_actions( 'cartflows_update_knowledge_base_data' );
445 }
446
447 return;
448 }
449
450 if ( false === get_option( 'cartflows_docs_data', false ) ) {
451 return;
452 }
453
454 // wp_set_option_autoload() landed in WP 6.4 and we still support 5.8.
455 if ( function_exists( 'wp_set_option_autoload' ) ) {
456 wp_set_option_autoload( 'cartflows_docs_data', false );
457 return;
458 }
459
460 $docs_data = get_option( 'cartflows_docs_data' );
461
462 delete_option( 'cartflows_docs_data' );
463 add_option( 'cartflows_docs_data', $docs_data, '', false );
464 }
465
466 /**
467 * Migrate old combined custom script meta to separate JS and CSS meta fields.
468 *
469 * Previously, a single textarea accepted both JS and CSS with <script>/<style> tags.
470 * Now we have separate CodeMirror editors for JS and CSS, so we need to split the
471 * old combined value into the new separate meta fields.
472 *
473 * @since 2.2.2
474 * @return void
475 */
476 public function migrate_custom_scripts() {
477
478 // Migrate step-level custom scripts.
479 $this->migrate_post_type_scripts(
480 'cartflows_step',
481 'wcf-custom-script',
482 'wcf-step-custom-js',
483 'wcf-step-custom-css'
484 );
485
486 // Migrate flow-level custom scripts.
487 $this->migrate_post_type_scripts(
488 'cartflows_flow',
489 'wcf-flow-custom-script',
490 'wcf-flow-custom-js',
491 'wcf-flow-custom-css'
492 );
493 }
494
495 /**
496 * Migrate custom scripts for a specific post type from old combined meta to separate JS/CSS meta.
497 *
498 * @param string $post_type The post type to query.
499 * @param string $old_meta_key The old combined script meta key.
500 * @param string $new_js_meta_key The new JS-only meta key.
501 * @param string $new_css_meta_key The new CSS-only meta key.
502 *
503 * @since 2.2.2
504 * @return void
505 */
506 public function migrate_post_type_scripts( $post_type, $old_meta_key, $new_js_meta_key, $new_css_meta_key ) {
507
508 global $wpdb;
509
510 // Get all posts of this type that have the old custom script meta with a non-empty value.
511 $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
512 $wpdb->prepare(
513 "SELECT p.ID, pm.meta_value FROM {$wpdb->posts} p
514 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
515 WHERE p.post_type = %s AND pm.meta_key = %s AND pm.meta_value != ''
516 LIMIT 500", // Limit to 500 posts per batch to avoid memory issues.
517 $post_type,
518 $old_meta_key
519 )
520 );
521
522 if ( empty( $results ) || ! is_array( $results ) ) {
523 return;
524 }
525
526 foreach ( $results as $row ) {
527 $post_id = $row->ID;
528 $old_value = $row->meta_value;
529
530 // Skip if the new meta fields already have values (don't overwrite manual entries).
531 $existing_js = get_post_meta( $post_id, $new_js_meta_key, true );
532 $existing_css = get_post_meta( $post_id, $new_css_meta_key, true );
533
534 if ( ! empty( $existing_js ) || ! empty( $existing_css ) ) {
535 continue;
536 }
537
538 // Decode HTML entities first since the old value was stored encoded.
539 $decoded = html_entity_decode( $old_value, ENT_QUOTES, 'UTF-8' );
540
541 // Extract CSS and JS from the combined script.
542 $parsed = $this->parse_combined_script( $decoded );
543
544 if ( ! empty( $parsed['css'] ) ) {
545 // Store encoded with htmlentities to match the FILTER_SCRIPT save format.
546 update_post_meta( $post_id, $new_css_meta_key, htmlentities( $parsed['css'] ) );
547 }
548
549 if ( ! empty( $parsed['js'] ) ) {
550 // Store encoded with htmlentities to match the FILTER_SCRIPT save format.
551 update_post_meta( $post_id, $new_js_meta_key, htmlentities( $parsed['js'] ) );
552 }
553 }
554 }
555
556 /**
557 * Parse a combined script string to extract CSS and JS portions.
558 *
559 * The old custom script field could contain:
560 * - <style>...CSS...</style> blocks
561 * - <script>...JS...</script> blocks
562 * - Bare JS code (no tags, treated as JS)
563 *
564 * @param string $content The combined script content (decoded).
565 * @return array Associative array with 'js' and 'css' keys.
566 *
567 * @since 2.2.2
568 */
569 public function parse_combined_script( $content ) {
570
571 $css = '';
572 $js = '';
573
574 // Extract all <style>...</style> blocks as CSS.
575 if ( preg_match_all( '/<style[^>]*>(.*?)<\/style>/is', $content, $style_matches ) ) {
576 $css = trim( implode( "\n", $style_matches[1] ) );
577 }
578
579 // Extract all <script>...</script> blocks as JS.
580 if ( preg_match_all( '/<script[^>]*>(.*?)<\/script>/is', $content, $script_matches ) ) {
581 $js = trim( implode( "\n", $script_matches[1] ) );
582 }
583
584 // Remove all <style>...</style> and <script>...</script> blocks from the content.
585 $remaining = preg_replace( '/<style[^>]*>.*?<\/style>/is', '', $content );
586 if ( null === $remaining ) {
587 $remaining = '';
588 }
589
590 $remaining = preg_replace( '/<script[^>]*>.*?<\/script>/is', '', $remaining );
591 if ( null === $remaining ) {
592 $remaining = '';
593 }
594
595 $remaining = trim( $remaining );
596
597 // Any remaining content (not inside tags) is treated as JS.
598 if ( ! empty( $remaining ) ) {
599 $js = trim( $js . "\n" . $remaining );
600 }
601
602 return array(
603 'css' => $css,
604 'js' => $js,
605 );
606 }
607
608 /**
609 * Init
610 *
611 * @since 1.0.0
612 * @return void
613 */
614 public function changed_wp_templates() {
615
616 global $wpdb;
617
618 $query_results = $wpdb->get_results(
619 $wpdb->prepare(
620 "SELECT {$wpdb->posts}.ID FROM {$wpdb->posts} LEFT JOIN {$wpdb->postmeta} ON ( {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id )
621 where {$wpdb->posts}.post_type = %s AND {$wpdb->postmeta}.meta_key = %s AND {$wpdb->postmeta}.meta_value != %s AND {$wpdb->postmeta}.meta_value != %s",
622 'cartflows_step',
623 '_wp_page_template',
624 'cartflows-canvas',
625 'cartflows-default'
626 )
627 ); // db call ok; no-cache ok.
628
629 if ( is_array( $query_results ) && ! empty( $query_results ) ) {
630
631 require_once CARTFLOWS_DIR . 'classes/importer/batch-process/class-cartflows-change-template-batch.php';
632
633 wcf()->logger->log( '(✓) Update Templates BATCH Started!' );
634
635 $change_template_batch = new Cartflows_Change_Template_Batch();
636
637 foreach ( $query_results as $query_result ) {
638
639 wcf()->logger->log( '(✓) POST ID ' . $query_result->ID );
640 $change_template_batch->push_to_queue( $query_result->ID );
641 }
642
643 $change_template_batch->save()->dispatch();
644 }
645 }
646 }
647 /**
648 * Kicking this off by calling 'get_instance()' method
649 */
650 Cartflows_Update::get_instance();
651
652 endif;
653