diff --git a/admin-page.php b/admin-page.php
new file mode 100644
index 0000000..ede24d1
--- /dev/null
+++ b/admin-page.php
@@ -0,0 +1,246 @@
+
+ * @since 1.0.0
+ * @return string
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+/**
+ * Render the export settings page
+ *
+ * @since 1.0.0
+ * @return void
+ */
+function dbvc_render_export_page() {
+ // Check user capabilities
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'dbvc' ) );
+ }
+
+ $custom_path = get_option( 'dbvc_sync_path', '' );
+ $selected_post_types = get_option( 'dbvc_post_types', [] );
+
+ // Handle custom sync path form.
+ if ( isset( $_POST['dbvc_sync_path_save'] ) && wp_verify_nonce( $_POST['dbvc_sync_path_nonce'], 'dbvc_sync_path_action' ) ) {
+ // Additional capability check
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'You do not have sufficient permissions to perform this action.', 'dbvc' ) );
+ }
+
+ $new_path = sanitize_text_field( wp_unslash( $_POST['dbvc_sync_path'] ) );
+
+ // Validate path to prevent directory traversal
+ $new_path = dbvc_validate_sync_path( $new_path );
+ if ( false === $new_path ) {
+ echo '
' . esc_html__( 'Invalid sync path provided. Path cannot contain ../ or other unsafe characters.', 'dbvc' ) . '
';
+ } else {
+ update_option( 'dbvc_sync_path', $new_path );
+ $custom_path = $new_path;
+
+ // Create the directory immediately to test the path.
+ $resolved_path = dbvc_get_sync_path();
+ if ( wp_mkdir_p( $resolved_path ) ) {
+ echo '' . sprintf( esc_html__( 'Sync folder updated and created at: %s', 'dbvc' ), '' . esc_html( $resolved_path ) . '' ) . '
';
+ } else {
+ echo '' . sprintf( esc_html__( 'Sync folder setting saved, but could not create directory at: %s. Please check permissions.', 'dbvc' ), '' . esc_html( $resolved_path ) . '' ) . '
';
+ }
+ }
+ }
+
+ // Handle post types selection form.
+ if ( isset( $_POST['dbvc_post_types_save'] ) && wp_verify_nonce( $_POST['dbvc_post_types_nonce'], 'dbvc_post_types_action' ) ) {
+ // Additional capability check
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'You do not have sufficient permissions to perform this action.', 'dbvc' ) );
+ }
+
+ $new_post_types = [];
+ if ( isset( $_POST['dbvc_post_types'] ) && is_array( $_POST['dbvc_post_types'] ) ) {
+ $new_post_types = array_map( 'sanitize_text_field', wp_unslash( $_POST['dbvc_post_types'] ) );
+
+ // Get all valid post types (public + FSE types)
+ $valid_post_types = get_post_types( [ 'public' => true ] );
+
+ // Add FSE post types to valid list if block theme is active
+ if ( wp_is_block_theme() ) {
+ $fse_types = [ 'wp_template', 'wp_template_part', 'wp_global_styles', 'wp_navigation' ];
+ $valid_post_types = array_merge( $valid_post_types, array_combine( $fse_types, $fse_types ) );
+ }
+
+ // Filter to only include valid post types
+ $new_post_types = array_intersect( $new_post_types, array_keys( $valid_post_types ) );
+ }
+
+ update_option( 'dbvc_post_types', $new_post_types );
+ $selected_post_types = $new_post_types;
+ echo '' . esc_html__( 'Post types selection updated!', 'dbvc' ) . '
';
+ }
+
+ // Handle export form.
+ if ( isset( $_POST['dbvc_export_nonce'] ) && wp_verify_nonce( $_POST['dbvc_export_nonce'], 'dbvc_export_action' ) ) {
+ // Additional capability check
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'You do not have sufficient permissions to perform this action.', 'dbvc' ) );
+ }
+
+ // Run full export.
+ DBVC_Sync_Posts::export_options_to_json();
+ DBVC_Sync_Posts::export_menus_to_json();
+
+ $posts = get_posts( [
+ 'post_type' => 'any',
+ 'posts_per_page' => -1,
+ 'post_status' => 'any',
+ ] );
+
+ foreach ( $posts as $post ) {
+ DBVC_Sync_Posts::export_post_to_json( $post->ID, $post );
+ }
+
+ // Create dated backup of export .json files - added 08/04/2025
+ if ( method_exists( 'DBVC_Sync_Posts', 'dbvc_create_backup_folder_and_copy_exports' ) ) {
+ DBVC_Sync_Posts::dbvc_create_backup_folder_and_copy_exports();
+} else {
+ error_log( '[DBVC] Static method dbvc_create_backup_folder_and_copy_exports not found in DBVC_Sync_Posts.' );
+}
+
+
+
+ echo '' . esc_html__( 'Full export completed!', 'dbvc' ) . '
';
+ }
+
+ if ( isset( $_POST['dbvc_import_button'] ) && wp_verify_nonce( $_POST['dbvc_import_nonce'], 'dbvc_import_action' ) ) {
+ if ( current_user_can( 'manage_options' ) ) {
+ $smart_import = ! empty( $_POST['dbvc_smart_import'] );
+ $import_menus = ! empty( $_POST['dbvc_import_menus'] );
+
+ DBVC_Sync_Posts::import_all( 0, $smart_import );
+
+ if ( $import_menus ) {
+ DBVC_Sync_Posts::import_menus_from_json();
+ }
+
+ echo '' . esc_html__( 'Import completed.', 'dbvc' ) . '
';
+ } else {
+ wp_die( esc_html__( 'You do not have sufficient permissions to perform this action.', 'dbvc' ) );
+ }
+}
+
+ // Get the current resolved path for display.
+ $resolved_path = dbvc_get_sync_path();
+
+ // Get all public post types.
+ $all_post_types = dbvc_get_available_post_types();
+
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true ], 'objects' );
+
+ // Add FSE post types if block theme is active
+ if ( wp_is_block_theme() ) {
+ $fse_types = [
+ 'wp_template' => (object) [
+ 'label' => __( 'Templates (FSE)', 'dbvc' ),
+ 'name' => 'wp_template'
+ ],
+ 'wp_template_part' => (object) [
+ 'label' => __( 'Template Parts (FSE)', 'dbvc' ),
+ 'name' => 'wp_template_part'
+ ],
+ 'wp_global_styles' => (object) [
+ 'label' => __( 'Global Styles (FSE)', 'dbvc' ),
+ 'name' => 'wp_global_styles'
+ ],
+ 'wp_navigation' => (object) [
+ 'label' => __( 'Navigation (FSE)', 'dbvc' ),
+ 'name' => 'wp_navigation'
+ ],
+ ];
+
+ $post_types = array_merge( $post_types, $fse_types );
+ }
+
+ return $post_types;
+}
diff --git a/class-menu-importer.php b/class-menu-importer.php
new file mode 100644
index 0000000..e3cf816
--- /dev/null
+++ b/class-menu-importer.php
@@ -0,0 +1,147 @@
+get_results(
+ "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_dbvc_original_id'"
+ );
+ foreach ( $imported_ids as $row ) {
+ $post_map[ $row->meta_value ] = $row->post_id;
+ }
+
+ foreach ( $menus as $menu_data ) {
+ if ( ! isset( $menu_data['name'] ) || ! is_array( $menu_data['items'] ?? null ) ) continue;
+
+ $existing_menu = wp_get_nav_menu_object( $menu_data['name'] );
+ $menu_id = $existing_menu ? $existing_menu->term_id : wp_create_nav_menu( $menu_data['name'] );
+
+ if ( is_wp_error( $menu_id ) ) {
+ error_log( '[DBVC] Failed to create/reuse menu "' . $menu_data['name'] . '"' );
+ continue;
+ }
+
+ if ( $existing_menu ) {
+ foreach ( wp_get_nav_menu_items( $menu_id ) as $old_item ) {
+ wp_delete_post( $old_item->ID, true );
+ }
+ }
+
+ $id_map = [];
+ $meta_map = [];
+ foreach ( $menu_data['items'] as $item ) {
+ $original_id = (int)( $item['ID'] ?? 0 );
+ $object_id = isset( $item['object_id'] ) ? (int)( $post_map[ $item['object_id'] ] ?? $item['object_id'] ) : 0;
+
+ $new_id = wp_update_nav_menu_item( $menu_id, 0, [
+ 'menu-item-title' => $item['title'] ?? '',
+ 'menu-item-object' => $item['object'] ?? '',
+ 'menu-item-object-id' => $object_id,
+ 'menu-item-type' => $item['type'] ?? '',
+ 'menu-item-status' => 'publish',
+ 'menu-item-url' => $item['url'] ?? '',
+ 'menu-item-attr-title' => $item['attr_title'] ?? '',
+ 'menu-item-description'=> $item['description'] ?? '',
+ 'menu-item-target' => $item['target'] ?? '',
+ 'menu-item-xfn' => $item['xfn'] ?? '',
+ 'menu-item-classes' => implode( ' ', $item['classes'] ?? [] ),
+ ]);
+
+ if ( ! is_wp_error( $new_id ) ) {
+ $id_map[ $original_id ] = $new_id;
+ $meta_map[ $new_id ] = $item['meta'] ?? [];
+ }
+ }
+
+ foreach ( $menu_data['items'] as $item ) {
+ $original_id = (int)( $item['ID'] ?? 0 );
+ $parent_original_id = (int)( $item['menu_item_parent'] ?? 0 );
+
+ if ( $parent_original_id && isset( $id_map[ $original_id ], $id_map[ $parent_original_id ] ) ) {
+ wp_update_post([
+ 'ID' => $id_map[ $original_id ],
+ 'post_parent' => $id_map[ $parent_original_id ],
+ ]);
+ }
+
+ // Restore custom meta if available
+ $new_id = $id_map[ $original_id ] ?? 0;
+ if ( $new_id && ! empty( $meta_map[ $new_id ] ) ) {
+ foreach ( $meta_map[ $new_id ] as $key => $val ) {
+ update_post_meta( $new_id, $key, maybe_unserialize( $val ) );
+ }
+ }
+ }
+
+ if ( isset( $menu_data['locations'] ) && is_array( $menu_data['locations'] ) ) {
+ $locations = get_nav_menu_locations();
+ foreach ( $menu_data['locations'] as $loc ) {
+ $locations[ $loc ] = $menu_id;
+ }
+ set_theme_mod( 'nav_menu_locations', $locations );
+ }
+ }
+ }
+
+ /**
+ * Export menus and preserve all meta.
+ */
+ public static function export_menus_to_json() {
+ $menus = wp_get_nav_menus();
+ $data = [];
+
+ foreach ( $menus as $menu ) {
+ $items = wp_get_nav_menu_items( $menu->term_id );
+ $item_array = [];
+
+ foreach ( $items as $item ) {
+ $meta = get_post_meta( $item->ID );
+ $flat_meta = [];
+ foreach ( $meta as $k => $v ) {
+ $flat_meta[ $k ] = maybe_serialize( $v[0] ?? '' );
+ }
+
+ $arr = (array) $item;
+ $arr['meta'] = $flat_meta;
+ $item_array[] = $arr;
+ }
+
+ $menu_data = [
+ 'name' => $menu->name,
+ 'slug' => $menu->slug,
+ 'locations' => array_keys( array_filter( get_nav_menu_locations(), fn( $id ) => $id === $menu->term_id ) ),
+ 'items' => $item_array,
+ ];
+
+ $data[] = apply_filters( 'dbvc_export_menu_data', $menu_data, $menu );
+ }
+
+ $data = apply_filters( 'dbvc_export_menus_data', $data );
+
+ $path = dbvc_get_sync_path();
+ if ( ! is_dir( $path ) ) wp_mkdir_p( $path );
+
+ $file_path = apply_filters( 'dbvc_export_menus_file_path', $path . 'menus.json' );
+
+ file_put_contents( $file_path, wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
+
+ do_action( 'dbvc_after_export_menus', $file_path, $data );
+ }
+}
diff --git a/class-sync-posts.php b/class-sync-posts.php
new file mode 100644
index 0000000..e301109
--- /dev/null
+++ b/class-sync-posts.php
@@ -0,0 +1,1060 @@
+
+ * @since 1.0.0
+ * @return string
+ */
+class DBVC_Sync_Posts {
+
+ /**
+ * Get the selected post types for export/import.
+ *
+ * @since 1.0.0
+ * @return array
+ */
+ public static function get_supported_post_types() {
+ $selected_types = get_option( 'dbvc_post_types', [] );
+
+ // If no post types are selected, default to post, page, and FSE types.
+ if ( empty( $selected_types ) ) {
+ $selected_types = [ 'post', 'page', 'wp_template', 'wp_template_part', 'wp_global_styles', 'wp_navigation' ];
+ }
+
+ // Allow other plugins to modify supported post types.
+ return apply_filters( 'dbvc_supported_post_types', $selected_types );
+ }
+
+ public static function import_all( $offset = 0, $smart_import = false ) {
+ $path = dbvc_get_sync_path(); // Do not append 'posts' subfolder
+
+ if ( ! is_dir( $path ) ) {
+ error_log( '[DBVC] Import path not found: ' . $path );
+ return;
+ }
+
+ $supported_types = self::get_supported_post_types();
+ $processed = 0;
+
+ foreach ( $supported_types as $post_type ) {
+ $folder = trailingslashit( $path ) . sanitize_key( $post_type );
+ if ( ! is_dir( $folder ) ) {
+ continue;
+ }
+
+ $json_files = glob( $folder . '/' . sanitize_key( $post_type ) . '-*.json' );
+ foreach ( $json_files as $filepath ) {
+ self::import_post_from_json( $filepath, $smart_import );
+ $processed++;
+ }
+ }
+
+ return [
+ 'processed' => $processed,
+ 'remaining' => 0,
+ 'total' => $processed,
+ 'offset' => $offset + $processed,
+ ];
+ }
+
+ public static function import_post_from_json( $filepath, $smart_import = false ) {
+ if ( ! file_exists( $filepath ) ) {
+ return;
+ }
+
+ $json = json_decode( file_get_contents( $filepath ), true );
+
+ // Skip if not a valid post structure
+ if (
+ empty( $json )
+ || ! is_array( $json )
+ || ! isset( $json['ID'], $json['post_type'], $json['post_title'] )
+ ) {
+ error_log("[DBVC] Skipped non-post JSON file: {$filepath}");
+ return;
+ }
+
+ $existing = get_post( absint( $json['ID'] ) );
+
+ // 🧠Smart Import Check — skip unchanged posts
+ if ( $smart_import && $existing ) {
+ $hash_key = '_dbvc_import_hash';
+
+ // Exclude hash key from hash computation to avoid false mismatches
+ $meta = $json['meta'] ?? [];
+ unset( $meta['_dbvc_import_hash'] );
+
+ $new_hash = md5( serialize( [ $json['post_content'], $meta ] ) );
+ $existing_hash = get_post_meta( $existing->ID, $hash_key, true );
+
+ if ( $new_hash === $existing_hash ) {
+ error_log("[DBVC] Skipping unchanged post ID {$json['ID']}");
+ return;
+ }
+ }
+
+ $post_array = [
+ 'ID' => absint( $json['ID'] ),
+ 'post_title' => sanitize_text_field( $json['post_title'] ),
+ 'post_content' => wp_kses_post( $json['post_content'] ?? '' ),
+ 'post_excerpt' => sanitize_textarea_field( $json['post_excerpt'] ?? '' ),
+ 'post_type' => sanitize_text_field( $json['post_type'] ),
+ 'post_status' => sanitize_text_field( $json['post_status'] ?? 'draft' ),
+ ];
+
+ $post_id = wp_insert_post( $post_array );
+
+ // Import post meta
+ if ( ! is_wp_error( $post_id ) && isset( $json['meta'] ) && is_array( $json['meta'] ) ) {
+ foreach ( $json['meta'] as $key => $values ) {
+ if ( is_array( $values ) ) {
+ foreach ( $values as $value ) {
+ update_post_meta( $post_id, sanitize_text_field( $key ), maybe_unserialize( $value ) );
+ }
+ }
+ }
+ }
+
+ // ✅ Save hash excluding its own key
+ if ( isset( $json['ID'] ) && $post_id ) {
+ $meta = $json['meta'] ?? [];
+ unset( $meta['_dbvc_import_hash'] );
+
+ $hash = md5( serialize( [ $json['post_content'], $meta ] ) );
+ update_post_meta( $post_id, '_dbvc_import_hash', $hash );
+ }
+
+ error_log("[DBVC] Imported post ID {$post_id}");
+}
+
+/**
+ * Process backups folder to uploads - up to 10 timestamped backups with auto delete oldest
+ *
+ * @since 1.1.0
+ * @return void
+ */
+public static function dbvc_create_backup_folder_and_copy_exports() {
+ $export_dir = dbvc_get_sync_path();
+ $upload_dir = wp_upload_dir();
+ $sync_dir = $upload_dir['basedir'] . '/sync';
+ $backup_base = $sync_dir . '/db-version-control-backups';
+ $timestamp = date( 'm-d-Y-His' );
+ $backup_path = $backup_base . '/' . $timestamp;
+
+ error_log( '[DBVC] Attempting backup to: ' . $backup_path );
+
+ // Ensure top-level /sync/ folder has .htaccess to disable indexing
+ if ( is_dir( $sync_dir ) ) {
+ $sync_htaccess = $sync_dir . '/.htaccess';
+ if ( ! file_exists( $sync_htaccess ) ) {
+ file_put_contents( $sync_htaccess, "Options -Indexes\n" );
+ error_log( '[DBVC] Created .htaccess in /sync/ folder' );
+ }
+ }
+
+ // Create backup folder
+ if ( ! file_exists( $backup_path ) ) {
+ if ( wp_mkdir_p( $backup_path ) ) {
+ error_log( '[DBVC] Created backup folder: ' . $backup_path );
+
+ // Add enhanced .htaccess file to restrict all access (Apache 2.2 + 2.4+ compatible)
+ $htaccess = <<
+ Require all denied
+
+
+Options -Indexes
+HT;
+ file_put_contents( $backup_path . '/.htaccess', $htaccess );
+ error_log( '[DBVC] Created .htaccess in backup folder' );
+
+ // Add index.php to prevent directory browsing
+ $index_php = "getExtension() !== 'json' ) {
+ continue;
+ }
+
+ $relative_path = str_replace( $export_dir, '', $file->getPathname() );
+ $dest_path = $backup_path . '/' . ltrim( $relative_path, '/' );
+
+ // Ensure destination subdirectory exists
+ $dest_dir = dirname( $dest_path );
+ if ( ! file_exists( $dest_dir ) ) {
+ wp_mkdir_p( $dest_dir );
+ }
+
+ $copy_result = copy( $file->getPathname(), $dest_path );
+ if ( $copy_result ) {
+ error_log( "[DBVC] Copied {$relative_path} to {$dest_path}" );
+ } else {
+ error_log( "[DBVC] ERROR: Failed to copy {$relative_path} to {$dest_path}" );
+ }
+ }
+
+ // Cleanup: Keep only 10 latest backup folders
+ $folders = glob( $backup_base . '/*', GLOB_ONLYDIR );
+ usort( $folders, function ( $a, $b ) {
+ return filemtime( $b ) <=> filemtime( $a );
+ });
+
+ $old_folders = array_slice( $folders, 10 );
+ foreach ( $old_folders as $old_folder ) {
+ dbvc_delete_folder_recursive( $old_folder );
+ error_log( "[DBVC] Deleted old backup folder: $old_folder" );
+ }
+}
+
+
+
+ /**
+ * Export a single post to JSON file.
+ *
+ * @param int $post_id Post ID.
+ * @param object $post Post object.
+ *
+ * @since 1.0.0
+ * @return void
+ */
+ public static function export_post_to_json( $post_id, $post ) {
+ // Validate inputs.
+ if ( ! is_numeric( $post_id ) || $post_id <= 0 ) {
+ return;
+ }
+
+ if ( ! is_object( $post ) || ! isset( $post->post_type ) ) {
+ return;
+ }
+
+ if ( wp_is_post_revision( $post_id ) ) {
+ return;
+ }
+
+ // For FSE content, allow draft status as templates can be in draft.
+ $allowed_statuses = [ 'publish' ];
+ if ( in_array( $post->post_type, [ 'wp_template', 'wp_template_part', 'wp_global_styles', 'wp_navigation' ], true ) ) {
+ $allowed_statuses[] = 'draft';
+ $allowed_statuses[] = 'auto-draft';
+ }
+
+ if ( ! in_array( $post->post_status, $allowed_statuses, true ) ) {
+ return;
+ }
+
+ $supported_types = self::get_supported_post_types();
+ if ( ! in_array( $post->post_type, $supported_types, true ) ) {
+ return;
+ }
+
+ // Check if user has permission to read this post type (skip for WP-CLI).
+ if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
+ $post_type_obj = get_post_type_object( $post->post_type );
+ if ( ! $post_type_obj || ! current_user_can( $post_type_obj->cap->read_post, $post_id ) ) {
+ return;
+ }
+ }
+
+ $data = [
+ 'ID' => absint( $post_id ),
+ 'post_title' => sanitize_text_field( $post->post_title ),
+ 'post_content' => wp_kses_post( $post->post_content ),
+ 'post_excerpt' => sanitize_textarea_field( $post->post_excerpt ),
+ 'post_type' => sanitize_text_field( $post->post_type ),
+ 'post_status' => sanitize_text_field( $post->post_status ),
+ 'post_name' => sanitize_text_field( $post->post_name ),
+ 'meta' => self::sanitize_post_meta( get_post_meta( $post_id ) ),
+ ];
+
+ // Add FSE-specific data.
+ if ( in_array( $post->post_type, [ 'wp_template', 'wp_template_part' ], true ) ) {
+ $data['theme'] = get_stylesheet();
+ $data['slug'] = $post->post_name;
+ $data['source'] = get_post_meta( $post_id, 'origin', true ) ?: 'custom';
+ }
+
+ // Allow other plugins to modify the export data
+ $data = apply_filters( 'dbvc_export_post_data', $data, $post_id, $post );
+
+ // Sanitize the final data
+ $data = dbvc_sanitize_json_data( $data );
+
+ $path = dbvc_get_sync_path( $post->post_type );
+
+ if ( ! is_dir( $path ) ) {
+ if ( ! wp_mkdir_p( $path ) ) {
+ error_log( 'DBVC: Failed to create directory: ' . $path );
+ return;
+ }
+ }
+
+ $file_path = $path . sanitize_file_name( $post->post_type . '-' . $post_id . '.json' );
+
+ // Allow other plugins to modify the file path.
+ $file_path = apply_filters( 'dbvc_export_post_file_path', $file_path, $post_id, $post );
+
+ // Validate the final file path
+ if ( ! dbvc_is_safe_file_path( $file_path ) ) {
+ error_log( 'DBVC: Unsafe file path detected: ' . $file_path );
+ return;
+ }
+
+ $json_content = wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
+ if ( false === $json_content ) {
+ error_log( 'DBVC: Failed to encode JSON for post ' . $post_id );
+ return;
+ }
+
+ $result = file_put_contents( $file_path, $json_content );
+ if ( false === $result ) {
+ error_log( 'DBVC: Failed to write file: ' . $file_path );
+ return;
+ }
+
+ // Allow other plugins to perform additional actions after export.
+ do_action( 'dbvc_after_export_post', $post_id, $post, $file_path );
+ }
+
+ /**
+ * Import all JSON files for supported post types.
+ *
+ * @since 1.0.0
+ * @return void
+ */
+ public static function import_all_json_files() {
+ $supported_types = self::get_supported_post_types();
+
+ foreach ( $supported_types as $post_type ) {
+ $path = dbvc_get_sync_path( $post_type );
+ $files = glob( $path . '*.json' );
+
+ if ( empty( $files ) ) {
+ continue;
+ }
+
+ foreach ( $files as $file ) {
+ $json = json_decode( file_get_contents( $file ), true );
+ if ( empty( $json ) ) {
+ continue;
+ }
+
+ $post_id = wp_insert_post( [
+ 'ID' => $json['ID'],
+ 'post_title' => $json['post_title'],
+ 'post_content' => $json['post_content'],
+ 'post_excerpt' => $json['post_excerpt'],
+ 'post_type' => $json['post_type'],
+ 'post_status' => $json['post_status'],
+ ] );
+
+ if ( ! is_wp_error( $post_id ) && isset( $json['meta'] ) ) {
+ foreach ( $json['meta'] as $key => $values ) {
+ foreach ( $values as $value ) {
+ update_post_meta( $post_id, $key, maybe_unserialize( $value ) );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Export options to JSON file.
+ *
+ * @since 1.0.0
+ * @return void
+ */
+ public static function export_options_to_json() {
+ // Check user capabilities for options export (skip for WP-CLI)
+ if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+ }
+
+ $all_options = wp_load_alloptions();
+ $excluded_keys = [
+ 'siteurl', 'home', 'blogname', 'blogdescription',
+ 'admin_email', 'users_can_register', 'start_of_week', 'upload_path',
+ 'upload_url_path', 'cron', 'recently_edited', 'rewrite_rules',
+ // Security-sensitive options
+ 'auth_key', 'auth_salt', 'logged_in_key', 'logged_in_salt',
+ 'nonce_key', 'nonce_salt', 'secure_auth_key', 'secure_auth_salt',
+ 'secret_key', 'db_version', 'initial_db_version',
+ ];
+
+ // Allow other plugins to modify excluded keys
+ $excluded_keys = apply_filters( 'dbvc_excluded_option_keys', $excluded_keys );
+
+ $filtered = array_diff_key( $all_options, array_flip( $excluded_keys ) );
+
+ // Sanitize options data
+ $filtered = self::sanitize_options_data( $filtered );
+
+ // Allow other plugins to modify the options data before export
+ $filtered = apply_filters( 'dbvc_export_options_data', $filtered );
+
+ $path = dbvc_get_sync_path();
+ if ( ! is_dir( $path ) ) {
+ if ( ! wp_mkdir_p( $path ) ) {
+ error_log( 'DBVC: Failed to create directory: ' . $path );
+ return;
+ }
+ }
+
+ $file_path = $path . 'options.json';
+
+ // Allow other plugins to modify the options file path.
+ $file_path = apply_filters( 'dbvc_export_options_file_path', $file_path );
+
+ // Validate file path
+ if ( ! dbvc_is_safe_file_path( $file_path ) ) {
+ error_log( 'DBVC: Unsafe file path detected: ' . $file_path );
+ return;
+ }
+
+ $json_content = wp_json_encode( $filtered, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
+ if ( false === $json_content ) {
+ error_log( 'DBVC: Failed to encode options JSON' );
+ return;
+ }
+
+ $result = file_put_contents( $file_path, $json_content );
+ if ( false === $result ) {
+ error_log( 'DBVC: Failed to write options file: ' . $file_path );
+ return;
+ }
+
+ // Allow other plugins to perform additional actions after options export
+ do_action( 'dbvc_after_export_options', $file_path, $filtered );
+ }
+
+ /**
+ * Sanitize post meta data.
+ *
+ * @param array $meta_data Raw meta data.
+ *
+ * @since 1.0.0
+ * @return array Sanitized meta data.
+ */
+ private static function sanitize_post_meta( $meta_data ) {
+ $sanitized = [];
+
+ // Define keywords that suggest the value may contain HTML
+ $allow_html_if_key_contains = [
+ 'section',
+ 'description',
+ 'wysiwyg',
+ 'text',
+ 'textarea',
+ 'details',
+ 'content',
+ 'info',
+ 'header',
+ ];
+
+ foreach ( $meta_data as $key => $values ) {
+ $key = sanitize_text_field( $key );
+ $sanitized[$key] = [];
+
+ foreach ( $values as $value ) {
+ if ( is_serialized( $value ) ) {
+ $unserialized = maybe_unserialize( $value );
+ $sanitized[ $key ][] = dbvc_sanitize_json_data( $unserialized );
+ } else {
+ // Check if key contains any of the wildcard keywords
+ $allow_html = false;
+ foreach ( $allow_html_if_key_contains as $match ) {
+ if ( stripos( $key, $match ) !== false ) {
+ $allow_html = true;
+ break;
+ }
+ }
+
+ if ( $allow_html ) {
+ $sanitized[$key][] = wp_kses_post( $value );
+ } else {
+ $sanitized[$key][] = sanitize_textarea_field( $value );
+ }
+ }
+ }
+ }
+
+ return $sanitized;
+}
+
+
+ /**
+ * Sanitize options data.
+ *
+ * @param array $options_data Raw options data.
+ *
+ * @since 1.0.0
+ * @return array Sanitized options data.
+ */
+ private static function sanitize_options_data( $options_data ) {
+ $sanitized = [];
+
+ foreach ( $options_data as $key => $value ) {
+ $key = sanitize_text_field( $key );
+
+ if ( is_serialized( $value ) ) {
+ $unserialized = maybe_unserialize( $value );
+ $sanitized[ $key ] = dbvc_sanitize_json_data( $unserialized );
+ } else {
+ $sanitized[ $key ] = dbvc_sanitize_json_data( $value );
+ }
+ }
+
+ return $sanitized;
+ }
+
+ /**
+ * Import options from JSON file.
+ *
+ * @since 1.0.0
+ * @return void
+ */
+ public static function import_options_from_json() {
+ $file_path = dbvc_get_sync_path() . 'options.json';
+ if ( ! file_exists( $file_path ) ) {
+ return;
+ }
+
+ $options = json_decode( file_get_contents( $file_path ), true );
+ if ( empty( $options ) ) {
+ return;
+ }
+
+ foreach ( $options as $key => $value ) {
+ update_option( $key, maybe_unserialize( $value ) );
+ }
+ }
+
+ /**
+ * Export all menus to JSON file.
+ *
+ * @since 1.0.0
+ * @return void
+ */
+ public static function export_menus_to_json() {
+ $menus = wp_get_nav_menus();
+ $data = [];
+
+ foreach ( $menus as $menu ) {
+ $items = wp_get_nav_menu_items( $menu->term_id, [ 'post_status' => 'any' ] );
+ $formatted_items = [];
+
+ foreach ( $items as $item ) {
+ $post_array = get_post( $item->ID, ARRAY_A );
+ $meta = get_post_meta( $item->ID );
+ $original_id = isset( $meta['_dbvc_original_id'][0] ) ? (int) $meta['_dbvc_original_id'][0] : (int) $item->object_id;
+
+ $post_array['db_id'] = $item->db_id;
+ $post_array['menu_item_parent'] = $item->menu_item_parent;
+ $post_array['object_id'] = $item->object_id;
+ $post_array['object'] = $item->object;
+ $post_array['type'] = $item->type;
+ $post_array['type_label'] = $item->type_label;
+ $post_array['title'] = $item->title;
+ $post_array['url'] = $item->url;
+ $post_array['target'] = $item->target;
+ $post_array['attr_title'] = $item->attr_title;
+ $post_array['description'] = $item->description;
+ $post_array['classes'] = is_array( $item->classes ) ? $item->classes : explode( ' ', $item->classes );
+ $post_array['xfn'] = $item->xfn;
+ $post_array['original_id'] = $original_id;
+ $post_array['meta'] = $meta;
+
+ $formatted_items[] = $post_array;
+ }
+
+ $menu_data = [
+ 'name' => $menu->name,
+ 'slug' => $menu->slug,
+ 'locations' => array_keys(
+ array_filter(
+ get_nav_menu_locations(),
+ fn( $id ) => $id === $menu->term_id
+ )
+ ),
+ 'items' => $formatted_items,
+ ];
+
+ $data[] = $menu_data;
+ }
+
+ $path = dbvc_get_sync_path();
+ if ( ! is_dir( $path ) ) {
+ wp_mkdir_p( $path );
+ }
+
+ file_put_contents(
+ $path . 'menus.json',
+ wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES )
+ );
+
+ error_log( '[DBVC] Exported menus to menus.json' );
+}
+
+
+
+
+ /**
+ * Import menus from JSON file.
+ *
+ * @since 1.0.0
+ * @return void
+ */
+ public static function import_menus_from_json() {
+ $file = dbvc_get_sync_path() . 'menus.json';
+ if ( ! file_exists( $file ) ) {
+ error_log( '[DBVC] Menus JSON file not found at: ' . $file );
+ return;
+ }
+
+ $menus = json_decode( file_get_contents( $file ), true );
+ if ( ! is_array( $menus ) ) {
+ error_log( '[DBVC] Invalid JSON format in menus.json' );
+ return;
+ }
+
+ global $wpdb;
+
+ // Build post ID map for remapping referenced objects
+ $post_map = [];
+ $imported_ids = $wpdb->get_results(
+ "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_dbvc_original_id'"
+ );
+ foreach ( $imported_ids as $row ) {
+ $post_map[ $row->meta_value ] = $row->post_id;
+ }
+
+ foreach ( $menus as $menu_data ) {
+ if ( ! isset( $menu_data['name'] ) || ! is_array( $menu_data['items'] ?? null ) ) {
+ error_log( '[DBVC] Skipped invalid menu structure.' );
+ continue;
+ }
+
+ $existing_menu = wp_get_nav_menu_object( $menu_data['name'] );
+ $menu_id = $existing_menu ? $existing_menu->term_id : wp_create_nav_menu( $menu_data['name'] );
+
+ if ( is_wp_error( $menu_id ) ) {
+ error_log( '[DBVC] Failed to create/reuse menu "' . $menu_data['name'] . '": ' . $menu_id->get_error_message() );
+ continue;
+ }
+
+ if ( $existing_menu ) {
+ $old_items = wp_get_nav_menu_items( $menu_id );
+ if ( $old_items ) {
+ foreach ( $old_items as $old_item ) {
+ wp_delete_post( $old_item->ID, true );
+ }
+ error_log( "[DBVC] Cleared existing menu items for '{$menu_data['name']}'" );
+ }
+ }
+
+ $created_items = []; // old_id => new_id
+ $pending_parents = []; // child => original parent
+
+ foreach ( $menu_data['items'] as $item ) {
+ $original_id = (int)( $item['db_id'] ?? 0 );
+ $object_id = (int)( $item['object_id'] ?? 0 );
+ $type = $item['type'] ?? '';
+ $object = $item['object'] ?? '';
+
+ $mapped_object_id = 0;
+
+ // Handle only post_type or taxonomy object ID mapping
+ if ( in_array( $type, ['post_type', 'taxonomy'], true ) ) {
+ $mapped_object_id = $post_map[ $object_id ] ?? $object_id;
+ if ( ! get_post_status( $mapped_object_id ) && $type === 'post_type' ) {
+ error_log( "[DBVC] Skipping menu item due to missing post object ID: $mapped_object_id" );
+ continue;
+ }
+ }
+
+ $classes = is_array( $item['classes'] ) ? implode( ' ', $item['classes'] ) : (string) $item['classes'];
+
+ $item_args = [
+ 'menu-item-title' => $item['title'] ?? '',
+ 'menu-item-object' => $object,
+ 'menu-item-object-id' => $mapped_object_id,
+ 'menu-item-type' => $type,
+ 'menu-item-status' => 'publish',
+ 'menu-item-url' => $item['url'] ?? '',
+ 'menu-item-classes' => $classes,
+ 'menu-item-xfn' => $item['xfn'] ?? '',
+ 'menu-item-target' => $item['target'] ?? '',
+ 'menu-item-attr-title' => $item['attr_title'] ?? '',
+ 'menu-item-description'=> $item['description'] ?? '',
+ 'menu-item-position' => $item['menu_order'] ?? 0,
+ 'menu-item-parent-id' => 0,
+ ];
+
+ $item_id = wp_update_nav_menu_item( $menu_id, 0, $item_args );
+
+if ( is_wp_error( $item_id ) ) {
+ error_log( '[DBVC] Failed to add menu item "' . $item['title'] . '": ' . $item_id->get_error_message() );
+ continue;
+}
+
+$created_items[ $original_id ] = $item_id;
+update_post_meta( $item_id, '_dbvc_original_id', $original_id );
+
+// ✅ Restore all other meta fields
+if ( ! empty( $item['meta'] ) && is_array( $item['meta'] ) ) {
+ foreach ( $item['meta'] as $meta_key => $meta_values ) {
+ if ( $meta_key === '_dbvc_original_id' ) {
+ continue;
+ }
+ delete_post_meta( $item_id, $meta_key );
+ foreach ( $meta_values as $meta_value ) {
+ add_post_meta( $item_id, $meta_key, maybe_unserialize( $meta_value ) );
+ }
+ }
+}
+
+if ( ! empty( $item['menu_item_parent'] ) ) {
+ $pending_parents[] = [
+ 'child_id' => $item_id,
+ 'original_parent_id' => $item['menu_item_parent'],
+ ];
+}
+
+ error_log( '[DBVC] Imported menu item ID: ' . $item_id );
+ }
+
+ foreach ( $pending_parents as $pending ) {
+ $child_id = $pending['child_id'];
+ $parent_id = $created_items[ $pending['original_parent_id'] ] ?? 0;
+
+ if ( $parent_id ) {
+ wp_update_post( [
+ 'ID' => $child_id,
+ 'post_parent' => $parent_id,
+ ] );
+ update_post_meta( $child_id, '_menu_item_menu_item_parent', $parent_id );
+ error_log( "[DBVC] Set parent for menu item ID $child_id to $parent_id" );
+ }
+ }
+
+ if ( isset( $menu_data['locations'] ) && is_array( $menu_data['locations'] ) ) {
+ $locations = get_nav_menu_locations();
+ foreach ( $menu_data['locations'] as $loc ) {
+ $locations[ $loc ] = $menu_id;
+ error_log( '[DBVC] Set menu "' . $menu_data['name'] . '" to location "' . $loc . '"' );
+ }
+ set_theme_mod( 'nav_menu_locations', $locations );
+ }
+ }
+}
+
+
+
+
+
+ /**
+ * Export posts in batches for better performance.
+ *
+ * @param int $batch_size Number of posts to process per batch.
+ * @param int $offset Starting offset for the batch.
+ *
+ * @since 1.0.0
+ * @return array Results with processed count and remaining count.
+ */
+ public static function export_posts_batch( $batch_size = 50, $offset = 0 ) {
+ $supported_types = self::get_supported_post_types();
+
+ $posts = get_posts( [
+ 'post_type' => $supported_types,
+ 'posts_per_page' => $batch_size,
+ 'offset' => $offset,
+ 'post_status' => 'any',
+ ] );
+
+ $processed = 0;
+ foreach ( $posts as $post ) {
+ self::export_post_to_json( $post->ID, $post );
+ $processed++;
+ }
+
+ // Get total count for progress tracking
+ $total_posts = self::wp_count_posts_by_type( $supported_types );
+ $remaining = max( 0, $total_posts - ( $offset + $processed ) );
+
+ return [
+ 'processed' => $processed,
+ 'remaining' => $remaining,
+ 'total' => $total_posts,
+ 'offset' => $offset + $processed,
+ ];
+ }
+
+ /**
+ * Import posts in batches for better performance.
+ *
+ * @param int $batch_size Number of files to process per batch.
+ * @param int $offset Starting offset for the batch.
+ *
+ * @since 1.0.0
+ * @return array Results with processed count and remaining count.
+ */
+ public static function import_posts_batch( $batch_size = 50, $offset = 0 ) {
+ $supported_types = self::get_supported_post_types();
+ $all_files = [];
+
+ // Collect all JSON files from all post type directories
+ foreach ( $supported_types as $post_type ) {
+ $path = dbvc_get_sync_path( $post_type );
+ $files = glob( $path . '*.json' );
+ if ( ! empty( $files ) ) {
+ $all_files = array_merge( $all_files, $files );
+ }
+ }
+
+ // Process batch
+ $batch_files = array_slice( $all_files, $offset, $batch_size );
+ $processed = 0;
+
+ foreach ( $batch_files as $file ) {
+ $json = json_decode( file_get_contents( $file ), true );
+ if ( empty( $json ) ) {
+ continue;
+ }
+
+ // Validate required fields
+ if ( ! isset( $json['ID'], $json['post_type'], $json['post_title'] ) ) {
+ continue;
+ }
+
+ $post_id = wp_insert_post( [
+ 'ID' => absint( $json['ID'] ),
+ 'post_title' => sanitize_text_field( $json['post_title'] ),
+ 'post_content' => wp_kses_post( $json['post_content'] ?? '' ),
+ 'post_excerpt' => sanitize_textarea_field( $json['post_excerpt'] ?? '' ),
+ 'post_type' => sanitize_text_field( $json['post_type'] ),
+ 'post_status' => sanitize_text_field( $json['post_status'] ?? 'draft' ),
+ ] );
+
+ if ( ! is_wp_error( $post_id ) && isset( $json['meta'] ) && is_array( $json['meta'] ) ) {
+ foreach ( $json['meta'] as $key => $values ) {
+ if ( is_array( $values ) ) {
+ foreach ( $values as $value ) {
+ update_post_meta( $post_id, sanitize_text_field( $key ), maybe_unserialize( $value ) );
+ }
+ }
+ }
+ }
+
+ $processed++;
+ }
+
+ $total_files = count( $all_files );
+ $remaining = max( 0, $total_files - ( $offset + $processed ) );
+
+ return [
+ 'processed' => $processed,
+ 'remaining' => $remaining,
+ 'total' => $total_files,
+ 'offset' => $offset + $processed,
+ ];
+ }
+
+ /**
+ * Get total count of posts for all supported post types.
+ *
+ * @param array $post_types Post types to count.
+ *
+ * @since 1.0.0
+ * @return int Total post count.
+ */
+ private static function wp_count_posts_by_type( $post_types ) {
+ $total = 0;
+
+ foreach ( $post_types as $post_type ) {
+ $counts = wp_count_posts( $post_type );
+ if ( $counts ) {
+ foreach ( $counts as $status => $count ) {
+ $total += $count;
+ }
+ }
+ }
+
+ return $total;
+ }
+
+ /**
+ * Export FSE theme data to JSON.
+ *
+ * @since 1.1.0
+ * @return void
+ */
+ public static function export_fse_theme_data() {
+ // Check if WordPress is fully loaded.
+ if ( ! did_action( 'wp_loaded' ) ) {
+ return;
+ }
+
+ $existing = get_post( absint( $json['ID'] ) );
+
+ if ( $smart_import && $existing ) {
+ $hash_key = '_dbvc_import_hash';
+ $new_hash = md5( serialize( [ $json['post_content'], $json['meta'] ?? [] ] ) );
+ $existing_hash = get_post_meta( $existing->ID, $hash_key, true );
+
+ if ( $new_hash === $existing_hash ) {
+ return; // Skip unchanged post
+ }
+ }
+
+
+ if ( ! wp_is_block_theme() ) {
+ return;
+ }
+
+ // Skip during admin page loads to prevent conflicts.
+ if ( is_admin() && ! wp_doing_ajax() && ! defined( 'WP_CLI' ) ) {
+ return;
+ }
+
+ // Check user capabilities for FSE export (skip for WP-CLI).
+ if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ return;
+ }
+ }
+
+ $theme_data = [
+ 'theme_name' => get_stylesheet(),
+ 'custom_css' => wp_get_custom_css(),
+ ];
+
+ // Safely get theme JSON data - only if the system is ready.
+ if ( class_exists( 'WP_Theme_JSON_Resolver' ) ) {
+ try {
+ // Additional check to ensure the theme JSON system is initialized.
+ if ( did_action( 'init' ) && ! is_admin() ) {
+ $theme_json_resolver = WP_Theme_JSON_Resolver::get_merged_data();
+ if ( $theme_json_resolver && method_exists( $theme_json_resolver, 'get_raw_data' ) ) {
+ $theme_data['theme_json'] = $theme_json_resolver->get_raw_data();
+ } else {
+ $theme_data['theme_json'] = [];
+ }
+ } else {
+ // Skip theme JSON during admin loads.
+ $theme_data['theme_json'] = [];
+ }
+ } catch ( Exception $e ) {
+ error_log( 'DBVC: Failed to get theme JSON data: ' . $e->getMessage() );
+ $theme_data['theme_json'] = [];
+ } catch ( Error $e ) {
+ error_log( 'DBVC: Fatal error getting theme JSON data: ' . $e->getMessage() );
+ $theme_data['theme_json'] = [];
+ }
+ } else {
+ $theme_data['theme_json'] = [];
+ }
+
+ // Allow other plugins to modify FSE theme data.
+ $theme_data = apply_filters( 'dbvc_export_fse_theme_data', $theme_data );
+
+ $path = dbvc_get_sync_path( 'theme' );
+ if ( ! is_dir( $path ) ) {
+ if ( ! wp_mkdir_p( $path ) ) {
+ error_log( 'DBVC: Failed to create theme directory: ' . $path );
+ return;
+ }
+ }
+
+ $file_path = $path . 'theme-data.json';
+
+ // Allow other plugins to modify the FSE theme file path.
+ $file_path = apply_filters( 'dbvc_export_fse_theme_file_path', $file_path );
+
+ // Validate file path.
+ if ( ! dbvc_is_safe_file_path( $file_path ) ) {
+ error_log( 'DBVC: Unsafe file path detected: ' . $file_path );
+ return;
+ }
+
+ $json_content = wp_json_encode( $theme_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
+ if ( false === $json_content ) {
+ error_log( 'DBVC: Failed to encode FSE theme JSON' );
+ return;
+ }
+
+ $result = file_put_contents( $file_path, $json_content );
+ if ( false === $result ) {
+ error_log( 'DBVC: Failed to write FSE theme file: ' . $file_path );
+ return;
+ }
+
+ do_action( 'dbvc_after_export_fse_theme_data', $file_path, $theme_data );
+ }
+
+ /**
+ * Import FSE theme data from JSON.
+ *
+ * @since 1.1.0
+ * @return void
+ */
+ public static function import_fse_theme_data() {
+ // Check user capabilities for FSE import.
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ return;
+ }
+
+ $file_path = dbvc_get_sync_path( 'theme' ) . 'theme-data.json';
+ if ( ! file_exists( $file_path ) ) {
+ return;
+ }
+
+ $theme_data = json_decode( file_get_contents( $file_path ), true );
+ if ( empty( $theme_data ) ) {
+ return;
+ }
+
+ // Import custom CSS.
+ if ( isset( $theme_data['custom_css'] ) && ! empty( $theme_data['custom_css'] ) ) {
+ wp_update_custom_css_post( $theme_data['custom_css'] );
+ }
+
+ // Allow other plugins to handle additional FSE import data.
+ do_action( 'dbvc_after_import_fse_theme_data', $theme_data );
+ }
+
+
+
+}
diff --git a/functions.php b/functions.php
new file mode 100644
index 0000000..846e9f1
--- /dev/null
+++ b/functions.php
@@ -0,0 +1,177 @@
+
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+/**
+ * Get the sync path for exports
+ *
+ * @param string $subfolder Optional subfolder name
+ *
+ * @since 1.0.0
+ * @return string
+ */
+function dbvc_get_sync_path( $subfolder = '' ) {
+ $custom_path = get_option( 'dbvc_sync_path', '' );
+
+ if ( ! empty( $custom_path ) ) {
+ // Validate and sanitize the custom path
+ $custom_path = dbvc_validate_sync_path( $custom_path );
+ if ( false === $custom_path ) {
+ // Fall back to default if invalid
+ $base_path = DBVC_PLUGIN_PATH . 'sync/';
+ } else {
+ // Remove leading slash and treat as relative to ABSPATH
+ $custom_path = ltrim( $custom_path, '/' );
+ $base_path = trailingslashit( ABSPATH ) . $custom_path;
+ }
+ } else {
+ // Default to plugin's sync folder
+ $base_path = DBVC_PLUGIN_PATH . 'sync/';
+ }
+
+ $base_path = trailingslashit( $base_path );
+
+ if ( ! empty( $subfolder ) ) {
+ // Sanitize subfolder name
+ $subfolder = sanitize_file_name( $subfolder );
+ $base_path .= trailingslashit( $subfolder );
+ }
+
+ return $base_path;
+}
+
+/**
+ * Validate sync path to prevent directory traversal and other security issues.
+ *
+ * @param string $path The path to validate.
+ *
+ * @since 1.0.0
+ * @return string|false Validated path or false if invalid.
+ */
+function dbvc_validate_sync_path( $path ) {
+ if ( empty( $path ) ) {
+ return '';
+ }
+
+ // Remove any null bytes
+ $path = str_replace( chr( 0 ), '', $path );
+
+ // Check for directory traversal attempts
+ if ( strpos( $path, '..' ) !== false ) {
+ return false;
+ }
+
+ // Check for other potentially dangerous characters
+ $dangerous_chars = [ '<', '>', '"', '|', '?', '*', chr( 0 ) ];
+ foreach ( $dangerous_chars as $char ) {
+ if ( strpos( $path, $char ) !== false ) {
+ return false;
+ }
+ }
+
+ // Normalize slashes
+ $path = str_replace( '\\', '/', $path );
+
+ // Remove any double slashes
+ $path = preg_replace( '#/+#', '/', $path );
+
+ // Ensure path is within allowed boundaries (wp-content or plugin directory)
+ $allowed_prefixes = [
+ 'wp-content/',
+ 'wp-content/plugins/',
+ 'wp-content/uploads/',
+ 'wp-content/themes/',
+ ];
+
+ $is_allowed = false;
+ foreach ( $allowed_prefixes as $prefix ) {
+ if ( strpos( ltrim( $path, '/' ), $prefix ) === 0 ) {
+ $is_allowed = true;
+ break;
+ }
+ }
+
+ // Also allow relative paths within the plugin directory
+ if ( ! $is_allowed && strpos( $path, '/' ) !== 0 ) {
+ $is_allowed = true;
+ }
+
+ return $is_allowed ? $path : false;
+}
+
+/**
+ * Sanitize JSON file content before writing.
+ *
+ * @param mixed $data The data to sanitize.
+ *
+ * @since 1.0.0
+ * @return mixed Sanitized data.
+ */
+function dbvc_sanitize_json_data( $data ) {
+ if ( is_array( $data ) ) {
+ return array_map( 'dbvc_sanitize_json_data', $data );
+ }
+
+ if ( is_string( $data ) ) {
+ // Remove any null bytes and other control characters
+ $data = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $data );
+ }
+
+ return $data;
+}
+
+/**
+ * Check if a file path is safe for writing.
+ *
+ * @param string $file_path The file path to check.
+ *
+ * @since 1.0.0
+ * @return bool True if safe, false otherwise.
+ */
+function dbvc_is_safe_file_path( $file_path ) {
+ // Check for null bytes
+ if ( strpos( $file_path, chr( 0 ) ) !== false ) {
+ return false;
+ }
+
+ // Check for directory traversal
+ if ( strpos( $file_path, '..' ) !== false ) {
+ return false;
+ }
+
+ // Ensure file is within WordPress directory structure
+ $wp_path = realpath( ABSPATH );
+ $resolved_path = realpath( dirname( $file_path ) );
+
+ if ( false === $resolved_path || strpos( $resolved_path, $wp_path ) !== 0 ) {
+ return false;
+ }
+
+ // Check file extension
+ $allowed_extensions = [ 'json' ];
+ $extension = pathinfo( $file_path, PATHINFO_EXTENSION );
+
+ return in_array( strtolower( $extension ), $allowed_extensions, true );
+}
+
+
+/**
+ * Remove the current site URL from content to make exports portable.
+ */
+function dbvc_remove_site_url_from_content( $value ) {
+ if ( is_string( $value ) ) {
+ $site_url = home_url();
+ $value = str_replace( $site_url, '', $value );
+ $value = str_replace( esc_url_raw( $site_url ), '', $value );
+ }
+ return $value;
+}