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

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