diff --git a/inc/site-exporter/class-site-exporter.php b/inc/site-exporter/class-site-exporter.php index 40491c410..8288106a0 100644 --- a/inc/site-exporter/class-site-exporter.php +++ b/inc/site-exporter/class-site-exporter.php @@ -168,30 +168,40 @@ public function maybe_protect_export_folder(): void { * Set up integration with the default WordPress Sites page. * * This allows exporting sites even before Ultimate Multisite is fully set up, - * making migration from other solutions easier. + * making migration from other solutions easier. In single-site (non-multisite) + * installs, a Tools > Export & Import menu page is registered instead of the + * network-admin Sites page integration. * * @since 2.5.0 * @return void */ private function setup_wordpress_sites_integration(): void { - // Add export action link to each site row - add_filter('manage_sites_action_links', [$this, 'add_wp_sites_row_actions'], 10, 2); + if ( is_multisite() ) { + // Add export action link to each site row + add_filter('manage_sites_action_links', [$this, 'add_wp_sites_row_actions'], 10, 2); - // Add bulk export action - add_filter('bulk_actions-sites-network', [$this, 'add_wp_sites_bulk_actions']); + // Add bulk export action + add_filter('bulk_actions-sites-network', [$this, 'add_wp_sites_bulk_actions']); - // Handle bulk export action - add_filter('handle_network_bulk_actions-sites-network', [$this, 'handle_wp_sites_bulk_action'], 10, 3); + // Handle bulk export action + add_filter('handle_network_bulk_actions-sites-network', [$this, 'handle_wp_sites_bulk_action'], 10, 3); - // Add admin menu page for export/import - add_action('network_admin_menu', [$this, 'add_wp_export_menu_page']); + // Add admin menu page for export/import (network admin) + add_action('network_admin_menu', [$this, 'add_wp_export_menu_page']); - // Handle direct export requests - add_action('admin_init', [$this, 'handle_direct_export_request']); + // Display admin notices (network admin) + add_action('network_admin_notices', [$this, 'display_export_notices']); + } else { + // Single-site: add Tools > Export & Import menu page + add_action('admin_menu', [$this, 'add_single_site_export_menu_page']); - // Display admin notices - add_action('network_admin_notices', [$this, 'display_export_notices']); + // Display admin notices in regular admin + add_action('admin_notices', [$this, 'display_export_notices']); + } + + // Handle direct export requests (works in both multisite and single-site) + add_action('admin_init', [$this, 'handle_direct_export_request']); // Enqueue scripts for WordPress sites page add_action('admin_enqueue_scripts', [$this, 'enqueue_wp_sites_scripts']); @@ -200,6 +210,11 @@ private function setup_wordpress_sites_integration(): void { /** * Add export action link to WordPress Sites page rows. * + * In multisite, the main site is excluded from the network Sites list because + * it is exported through the regular admin Tools > Export & Import page. + * In single-site installs this filter is never called (the network Sites page + * does not exist), so the guard is multisite-specific. + * * @since 2.5.0 * * @param array $actions Existing actions. @@ -208,8 +223,9 @@ private function setup_wordpress_sites_integration(): void { */ public function add_wp_sites_row_actions(array $actions, int $blog_id): array { - // Don't add for main site - if (is_main_site($blog_id)) { + // In multisite, skip the main site — it is exported from the regular admin + // Tools > Export & Import page to avoid confusion in the network admin. + if ( is_multisite() && is_main_site($blog_id) ) { return $actions; } @@ -284,7 +300,25 @@ public function handle_wp_sites_bulk_action(string $redirect_url, string $action } /** - * Add export/import menu page under Sites. + * Get the admin base URL for the export/import page. + * + * Returns the network admin Sites URL in multisite installations and the + * regular admin Tools URL in single-site installations. + * + * @since 2.5.1 + * @return string + */ + private function get_export_admin_base_url(): string { + + if ( is_multisite() ) { + return network_admin_url('sites.php'); + } + + return admin_url('tools.php'); + } + + /** + * Add export/import menu page under Sites (multisite network admin). * * @since 2.5.0 * @return void @@ -301,6 +335,339 @@ public function add_wp_export_menu_page(): void { ); } + /** + * Add export/import menu page under Tools for single-site installs. + * + * Registers a Tools > Export & Import page when the plugin is active on a + * plain (non-multisite) WordPress installation so that site owners can still + * export their site or import an export package without network admin access. + * + * @since 2.5.1 + * @return void + */ + public function add_single_site_export_menu_page(): void { + + add_management_page( + __('Export & Import', 'ultimate-multisite'), + __('Export & Import', 'ultimate-multisite'), + 'manage_options', + 'wu-site-export', + [$this, 'render_single_site_export_page'] + ); + } + + /** + * Render the export/import page for single-site WordPress installs. + * + * Shown at Tools > Export & Import when running outside of a multisite + * network. Supports exporting the current site and importing a ZIP package + * that was produced by a previous export. + * + * @since 2.5.1 + * @return void + */ + public function render_single_site_export_page(): void { + + $action = wu_request('action', ''); + + $exports = wu_exporter_get_all_exports(); + $pending_exports = function_exists('wu_exporter_get_pending') ? wu_exporter_get_pending() : []; + $pending_imports = wu_exporter_get_pending_imports(); + + ?> +
+

+ + + render_single_site_export_form(); ?> + + render_single_site_dashboard($exports, $pending_exports, $pending_imports); ?> + +
+ 'wu-site-export', + 'action' => 'do_export', + 'site_id' => $site_id, + ], + admin_url('tools.php') + ), + 'wu_export_site_' . $site_id + ); + + ?> +
+

+ +

