PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.1.1
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.1.1
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.1.1, at classes/class-cartflows-update.php

609 lines 17.7 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 // Update auto saved version number.
182 update_option( 'cartflows-version', CARTFLOWS_VER );
183
184 // Update cartflows asset version to regenerate the dynamic css. We are using the time() function to add the random number.
185 update_option( 'cartflows-assets-version', time() );
186
187 do_action( 'cartflows_update_after' );
188 }
189
190 /**
191 * Loading logger files.
192 *
193 * @since 1.0.0
194 * @return void
195 */
196 public function logger_files() {
197
198 if ( ! defined( 'CARTFLOWS_LOG_DIR' ) ) {
199
200 $upload_dir = wp_upload_dir( null, false );
201
202 define( 'CARTFLOWS_LOG_DIR', $upload_dir['basedir'] . '/cartflows-logs/' );
203 }
204
205 wcf()->create_files();
206 }
207
208 /**
209 * Set script migration status.
210 *
211 * @param string $status Migration status value.
212 * @since 2.2.2
213 * @return void
214 */
215 public function set_script_migration_status( $status ) {
216 update_option( 'cartflows_script_migration_status', $status );
217 }
218
219 /**
220 * Determine if the migration notice should be shown.
221 *
222 * Returns false if migration is completed or accepted.
223 * Returns false if status is 'skipped' and 7 days have not yet passed.
224 * Returns false if no old script values exist (auto-sets status to 'completed').
225 *
226 * @since 2.2.2
227 * @return bool
228 */
229 public function should_show_migration_notice() {
230 $status = \CartFlows_Helper::get_script_migration_status();
231
232 if ( 'completed' === $status || 'accepted' === $status ) {
233 return false;
234 }
235
236 if ( 'skipped' === $status ) {
237 $skip_timestamp = (int) get_option( 'cartflows_script_migration_skip_timestamp', 0 );
238
239 // Show again only after 7 days have passed since last skip.
240 if ( ( time() - $skip_timestamp ) < WEEK_IN_SECONDS ) {
241 return false;
242 }
243 }
244
245 // Check cached result first to avoid running the meta query on every page load.
246 $has_old_scripts = get_option( 'cartflows_has_old_custom_scripts', '' );
247
248 if ( '' === $has_old_scripts ) {
249 // First time check — run the query once and cache the result.
250 $has_old_scripts = $this->has_old_custom_scripts() ? 'yes' : 'no';
251 update_option( 'cartflows_has_old_custom_scripts', $has_old_scripts, true );
252 }
253
254 // If no old script values exist, auto-complete migration and enable the code editor.
255 if ( 'yes' !== $has_old_scripts ) {
256 $this->set_script_migration_status( 'completed' );
257 return false;
258 }
259
260 return true;
261 }
262
263 /**
264 * Check if any posts have old combined custom script meta values.
265 *
266 * Queries the database for any cartflows_step or cartflows_flow posts
267 * that have non-empty 'wcf-custom-script' or 'wcf-flow-custom-script' meta.
268 *
269 * @since 2.2.3
270 * @return bool True if old script data exists, false otherwise.
271 */
272 public function has_old_custom_scripts() {
273
274 global $wpdb;
275
276 $result = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
277 $wpdb->prepare(
278 "SELECT 1 FROM {$wpdb->postmeta} pm
279 INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
280 WHERE p.post_type IN (%s, %s)
281 AND pm.meta_key IN (%s, %s)
282 AND pm.meta_value != ''
283 LIMIT 1",
284 'cartflows_step',
285 'cartflows_flow',
286 'wcf-custom-script',
287 'wcf-flow-custom-script'
288 )
289 );
290
291 return ( null !== $result );
292 }
293
294 /**
295 * Get current migration skip count.
296 *
297 * @since 2.2.2
298 * @return int
299 */
300 public function get_migration_skip_count() {
301 return (int) get_option( 'cartflows_script_migration_skip_count', 0 );
302 }
303
304 /**
305 * Increment migration skip count and record the skip timestamp.
306 *
307 * @since 2.2.2
308 * @return void
309 */
310 public function increment_migration_skip_count() {
311 $count = $this->get_migration_skip_count();
312 update_option( 'cartflows_script_migration_skip_count', $count + 1 );
313 update_option( 'cartflows_script_migration_skip_timestamp', time() );
314 }
315
316 /**
317 * Run on-demand migration of custom scripts triggered by user action.
318 *
319 * Unlike migrate_custom_scripts() which runs during version updates,
320 * this method is called only when the user explicitly clicks "Migrate Data"
321 * in the admin notice. It returns a count of migrated posts and updates
322 * the migration status to 'completed'.
323 *
324 * @since 2.2.2
325 * @return int Number of posts that had data migrated.
326 */
327 public function migrate_custom_scripts_on_demand() {
328
329 $migrated_count = 0;
330
331 // Migrate step-level custom scripts.
332 $migrated_count += $this->migrate_post_type_scripts_counted(
333 'cartflows_step',
334 'wcf-custom-script',
335 'wcf-step-custom-js',
336 'wcf-step-custom-css'
337 );
338
339 // Migrate flow-level custom scripts.
340 $migrated_count += $this->migrate_post_type_scripts_counted(
341 'cartflows_flow',
342 'wcf-flow-custom-script',
343 'wcf-flow-custom-js',
344 'wcf-flow-custom-css'
345 );
346
347 $this->set_script_migration_status( 'completed' );
348
349 return $migrated_count;
350 }
351
352 /**
353 * Migrate custom scripts for a specific post type and return the count of migrated posts.
354 *
355 * @param string $post_type The post type to query.
356 * @param string $old_meta_key The old combined script meta key.
357 * @param string $new_js_meta_key The new JS-only meta key.
358 * @param string $new_css_meta_key The new CSS-only meta key.
359 *
360 * @since 2.2.2
361 * @return int Number of posts migrated.
362 */
363 public function migrate_post_type_scripts_counted( $post_type, $old_meta_key, $new_js_meta_key, $new_css_meta_key ) {
364
365 global $wpdb;
366
367 $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
368 $wpdb->prepare(
369 "SELECT p.ID, pm.meta_value FROM {$wpdb->posts} p
370 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
371 WHERE p.post_type = %s AND pm.meta_key = %s AND pm.meta_value != ''
372 LIMIT 500",
373 $post_type,
374 $old_meta_key
375 )
376 );
377
378 if ( empty( $results ) || ! is_array( $results ) ) {
379 return 0;
380 }
381
382 $migrated = 0;
383
384 foreach ( $results as $row ) {
385 $post_id = $row->ID;
386 $old_value = $row->meta_value;
387
388 // Skip if the new meta fields already have values (don't overwrite manual entries).
389 $existing_js = get_post_meta( $post_id, $new_js_meta_key, true );
390 $existing_css = get_post_meta( $post_id, $new_css_meta_key, true );
391
392 if ( ! empty( $existing_js ) || ! empty( $existing_css ) ) {
393 continue;
394 }
395
396 // Decode HTML entities first since the old value was stored encoded.
397 $decoded = html_entity_decode( $old_value, ENT_QUOTES, 'UTF-8' );
398
399 // Extract CSS and JS from the combined script.
400 $parsed = $this->parse_combined_script( $decoded );
401
402 $did_migrate = false;
403
404 if ( ! empty( $parsed['css'] ) ) {
405 update_post_meta( $post_id, $new_css_meta_key, htmlentities( $parsed['css'] ) );
406 $did_migrate = true;
407 }
408
409 if ( ! empty( $parsed['js'] ) ) {
410 update_post_meta( $post_id, $new_js_meta_key, htmlentities( $parsed['js'] ) );
411 $did_migrate = true;
412 }
413
414 if ( $did_migrate ) {
415 ++$migrated;
416 }
417 }
418
419 return $migrated;
420 }
421
422 /**
423 * Migrate old combined custom script meta to separate JS and CSS meta fields.
424 *
425 * Previously, a single textarea accepted both JS and CSS with <script>/<style> tags.
426 * Now we have separate CodeMirror editors for JS and CSS, so we need to split the
427 * old combined value into the new separate meta fields.
428 *
429 * @since 2.2.2
430 * @return void
431 */
432 public function migrate_custom_scripts() {
433
434 // Migrate step-level custom scripts.
435 $this->migrate_post_type_scripts(
436 'cartflows_step',
437 'wcf-custom-script',
438 'wcf-step-custom-js',
439 'wcf-step-custom-css'
440 );
441
442 // Migrate flow-level custom scripts.
443 $this->migrate_post_type_scripts(
444 'cartflows_flow',
445 'wcf-flow-custom-script',
446 'wcf-flow-custom-js',
447 'wcf-flow-custom-css'
448 );
449 }
450
451 /**
452 * Migrate custom scripts for a specific post type from old combined meta to separate JS/CSS meta.
453 *
454 * @param string $post_type The post type to query.
455 * @param string $old_meta_key The old combined script meta key.
456 * @param string $new_js_meta_key The new JS-only meta key.
457 * @param string $new_css_meta_key The new CSS-only meta key.
458 *
459 * @since 2.2.2
460 * @return void
461 */
462 public function migrate_post_type_scripts( $post_type, $old_meta_key, $new_js_meta_key, $new_css_meta_key ) {
463
464 global $wpdb;
465
466 // Get all posts of this type that have the old custom script meta with a non-empty value.
467 $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
468 $wpdb->prepare(
469 "SELECT p.ID, pm.meta_value FROM {$wpdb->posts} p
470 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
471 WHERE p.post_type = %s AND pm.meta_key = %s AND pm.meta_value != ''
472 LIMIT 500", // Limit to 500 posts per batch to avoid memory issues.
473 $post_type,
474 $old_meta_key
475 )
476 );
477
478 if ( empty( $results ) || ! is_array( $results ) ) {
479 return;
480 }
481
482 foreach ( $results as $row ) {
483 $post_id = $row->ID;
484 $old_value = $row->meta_value;
485
486 // Skip if the new meta fields already have values (don't overwrite manual entries).
487 $existing_js = get_post_meta( $post_id, $new_js_meta_key, true );
488 $existing_css = get_post_meta( $post_id, $new_css_meta_key, true );
489
490 if ( ! empty( $existing_js ) || ! empty( $existing_css ) ) {
491 continue;
492 }
493
494 // Decode HTML entities first since the old value was stored encoded.
495 $decoded = html_entity_decode( $old_value, ENT_QUOTES, 'UTF-8' );
496
497 // Extract CSS and JS from the combined script.
498 $parsed = $this->parse_combined_script( $decoded );
499
500 if ( ! empty( $parsed['css'] ) ) {
501 // Store encoded with htmlentities to match the FILTER_SCRIPT save format.
502 update_post_meta( $post_id, $new_css_meta_key, htmlentities( $parsed['css'] ) );
503 }
504
505 if ( ! empty( $parsed['js'] ) ) {
506 // Store encoded with htmlentities to match the FILTER_SCRIPT save format.
507 update_post_meta( $post_id, $new_js_meta_key, htmlentities( $parsed['js'] ) );
508 }
509 }
510 }
511
512 /**
513 * Parse a combined script string to extract CSS and JS portions.
514 *
515 * The old custom script field could contain:
516 * - <style>...CSS...</style> blocks
517 * - <script>...JS...</script> blocks
518 * - Bare JS code (no tags, treated as JS)
519 *
520 * @param string $content The combined script content (decoded).
521 * @return array Associative array with 'js' and 'css' keys.
522 *
523 * @since 2.2.2
524 */
525 public function parse_combined_script( $content ) {
526
527 $css = '';
528 $js = '';
529
530 // Extract all <style>...</style> blocks as CSS.
531 if ( preg_match_all( '/<style[^>]*>(.*?)<\/style>/is', $content, $style_matches ) ) {
532 $css = trim( implode( "\n", $style_matches[1] ) );
533 }
534
535 // Extract all <script>...</script> blocks as JS.
536 if ( preg_match_all( '/<script[^>]*>(.*?)<\/script>/is', $content, $script_matches ) ) {
537 $js = trim( implode( "\n", $script_matches[1] ) );
538 }
539
540 // Remove all <style>...</style> and <script>...</script> blocks from the content.
541 $remaining = preg_replace( '/<style[^>]*>.*?<\/style>/is', '', $content );
542 if ( null === $remaining ) {
543 $remaining = '';
544 }
545
546 $remaining = preg_replace( '/<script[^>]*>.*?<\/script>/is', '', $remaining );
547 if ( null === $remaining ) {
548 $remaining = '';
549 }
550
551 $remaining = trim( $remaining );
552
553 // Any remaining content (not inside tags) is treated as JS.
554 if ( ! empty( $remaining ) ) {
555 $js = trim( $js . "\n" . $remaining );
556 }
557
558 return array(
559 'css' => $css,
560 'js' => $js,
561 );
562 }
563
564 /**
565 * Init
566 *
567 * @since 1.0.0
568 * @return void
569 */
570 public function changed_wp_templates() {
571
572 global $wpdb;
573
574 $query_results = $wpdb->get_results(
575 $wpdb->prepare(
576 "SELECT {$wpdb->posts}.ID FROM {$wpdb->posts} LEFT JOIN {$wpdb->postmeta} ON ( {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id )
577 where {$wpdb->posts}.post_type = %s AND {$wpdb->postmeta}.meta_key = %s AND {$wpdb->postmeta}.meta_value != %s AND {$wpdb->postmeta}.meta_value != %s",
578 'cartflows_step',
579 '_wp_page_template',
580 'cartflows-canvas',
581 'cartflows-default'
582 )
583 ); // db call ok; no-cache ok.
584
585 if ( is_array( $query_results ) && ! empty( $query_results ) ) {
586
587 require_once CARTFLOWS_DIR . 'classes/importer/batch-process/class-cartflows-change-template-batch.php';
588
589 wcf()->logger->log( '(✓) Update Templates BATCH Started!' );
590
591 $change_template_batch = new Cartflows_Change_Template_Batch();
592
593 foreach ( $query_results as $query_result ) {
594
595 wcf()->logger->log( '(✓) POST ID ' . $query_result->ID );
596 $change_template_batch->push_to_queue( $query_result->ID );
597 }
598
599 $change_template_batch->save()->dispatch();
600 }
601 }
602 }
603 /**
604 * Kicking this off by calling 'get_instance()' method
605 */
606 Cartflows_Update::get_instance();
607
608 endif;
609