diff --git a/adminpages/admin_header.php b/adminpages/admin_header.php index 69375ad..8bf72a1 100644 --- a/adminpages/admin_header.php +++ b/adminpages/admin_header.php @@ -16,6 +16,7 @@
diff --git a/adminpages/menus.php b/adminpages/menus.php new file mode 100644 index 0000000..e77b0a6 --- /dev/null +++ b/adminpages/menus.php @@ -0,0 +1,477 @@ + 0 ) { + $new_menu_id = memberlite_duplicate_menu( $source_menu_id, $new_menu_name ); + } else { + $new_menu_id = wp_create_nav_menu( $new_menu_name ); + } + + if ( $new_menu_id && ! is_wp_error( $new_menu_id ) ) { + // Redirect back to the custom menus page with success message. + wp_safe_redirect( admin_url( 'admin.php?page=memberlite-custom-menus&created=' . $new_menu_id ) ); + exit; + } else { + wp_safe_redirect( admin_url( 'admin.php?page=memberlite-custom-menus&error=create' ) ); + exit; + } + } + + // Check for duplicate action. + $action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : ''; + if ( $action === 'duplicate' && isset( $_GET['menu'] ) ) { + // Verify nonce. + if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'memberlite_duplicate_menu_' . intval( $_GET['menu'] ) ) ) { + wp_die( esc_html__( 'Security check failed.', 'memberlite' ) ); + } + + // Check capabilities. + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_die( esc_html__( 'You do not have permission to do this.', 'memberlite' ) ); + } + + $menu_id = intval( $_GET['menu'] ); + $source_menu = wp_get_nav_menu_object( $menu_id ); + + if ( $source_menu ) { + /* translators: %s: original menu name */ + $new_menu_name = sprintf( __( '%s (Copy)', 'memberlite' ), $source_menu->name ); + $new_menu_id = memberlite_duplicate_menu( $menu_id, $new_menu_name ); + + if ( $new_menu_id ) { + wp_safe_redirect( admin_url( 'admin.php?page=memberlite-custom-menus&duplicated=' . $new_menu_id ) ); + exit; + } + } + + wp_safe_redirect( admin_url( 'admin.php?page=memberlite-custom-menus&error=duplicate' ) ); + exit; + } + + // Check for delete action. + if ( $action === 'delete' && isset( $_GET['menu'] ) ) { + // Verify nonce. + if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'memberlite_delete_menu_' . intval( $_GET['menu'] ) ) ) { + wp_die( esc_html__( 'Security check failed.', 'memberlite' ) ); + } + + // Check capabilities. + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_die( esc_html__( 'You do not have permission to do this.', 'memberlite' ) ); + } + + $menu_id = intval( $_GET['menu'] ); + $result = wp_delete_nav_menu( $menu_id ); + + if ( $result && ! is_wp_error( $result ) ) { + wp_safe_redirect( admin_url( 'admin.php?page=memberlite-custom-menus&deleted=1' ) ); + } else { + wp_safe_redirect( admin_url( 'admin.php?page=memberlite-custom-menus&error=delete' ) ); + } + exit; + } +} +add_action( 'admin_init', 'memberlite_process_menu_actions' ); + +/** + * Display admin notices for menu actions on the nav-menus.php page. + * + * @since TBD + */ +function memberlite_menu_admin_notices() { + $screen = get_current_screen(); + if ( ! $screen || $screen->base !== 'nav-menus' ) { + return; + } + + if ( isset( $_GET['memberlite_duplicated'] ) ) { + ?> +%2$s
+ + ' . esc_html__( 'Learn more about menus in Memberlite', 'memberlite' ) . ''; + ?> +
+ + + + + + + + $menu_item ) { + $args = array( + 'post_title' => $menu_item->title, + 'post_content' => $menu_item->description, + 'post_excerpt' => $menu_item->attr_title, + 'post_status' => $menu_item->post_status, + 'post_type' => 'nav_menu_item', + 'menu_order' => $index, + 'comment_status' => 'closed', + 'ping_status' => 'closed', + 'tax_input' => array( + 'nav_menu' => array( $new_menu_id ), + ), + ); + + // Copy menu item meta. + $meta_input = array(); + $meta_keys = get_post_meta( $menu_item->db_id ); + + foreach ( $meta_keys as $meta_key => $meta_values ) { + if ( $meta_key === '_menu_item_menu_item_parent' ) { + continue; // Handle parent separately. + } + $meta_value = $meta_values[0]; + if ( is_serialized( $meta_value ) ) { + $meta_value = maybe_unserialize( $meta_value ); + } + $meta_input[ $meta_key ] = $meta_value; + } + + $args['meta_input'] = $meta_input; + + // Insert the new menu item. + $new_item_id = wp_insert_post( $args ); + + // Skip this item if insertion failed. + if ( is_wp_error( $new_item_id ) ) { + continue; + } + + // Store the ID mapping. + $id_map[ $menu_item->db_id ] = $new_item_id; + + // Update parent relationship using the mapped ID. + if ( ! empty( $menu_item->menu_item_parent ) && isset( $id_map[ $menu_item->menu_item_parent ] ) ) { + update_post_meta( $new_item_id, '_menu_item_menu_item_parent', $id_map[ $menu_item->menu_item_parent ] ); + } + } + + return $new_menu_id; +} diff --git a/adminpages/sidebars.php b/adminpages/sidebars.php index fbc30ac..904e3db 100644 --- a/adminpages/sidebars.php +++ b/adminpages/sidebars.php @@ -229,7 +229,7 @@ function memberlite_custom_sidebars() { 'memberlite_delete_custom_sidebar' ); ?> - + "> $template, ); - $options = array(); + // Export theme mods if selected. + if ( ! empty( $_POST['memberlite_export_theme_mods'] ) ) { + // All theme mods = all Customizer settings for the active theme. + $mods = get_theme_mods(); + if ( ! is_array( $mods ) ) { + $mods = array(); + } + $data['mods'] = $mods; + + /** + * Filter the option keys to export when exporting Memberlite theme settings. + * By default, we export the site icon, custom sidebars, and sidebar assignments for custom post types. + * + * Note: This same filter is used for resetting options in memberlite_reset_theme_settings(). + * + * @since 6.1 + * @param array $option_keys Array of option keys to export. + */ + $option_keys = apply_filters( + 'memberlite_export_option_keys', + array( + 'site_icon', + 'memberlite_cpt_sidebars', + 'memberlite_custom_sidebars', + ) + ); + + $options = array(); + + foreach ( $option_keys as $key ) { + $value = get_option( $key, null ); + if ( null !== $value ) { + $options[ $key ] = $value; + } + } - foreach ( $option_keys as $key ) { - $value = get_option( $key, null ); - if ( null !== $value ) { - $options[ $key ] = $value; + $data['options'] = $options; + + if ( function_exists( 'wp_get_custom_css' ) ) { + $data['wp_css'] = wp_get_custom_css(); } } - $data = array( - 'template' => $template, - 'mods' => $mods, - 'options' => $options, - ); + // Export menus if selected. + if ( ! empty( $_POST['memberlite_export_menus'] ) && ! empty( $_POST['memberlite_export_menu_ids'] ) ) { + $menu_ids = array_map( 'intval', $_POST['memberlite_export_menu_ids'] ); + $menus_data = array(); + + // Get current menu location assignments to export with menus. + $menu_locations = get_nav_menu_locations(); + $menu_id_to_locations = array(); + foreach ( $menu_locations as $location => $assigned_menu_id ) { + if ( ! empty( $assigned_menu_id ) ) { + if ( ! isset( $menu_id_to_locations[ $assigned_menu_id ] ) ) { + $menu_id_to_locations[ $assigned_menu_id ] = array(); + } + $menu_id_to_locations[ $assigned_menu_id ][] = $location; + } + } - if ( function_exists( 'wp_get_custom_css' ) ) { - $data['wp_css'] = wp_get_custom_css(); + foreach ( $menu_ids as $menu_id ) { + $menu = wp_get_nav_menu_object( $menu_id ); + if ( ! $menu ) { + continue; + } + + $menu_items = wp_get_nav_menu_items( $menu_id ); + $items_data = array(); + + if ( ! empty( $menu_items ) ) { + // Build index map for parent references. + $id_to_index = array(); + foreach ( $menu_items as $index => $item ) { + $id_to_index[ $item->db_id ] = $index; + } + + foreach ( $menu_items as $index => $item ) { + // Determine parent index (null if top-level). + $parent_index = null; + if ( ! empty( $item->menu_item_parent ) && isset( $id_to_index[ $item->menu_item_parent ] ) ) { + $parent_index = $id_to_index[ $item->menu_item_parent ]; + } + + // Make internal URLs relative for portability across environments. + $item_url = $item->url; + $site_url = home_url(); + if ( strpos( $item_url, $site_url ) === 0 ) { + $item_url = substr( $item_url, strlen( $site_url ) ); + // Ensure empty path becomes root slash. + if ( empty( $item_url ) ) { + $item_url = '/'; + } + } + + $items_data[] = array( + 'label' => $item->title, + 'url' => $item_url, + 'target' => $item->target, + 'attr_title' => $item->attr_title, + 'description' => $item->description, + 'classes' => array_filter( (array) $item->classes ), + 'xfn' => $item->xfn, + 'parent' => $parent_index, + ); + } + } + + $menu_export_data = array( + 'name' => $menu->name, + 'items' => $items_data, + ); + + // Include location assignments for this menu. + if ( isset( $menu_id_to_locations[ $menu_id ] ) ) { + $menu_export_data['locations'] = $menu_id_to_locations[ $menu_id ]; + } + + $menus_data[] = $menu_export_data; + } + + if ( ! empty( $menus_data ) ) { + $data['menus'] = $menus_data; + } } - $json = wp_json_encode( $data ); + $json = wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); if ( false === $json ) { wp_die( esc_html__( 'Error encoding export data.', 'memberlite' ) ); @@ -170,6 +261,7 @@ function memberlite_export_theme_settings() { * Handle Memberlite theme settings import. * * @since 6.1 + * @since TBD Added navigation menu import support. */ function memberlite_import_theme_settings() { if ( ! current_user_can( 'edit_theme_options' ) ) { @@ -196,7 +288,8 @@ function memberlite_import_theme_settings() { // Decode JSON created by the export tool. $data = json_decode( $raw, true ); - if ( ! is_array( $data ) || empty( $data['template'] ) || ! isset( $data['mods'] ) ) { + // Validate file structure - must have template and either mods or menus. + if ( ! is_array( $data ) || empty( $data['template'] ) || ( ! isset( $data['mods'] ) && ! isset( $data['menus'] ) ) ) { memberlite_import_settings_redirect( 'invalid_file' ); } @@ -207,7 +300,7 @@ function memberlite_import_theme_settings() { memberlite_import_settings_redirect( 'wrong_theme' ); } - // Overwrite current theme mods. + // Overwrite current theme mods if present. if ( isset( $data['mods'] ) && is_array( $data['mods'] ) ) { // Clear existing mods so we don't leave stale ones behind. remove_theme_mods(); @@ -229,6 +322,148 @@ function memberlite_import_theme_settings() { wp_update_custom_css_post( $data['wp_css'] ); } + // Import navigation menus if present. + if ( ! empty( $data['menus'] ) && is_array( $data['menus'] ) ) { + // Track location assignments to apply after all menus are created. + $location_assignments = array(); + + // Check if user wants to replace existing menus. + $replace_existing = ! empty( $_POST['memberlite_replace_existing_menus'] ); + + foreach ( $data['menus'] as $menu_data ) { + if ( empty( $menu_data['name'] ) ) { + continue; + } + + $menu_name = sanitize_text_field( $menu_data['name'] ); + + // Check if a menu with this exact name already exists. + $existing_menu = wp_get_nav_menu_object( $menu_name ); + + if ( $existing_menu ) { + if ( $replace_existing ) { + // Replace mode: Delete existing menu items and import new ones into existing menu. + $menu_id = $existing_menu->term_id; + $location_menu_id = $menu_id; + + $existing_items = wp_get_nav_menu_items( $menu_id ); + if ( ! empty( $existing_items ) ) { + foreach ( $existing_items as $item ) { + wp_delete_post( $item->db_id, true ); + } + } + } else { + // Non-replace mode: Create duplicate menu, but assign original to locations. + $location_menu_id = $existing_menu->term_id; + + // Create duplicate with numbered name. + $duplicate_name = $menu_name; + $counter = 1; + while ( wp_get_nav_menu_object( $duplicate_name ) ) { + $counter++; + $duplicate_name = $menu_name . ' (' . $counter . ')'; + } + + $menu_id = wp_create_nav_menu( $duplicate_name ); + + if ( is_wp_error( $menu_id ) ) { + continue; + } + } + } else { + // Menu doesn't exist - create it and use it for locations. + $menu_id = wp_create_nav_menu( $menu_name ); + + if ( is_wp_error( $menu_id ) ) { + continue; + } + + $location_menu_id = $menu_id; + } + + // Track location assignments using the appropriate menu ID. + if ( ! empty( $menu_data['locations'] ) && is_array( $menu_data['locations'] ) ) { + foreach ( $menu_data['locations'] as $location ) { + $location_assignments[ sanitize_key( $location ) ] = $location_menu_id; + } + } + + // Import menu items. + if ( ! empty( $menu_data['items'] ) && is_array( $menu_data['items'] ) ) { + // Map of old index to new menu item ID for parent relationships. + $index_to_new_id = array(); + + foreach ( $menu_data['items'] as $index => $item_data ) { + $label = isset( $item_data['label'] ) ? sanitize_text_field( $item_data['label'] ) : ''; + $url = isset( $item_data['url'] ) ? $item_data['url'] : ''; + + // Convert relative URLs to absolute using the current site URL. + if ( ! empty( $url ) && strpos( $url, '/' ) === 0 && strpos( $url, '//' ) !== 0 ) { + $url = home_url( $url ); + } + + $url = esc_url_raw( $url ); + + if ( empty( $label ) || empty( $url ) ) { + continue; + } + + // Determine parent menu item ID. + $parent_id = 0; + if ( isset( $item_data['parent'] ) && is_int( $item_data['parent'] ) && isset( $index_to_new_id[ $item_data['parent'] ] ) ) { + $parent_id = $index_to_new_id[ $item_data['parent'] ]; + } + + // Prepare menu item data. + $menu_item_data = array( + 'menu-item-title' => $label, + 'menu-item-url' => $url, + 'menu-item-status' => 'publish', + 'menu-item-type' => 'custom', + 'menu-item-parent-id' => $parent_id, + ); + + // Add optional fields if present. + if ( ! empty( $item_data['target'] ) ) { + $menu_item_data['menu-item-target'] = sanitize_text_field( $item_data['target'] ); + } + + if ( ! empty( $item_data['attr_title'] ) ) { + $menu_item_data['menu-item-attr-title'] = sanitize_text_field( $item_data['attr_title'] ); + } + + if ( ! empty( $item_data['description'] ) ) { + $menu_item_data['menu-item-description'] = sanitize_textarea_field( $item_data['description'] ); + } + + if ( ! empty( $item_data['classes'] ) && is_array( $item_data['classes'] ) ) { + $menu_item_data['menu-item-classes'] = implode( ' ', array_map( 'sanitize_html_class', $item_data['classes'] ) ); + } + + if ( ! empty( $item_data['xfn'] ) ) { + $menu_item_data['menu-item-xfn'] = sanitize_text_field( $item_data['xfn'] ); + } + + // Insert the menu item. + $new_item_id = wp_update_nav_menu_item( $menu_id, 0, $menu_item_data ); + + if ( ! is_wp_error( $new_item_id ) ) { + $index_to_new_id[ $index ] = $new_item_id; + } + } + } + } + + // Apply menu location assignments after all menus are created. + if ( ! empty( $location_assignments ) ) { + $current_locations = get_nav_menu_locations(); + foreach ( $location_assignments as $location => $menu_id ) { + $current_locations[ $location ] = $menu_id; + } + set_theme_mod( 'nav_menu_locations', $current_locations ); + } + } + memberlite_import_settings_redirect( 'import_ok' ); } add_action( 'admin_post_memberlite_import_theme_settings', 'memberlite_import_theme_settings' ); diff --git a/adminpages/tools/export-settings.php b/adminpages/tools/export-settings.php index e32bbbc..d9d2b9e 100644 --- a/adminpages/tools/export-settings.php +++ b/adminpages/tools/export-settings.php @@ -8,6 +8,8 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } + +$menus = wp_get_nav_menus(); ?>- +