+ ' . esc_html($sitename) . '', + esc_html($site_url) + ); + ?> +

+ +
+ + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ +

+ + +

+
+
+ 'wu-site-export', + 'action' => 'export', + ], + admin_url('tools.php') + ); + + ?> +
+

+

+

+ + + +

+
+ + +
+

+

+ + + + + + + + + + + + + + + +
options[0] ?? __('Unknown', 'ultimate-multisite')); ?>
+
+ + + +
+

+

+ + + + + + + + + $pending) : ?> + + + + + + +
options[0] ?? '')); ?> + + + +
+
+ + +
+

+ + +

+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ +
+ +
+

+

+ +
+

+
+ +
+ + + + + + + + + +
+ + +

+
+ +
+ +

+ +

+
+
+ Export & Import page). The required capability and + * redirect destination are determined by `get_export_admin_base_url()` and + * `current_user_can_export()`. * * @since 2.5.0 * @return void @@ -618,6 +990,10 @@ public function handle_direct_export_request(): void { return; } + // Required capability differs between multisite and single-site. + $required_cap = is_multisite() ? 'manage_network' : 'manage_options'; + $base_url = $this->get_export_admin_base_url(); + // Handle export if ('do_export' === $action) { $site_id = absint(wu_request('site_id', 0)); @@ -626,7 +1002,7 @@ public function handle_direct_export_request(): void { wp_die(esc_html__('Security check failed.', 'ultimate-multisite')); } - if (! current_user_can('manage_network')) { + if (! current_user_can($required_cap)) { wp_die(esc_html__('You do not have permission to export sites.', 'ultimate-multisite')); } @@ -647,7 +1023,7 @@ public function handle_direct_export_request(): void { 'page' => 'wu-site-export', 'message' => 'export_error', ], - network_admin_url('sites.php') + $base_url ) ); exit; @@ -661,7 +1037,7 @@ public function handle_direct_export_request(): void { 'page' => 'wu-site-export', 'message' => $message, ], - network_admin_url('sites.php') + $base_url ) ); exit; @@ -675,7 +1051,7 @@ public function handle_direct_export_request(): void { wp_die(esc_html__('Security check failed.', 'ultimate-multisite')); } - if (! current_user_can('manage_network')) { + if (! current_user_can($required_cap)) { wp_die(esc_html__('You do not have permission to delete exports.', 'ultimate-multisite')); } @@ -694,7 +1070,7 @@ public function handle_direct_export_request(): void { 'page' => 'wu-site-export', 'message' => 'deleted', ], - network_admin_url('sites.php') + $base_url ) ); exit; @@ -706,15 +1082,22 @@ public function handle_direct_export_request(): void { wp_die(esc_html__('Security check failed.', 'ultimate-multisite')); } - if (! current_user_can('manage_network')) { + if (! current_user_can($required_cap)) { wp_die(esc_html__('You do not have permission to import sites.', 'ultimate-multisite')); } $zip_url = sanitize_text_field(wp_unslash($_POST['zip_url'])); - $new_url = sanitize_text_field(wp_unslash($_POST['new_url'] ?? '')); $delete_zip = ! empty($_POST['delete_zip']); - if (empty($zip_url) || empty($new_url)) { + // In single-site installs the import overwrites the current site, so + // new_url is not required — fall back to the current site URL. + if ( is_multisite() ) { + $new_url = sanitize_text_field(wp_unslash($_POST['new_url'] ?? '')); + } else { + $new_url = sanitize_text_field(wp_unslash($_POST['new_url'] ?? get_site_url())); + } + + if ( empty($zip_url) || ( is_multisite() && empty($new_url) ) ) { wp_safe_redirect( add_query_arg( [ @@ -722,7 +1105,7 @@ public function handle_direct_export_request(): void { 'message' => 'import_error', 'error' => 'missing_fields', ], - network_admin_url('sites.php') + $base_url ) ); exit; @@ -738,7 +1121,7 @@ public function handle_direct_export_request(): void { 'message' => 'import_error', 'error' => 'file_not_found', ], - network_admin_url('sites.php') + $base_url ) ); exit; @@ -760,7 +1143,7 @@ public function handle_direct_export_request(): void { 'page' => 'wu-site-export', 'message' => 'import_started', ], - network_admin_url('sites.php') + $base_url ) ); exit; @@ -824,7 +1207,11 @@ public function display_export_notices(): void { */ public function enqueue_wp_sites_scripts(string $hook): void { - if ('sites_page_wu-site-export' !== $hook) { + // Multisite: hook is "sites_page_wu-site-export". + // Single-site (Tools menu): hook is "tools_page_wu-site-export". + $allowed_hooks = ['sites_page_wu-site-export', 'tools_page_wu-site-export']; + + if (! in_array($hook, $allowed_hooks, true)) { return; } diff --git a/inc/site-exporter/mu-migration/includes/commands/class-mu-migration-import.php b/inc/site-exporter/mu-migration/includes/commands/class-mu-migration-import.php index c6e123928..53988571d 100644 --- a/inc/site-exporter/mu-migration/includes/commands/class-mu-migration-import.php +++ b/inc/site-exporter/mu-migration/includes/commands/class-mu-migration-import.php @@ -460,7 +460,10 @@ public function all($args = [], $assoc_args = []) { } elseif ( $is_multisite ) { $blog_id = (int) $assoc_args['blog_id']; } else { + // Single-site install: import into the existing site (blog_id = 1). + // The import will overwrite the current site's database tables. $blog_id = 1; + WP_CLI::log( __( 'Single-site install detected. Importing into the existing site (blog_id=1). Existing content will be overwritten by the imported data.', 'mu-migration' ) ); } if ( ! $blog_id ) {