Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 246 additions & 0 deletions admin-page.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
<?php

/**
* Get the sync path for exports
*
* @package DB Version Control
* @author Robert DeVore <[email protected]>
* @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 '<div class="notice notice-error"><p>' . esc_html__( 'Invalid sync path provided. Path cannot contain ../ or other unsafe characters.', 'dbvc' ) . '</p></div>';
} 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 '<div class="notice notice-success"><p>' . sprintf( esc_html__( 'Sync folder updated and created at: %s', 'dbvc' ), '<code>' . esc_html( $resolved_path ) . '</code>' ) . '</p></div>';
} else {
echo '<div class="notice notice-error"><p>' . sprintf( esc_html__( 'Sync folder setting saved, but could not create directory at: %s. Please check permissions.', 'dbvc' ), '<code>' . esc_html( $resolved_path ) . '</code>' ) . '</p></div>';
}
}
}

// 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 '<div class="notice notice-success"><p>' . esc_html__( 'Post types selection updated!', 'dbvc' ) . '</p></div>';
}

// 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 '<div class="notice notice-success"><p>' . esc_html__( 'Full export completed!', 'dbvc' ) . '</p></div>';
}

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 '<div class="notice notice-success"><p>' . esc_html__( 'Import completed.', 'dbvc' ) . '</p></div>';
} 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();

?>
<div class="wrap">
<h1><?php esc_html_e( 'DB Version Control', 'dbvc' ); ?></h1>
<form method="post">
<?php wp_nonce_field( 'dbvc_export_action', 'dbvc_export_nonce' ); ?>
<p><?php esc_html_e( 'This will export all posts, options, and menus to JSON files.', 'dbvc' ); ?></p>
<?php submit_button( esc_html__( 'Run Full Export', 'dbvc' ) ); ?>
</form>

<hr />

<form method="post">
<?php wp_nonce_field( 'dbvc_import_action', 'dbvc_import_nonce' ); ?>
<h2><?php esc_html_e( 'Import from JSON', 'dbvc' ); ?></h2>
<p><?php esc_html_e( 'This will import posts and CPTs from the sync folder. Optionally, only import new or changed content.', 'dbvc' ); ?></p>

<label>
<input type="checkbox" name="dbvc_smart_import" value="1" />
<?php esc_html_e( 'Only import new or modified posts', 'dbvc' ); ?>
</label>
<br>
<label>
<input type="checkbox" name="dbvc_import_menus" value="1" />
<?php esc_html_e( 'Also import menus', 'dbvc' ); ?>
</label>

<?php submit_button( esc_html__( 'Run Import', 'dbvc' ), 'primary', 'dbvc_import_button' ); ?>
</form>


<hr />

<form method="post">
<?php wp_nonce_field( 'dbvc_post_types_action', 'dbvc_post_types_nonce' ); ?>
<h2><?php esc_html_e( 'Post Types to Export/Import', 'dbvc' ); ?></h2>
<p><?php esc_html_e( 'Select which post types should be included in exports and imports.', 'dbvc' ); ?></p>
<select name="dbvc_post_types[]" multiple="multiple" id="dbvc-post-types-select" style="width: 100%;">
<?php foreach ( $all_post_types as $post_type => $post_type_obj ) : ?>
<option value="<?php echo esc_attr( $post_type ); ?>" <?php selected( in_array( $post_type, $selected_post_types, true ) ); ?>>
<?php echo esc_html( $post_type_obj->label ); ?> (<?php echo esc_html( $post_type ); ?>)
</option>
<?php endforeach; ?>
</select>
<?php submit_button( esc_html__( 'Save Post Types', 'dbvc' ), 'secondary', 'dbvc_post_types_save' ); ?>
</form>

<hr />

<form method="post">
<?php wp_nonce_field( 'dbvc_sync_path_action', 'dbvc_sync_path_nonce' ); ?>
<h2><?php esc_html_e( 'Custom Sync Folder Path', 'dbvc' ); ?></h2>
<p><?php esc_html_e( 'Enter the full or relative path (from site root) where JSON files should be saved.', 'dbvc' ); ?></p>
<input type="text" name="dbvc_sync_path" value="<?php echo esc_attr( $custom_path ); ?>" style="width: 100%;" placeholder="<?php esc_attr_e( 'e.g., wp-content/plugins/db-version-control/sync-testing-folder/', 'dbvc' ); ?>">
<p><strong><?php esc_html_e( 'Current resolved path:', 'dbvc' ); ?></strong> <code><?php echo esc_html( $resolved_path ); ?></code></p>
<?php submit_button( esc_html__( 'Save Folder Path', 'dbvc' ), 'secondary', 'dbvc_sync_path_save' ); ?>
</form>
</div>

<script>
jQuery(document).ready(function($) {
$('#dbvc-post-types-select').select2({
placeholder: <?php echo wp_json_encode( esc_html__( 'Select post types...', 'dbvc' ) ); ?>,
allowClear: false
});
});
</script>
<?php
}
/**
* Get all available post types for the settings page.
*
* @since 1.1.0
* @return array
*/
function dbvc_get_available_post_types() {
$post_types = get_post_types( [ 'public' => 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;
}
147 changes: 147 additions & 0 deletions class-menu-importer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php
namespace DBVC;

class DBVC_MenuImporter {
/**
* Import menus and restore hierarchy + meta.
*/
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;
$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 ) ) 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 );
}
}
Loading