logger_files();
if ( version_compare( $saved_version, '1.1.22', '<' ) ) {
update_option( 'wcf_setup_skipped', true );
}
if ( version_compare( $saved_version, '1.2.0', '<' ) ) {
$this->changed_wp_templates();
}
/* Add legacy admin option */
if ( version_compare( $saved_version, '1.6.0', '<' ) ) {
update_option( 'cartflows-old-ui-user', true );
update_option( 'cartflows-legacy-admin', true );
update_option( 'cartflows-legacy-meta-show-design-options', true );
}
/* Updating meta for global checkout migration & Permalinks */
if ( version_compare( $saved_version, '1.10.0', '<' ) ) {
$global_checkout = \Cartflows_Helper::get_common_setting( 'global_checkout' );
if ( $global_checkout ) {
$flow_id = wcf()->utils->get_flow_id_from_step_id( $global_checkout );
if ( $flow_id ) {
update_option( '_cartflows_store_checkout', $flow_id );
update_option( '_cartflows_old_global_checkout', $global_checkout );
delete_post_meta( $global_checkout, 'wcf-checkout-products' );
}
}
update_option( 'cartflows_show_weekly_report_email_notice', 'yes' );
$permalink_settings = Cartflows_Helper::get_admin_settings_option( '_cartflows_permalink', false, false );
if ( ! $permalink_settings ) {
$default_settings = array(
'permalink' => CARTFLOWS_STEP_POST_TYPE,
'permalink_flow_base' => CARTFLOWS_FLOW_POST_TYPE,
'permalink_structure' => '',
);
Cartflows_Helper::update_admin_settings_option( '_cartflows_permalink', $default_settings );
}
}
// Update required license key to lowercase. Can be removed after 3 major update.
if ( version_compare( $saved_version, '1.11.1', '<' ) && _is_cartflows_pro() ) {
$api_key = get_option( 'wc_am_client_CartFlows_api_key' );
$api_key_data = get_option( 'wc_am_client_CartFlows' );
// Take backup of options.
update_option(
'cartflows_license_backup_data',
array(
'wc_am_client_cartflows_api_key' => $api_key,
'wc_am_client_cartflows' => $api_key_data,
)
);
delete_option( 'wc_am_client_CartFlows_api_key' );
delete_option( 'wc_am_client_CartFlows' );
// Delete the cached value for old user ( before v1.11.0 and after v1.11.0 ) so WP can add new value.
wp_cache_flush();
if ( $api_key_data ) {
$new_data = array(
'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'],
);
update_option( 'wc_am_client_cartflows', $new_data );
}
if ( $api_key ) {
update_option( 'wc_am_client_cartflows_api_key', $api_key );
}
}
// Set default page builder to "Other" for those who have set it as "DIVI".
if ( version_compare( $saved_version, '2.0.0', '<' ) ) {
$common_settings = get_option( '_cartflows_common', array() );
if ( is_array( $common_settings ) && 'divi' === $common_settings['default_page_builder'] ) {
$common_settings['default_page_builder'] = 'elementor';
update_option( '_cartflows_common', $common_settings );
}
}
// Migrate old combined custom script meta to separate JS and CSS meta fields.
if ( version_compare( $saved_version, '2.2.2', '<' ) ) {
$this->migrate_custom_scripts();
}
// 3.0 ships a redesigned admin UI. Existing users (saved_version < 3.0.0) keep the
// legacy admin until they opt into the new UI from the in-app notice or the
// Advanced-tab toggle. Saved as 'enable' to match the toggle's backComp format
// so the field round-trips correctly. Fresh installs short-circuit at the
// `! $saved_version` early return above and default to the new UI.
if ( is_string( $saved_version ) && version_compare( $saved_version, '3.0.0', '<' ) ) {
update_option( 'cartflows-legacy-admin', 'enable' );
}
// Update auto saved version number.
update_option( 'cartflows-version', CARTFLOWS_VER );
// Update cartflows asset version to regenerate the dynamic css. We are using the time() function to add the random number.
update_option( 'cartflows-assets-version', time() );
do_action( 'cartflows_update_after' );
}
/**
* Loading logger files.
*
* @since 1.0.0
* @return void
*/
public function logger_files() {
if ( ! defined( 'CARTFLOWS_LOG_DIR' ) ) {
$upload_dir = wp_upload_dir( null, false );
define( 'CARTFLOWS_LOG_DIR', $upload_dir['basedir'] . '/cartflows-logs/' );
}
wcf()->create_files();
}
/**
* Set script migration status.
*
* @param string $status Migration status value.
* @since 2.2.2
* @return void
*/
public function set_script_migration_status( $status ) {
update_option( 'cartflows_script_migration_status', $status );
}
/**
* Determine if the migration notice should be shown.
*
* Returns false if migration is completed or accepted.
* Returns false if status is 'skipped' and 7 days have not yet passed.
* Returns false if no old script values exist (auto-sets status to 'completed').
*
* @since 2.2.2
* @return bool
*/
public function should_show_migration_notice() {
$status = \CartFlows_Helper::get_script_migration_status();
if ( 'completed' === $status || 'accepted' === $status ) {
return false;
}
if ( 'skipped' === $status ) {
$skip_timestamp = (int) get_option( 'cartflows_script_migration_skip_timestamp', 0 );
// Show again only after 7 days have passed since last skip.
if ( ( time() - $skip_timestamp ) < WEEK_IN_SECONDS ) {
return false;
}
}
// Check cached result first to avoid running the meta query on every page load.
$has_old_scripts = get_option( 'cartflows_has_old_custom_scripts', '' );
if ( '' === $has_old_scripts ) {
// First time check — run the query once and cache the result.
$has_old_scripts = $this->has_old_custom_scripts() ? 'yes' : 'no';
update_option( 'cartflows_has_old_custom_scripts', $has_old_scripts, true );
}
// If no old script values exist, auto-complete migration and enable the code editor.
if ( 'yes' !== $has_old_scripts ) {
$this->set_script_migration_status( 'completed' );
return false;
}
return true;
}
/**
* Check if any posts have old combined custom script meta values.
*
* Queries the database for any cartflows_step or cartflows_flow posts
* that have non-empty 'wcf-custom-script' or 'wcf-flow-custom-script' meta.
*
* @since 2.2.3
* @return bool True if old script data exists, false otherwise.
*/
public function has_old_custom_scripts() {
global $wpdb;
$result = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->prepare(
"SELECT 1 FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
WHERE p.post_type IN (%s, %s)
AND pm.meta_key IN (%s, %s)
AND pm.meta_value != ''
LIMIT 1",
'cartflows_step',
'cartflows_flow',
'wcf-custom-script',
'wcf-flow-custom-script'
)
);
return ( null !== $result );
}
/**
* Get current migration skip count.
*
* @since 2.2.2
* @return int
*/
public function get_migration_skip_count() {
return (int) get_option( 'cartflows_script_migration_skip_count', 0 );
}
/**
* Increment migration skip count and record the skip timestamp.
*
* @since 2.2.2
* @return void
*/
public function increment_migration_skip_count() {
$count = $this->get_migration_skip_count();
update_option( 'cartflows_script_migration_skip_count', $count + 1 );
update_option( 'cartflows_script_migration_skip_timestamp', time() );
}
/**
* Run on-demand migration of custom scripts triggered by user action.
*
* Unlike migrate_custom_scripts() which runs during version updates,
* this method is called only when the user explicitly clicks "Migrate Data"
* in the admin notice. It returns a count of migrated posts and updates
* the migration status to 'completed'.
*
* @since 2.2.2
* @return int Number of posts that had data migrated.
*/
public function migrate_custom_scripts_on_demand() {
$migrated_count = 0;
// Migrate step-level custom scripts.
$migrated_count += $this->migrate_post_type_scripts_counted(
'cartflows_step',
'wcf-custom-script',
'wcf-step-custom-js',
'wcf-step-custom-css'
);
// Migrate flow-level custom scripts.
$migrated_count += $this->migrate_post_type_scripts_counted(
'cartflows_flow',
'wcf-flow-custom-script',
'wcf-flow-custom-js',
'wcf-flow-custom-css'
);
$this->set_script_migration_status( 'completed' );
return $migrated_count;
}
/**
* Migrate custom scripts for a specific post type and return the count of migrated posts.
*
* @param string $post_type The post type to query.
* @param string $old_meta_key The old combined script meta key.
* @param string $new_js_meta_key The new JS-only meta key.
* @param string $new_css_meta_key The new CSS-only meta key.
*
* @since 2.2.2
* @return int Number of posts migrated.
*/
public function migrate_post_type_scripts_counted( $post_type, $old_meta_key, $new_js_meta_key, $new_css_meta_key ) {
global $wpdb;
$results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->prepare(
"SELECT p.ID, pm.meta_value FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = %s AND pm.meta_key = %s AND pm.meta_value != ''
LIMIT 500",
$post_type,
$old_meta_key
)
);
if ( empty( $results ) || ! is_array( $results ) ) {
return 0;
}
$migrated = 0;
foreach ( $results as $row ) {
$post_id = $row->ID;
$old_value = $row->meta_value;
// Skip if the new meta fields already have values (don't overwrite manual entries).
$existing_js = get_post_meta( $post_id, $new_js_meta_key, true );
$existing_css = get_post_meta( $post_id, $new_css_meta_key, true );
if ( ! empty( $existing_js ) || ! empty( $existing_css ) ) {
continue;
}
// Decode HTML entities first since the old value was stored encoded.
$decoded = html_entity_decode( $old_value, ENT_QUOTES, 'UTF-8' );
// Extract CSS and JS from the combined script.
$parsed = $this->parse_combined_script( $decoded );
$did_migrate = false;
if ( ! empty( $parsed['css'] ) ) {
update_post_meta( $post_id, $new_css_meta_key, htmlentities( $parsed['css'] ) );
$did_migrate = true;
}
if ( ! empty( $parsed['js'] ) ) {
update_post_meta( $post_id, $new_js_meta_key, htmlentities( $parsed['js'] ) );
$did_migrate = true;
}
if ( $did_migrate ) {
++$migrated;
}
}
return $migrated;
}
/**
* Migrate old combined custom script meta to separate JS and CSS meta fields.
*
* Previously, a single textarea accepted both JS and CSS with blocks
* - Bare JS code (no tags, treated as JS)
*
* @param string $content The combined script content (decoded).
* @return array Associative array with 'js' and 'css' keys.
*
* @since 2.2.2
*/
public function parse_combined_script( $content ) {
$css = '';
$js = '';
// Extract all blocks as CSS.
if ( preg_match_all( '/ and blocks from the content.
$remaining = preg_replace( '/