From 6eb2053293c53c1b4e0e02501c6336448ae140ac Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 15 Sep 2025 09:34:00 -0600 Subject: [PATCH 1/9] WiP. Add page to enable multisite. --- inc/admin-pages/class-base-admin-page.php | 2 +- .../class-multisite-setup-admin-page.php | 626 ++++++++++++++++++ .../class-setup-wizard-admin-page.php | 10 +- .../class-default-content-installer.php | 5 +- 4 files changed, 639 insertions(+), 4 deletions(-) create mode 100644 inc/admin-pages/class-multisite-setup-admin-page.php diff --git a/inc/admin-pages/class-base-admin-page.php b/inc/admin-pages/class-base-admin-page.php index 5cb9f17e..4109af08 100644 --- a/inc/admin-pages/class-base-admin-page.php +++ b/inc/admin-pages/class-base-admin-page.php @@ -216,7 +216,7 @@ public function get_id() { } /** - * Returns the appropriate capability for a this page, depending on the context. + * Returns the appropriate capability for this page, depending on the context. * * @since 2.0.0 * @return string diff --git a/inc/admin-pages/class-multisite-setup-admin-page.php b/inc/admin-pages/class-multisite-setup-admin-page.php new file mode 100644 index 00000000..dd6ef9a6 --- /dev/null +++ b/inc/admin-pages/class-multisite-setup-admin-page.php @@ -0,0 +1,626 @@ + 'capability_here' + * To add a page to the network admin (wp-admin/network), use: 'network_admin_menu' => 'capability_here' + * To add a page to the user (wp-admin/user) admin, use: 'user_admin_menu' => 'capability_here' + * + * @since 2.0.0 + * @var array + */ + protected $supported_panels = [ + 'admin_menu' => 'manage_options', + ]; + + /** + * Constructor method. + * + * @since 2.0.0 + * @return void + */ + public function __construct() { + + $this->type = 'menu'; + $this->position = 10_101_010; + $this->menu_icon = 'dashicons-wu-wp-ultimo'; + + parent::__construct(); + + add_action('admin_enqueue_scripts', [$this, 'register_scripts']); + } + + /** + * Returns the title of the page. + * + * @since 2.0.0 + * @return string Title of the page. + */ + public function get_title(): string { + return __('Enable WordPress Multisite', 'multisite-ultimate'); + } + + /** + * Returns the title of menu for this page. + * + * @since 2.0.0 + * @return string Menu label of the page. + */ + public function get_menu_title() { + return __('Multisite Ultimate', 'multisite-ultimate'); + } + + /** + * Returns the logo for the wizard. + * + * @since 2.0.0 + * @return string + */ + public function get_logo() { + return wu_get_asset('logo.webp', 'img'); + } + + /** + * Returns the sections for this Wizard. + * + * @since 2.0.0 + * @return array + */ + public function get_sections() { + + return [ + 'welcome' => [ + 'title' => __('Multisite Required', 'multisite-ultimate'), + 'description' => implode( + '

', + [ + __('WordPress Multisite is required for Multisite Ultimate to function properly.', 'multisite-ultimate'), + __('This wizard will guide you through enabling WordPress Multisite and configuring your network.', 'multisite-ultimate'), + __('We recommend creating a backup of your files and database before proceeding.', 'multisite-ultimate'), + ] + ), + 'next_label' => __('Get Started →', 'multisite-ultimate'), + 'back' => false, + ], + 'configure' => [ + 'title' => __('Network Configuration', 'multisite-ultimate'), + 'description' => __('Configure your network settings. These settings determine how your sites will be structured.', 'multisite-ultimate'), + 'next_label' => __('Create Network', 'multisite-ultimate'), + 'handler' => [$this, 'handle_configure'], + 'fields' => [$this, 'get_network_configuration_fields'], + ], + 'complete' => [ + 'title' => __('Setup Complete', 'multisite-ultimate'), + 'description' => __('WordPress Multisite setup is now complete!', 'multisite-ultimate'), + 'view' => [$this, 'section_complete'], + 'back' => false, + 'next' => false, + ], + ]; + } + + /** + * Welcome section view. + * + * @since 2.0.0 + * @return void + */ + public function section_welcome(): void { + + ?> +
+

+ +

+ +
+ +
+

+ +

+

+ +

+
    +
  1. +
  2. +
  3. +
  4. +
+
+ +
+
+
+ +
+
+

+ +

+

+ +

+
+
+
+ render_submit_box(); + } + + /** + * Returns the network configuration fields. + * + * @since 2.0.0 + * @return array + */ + public function get_network_configuration_fields() { + + $home_url = get_option('home'); + $base_domain = parse_url($home_url, PHP_URL_HOST); + $user = wp_get_current_user(); + + return [ + 'network_structure_header' => [ + 'type' => 'header', + 'title' => __('Network Structure', 'multisite-ultimate'), + 'desc' => __('Choose how you want your network sites to be organized:', 'multisite-ultimate'), + ], + 'subdomain_install' => [ + 'type' => 'radio', + 'title' => __('Site Structure', 'multisite-ultimate'), + 'desc' => __('Choose between subdirectories or subdomains for your network sites.', 'multisite-ultimate'), + 'options' => [ + 'sub0' => sprintf(__('Sites will use sub-directories like %s (Recommended)', 'multisite-ultimate'), '' . esc_html($base_domain) . '/site1'), + + 'sub1' => sprintf(__('Sites will use sub-domains like %s (Requires wildcard DNS)', 'multisite-ultimate'), 'site1.' . esc_html($base_domain) . ''), + + ], + 'default' => '0', + ], + 'network_details_header' => [ + 'type' => 'header', + 'title' => __('Network Details', 'multisite-ultimate'), + ], + 'sitename' => [ + 'type' => 'text', + 'title' => __('Network Title', 'multisite-ultimate'), + 'desc' => __('This will be the title of your network.', 'multisite-ultimate'), + 'placeholder' => __('Enter network title', 'multisite-ultimate'), + 'default' => get_option('blogname'), + ], + 'email' => [ + 'type' => 'email', + 'title' => __('Network Admin Email', 'multisite-ultimate'), + 'desc' => __('This email address will be used for network administration.', 'multisite-ultimate'), + 'placeholder' => __('Enter admin email', 'multisite-ultimate'), + 'default' => $user->user_email, + ], + 'backup_warning' => [ + 'type' => 'note', + 'desc' => '
+
+
+ +
+
+

' . __('Before You Continue', 'multisite-ultimate') . '

+

' . __('Please ensure you have a recent backup of your website files and database. The multisite setup process will modify your wp-config.php file and create new database tables.', 'multisite-ultimate') . '

+
+
+
', + ], + ]; + } + + /** + * Handles the network configuration form submission. + * + * @since 2.0.0 + * @return void + */ + public function handle_configure(): void { + + if (! current_user_can('manage_options')) { + wp_die(__('Permission denied.', 'multisite-ultimate')); + } + + $subdomain_install = (bool) wu_request('subdomain_install', 0); + $sitename = sanitize_text_field(wu_request('sitename', '')); + $email = sanitize_email(wu_request('email', '')); + + // Store values in transients for completion page + set_transient('wu_multisite_subdomain_install', $subdomain_install, 300); + set_transient('wu_multisite_sitename', $sitename, 300); + set_transient('wu_multisite_email', $email, 300); + + // Try to enable multisite + $wp_config_modified = $this->modify_wp_config(); + $network_created = false; + + if ($wp_config_modified) { + // Create the network + $network_created = $this->create_network($subdomain_install, $sitename, $email); + } + + // Store results + set_transient('wu_multisite_wp_config_modified', $wp_config_modified, 300); + set_transient('wu_multisite_network_created', $network_created, 300); + + // Redirect to completion step + wp_safe_redirect($this->get_next_section_link()); + exit; + } + + /** + * Completion section view. + * + * @since 2.0.0 + * @return void + */ + public function section_complete(): void { + + $wp_config_modified = get_transient('wu_multisite_wp_config_modified'); + $network_created = get_transient('wu_multisite_network_created'); + + if ($network_created && $wp_config_modified) : + ?> +
+
+
+ +
+
+

+ +

+

+ +

+
+
+
+ +
+ + + + +
+ display_manual_instructions(); + endif; + + // Clean up transients + delete_transient('wu_multisite_wp_config_modified'); + delete_transient('wu_multisite_network_created'); + delete_transient('wu_multisite_subdomain_install'); + delete_transient('wu_multisite_sitename'); + delete_transient('wu_multisite_email'); + } + + /** + * Display manual configuration instructions. + * + * @since 2.0.0 + * @return void + */ + protected function display_manual_instructions(): void { + + $home_url = get_option('home'); + $base_domain = parse_url($home_url, PHP_URL_HOST); + $subdomain_install = get_transient('wu_multisite_subdomain_install'); + + $wp_config_constants = "define( 'WP_ALLOW_MULTISITE', true ); +define( 'MULTISITE', true ); +define( 'SUBDOMAIN_INSTALL', " . ($subdomain_install ? 'true' : 'false') . " ); +define( 'DOMAIN_CURRENT_SITE', '{$base_domain}' ); +define( 'PATH_CURRENT_SITE', '/' ); +define( 'SITE_ID_CURRENT_SITE', 1 ); +define( 'BLOG_ID_CURRENT_SITE', 1 );"; + + $htaccess_rules = 'RewriteEngine On +RewriteRule ^index\.php$ - [L] + +# add a trailing slash to /wp-admin +RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L] + +RewriteCond %{REQUEST_FILENAME} -f [OR] +RewriteCond %{REQUEST_FILENAME} -d +RewriteRule ^ - [L] +RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L] +RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L] +RewriteRule . index.php [L]'; + + ?> +
+

+ +

+
+ +
+

+ +

+

+ wp-config.php', + '/* That\'s all, stop editing! Happy publishing. */' + ); + ?> +

+
+
+
+
+ +
+

+ +

+

+ +

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

+ +

+

+ +

+
+
+
+ +
+ + + + +
+ add_final_multisite_constants($subdomain_install, $domain); + + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Adds the final multisite constants to wp-config.php. + * + * @since 2.0.0 + * @param bool $subdomain_install Whether subdomains are used. + * @param string $domain The main domain. + * @return bool Whether the modification was successful. + */ + protected function add_final_multisite_constants(bool $subdomain_install, string $domain): bool { + + $wp_config_path = ABSPATH . 'wp-config.php'; + + if (! file_exists($wp_config_path) || ! is_writable($wp_config_path)) { + return false; + } + + $config_content = file_get_contents($wp_config_path); + + if ($config_content === false) { + return false; + } + + // Check if MULTISITE is already defined + if (strpos($config_content, 'MULTISITE') !== false) { + return true; // Already configured + } + + $constants_to_add = "\n// Multisite Ultimate: Multisite Configuration\n"; + $constants_to_add .= "define( 'MULTISITE', true );\n"; + $constants_to_add .= "define( 'SUBDOMAIN_INSTALL', " . ($subdomain_install ? 'true' : 'false') . " );\n"; + $constants_to_add .= "define( 'DOMAIN_CURRENT_SITE', '{$domain}' );\n"; + $constants_to_add .= "define( 'PATH_CURRENT_SITE', '/' );\n"; + $constants_to_add .= "define( 'SITE_ID_CURRENT_SITE', 1 );\n"; + $constants_to_add .= "define( 'BLOG_ID_CURRENT_SITE', 1 );\n\n"; + + // Find the location to insert the constants (after WP_ALLOW_MULTISITE) + $search = "define( 'WP_ALLOW_MULTISITE', true );"; + $insert_position = strpos($config_content, $search); + + if ($insert_position !== false) { + $insert_position += strlen($search); + $new_content = substr_replace($config_content, $constants_to_add, $insert_position, 0); + return file_put_contents($wp_config_path, $new_content) !== false; + } + + return false; + } + + /** + * Register page scripts and styles. + * + * @since 2.0.0 + * @return void + */ + public function register_scripts(): void { + + if (get_current_screen()->id !== 'toplevel_page_wp-ultimo-multisite-setup') { + return; + } + + wp_add_inline_script( + 'wp-admin', + ' + // Copy to clipboard functionality + document.addEventListener("DOMContentLoaded", function() { + document.querySelectorAll("button[onclick*=\'navigator.clipboard.writeText\']").forEach(function(button) { + button.addEventListener("click", function() { + var textarea = this.nextElementSibling; + if (textarea && textarea.tagName === "TEXTAREA") { + navigator.clipboard.writeText(textarea.value).then(function() { + button.textContent = "Copied!"; + setTimeout(function() { + button.textContent = "Copy to clipboard"; + }, 2000); + }); + } + }); + }); + }); + ' + ); + } +} \ No newline at end of file diff --git a/inc/admin-pages/class-setup-wizard-admin-page.php b/inc/admin-pages/class-setup-wizard-admin-page.php index 8c9906e0..685f3ad3 100644 --- a/inc/admin-pages/class-setup-wizard-admin-page.php +++ b/inc/admin-pages/class-setup-wizard-admin-page.php @@ -221,7 +221,15 @@ public function set_settings(): void { */ public function redirect_to_wizard(): void { - if ( ! Requirements::run_setup() && wu_request('page') !== 'wp-ultimo-setup') { + // If multisite is not enabled, redirect to multisite setup page + if ( ! is_multisite() && wu_request('page') !== 'wp-ultimo-multisite-setup') { + wp_safe_redirect(admin_url('admin.php?page=wp-ultimo-multisite-setup')); + + exit; + } + + // If multisite is enabled but setup is not finished, redirect to setup wizard + if ( is_multisite() && ! Requirements::run_setup() && wu_request('page') !== 'wp-ultimo-setup') { wp_safe_redirect(wu_network_admin_url('wp-ultimo-setup')); exit; diff --git a/inc/installers/class-default-content-installer.php b/inc/installers/class-default-content-installer.php index fc278113..cc0b1030 100644 --- a/inc/installers/class-default-content-installer.php +++ b/inc/installers/class-default-content-installer.php @@ -52,8 +52,9 @@ public function init(): void { */ protected function done_creating_template_site() { - $current_site = get_current_site(); - + if (! is_multisite()) { + return false; + } $d = wu_get_site_domain_and_path('template'); return domain_exists($d->domain, $d->path, get_current_network_id()); From f844d95c59932b8b91ef7e3d6a407dd17214c886 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 15 Sep 2025 09:34:21 -0600 Subject: [PATCH 2/9] WiP. Add page to enable multisite. --- inc/class-wp-ultimo.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php index e9b68296..ac2ac33a 100644 --- a/inc/class-wp-ultimo.php +++ b/inc/class-wp-ultimo.php @@ -140,6 +140,11 @@ public function init(): void { */ new WP_Ultimo\Admin_Pages\Setup_Wizard_Admin_Page(); + /* + * Multisite Setup for non-multisite installations + */ + new WP_Ultimo\Admin_Pages\Multisite_Setup_Admin_Page(); + /* * Loads the Multisite Ultimate settings helper class. */ From 2b4c5ece2fa63b7fd3e8e5fbc7151530674315b3 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 9 Feb 2026 20:47:57 -0700 Subject: [PATCH 3/9] Fix single-site compatibility and dashboard widget setup status - Guard multisite-only functions (switch_to_blog, get_blog_details) with is_multisite() checks to prevent fatal errors on single-site installs - Defer Multisite_Setup_Admin_Page instantiation to init action and only load on non-multisite installations - Add admin_init redirect to guide users to the appropriate setup wizard - Fix wp-config.php constant detection to use regex instead of strpos, preventing false matches on commented-out constants - Fix First Steps dashboard widget not recognizing setup completion when stored as a timestamp (wu_string_to_bool fails on timestamp strings) Co-Authored-By: Claude Opus 4.6 --- inc/admin-pages/class-multisite-setup-admin-page.php | 8 ++++---- inc/admin-pages/class-setup-wizard-admin-page.php | 1 + .../signup-fields/class-signup-field-username.php | 6 +++--- inc/class-dashboard-widgets.php | 2 +- inc/class-wp-ultimo.php | 6 +++++- inc/functions/date.php | 8 ++++++-- inc/functions/site.php | 4 ++++ 7 files changed, 24 insertions(+), 11 deletions(-) diff --git a/inc/admin-pages/class-multisite-setup-admin-page.php b/inc/admin-pages/class-multisite-setup-admin-page.php index dd6ef9a6..8144ae24 100644 --- a/inc/admin-pages/class-multisite-setup-admin-page.php +++ b/inc/admin-pages/class-multisite-setup-admin-page.php @@ -474,8 +474,8 @@ protected function modify_wp_config(): bool { return false; } - // Check if WP_ALLOW_MULTISITE is already defined - if (strpos($config_content, 'WP_ALLOW_MULTISITE') !== false) { + // Check if WP_ALLOW_MULTISITE is already actively defined (not commented out) + if (preg_match('/^\s*define\s*\(\s*[\'"]WP_ALLOW_MULTISITE[\'"]/m', $config_content)) { return true; // Already configured } @@ -563,8 +563,8 @@ protected function add_final_multisite_constants(bool $subdomain_install, string return false; } - // Check if MULTISITE is already defined - if (strpos($config_content, 'MULTISITE') !== false) { + // Check if MULTISITE is already actively defined (not commented out) + if (preg_match('/^\s*define\s*\(\s*[\'"]MULTISITE[\'"]/m', $config_content)) { return true; // Already configured } diff --git a/inc/admin-pages/class-setup-wizard-admin-page.php b/inc/admin-pages/class-setup-wizard-admin-page.php index 46ed7a81..1d20f523 100644 --- a/inc/admin-pages/class-setup-wizard-admin-page.php +++ b/inc/admin-pages/class-setup-wizard-admin-page.php @@ -146,6 +146,7 @@ public function __construct() { * Redirect on activation */ add_action('wu_activation', [$this, 'redirect_to_wizard']); + add_action('admin_init', [$this, 'redirect_to_wizard']); add_action('admin_init', [$this, 'alert_incomplete_installation']); } diff --git a/inc/checkout/signup-fields/class-signup-field-username.php b/inc/checkout/signup-fields/class-signup-field-username.php index bf225483..ecd2fd95 100644 --- a/inc/checkout/signup-fields/class-signup-field-username.php +++ b/inc/checkout/signup-fields/class-signup-field-username.php @@ -123,8 +123,8 @@ public function get_icon() { public function defaults() { return [ - 'auto_generate_username' => false, - 'enable_inline_login_username' => false, + 'auto_generate_username' => false, + 'enable_inline_login_username' => false, ]; } @@ -166,7 +166,7 @@ public function force_attributes() { public function get_fields() { return [ - 'auto_generate_username' => [ + 'auto_generate_username' => [ 'type' => 'toggle', 'title' => __('Auto-generate', 'ultimate-multisite'), 'desc' => __('Check this option to auto-generate this field based on the email address of the customer.', 'ultimate-multisite'), diff --git a/inc/class-dashboard-widgets.php b/inc/class-dashboard-widgets.php index f1aa7203..d539604e 100644 --- a/inc/class-dashboard-widgets.php +++ b/inc/class-dashboard-widgets.php @@ -186,7 +186,7 @@ public function output_widget_first_steps(): void { 'desc' => __('Go through the initial Setup Wizard to configure the basic settings of your network.', 'ultimate-multisite'), 'action_label' => __('Finish the Setup Wizard', 'ultimate-multisite'), 'action_link' => wu_network_admin_url('wp-ultimo-setup'), - 'done' => wu_string_to_bool($initial_setup_done), + 'done' => ! empty($initial_setup_done), ], 'payment-method' => [ 'title' => __('Payment Method', 'ultimate-multisite'), diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php index 25ed4ff1..9a3da72e 100644 --- a/inc/class-wp-ultimo.php +++ b/inc/class-wp-ultimo.php @@ -153,7 +153,11 @@ public function init(): void { /* * Multisite Setup for non-multisite installations */ - new WP_Ultimo\Admin_Pages\Multisite_Setup_Admin_Page(); + if ( ! is_multisite()) { + add_action('init', function() { + new WP_Ultimo\Admin_Pages\Multisite_Setup_Admin_Page(); + }); + } /* * Loads the Ultimate Multisite settings helper class. diff --git a/inc/functions/date.php b/inc/functions/date.php index 84ddc6ca..450d5291 100644 --- a/inc/functions/date.php +++ b/inc/functions/date.php @@ -88,11 +88,15 @@ function wu_get_days_ago($date_1, $date_2 = false) { */ function wu_get_current_time($type = 'mysql', $gmt = false) { - switch_to_blog(wu_get_main_site_id()); + if (is_multisite()) { + switch_to_blog(wu_get_main_site_id()); + } $time = current_time($type, $gmt); // phpcs:ignore - restore_current_blog(); + if (is_multisite()) { + restore_current_blog(); + } return $time; } diff --git a/inc/functions/site.php b/inc/functions/site.php index 56f24661..98fc17ec 100644 --- a/inc/functions/site.php +++ b/inc/functions/site.php @@ -17,6 +17,10 @@ */ function wu_get_current_site() { + if ( ! is_multisite()) { + return null; + } + static $sites = array(); $blog_id = get_current_blog_id(); From e4dab305d9b1e04b591774b975840892107d1656 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 10 Feb 2026 15:16:57 -0700 Subject: [PATCH 4/9] Address CodeRabbit review comments on multisite setup wizard - Fix radio boolean conversion: use '0'/'1' keys instead of 'sub0'/'sub1' and compare with === '1' instead of (bool) cast (#3) - Fix namespace: catch (\Exception) instead of catch (Exception) (#4) - Use regex for flexible WP_ALLOW_MULTISITE anchor matching with fallback insertion point in add_final_multisite_constants() (#5) - Propagate add_final_multisite_constants() failure to create_network() (#6) - Support wp-config.php one level above ABSPATH via get_wp_config_path() (#7) - Replace parse_url() with wp_parse_url() at all 4 locations (#8) - Escape wp_die() output with esc_html__() (#9) - Use Yoda conditions for false comparisons (#10) - Wire up welcome view callback in sections array (#12) - Fix inline script handle from 'wp-admin' to 'jquery' (#13) - Provide subdomain-specific .htaccess rules in manual instructions (#15) - Guard redirect_to_wizard() with wp_doing_ajax() and capability check (#1) - Update wu_get_current_site() @return type to include null (#2) Co-Authored-By: Claude Opus 4.6 --- .../class-multisite-setup-admin-page.php | 108 +++++++++++++----- .../class-setup-wizard-admin-page.php | 4 + inc/functions/site.php | 2 +- 3 files changed, 83 insertions(+), 31 deletions(-) diff --git a/inc/admin-pages/class-multisite-setup-admin-page.php b/inc/admin-pages/class-multisite-setup-admin-page.php index 8144ae24..1bd62753 100644 --- a/inc/admin-pages/class-multisite-setup-admin-page.php +++ b/inc/admin-pages/class-multisite-setup-admin-page.php @@ -133,6 +133,7 @@ public function get_sections() { ), 'next_label' => __('Get Started →', 'multisite-ultimate'), 'back' => false, + 'view' => [$this, 'section_welcome'], ], 'configure' => [ 'title' => __('Network Configuration', 'multisite-ultimate'), @@ -216,7 +217,7 @@ public function section_welcome(): void { public function get_network_configuration_fields() { $home_url = get_option('home'); - $base_domain = parse_url($home_url, PHP_URL_HOST); + $base_domain = wp_parse_url($home_url, PHP_URL_HOST); $user = wp_get_current_user(); return [ @@ -230,10 +231,8 @@ public function get_network_configuration_fields() { 'title' => __('Site Structure', 'multisite-ultimate'), 'desc' => __('Choose between subdirectories or subdomains for your network sites.', 'multisite-ultimate'), 'options' => [ - 'sub0' => sprintf(__('Sites will use sub-directories like %s (Recommended)', 'multisite-ultimate'), '' . esc_html($base_domain) . '/site1'), - - 'sub1' => sprintf(__('Sites will use sub-domains like %s (Requires wildcard DNS)', 'multisite-ultimate'), 'site1.' . esc_html($base_domain) . ''), - + '0' => sprintf(__('Sites will use sub-directories like %s (Recommended)', 'multisite-ultimate'), '' . esc_html($base_domain) . '/site1'), + '1' => sprintf(__('Sites will use sub-domains like %s (Requires wildcard DNS)', 'multisite-ultimate'), 'site1.' . esc_html($base_domain) . ''), ], 'default' => '0', ], @@ -281,10 +280,10 @@ public function get_network_configuration_fields() { public function handle_configure(): void { if (! current_user_can('manage_options')) { - wp_die(__('Permission denied.', 'multisite-ultimate')); + wp_die(esc_html__('Permission denied.', 'multisite-ultimate')); } - $subdomain_install = (bool) wu_request('subdomain_install', 0); + $subdomain_install = wu_request('subdomain_install', '0') === '1'; $sitename = sanitize_text_field(wu_request('sitename', '')); $email = sanitize_email(wu_request('email', '')); @@ -368,7 +367,7 @@ public function section_complete(): void { protected function display_manual_instructions(): void { $home_url = get_option('home'); - $base_domain = parse_url($home_url, PHP_URL_HOST); + $base_domain = wp_parse_url($home_url, PHP_URL_HOST); $subdomain_install = get_transient('wu_multisite_subdomain_install'); $wp_config_constants = "define( 'WP_ALLOW_MULTISITE', true ); @@ -379,7 +378,25 @@ protected function display_manual_instructions(): void { define( 'SITE_ID_CURRENT_SITE', 1 ); define( 'BLOG_ID_CURRENT_SITE', 1 );"; - $htaccess_rules = 'RewriteEngine On + if ($subdomain_install) { + $htaccess_rules = 'RewriteEngine On +RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] +RewriteBase / +RewriteRule ^index\.php$ - [L] + +# add a trailing slash to /wp-admin +RewriteRule ^wp-admin$ wp-admin/ [R=301,L] + +RewriteCond %{REQUEST_FILENAME} -f [OR] +RewriteCond %{REQUEST_FILENAME} -d +RewriteRule ^ - [L] +RewriteRule ^(wp-(content|admin|includes).*) $1 [L] +RewriteRule ^(.*\.php)$ $1 [L] +RewriteRule . index.php [L]'; + } else { + $htaccess_rules = 'RewriteEngine On +RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] +RewriteBase / RewriteRule ^index\.php$ - [L] # add a trailing slash to /wp-admin @@ -391,6 +408,7 @@ protected function display_manual_instructions(): void { RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L] RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L] RewriteRule . index.php [L]'; + } ?>
@@ -454,6 +472,30 @@ protected function display_manual_instructions(): void { get_wp_config_path(); - if (! file_exists($wp_config_path) || ! is_writable($wp_config_path)) { + if (false === $wp_config_path) { return false; } $config_content = file_get_contents($wp_config_path); - if ($config_content === false) { + if (false === $config_content) { return false; } @@ -483,13 +525,13 @@ protected function modify_wp_config(): bool { $search = "/* That's all, stop editing! Happy publishing. */"; $insert_position = strpos($config_content, $search); - if ($insert_position === false) { + if (false === $insert_position) { // Fallback: look for the wp-settings.php include $search = "require_once ABSPATH . 'wp-settings.php';"; $insert_position = strpos($config_content, $search); } - if ($insert_position === false) { + if (false === $insert_position) { return false; // Can't find a safe place to insert } @@ -522,8 +564,8 @@ protected function create_network(bool $subdomain_install, string $sitename, str // Create network tables install_network(); - $base = parse_url(trailingslashit(get_option('home')), PHP_URL_PATH); - $domain = parse_url(get_option('home'), PHP_URL_HOST); + $base = wp_parse_url(trailingslashit(get_option('home')), PHP_URL_PATH); + $domain = wp_parse_url(get_option('home'), PHP_URL_HOST); // Populate network $result = populate_network(1, $domain, $email, $sitename, $base, $subdomain_install); @@ -533,10 +575,8 @@ protected function create_network(bool $subdomain_install, string $sitename, str } // Add final multisite constants to wp-config.php - $this->add_final_multisite_constants($subdomain_install, $domain); - - return true; - } catch (Exception $e) { + return $this->add_final_multisite_constants($subdomain_install, $domain); + } catch (\Exception $e) { return false; } } @@ -551,15 +591,15 @@ protected function create_network(bool $subdomain_install, string $sitename, str */ protected function add_final_multisite_constants(bool $subdomain_install, string $domain): bool { - $wp_config_path = ABSPATH . 'wp-config.php'; + $wp_config_path = $this->get_wp_config_path(); - if (! file_exists($wp_config_path) || ! is_writable($wp_config_path)) { + if (false === $wp_config_path) { return false; } $config_content = file_get_contents($wp_config_path); - if ($config_content === false) { + if (false === $config_content) { return false; } @@ -576,13 +616,21 @@ protected function add_final_multisite_constants(bool $subdomain_install, string $constants_to_add .= "define( 'SITE_ID_CURRENT_SITE', 1 );\n"; $constants_to_add .= "define( 'BLOG_ID_CURRENT_SITE', 1 );\n\n"; - // Find the location to insert the constants (after WP_ALLOW_MULTISITE) - $search = "define( 'WP_ALLOW_MULTISITE', true );"; + // Find the location to insert the constants (after WP_ALLOW_MULTISITE) using regex for flexible spacing + if (preg_match('/define\s*\(\s*[\'"]WP_ALLOW_MULTISITE[\'"]\s*,\s*true\s*\)\s*;/i', $config_content, $matches, PREG_OFFSET_CAPTURE)) { + $insert_position = $matches[0][1] + strlen($matches[0][0]); + $new_content = substr_replace($config_content, $constants_to_add, $insert_position, 0); + + return file_put_contents($wp_config_path, $new_content) !== false; + } + + // Fallback: insert before "That's all" comment + $search = "/* That's all, stop editing! Happy publishing. */"; $insert_position = strpos($config_content, $search); - if ($insert_position !== false) { - $insert_position += strlen($search); - $new_content = substr_replace($config_content, $constants_to_add, $insert_position, 0); + if (false !== $insert_position) { + $new_content = substr_replace($config_content, $constants_to_add, $insert_position, 0); + return file_put_contents($wp_config_path, $new_content) !== false; } @@ -602,7 +650,7 @@ public function register_scripts(): void { } wp_add_inline_script( - 'wp-admin', + 'jquery', ' // Copy to clipboard functionality document.addEventListener("DOMContentLoaded", function() { @@ -623,4 +671,4 @@ public function register_scripts(): void { ' ); } -} \ No newline at end of file +} diff --git a/inc/admin-pages/class-setup-wizard-admin-page.php b/inc/admin-pages/class-setup-wizard-admin-page.php index 1d20f523..93e717bb 100644 --- a/inc/admin-pages/class-setup-wizard-admin-page.php +++ b/inc/admin-pages/class-setup-wizard-admin-page.php @@ -225,6 +225,10 @@ public function set_settings(): void { */ public function redirect_to_wizard(): void { + if (wp_doing_ajax() || ! current_user_can('manage_options')) { + return; + } + // If multisite is not enabled, redirect to multisite setup page if ( ! is_multisite() && wu_request('page') !== 'wp-ultimo-multisite-setup') { wp_safe_redirect(admin_url('admin.php?page=wp-ultimo-multisite-setup')); diff --git a/inc/functions/site.php b/inc/functions/site.php index 98fc17ec..02e0d5ad 100644 --- a/inc/functions/site.php +++ b/inc/functions/site.php @@ -13,7 +13,7 @@ * Returns the current site. * * @since 2.0.0 - * @return \WP_Ultimo\Models\Site + * @return \WP_Ultimo\Models\Site|null */ function wu_get_current_site() { From c6de8f3d0b2a5f20726077593adf53fe81d4f2f6 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 10 Feb 2026 19:47:03 -0700 Subject: [PATCH 5/9] Fix: flush rewrite rules when signup pages are created or modified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checkout rewrite rules (e.g. /register/plan-slug) depend on the registration page's slug, but flush_rewrite_rules() was only called on settings page save or plugin update. This left stale/missing rules when pages were created by the setup wizard or had their slug changed. - Hook save_post_page and wp_trash_post in Checkout_Pages to flush rewrite rules when any signup page is modified - Flush rewrite rules in Default_Content_Installer after creating the registration page during setup - Flush rewrite rules in Migrator after creating the registration page during v1→v2 migration - Fix docblock @param and @return types in Current class Co-Authored-By: Claude Opus 4.6 --- inc/checkout/class-checkout-pages.php | 48 +++++++++++++++++++ inc/class-current.php | 8 ++-- .../class-default-content-installer.php | 6 +++ inc/installers/class-migrator.php | 5 ++ 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/inc/checkout/class-checkout-pages.php b/inc/checkout/class-checkout-pages.php index b316e0ed..8ce32128 100644 --- a/inc/checkout/class-checkout-pages.php +++ b/inc/checkout/class-checkout-pages.php @@ -55,6 +55,9 @@ public function init(): void { if (is_main_site()) { add_action('before_signup_header', [$this, 'redirect_to_registration_page']); + add_action('save_post_page', [$this, 'maybe_flush_rewrite_rules_on_page_save'], 20); + add_action('wp_trash_post', [$this, 'maybe_flush_rewrite_rules_on_page_trash']); + if ( ! $use_custom_login) { return; } @@ -154,6 +157,51 @@ public function handle_compat_mode_setting($post_id): void { } } + /** + * Flush rewrite rules when a signup page is saved and its slug may have changed. + * + * The checkout rewrite rules depend on the registration page slug, + * so they must be refreshed whenever a signup page is modified. + * + * @since 2.3.0 + * + * @param int $post_id The post ID. + * @return void + */ + public function maybe_flush_rewrite_rules_on_page_save(int $post_id): void { + + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { + return; + } + + $signup_page_ids = array_filter(array_map('absint', array_values($this->get_signup_pages()))); + + if (in_array(absint($post_id), $signup_page_ids, true)) { + flush_rewrite_rules(); + } + } + + /** + * Flush rewrite rules when a signup page is trashed. + * + * @since 2.3.0 + * + * @param int $post_id The post ID. + * @return void + */ + public function maybe_flush_rewrite_rules_on_page_trash(int $post_id): void { + + if (get_post_type($post_id) !== 'page') { + return; + } + + $signup_page_ids = array_filter(array_map('absint', array_values($this->get_signup_pages()))); + + if (in_array(absint($post_id), $signup_page_ids, true)) { + flush_rewrite_rules(); + } + } + /** * Replace wp-login.php in email URLs. * diff --git a/inc/class-current.php b/inc/class-current.php index 9297bfbe..3e7f260c 100644 --- a/inc/class-current.php +++ b/inc/class-current.php @@ -136,7 +136,7 @@ public function add_rewrite_rules(): void { * @since 2.0.0 * * @param array $query_vars The WP_Query object. - * @return \WP_Query + * @return array */ public function add_query_vars($query_vars) { @@ -332,7 +332,7 @@ public function set_site($site): void { * @since 2.0.9 * * @param \WP_Ultimo\Models\Site $site The current site to set. - * @param self The Current class instance. + * @param self $current The Current class instance. * @return \WP_Ultimo\Models\Site */ $site = apply_filters('wu_current_set_site', $site, $this); @@ -378,7 +378,7 @@ public function set_customer($customer): void { * @since 2.0.9 * * @param \WP_Ultimo\Models\Customer $customer The current customer to set. - * @param self The Current class instance. + * @param self $current The Current class instance. * @return \WP_Ultimo\Models\Customer */ $customer = apply_filters('wu_current_set_customer', $customer, $this); @@ -413,7 +413,7 @@ public function set_membership($membership): void { * @since 2.0.18 * * @param \WP_Ultimo\Models\Membership $membership The current membership to set. - * @param self The Current class instance. + * @param self $current The Current class instance. * @return \WP_Ultimo\Models\Membership */ $membership = apply_filters('wu_current_set_membership', $membership, $this); diff --git a/inc/installers/class-default-content-installer.php b/inc/installers/class-default-content-installer.php index 62b7ec21..92a8b792 100644 --- a/inc/installers/class-default-content-installer.php +++ b/inc/installers/class-default-content-installer.php @@ -384,6 +384,12 @@ public function _install_create_checkout(): void { * Set page as the default registration page. */ wu_save_setting('default_registration_page', $page_id); + + /* + * Flush rewrite rules so checkout URL patterns + * (e.g. /register/plan-slug) work immediately. + */ + flush_rewrite_rules(true); } /** diff --git a/inc/installers/class-migrator.php b/inc/installers/class-migrator.php index 2a95b241..221a39e4 100644 --- a/inc/installers/class-migrator.php +++ b/inc/installers/class-migrator.php @@ -2359,6 +2359,11 @@ protected function _install_forms() { */ wu_save_setting('default_registration_page', $page_id); + /* + * Flush rewrite rules so checkout URL patterns work immediately. + */ + flush_rewrite_rules(true); + /* * Get post name based on setting for login page */ From 204319af04d0e3e5694492d3cbaae9f459d6a3a6 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 10 Feb 2026 20:28:18 -0700 Subject: [PATCH 6/9] Improve multisite setup wizard network type field - Change network type from radio buttons to select dropdown - Default to subdomains (recommended) instead of subdirectories - Add translators comments for placeholder strings - Use WP_Filesystem instead of direct is_writable() calls Co-Authored-By: Claude Opus 4.6 --- .../class-multisite-setup-admin-page.php | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/inc/admin-pages/class-multisite-setup-admin-page.php b/inc/admin-pages/class-multisite-setup-admin-page.php index 1bd62753..922b74a0 100644 --- a/inc/admin-pages/class-multisite-setup-admin-page.php +++ b/inc/admin-pages/class-multisite-setup-admin-page.php @@ -227,14 +227,22 @@ public function get_network_configuration_fields() { 'desc' => __('Choose how you want your network sites to be organized:', 'multisite-ultimate'), ], 'subdomain_install' => [ - 'type' => 'radio', + 'type' => 'select', 'title' => __('Site Structure', 'multisite-ultimate'), - 'desc' => __('Choose between subdirectories or subdomains for your network sites.', 'multisite-ultimate'), + 'desc' => __('Choose between subdomains or subdirectories for your network sites.', 'multisite-ultimate'), 'options' => [ - '0' => sprintf(__('Sites will use sub-directories like %s (Recommended)', 'multisite-ultimate'), '' . esc_html($base_domain) . '/site1'), - '1' => sprintf(__('Sites will use sub-domains like %s (Requires wildcard DNS)', 'multisite-ultimate'), 'site1.' . esc_html($base_domain) . ''), + '1' => sprintf( + /* translators: %s is an example subdomain URL like site1.example.com */ + __('Sub-domains — e.g. %s (Recommended)', 'multisite-ultimate'), + 'site1.' . esc_html($base_domain) + ), + '0' => sprintf( + /* translators: %s is an example subdirectory URL like example.com/site1 */ + __('Sub-directories — e.g. %s', 'multisite-ultimate'), + esc_html($base_domain) . '/site1' + ), ], - 'default' => '0', + 'default' => '1', ], 'network_details_header' => [ 'type' => 'header', @@ -424,6 +432,7 @@ protected function display_manual_instructions(): void {

wp-config.php', '/* That\'s all, stop editing! Happy publishing. */' @@ -480,16 +489,24 @@ protected function display_manual_instructions(): void { */ protected function get_wp_config_path() { + global $wp_filesystem; + + if ( ! $wp_filesystem) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + + WP_Filesystem(); + } + $wp_config_path = ABSPATH . 'wp-config.php'; - if (file_exists($wp_config_path) && is_writable($wp_config_path)) { + if ($wp_filesystem->exists($wp_config_path) && $wp_filesystem->is_writable($wp_config_path)) { return $wp_config_path; } // WordPress supports wp-config.php one level above ABSPATH $wp_config_path = trailingslashit(dirname(ABSPATH)) . 'wp-config.php'; - if (file_exists($wp_config_path) && is_writable($wp_config_path)) { + if ($wp_filesystem->exists($wp_config_path) && $wp_filesystem->is_writable($wp_config_path)) { return $wp_config_path; } From ab8389c6a54876021b9731b19c124b392cc43eb4 Mon Sep 17 00:00:00 2001 From: David Stone Date: Wed, 11 Feb 2026 20:01:19 -0700 Subject: [PATCH 7/9] Fix multisite setup wizard error handling and add network activation step - Fix Base_Installer::handle() to return WP_Error on failure instead of silently swallowing exceptions - Fix setup_install() to check return value and send wp_send_json_error on failure - Register missing wp_ajax_wu_multisite_install AJAX action - Fix JS error callback to display error messages instead of blank text - Add network_activate step to Multisite_Network_Installer - Update Recommended_Plugins_Installer and Migrator handle() to return status for filter chain - Change wu_handle_ajax_installers from add_action to add_filter (matches apply_filters usage) - Configure PHPCS to not exit non-zero on warnings (only errors) Co-Authored-By: Claude Opus 4.6 --- .phpcs.xml.dist | 3 + assets/js/setup-wizard.js | 272 ++++++------ assets/js/setup-wizard.min.js | 2 +- .../class-multisite-setup-admin-page.php | 396 ++++++----------- .../class-setup-wizard-admin-page.php | 67 ++- inc/admin/class-configuration-checker.php | 2 +- inc/class-hooks.php | 18 + inc/class-requirements.php | 2 +- inc/class-wp-ultimo.php | 23 +- inc/helpers/class-wp-config.php | 10 +- inc/installers/class-base-installer.php | 9 +- inc/installers/class-migrator.php | 10 +- .../class-multisite-network-installer.php | 415 ++++++++++++++++++ .../class-recommended-plugins-installer.php | 16 +- views/wizards/multisite-setup/welcome.php | 50 +++ 15 files changed, 834 insertions(+), 461 deletions(-) create mode 100644 inc/installers/class-multisite-network-installer.php create mode 100644 views/wizards/multisite-setup/welcome.php diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 22b0ace2..a702c05b 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -18,6 +18,9 @@ + + + diff --git a/assets/js/setup-wizard.js b/assets/js/setup-wizard.js index 47acc46d..d7718d1e 100644 --- a/assets/js/setup-wizard.js +++ b/assets/js/setup-wizard.js @@ -1,212 +1,218 @@ /* global wu_setup, wu_setup_settings, ajaxurl, wu_block_ui_polyfill, _wu_block_ui_polyfill */ (function($) { - window._wu_block_ui_polyfill = wu_block_ui_polyfill; + window._wu_block_ui_polyfill = wu_block_ui_polyfill; - wu_block_ui_polyfill = function() { }; + wu_block_ui_polyfill = function() { }; - $(document).ready(function() { + $(document).ready(function() { - // Click button - // Generates queue - // Start to process queue items one by one - // Changes the status - // Move to the next item - // When all is done, redirect to the next page via a form submission - $('#poststuff').on('submit', 'form', function(e) { + // Click button + // Generates queue + // Start to process queue items one by one + // Changes the status + // Move to the next item + // When all is done, redirect to the next page via a form submission + $('#poststuff').on('submit', 'form', function(e) { - e.preventDefault(); + e.preventDefault(); - const $form = $(this); + const $form = $(this); - const install_id = $form.find('table[data-id]').data('id'); + const install_id = $form.find('table[data-id]').data('id'); - $form.find('[name=next]').attr('disabled', 'disabled'); + $form.find('[name=next]').attr('disabled', 'disabled'); - let queue = $form.find('tr[data-content]'); + let queue = $form.find('tr[data-content]'); - /* + /* * Only keep items selected on the queue. */ - queue = queue.filter(function() { + queue = queue.filter(function() { - const checkbox = $(this).find('input[type=checkbox]'); + const checkbox = $(this).find('input[type=checkbox]'); - if (checkbox.length) { + if (checkbox.length) { - return checkbox.is(':checked'); + return checkbox.is(':checked'); - } // end if; + } // end if; - return true; + return true; - }); + }); - let successes = 0; + let successes = 0; - let index = 0; + let index = 0; - process_queue_item(queue.eq(index)); + process_queue_item(queue.eq(index)); - /** - * Process the queue items one by one recursively. - * - * @param {string} item The item to process. - */ - function process_queue_item(item) { + /** + * Process the queue items one by one recursively. + * + * @param {string} item The item to process. + */ + function process_queue_item(item) { + + window.onbeforeunload = function() { + + return ''; - window.onbeforeunload = function() { + }; - return ''; + if (item.length === 0) { - }; + if (queue.length === successes || install_id === 'migration') { - if (item.length === 0) { + window.onbeforeunload = null; - if (queue.length === successes || install_id === 'migration') { + _wu_block_ui_polyfill($('#poststuff .inside')); - window.onbeforeunload = null; + setTimeout(() => { - _wu_block_ui_polyfill($('#poststuff .inside')); + $form.get(0).submit(); - setTimeout(() => { + }, 100); - $form.get(0).submit(); + } // end if; - }, 100); + $form.find('[name=next]').removeAttr('disabled'); - } // end if; + return false; - $form.find('[name=next]').removeAttr('disabled'); + } // end if; - return false; + const $item = $(item); - } // end if; + const content = $item.data('content'); - const $item = $(item); + $item.get(0).scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); - const content = $item.data('content'); + $item.find('td.status') + .attr('class', '') + .addClass('status') + .find('> span').html(wu_setup[ content ].installing).end() + .find('.spinner').addClass('is-active').end() + .find('a.help').slideUp(); - $item.get(0).scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); + // Ajax request + $.ajax({ + url: ajaxurl, + method: 'post', + data: { + action: wu_setup_settings.ajax_action || 'wu_setup_install', + installer: content, + 'dry-run': wu_setup_settings.dry_run, + }, + success(data) { - $item.find('td.status') - .attr('class', '') - .addClass('status') - .find('> span').html(wu_setup[content].installing).end() - .find('.spinner').addClass('is-active').end() - .find('a.help').slideUp(); + if (data.success === true) { - // Ajax request - $.ajax({ - url: ajaxurl, - method: 'post', - data: { - action: 'wu_setup_install', - installer: content, - 'dry-run': wu_setup_settings.dry_run, - }, - success(data) { + $item.find('td.status') + .attr('class', '') + .addClass('status wu-text-green-600') + .find('> span').html(wu_setup[ content ].success).end() + .find('.spinner').removeClass('is-active'); - if (data.success === true) { + $item.removeAttr('data-content'); - $item.find('td.status') - .attr('class', '') - .addClass('status wu-text-green-600') - .find('> span').html(wu_setup[content].success).end() - .find('.spinner').removeClass('is-active'); + successes++; - $item.removeAttr('data-content'); + } else { - successes++; + $item.find('td.status') + .attr('class', '') + .addClass('status wu-text-red-400') + .find('> span').html(data.data[ 0 ].message).end() + .find('.spinner').removeClass('is-active').end() + .find('a.help').slideDown(); - } else { + } // end if; - $item.find('td.status') - .attr('class', '') - .addClass('status wu-text-red-400') - .find('> span').html(data.data[0].message).end() - .find('.spinner').removeClass('is-active').end() - .find('a.help').slideDown(); + index++; - } // end if; + process_queue_item(queue.eq(index)); - index++; + }, + error(jqXHR) { - process_queue_item(queue.eq(index)); + let errorMessage = wu_setup_settings.generic_error_message || 'An error occurred.'; - }, - error() { + if (jqXHR.responseJSON && jqXHR.responseJSON.data && jqXHR.responseJSON.data[ 0 ]) { + errorMessage = jqXHR.responseJSON.data[ 0 ].message || errorMessage; + } - $item.find('td.status') - .attr('class', '') - .addClass('status wu-text-red-400') - .find('span').html('').end() - .find('.spinner').removeClass('is-active').end() - .find('a.help').slideDown(); + $item.find('td.status') + .attr('class', '') + .addClass('status wu-text-red-400') + .find('> span').html(errorMessage).end() + .find('.spinner').removeClass('is-active').end() + .find('a.help').slideDown(); - index++; + index++; - process_queue_item(queue.eq(index)); + process_queue_item(queue.eq(index)); - }, - }); + }, + }); - } // end process_queue_item; + } // end process_queue_item; - }); + }); - $('#poststuff [name=next]').removeAttr('disabled'); + $('#poststuff [name=next]').removeAttr('disabled'); - }); + }); }(jQuery)); if (typeof wu_initialize_tooltip !== 'function') { - const wu_initialize_tooltip = function() { + const wu_initialize_tooltip = function() { - jQuery('[role="tooltip"]').tipTip({ - attribute: 'aria-label', - }); + jQuery('[role="tooltip"]').tipTip({ + attribute: 'aria-label', + }); - }; // end wu_initialize_tooltip; + }; // end wu_initialize_tooltip; - // eslint-disable-next-line no-unused-vars - const wu_block_ui = function(el) { + // eslint-disable-next-line no-unused-vars + const wu_block_ui = function(el) { - jQuery(el).wu_block({ - message: 'Please wait...', - overlayCSS: { - backgroundColor: '#FFF', - opacity: 0.6, - }, - css: { - padding: 0, - margin: 0, - width: '50%', - fontSize: '14px !important', - top: '40%', - left: '35%', - textAlign: 'center', - color: '#000', - border: 'none', - backgroundColor: 'none', - cursor: 'wait', - }, - }); + jQuery(el).wu_block({ + message: 'Please wait...', + overlayCSS: { + backgroundColor: '#FFF', + opacity: 0.6, + }, + css: { + padding: 0, + margin: 0, + width: '50%', + fontSize: '14px !important', + top: '40%', + left: '35%', + textAlign: 'center', + color: '#000', + border: 'none', + backgroundColor: 'none', + cursor: 'wait', + }, + }); - return jQuery(el); + return jQuery(el); - }; + }; - (function($) { + (function($) { - $(document).ready(function() { + $(document).ready(function() { - wu_initialize_tooltip(); + wu_initialize_tooltip(); - }); + }); - }(jQuery)); + }(jQuery)); } // end if; diff --git a/assets/js/setup-wizard.min.js b/assets/js/setup-wizard.min.js index 249c6f6b..8bc8e88c 100644 --- a/assets/js/setup-wizard.min.js +++ b/assets/js/setup-wizard.min.js @@ -1 +1 @@ -if((r=>{window._wu_block_ui_polyfill=wu_block_ui_polyfill,wu_block_ui_polyfill=function(){},r(document).ready(function(){r("#poststuff").on("submit","form",function(t){t.preventDefault();let s=r(this),a=s.find("table[data-id]").data("id"),d=(s.find("[name=next]").attr("disabled","disabled"),s.find("tr[data-content]")),l=(d=d.filter(function(){var t=r(this).find("input[type=checkbox]");return!t.length||t.is(":checked")}),0),o=0;!function e(t){window.onbeforeunload=function(){return""};if(0===t.length)return d.length!==l&&"migration"!==a||(window.onbeforeunload=null,_wu_block_ui_polyfill(r("#poststuff .inside")),setTimeout(()=>{s.get(0).submit()},100)),s.find("[name=next]").removeAttr("disabled"),!1;let n=r(t);let i=n.data("content");n.get(0).scrollIntoView({behavior:"smooth",block:"center",inline:"nearest"});n.find("td.status").attr("class","").addClass("status").find("> span").html(wu_setup[i].installing).end().find(".spinner").addClass("is-active").end().find("a.help").slideUp();r.ajax({url:ajaxurl,method:"post",data:{action:"wu_setup_install",installer:i,"dry-run":wu_setup_settings.dry_run},success(t){!0===t.success?(n.find("td.status").attr("class","").addClass("status wu-text-green-600").find("> span").html(wu_setup[i].success).end().find(".spinner").removeClass("is-active"),n.removeAttr("data-content"),l++):n.find("td.status").attr("class","").addClass("status wu-text-red-400").find("> span").html(t.data[0].message).end().find(".spinner").removeClass("is-active").end().find("a.help").slideDown(),o++,e(d.eq(o))},error(){n.find("td.status").attr("class","").addClass("status wu-text-red-400").find("span").html("").end().find(".spinner").removeClass("is-active").end().find("a.help").slideDown(),o++,e(d.eq(o))}})}(d.eq(o))}),r("#poststuff [name=next]").removeAttr("disabled")})})(jQuery),"function"!=typeof wu_initialize_tooltip){let t=function(){jQuery('[role="tooltip"]').tipTip({attribute:"aria-label"})},e=function(t){return jQuery(t).wu_block({message:"Please wait...",overlayCSS:{backgroundColor:"#FFF",opacity:.6},css:{padding:0,margin:0,width:"50%",fontSize:"14px !important",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"none",backgroundColor:"none",cursor:"wait"}}),jQuery(t)};jQuery(document).ready(function(){t()})} \ No newline at end of file +if((l=>{window._wu_block_ui_polyfill=wu_block_ui_polyfill,wu_block_ui_polyfill=function(){},l(document).ready(function(){l("#poststuff").on("submit","form",function(t){t.preventDefault();let a=l(this),i=a.find("table[data-id]").data("id"),o=(a.find("[name=next]").attr("disabled","disabled"),a.find("tr[data-content]")),d=(o=o.filter(function(){var t=l(this).find("input[type=checkbox]");return!t.length||t.is(":checked")}),0),r=0;!function n(t){window.onbeforeunload=function(){return""};if(0===t.length)return o.length!==d&&"migration"!==i||(window.onbeforeunload=null,_wu_block_ui_polyfill(l("#poststuff .inside")),setTimeout(()=>{a.get(0).submit()},100)),a.find("[name=next]").removeAttr("disabled"),!1;let s=l(t);let e=s.data("content");s.get(0).scrollIntoView({behavior:"smooth",block:"center",inline:"nearest"});s.find("td.status").attr("class","").addClass("status").find("> span").html(wu_setup[e].installing).end().find(".spinner").addClass("is-active").end().find("a.help").slideUp();l.ajax({url:ajaxurl,method:"post",data:{action:wu_setup_settings.ajax_action||"wu_setup_install",installer:e,"dry-run":wu_setup_settings.dry_run},success(t){!0===t.success?(s.find("td.status").attr("class","").addClass("status wu-text-green-600").find("> span").html(wu_setup[e].success).end().find(".spinner").removeClass("is-active"),s.removeAttr("data-content"),d++):s.find("td.status").attr("class","").addClass("status wu-text-red-400").find("> span").html(t.data[0].message).end().find(".spinner").removeClass("is-active").end().find("a.help").slideDown(),r++,n(o.eq(r))},error(t){let e=wu_setup_settings.generic_error_message||"An error occurred.";t.responseJSON&&t.responseJSON.data&&t.responseJSON.data[0]&&(e=t.responseJSON.data[0].message||e),s.find("td.status").attr("class","").addClass("status wu-text-red-400").find("> span").html(e).end().find(".spinner").removeClass("is-active").end().find("a.help").slideDown(),r++,n(o.eq(r))}})}(o.eq(r))}),l("#poststuff [name=next]").removeAttr("disabled")})})(jQuery),"function"!=typeof wu_initialize_tooltip){let t=function(){jQuery('[role="tooltip"]').tipTip({attribute:"aria-label"})},e=function(t){return jQuery(t).wu_block({message:"Please wait...",overlayCSS:{backgroundColor:"#FFF",opacity:.6},css:{padding:0,margin:0,width:"50%",fontSize:"14px !important",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"none",backgroundColor:"none",cursor:"wait"}}),jQuery(t)};jQuery(document).ready(function(){t()})} \ No newline at end of file diff --git a/inc/admin-pages/class-multisite-setup-admin-page.php b/inc/admin-pages/class-multisite-setup-admin-page.php index 922b74a0..c8a8ad1d 100644 --- a/inc/admin-pages/class-multisite-setup-admin-page.php +++ b/inc/admin-pages/class-multisite-setup-admin-page.php @@ -15,6 +15,8 @@ // Exit if accessed directly defined('ABSPATH') || exit; +use WP_Ultimo\Installers\Multisite_Network_Installer; + /** * Multisite Setup Admin Page. */ @@ -80,6 +82,7 @@ public function __construct() { parent::__construct(); add_action('admin_enqueue_scripts', [$this, 'register_scripts']); + add_action('wp_ajax_wu_multisite_install', [$this, 'setup_install']); } /** @@ -122,25 +125,26 @@ public function get_sections() { return [ 'welcome' => [ - 'title' => __('Multisite Required', 'multisite-ultimate'), - 'description' => implode( - '

', - [ - __('WordPress Multisite is required for Multisite Ultimate to function properly.', 'multisite-ultimate'), - __('This wizard will guide you through enabling WordPress Multisite and configuring your network.', 'multisite-ultimate'), - __('We recommend creating a backup of your files and database before proceeding.', 'multisite-ultimate'), - ] - ), - 'next_label' => __('Get Started →', 'multisite-ultimate'), - 'back' => false, - 'view' => [$this, 'section_welcome'], + 'title' => __('Multisite Required', 'multisite-ultimate'), + 'next_label' => __('Get Started →', 'multisite-ultimate'), + 'back' => false, + 'view' => [$this, 'section_welcome'], ], 'configure' => [ 'title' => __('Network Configuration', 'multisite-ultimate'), 'description' => __('Configure your network settings. These settings determine how your sites will be structured.', 'multisite-ultimate'), - 'next_label' => __('Create Network', 'multisite-ultimate'), + 'next_label' => __('Continue →', 'multisite-ultimate'), 'handler' => [$this, 'handle_configure'], 'fields' => [$this, 'get_network_configuration_fields'], + 'back' => true, + ], + 'install' => [ + 'title' => __('Installing Network', 'multisite-ultimate'), + 'description' => __('Setting up your WordPress Multisite network...', 'multisite-ultimate'), + 'view' => [$this, 'section_install'], + 'next_label' => __('Install', 'multisite-ultimate'), + 'disable_next' => true, + 'back' => false, ], 'complete' => [ 'title' => __('Setup Complete', 'multisite-ultimate'), @@ -160,50 +164,7 @@ public function get_sections() { */ public function section_welcome(): void { - ?> -

-

- -

-
    -
  • -
  • -
  • -
  • -
-
- -
-

- -

-

- -

-
    -
  1. -
  2. -
  3. -
  4. -
-
- -
-
-
- -
-
-

- -

-

- -

-
-
-
- render_submit_box(); } @@ -253,14 +214,14 @@ public function get_network_configuration_fields() { 'title' => __('Network Title', 'multisite-ultimate'), 'desc' => __('This will be the title of your network.', 'multisite-ultimate'), 'placeholder' => __('Enter network title', 'multisite-ultimate'), - 'default' => get_option('blogname'), + 'value' => get_option('blogname') . ' Network', ], 'email' => [ 'type' => 'email', 'title' => __('Network Admin Email', 'multisite-ultimate'), 'desc' => __('This email address will be used for network administration.', 'multisite-ultimate'), 'placeholder' => __('Enter admin email', 'multisite-ultimate'), - 'default' => $user->user_email, + 'value' => $user->user_email, ], 'backup_warning' => [ 'type' => 'note', @@ -270,8 +231,8 @@ public function get_network_configuration_fields() {
-

' . __('Before You Continue', 'multisite-ultimate') . '

-

' . __('Please ensure you have a recent backup of your website files and database. The multisite setup process will modify your wp-config.php file and create new database tables.', 'multisite-ultimate') . '

+

' . esc_html__('Before You Continue', 'multisite-ultimate') . '

+

' . esc_html__('Please ensure you have a recent backup of your website files and database. The multisite setup process will modify your wp-config.php file and create new database tables.', 'multisite-ultimate') . '

', @@ -282,6 +243,9 @@ public function get_network_configuration_fields() { /** * Handles the network configuration form submission. * + * Validates inputs, stores the configuration in a transient, + * and redirects to the install step. + * * @since 2.0.0 * @return void */ @@ -295,29 +259,103 @@ public function handle_configure(): void { $sitename = sanitize_text_field(wu_request('sitename', '')); $email = sanitize_email(wu_request('email', '')); - // Store values in transients for completion page - set_transient('wu_multisite_subdomain_install', $subdomain_install, 300); - set_transient('wu_multisite_sitename', $sitename, 300); - set_transient('wu_multisite_email', $email, 300); - - // Try to enable multisite - $wp_config_modified = $this->modify_wp_config(); - $network_created = false; + $home_url = get_option('home'); + $base = wp_parse_url(trailingslashit($home_url), PHP_URL_PATH); + $domain = wp_parse_url($home_url, PHP_URL_HOST); + $port = wp_parse_url($home_url, PHP_URL_PORT); - if ($wp_config_modified) { - // Create the network - $network_created = $this->create_network($subdomain_install, $sitename, $email); + if ($port) { + $domain .= ':' . $port; } - // Store results - set_transient('wu_multisite_wp_config_modified', $wp_config_modified, 300); - set_transient('wu_multisite_network_created', $network_created, 300); + set_transient( + Multisite_Network_Installer::CONFIG_TRANSIENT, + [ + 'subdomain_install' => $subdomain_install, + 'sitename' => $sitename, + 'email' => $email, + 'domain' => $domain, + 'base' => $base, + ], + HOUR_IN_SECONDS + ); - // Redirect to completion step wp_safe_redirect($this->get_next_section_link()); exit; } + /** + * Renders the install section with AJAX-driven installation steps. + * + * @since 2.0.0 + * @return void + */ + public function section_install(): void { + + $installer = Multisite_Network_Installer::get_instance(); + $steps = $installer->get_steps(); + + wp_localize_script('wu-setup-wizard', 'wu_setup', $steps); + + wp_localize_script( + 'wu-setup-wizard', + 'wu_setup_settings', + [ + 'dry_run' => false, + 'ajax_action' => 'wu_multisite_install', + 'generic_error_message' => __('A server error happened while processing this item.', 'ultimate-multisite'), + ] + ); + + wp_enqueue_script('wu-setup-wizard'); + + echo wu_get_template_contents( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + 'wizards/setup/installation_steps', + [ + 'page' => $this, + 'steps' => $steps, + 'checks' => false, + ] + ); + + ?> + +
+
+ +
+
+ + handle(true, $installer_slug, $this); + + if (is_wp_error($result)) { + wp_send_json_error($result); + } + + wp_send_json_success(); + } + /** * Completion section view. * @@ -326,10 +364,9 @@ public function handle_configure(): void { */ public function section_complete(): void { - $wp_config_modified = get_transient('wu_multisite_wp_config_modified'); - $network_created = get_transient('wu_multisite_network_created'); + $result = wu_request('result', ''); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ($network_created && $wp_config_modified) : + if ('success' === $result || is_multisite()) : ?>
@@ -357,13 +394,6 @@ public function section_complete(): void { else : $this->display_manual_instructions(); endif; - - // Clean up transients - delete_transient('wu_multisite_wp_config_modified'); - delete_transient('wu_multisite_network_created'); - delete_transient('wu_multisite_subdomain_install'); - delete_transient('wu_multisite_sitename'); - delete_transient('wu_multisite_email'); } /** @@ -376,7 +406,12 @@ protected function display_manual_instructions(): void { $home_url = get_option('home'); $base_domain = wp_parse_url($home_url, PHP_URL_HOST); - $subdomain_install = get_transient('wu_multisite_subdomain_install'); + $port = wp_parse_url($home_url, PHP_URL_PORT); + $subdomain_install = defined('SUBDOMAIN_INSTALL') ? SUBDOMAIN_INSTALL : true; // @phpstan-ignore phpstanWP.wpConstant.fetch + + if ($port) { + $base_domain .= ':' . $port; + } $wp_config_constants = "define( 'WP_ALLOW_MULTISITE', true ); define( 'MULTISITE', true ); @@ -433,17 +468,18 @@ protected function display_manual_instructions(): void { wp-config.php', '/* That\'s all, stop editing! Happy publishing. */' ); ?>

-
+
+

@@ -455,6 +491,7 @@ protected function display_manual_instructions(): void {

+
@@ -473,7 +510,7 @@ protected function display_manual_instructions(): void {
- + @@ -481,179 +518,6 @@ protected function display_manual_instructions(): void { exists($wp_config_path) && $wp_filesystem->is_writable($wp_config_path)) { - return $wp_config_path; - } - - // WordPress supports wp-config.php one level above ABSPATH - $wp_config_path = trailingslashit(dirname(ABSPATH)) . 'wp-config.php'; - - if ($wp_filesystem->exists($wp_config_path) && $wp_filesystem->is_writable($wp_config_path)) { - return $wp_config_path; - } - - return false; - } - - /** - * Attempts to modify wp-config.php to enable multisite. - * - * @since 2.0.0 - * @return bool Whether the modification was successful. - */ - protected function modify_wp_config(): bool { - - $wp_config_path = $this->get_wp_config_path(); - - if (false === $wp_config_path) { - return false; - } - - $config_content = file_get_contents($wp_config_path); - - if (false === $config_content) { - return false; - } - - // Check if WP_ALLOW_MULTISITE is already actively defined (not commented out) - if (preg_match('/^\s*define\s*\(\s*[\'"]WP_ALLOW_MULTISITE[\'"]/m', $config_content)) { - return true; // Already configured - } - - // Find the location to insert the constant - $search = "/* That's all, stop editing! Happy publishing. */"; - $insert_position = strpos($config_content, $search); - - if (false === $insert_position) { - // Fallback: look for the wp-settings.php include - $search = "require_once ABSPATH . 'wp-settings.php';"; - $insert_position = strpos($config_content, $search); - } - - if (false === $insert_position) { - return false; // Can't find a safe place to insert - } - - $constant_to_add = "\n// Multisite Ultimate: Enable WordPress Multisite\ndefine( 'WP_ALLOW_MULTISITE', true );\n\n"; - - $new_content = substr_replace($config_content, $constant_to_add, $insert_position, 0); - - return file_put_contents($wp_config_path, $new_content) !== false; - } - - /** - * Creates the multisite network. - * - * @since 2.0.0 - * @param bool $subdomain_install Whether to use subdomains. - * @param string $sitename Network title. - * @param string $email Network admin email. - * @return bool Whether the network creation was successful. - */ - protected function create_network(bool $subdomain_install, string $sitename, string $email): bool { - - require_once ABSPATH . 'wp-admin/includes/upgrade.php'; - - // Load network functions - if (! function_exists('install_network')) { - require_once ABSPATH . 'wp-admin/includes/network.php'; - } - - try { - // Create network tables - install_network(); - - $base = wp_parse_url(trailingslashit(get_option('home')), PHP_URL_PATH); - $domain = wp_parse_url(get_option('home'), PHP_URL_HOST); - - // Populate network - $result = populate_network(1, $domain, $email, $sitename, $base, $subdomain_install); - - if (is_wp_error($result)) { - return false; - } - - // Add final multisite constants to wp-config.php - return $this->add_final_multisite_constants($subdomain_install, $domain); - } catch (\Exception $e) { - return false; - } - } - - /** - * Adds the final multisite constants to wp-config.php. - * - * @since 2.0.0 - * @param bool $subdomain_install Whether subdomains are used. - * @param string $domain The main domain. - * @return bool Whether the modification was successful. - */ - protected function add_final_multisite_constants(bool $subdomain_install, string $domain): bool { - - $wp_config_path = $this->get_wp_config_path(); - - if (false === $wp_config_path) { - return false; - } - - $config_content = file_get_contents($wp_config_path); - - if (false === $config_content) { - return false; - } - - // Check if MULTISITE is already actively defined (not commented out) - if (preg_match('/^\s*define\s*\(\s*[\'"]MULTISITE[\'"]/m', $config_content)) { - return true; // Already configured - } - - $constants_to_add = "\n// Multisite Ultimate: Multisite Configuration\n"; - $constants_to_add .= "define( 'MULTISITE', true );\n"; - $constants_to_add .= "define( 'SUBDOMAIN_INSTALL', " . ($subdomain_install ? 'true' : 'false') . " );\n"; - $constants_to_add .= "define( 'DOMAIN_CURRENT_SITE', '{$domain}' );\n"; - $constants_to_add .= "define( 'PATH_CURRENT_SITE', '/' );\n"; - $constants_to_add .= "define( 'SITE_ID_CURRENT_SITE', 1 );\n"; - $constants_to_add .= "define( 'BLOG_ID_CURRENT_SITE', 1 );\n\n"; - - // Find the location to insert the constants (after WP_ALLOW_MULTISITE) using regex for flexible spacing - if (preg_match('/define\s*\(\s*[\'"]WP_ALLOW_MULTISITE[\'"]\s*,\s*true\s*\)\s*;/i', $config_content, $matches, PREG_OFFSET_CAPTURE)) { - $insert_position = $matches[0][1] + strlen($matches[0][0]); - $new_content = substr_replace($config_content, $constants_to_add, $insert_position, 0); - - return file_put_contents($wp_config_path, $new_content) !== false; - } - - // Fallback: insert before "That's all" comment - $search = "/* That's all, stop editing! Happy publishing. */"; - $insert_position = strpos($config_content, $search); - - if (false !== $insert_position) { - $new_content = substr_replace($config_content, $constants_to_add, $insert_position, 0); - - return file_put_contents($wp_config_path, $new_content) !== false; - } - - return false; - } - /** * Register page scripts and styles. * @@ -666,6 +530,12 @@ public function register_scripts(): void { return; } + wp_enqueue_script('wu-block-ui', wu_get_asset('lib/jquery.blockUI.js', 'js'), ['jquery'], \WP_Ultimo::VERSION, true); + + wp_enqueue_script('wu-setup-wizard-extra', wu_get_asset('setup-wizard-extra.js', 'js'), ['jquery'], wu_get_version(), true); + + wp_register_script('wu-setup-wizard', wu_get_asset('setup-wizard.js', 'js'), ['jquery'], wu_get_version(), true); + wp_add_inline_script( 'jquery', ' diff --git a/inc/admin-pages/class-setup-wizard-admin-page.php b/inc/admin-pages/class-setup-wizard-admin-page.php index 93e717bb..930452f9 100644 --- a/inc/admin-pages/class-setup-wizard-admin-page.php +++ b/inc/admin-pages/class-setup-wizard-admin-page.php @@ -15,6 +15,7 @@ use WP_Ultimo\Installers\Migrator; use WP_Ultimo\Installers\Core_Installer; use WP_Ultimo\Installers\Default_Content_Installer; +use WP_Ultimo\Installers\Multisite_Network_Installer; use WP_Ultimo\Installers\Recommended_Plugins_Installer; use WP_Ultimo\Logger; use WP_Ultimo\Requirements; @@ -124,8 +125,7 @@ public function __construct() { add_action('admin_enqueue_scripts', [$this, 'register_scripts']); } - - add_action('init', [$this, 'start_init']); + parent::__construct(); add_action('admin_action_download_migration_logs', [$this, 'download_migration_logs']); @@ -137,18 +137,34 @@ public function __construct() { /* * Load installers */ - add_action('wu_handle_ajax_installers', [Core_Installer::get_instance(), 'handle'], 10, 3); - add_action('wu_handle_ajax_installers', [Default_Content_Installer::get_instance(), 'handle'], 10, 3); - add_action('wu_handle_ajax_installers', [Recommended_Plugins_Installer::get_instance(), 'handle'], 10, 3); - add_action('wu_handle_ajax_installers', [Migrator::get_instance(), 'handle'], 10, 3); - - /* - * Redirect on activation - */ - add_action('wu_activation', [$this, 'redirect_to_wizard']); - add_action('admin_init', [$this, 'redirect_to_wizard']); + add_filter('wu_handle_ajax_installers', [Core_Installer::get_instance(), 'handle'], 10, 3); + add_filter('wu_handle_ajax_installers', [Default_Content_Installer::get_instance(), 'handle'], 10, 3); + add_filter('wu_handle_ajax_installers', [Recommended_Plugins_Installer::get_instance(), 'handle'], 10, 3); + add_filter('wu_handle_ajax_installers', [Migrator::get_instance(), 'handle'], 10, 3); + add_filter('wu_handle_ajax_installers', [Multisite_Network_Installer::get_instance(), 'handle'], 10, 3); add_action('admin_init', [$this, 'alert_incomplete_installation']); + add_action('admin_init', [$this, 'redirect_multisite_setup_to_setup_wizard']); + } + + /** + * Redirects requests to the old multisite setup page to this setup wizard. + * + * Once multisite is enabled, the Multisite_Setup_Admin_Page is no longer + * registered. Bookmarks or login redirects pointing to its URL would 403. + * This catches those requests and redirects to the main setup wizard. + * + * @since 2.0.0 + * @return void + */ + public function redirect_multisite_setup_to_setup_wizard(): void { + + if (wu_request('page') !== 'wp-ultimo-multisite-setup' || ! is_multisite()) { + return; + } + + wp_safe_redirect(wu_network_admin_url('wp-ultimo-setup')); + exit; } /** @@ -217,33 +233,6 @@ public function set_settings(): void { WP_Ultimo()->settings->default_sections(); } - /** - * Redirects to the wizard, if we need to. - * - * @since 2.0.0 - * @return void - */ - public function redirect_to_wizard(): void { - - if (wp_doing_ajax() || ! current_user_can('manage_options')) { - return; - } - - // If multisite is not enabled, redirect to multisite setup page - if ( ! is_multisite() && wu_request('page') !== 'wp-ultimo-multisite-setup') { - wp_safe_redirect(admin_url('admin.php?page=wp-ultimo-multisite-setup')); - - exit; - } - - // If multisite is enabled but setup is not finished, redirect to setup wizard - if ( is_multisite() && ! Requirements::run_setup() && wu_request('page') !== 'wp-ultimo-setup') { - wp_safe_redirect(wu_network_admin_url('wp-ultimo-setup')); - - exit; - } - } - /** * Handles the ajax actions for installers and migrators. * diff --git a/inc/admin/class-configuration-checker.php b/inc/admin/class-configuration-checker.php index 21d84d8e..417ec4c5 100644 --- a/inc/admin/class-configuration-checker.php +++ b/inc/admin/class-configuration-checker.php @@ -46,7 +46,7 @@ public function check_cookie_domain_configuration(): void { return; } // Only check on subdomain installs - if ( ! is_subdomain_install()) { + if ( ! is_multisite() || ! is_subdomain_install()) { return; } diff --git a/inc/class-hooks.php b/inc/class-hooks.php index 3844bf71..b5b283cf 100644 --- a/inc/class-hooks.php +++ b/inc/class-hooks.php @@ -89,6 +89,24 @@ public static function on_activation_do(): void { * @return void */ do_action('wu_activation'); + + if (wp_doing_ajax() || ! current_user_can('manage_options')) { + return; + } + + // If multisite is not enabled, redirect to multisite setup page + if ( ! is_multisite() && wu_request('page') !== 'wp-ultimo-multisite-setup') { + wp_safe_redirect(admin_url('admin.php?page=wp-ultimo-multisite-setup')); + + exit; + } + + // If multisite is enabled but setup is not finished, redirect to setup wizard + if (is_multisite() && ! Requirements::run_setup() && wu_request('page') !== 'wp-ultimo-setup') { + wp_safe_redirect(wu_network_admin_url('wp-ultimo-setup')); + + exit; + } } } diff --git a/inc/class-requirements.php b/inc/class-requirements.php index 247e59a3..620c8193 100644 --- a/inc/class-requirements.php +++ b/inc/class-requirements.php @@ -327,7 +327,7 @@ public static function notice_unsupported_wp_version(): void { */ public static function notice_not_multisite(): void { - printf('', esc_html__('Ultimate Multisite requires a multisite install to run properly. To know more about WordPress Networks, visit this link:', 'ultimate-multisite'), esc_html__('Create a Network', 'ultimate-multisite')); + printf('', esc_html__('Ultimate Multisite requires a multisite install to run properly.', 'ultimate-multisite'), esc_url(admin_url('admin.php?page=wp-ultimo-multisite-setup')), esc_html__('Run the Multisite Setup Wizard', 'ultimate-multisite')); } /** diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php index 9a3da72e..56b897e6 100644 --- a/inc/class-wp-ultimo.php +++ b/inc/class-wp-ultimo.php @@ -145,19 +145,24 @@ public function init(): void { */ $this->load_public_apis(); - /* - * Setup Wizard - */ - new WP_Ultimo\Admin_Pages\Setup_Wizard_Admin_Page(); + add_action( + 'init', + function () { + new WP_Ultimo\Admin_Pages\Setup_Wizard_Admin_Page(); + } + ); /* - * Multisite Setup for non-multisite installations + * Multisite Setup page. On non-multisite installs, this shows the setup + * wizard. On multisite, it just registers a redirect to the main setup + * wizard so old URLs (bookmarks, login redirects) don't 403. */ - if ( ! is_multisite()) { - add_action('init', function() { + add_action( + 'init', + function () { new WP_Ultimo\Admin_Pages\Multisite_Setup_Admin_Page(); - }); - } + } + ); /* * Loads the Ultimate Multisite settings helper class. diff --git a/inc/helpers/class-wp-config.php b/inc/helpers/class-wp-config.php index ece41ab1..43a336ee 100644 --- a/inc/helpers/class-wp-config.php +++ b/inc/helpers/class-wp-config.php @@ -44,7 +44,15 @@ public function inject_wp_config_constant($constant, $value) { $line = $this->find_injected_line($config, $constant); - $content = str_pad(sprintf("define( '%s', '%s' );", $constant, $value), 50) . '// Automatically injected by Ultimate Multisite;'; + if (is_bool($value)) { + $formatted_value = $value ? 'true' : 'false'; + } elseif (is_int($value)) { + $formatted_value = (string) $value; + } else { + $formatted_value = "'{$value}'"; + } + + $content = str_pad(sprintf("define( '%s', %s );", $constant, $formatted_value), 50) . '// Automatically injected by Ultimate Multisite;'; if (false === $line) { diff --git a/inc/installers/class-base-installer.php b/inc/installers/class-base-installer.php index 6a77d0dd..fa578b40 100644 --- a/inc/installers/class-base-installer.php +++ b/inc/installers/class-base-installer.php @@ -71,7 +71,7 @@ public function all_done() { * @param bool|\WP_Error $status Status of the installer. * @param string $installer The installer name. * @param object $wizard Wizard class. - * @return void + * @return bool|\WP_Error */ public function handle($status, $installer, $wizard) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter @@ -85,7 +85,7 @@ public function handle($status, $installer, $wizard) { // phpcs:ignore Generic.C * No installer on this class. */ if ( ! is_callable($callable)) { - return; + return $status; } try { @@ -95,9 +95,12 @@ public function handle($status, $installer, $wizard) { // phpcs:ignore Generic.C } catch (\Throwable $e) { $wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching wu_log_add(\WP_Ultimo::LOG_HANDLE, $e->getMessage(), LogLevel::ERROR); - return; + + return new \WP_Error($installer, $e->getMessage()); } $wpdb->query('COMMIT'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + + return $status; } } diff --git a/inc/installers/class-migrator.php b/inc/installers/class-migrator.php index 221a39e4..5d1d07aa 100644 --- a/inc/installers/class-migrator.php +++ b/inc/installers/class-migrator.php @@ -460,7 +460,7 @@ protected function bypass_server_limits() { * @param string $installer The installer name. * @param object $wizard Wizard class. * - * @return void + * @return bool|\WP_Error */ public function handle($status, $installer, $wizard) { @@ -479,7 +479,7 @@ public function handle($status, $installer, $wizard) { * No installer on this class. */ if ( ! is_callable($callable)) { - return; + return $status; } /* @@ -558,6 +558,8 @@ public function handle($status, $installer, $wizard) { $session->set('back_traces', []); } + + return $status; } /** @@ -1752,9 +1754,7 @@ protected function _install_memberships() { throw new Exception(esc_html($membership->get_error_message())); } - /* - * Update statuses and check for other info. - */ + // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf -- Placeholder for future status checks. if ($membership) { } } diff --git a/inc/installers/class-multisite-network-installer.php b/inc/installers/class-multisite-network-installer.php new file mode 100644 index 00000000..3a9dbc9e --- /dev/null +++ b/inc/installers/class-multisite-network-installer.php @@ -0,0 +1,415 @@ +check_network_tables_exist(); + $has_wp_config_updated = defined('MULTISITE') && MULTISITE; // @phpstan-ignore phpstanWP.wpConstant.fetch + + return [ + 'enable_multisite' => [ + 'done' => $has_multisite_constant, + 'title' => __('Enable Multisite', 'ultimate-multisite'), + 'description' => __('Adds WP_ALLOW_MULTISITE constant to wp-config.php.', 'ultimate-multisite'), + 'pending' => __('Pending', 'ultimate-multisite'), + 'installing' => __('Enabling multisite...', 'ultimate-multisite'), + 'success' => __('Success!', 'ultimate-multisite'), + 'help' => '', + ], + 'create_network' => [ + 'done' => $has_network_tables, + 'title' => __('Create Network', 'ultimate-multisite'), + 'description' => __('Creates network database tables and populates network data.', 'ultimate-multisite'), + 'pending' => __('Pending', 'ultimate-multisite'), + 'installing' => __('Creating network tables...', 'ultimate-multisite'), + 'success' => __('Success!', 'ultimate-multisite'), + 'help' => '', + ], + 'update_wp_config' => [ + 'done' => $has_wp_config_updated, + 'title' => __('Update Configuration', 'ultimate-multisite'), + 'description' => __('Adds final multisite constants to wp-config.php.', 'ultimate-multisite'), + 'pending' => __('Pending', 'ultimate-multisite'), + 'installing' => __('Updating configuration...', 'ultimate-multisite'), + 'success' => __('Success!', 'ultimate-multisite'), + 'help' => '', + ], + 'cookie_fix' => [ + 'done' => $has_wp_config_updated, + 'title' => __('Fix Cookies', 'ultimate-multisite'), + 'description' => __('Ensures site URL is correct to prevent cookie issues after activation.', 'ultimate-multisite'), + 'pending' => __('Pending', 'ultimate-multisite'), + 'installing' => __('Fixing cookies...', 'ultimate-multisite'), + 'success' => __('Success!', 'ultimate-multisite'), + 'help' => '', + ], + 'network_activate' => [ + 'done' => $this->is_network_activated(), + 'title' => __('Network Activate Plugin', 'ultimate-multisite'), + 'description' => __('Network-activates Ultimate Multisite so it runs across the entire network.', 'ultimate-multisite'), + 'pending' => __('Pending', 'ultimate-multisite'), + 'installing' => __('Activating plugin...', 'ultimate-multisite'), + 'success' => __('Success!', 'ultimate-multisite'), + 'help' => '', + ], + ]; + } + + /** + * Checks whether Ultimate Multisite is network-activated. + * + * Uses direct DB query because this may run before multisite + * is active in the current PHP process. + * + * @since 2.0.0 + * @return bool + */ + protected function is_network_activated(): bool { + + global $wpdb; + + $sitemeta_table = $wpdb->base_prefix . 'sitemeta'; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $table_exists = $wpdb->get_var( + $wpdb->prepare('SHOW TABLES LIKE %s', $sitemeta_table) + ); + + if ($table_exists !== $sitemeta_table) { + return false; + } + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $active_plugins = $wpdb->get_var( + $wpdb->prepare( + "SELECT meta_value FROM $sitemeta_table WHERE site_id = 1 AND meta_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + 'active_sitewide_plugins' + ) + ); + + if (empty($active_plugins)) { + return false; + } + + $active_plugins = maybe_unserialize($active_plugins); + + return is_array($active_plugins) && isset($active_plugins['ultimate-multisite/ultimate-multisite.php']); + } + + /** + * Checks whether the multisite network tables exist. + * + * @since 2.0.0 + * @return bool + */ + protected function check_network_tables_exist(): bool { + + global $wpdb; + + $table_name = $wpdb->base_prefix . 'site'; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $result = $wpdb->get_var( + $wpdb->prepare('SHOW TABLES LIKE %s', $table_name) + ); + + return $result === $table_name; + } + + /** + * Returns the stored network configuration from the transient. + * + * @since 2.0.0 + * @throws \Exception When no configuration is found. + * @return array + */ + protected function get_config(): array { + + $config = get_transient(self::CONFIG_TRANSIENT); + + if (empty($config) || ! is_array($config)) { + throw new \Exception(esc_html__('Network configuration not found. Please go back and submit the configuration form again.', 'ultimate-multisite')); + } + + return $config; + } + + /** + * Step 1: Enable multisite by adding WP_ALLOW_MULTISITE to wp-config.php. + * + * @since 2.0.0 + * @throws \Exception When the constant cannot be injected. + * @return void + */ + public function _install_enable_multisite(): void { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + + if (defined('WP_ALLOW_MULTISITE') && WP_ALLOW_MULTISITE) { + return; + } + + $wp_config = \WP_Ultimo\Helpers\WP_Config::get_instance(); + + $result = $wp_config->inject_wp_config_constant('WP_ALLOW_MULTISITE', true); + + if (is_wp_error($result)) { + throw new \Exception(esc_html($result->get_error_message())); + } + } + + /** + * Step 2: Create network tables and populate network data. + * + * @since 2.0.0 + * @throws \Exception When network creation fails. + * @return void + */ + public function _install_create_network(): void { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + + global $wpdb; + + if ($this->check_network_tables_exist()) { + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $has_data = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->base_prefix}site"); + + if ($has_data) { + return; + } + } + + $config = $this->get_config(); + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + if (! function_exists('install_network')) { + require_once ABSPATH . 'wp-admin/includes/network.php'; + } + + // On a single-site install, $wpdb doesn't have multisite table names set. + foreach ($wpdb->ms_global_tables as $table) { + $wpdb->$table = $wpdb->base_prefix . $table; + } + + install_network(); + + $result = populate_network( + 1, + $config['domain'], + $config['email'], + $config['sitename'], + $config['base'], + $config['subdomain_install'] + ); + + // populate_network() returns WP_Error('no_wildcard_dns') for subdomain + // installs when wildcard DNS isn't configured. This is a warning, not + // a fatal error — the network tables are still created successfully. + if (is_wp_error($result) && ! in_array($result->get_error_code(), ['no_wildcard_dns', 'siteid_exists'], true)) { + throw new \Exception(esc_html($result->get_error_message())); + } + + // Fix siteurl trailing slash to prevent cookie hash change. + // Is this really needed? + // $wpdb->update( + // $wpdb->sitemeta, + // ['meta_value' => untrailingslashit(get_option('siteurl'))], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + // [ + // 'site_id' => 1, + // 'meta_key' => 'siteurl', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + // ] + // ); + } + + /** + * Step 3: Add final multisite constants to wp-config.php. + * + * @since 2.0.0 + * @throws \Exception When constants cannot be injected. + * @return void + */ + public function _install_update_wp_config(): void { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + + if (defined('MULTISITE') && MULTISITE) { // @phpstan-ignore phpstanWP.wpConstant.fetch + return; + } + + $config = $this->get_config(); + + $wp_config = \WP_Ultimo\Helpers\WP_Config::get_instance(); + + $constants = [ + 'MULTISITE' => true, + 'SUBDOMAIN_INSTALL' => $config['subdomain_install'], + 'DOMAIN_CURRENT_SITE' => $config['domain'], + 'PATH_CURRENT_SITE' => '/', + 'SITE_ID_CURRENT_SITE' => 1, + 'BLOG_ID_CURRENT_SITE' => 1, + ]; + + foreach ($constants as $constant => $value) { + $result = $wp_config->inject_wp_config_constant($constant, $value); + + if (is_wp_error($result)) { + throw new \Exception(esc_html($result->get_error_message())); + } + } + } + + /** + * Step 4: Verify and fix the siteurl trailing slash in sitemeta. + * + * This is an idempotent safety check to ensure the cookie hash + * remains consistent after multisite activation. + * + * @since 2.0.0 + * @throws \Exception When the fix fails. + * @return void + */ + public function _install_cookie_fix(): void { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + + global $wpdb; + + $sitemeta_table = $wpdb->base_prefix . 'sitemeta'; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $table_exists = $wpdb->get_var( + $wpdb->prepare('SHOW TABLES LIKE %s', $sitemeta_table) + ); + + if ($table_exists !== $sitemeta_table) { + return; + } + + $siteurl = get_option('siteurl'); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->update( + $sitemeta_table, + ['meta_value' => untrailingslashit($siteurl)], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + [ + 'site_id' => 1, + 'meta_key' => 'siteurl', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + ] + ); + } + + /** + * Step 5: Network-activate Ultimate Multisite. + * + * Writes directly to the sitemeta table because multisite + * is not yet active in the current PHP process. + * + * @since 2.0.0 + * @throws \Exception When the activation fails. + * @return void + */ + public function _install_network_activate(): void { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + + if ($this->is_network_activated()) { + return; + } + + global $wpdb; + + $sitemeta_table = $wpdb->base_prefix . 'sitemeta'; + $plugin = 'ultimate-multisite/ultimate-multisite.php'; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $table_exists = $wpdb->get_var( + $wpdb->prepare('SHOW TABLES LIKE %s', $sitemeta_table) + ); + + if ($table_exists !== $sitemeta_table) { + throw new \Exception(esc_html__('The sitemeta table does not exist. Network tables must be created first.', 'ultimate-multisite')); + } + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $active_plugins = $wpdb->get_var( + $wpdb->prepare( + "SELECT meta_value FROM $sitemeta_table WHERE site_id = 1 AND meta_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + 'active_sitewide_plugins' + ) + ); + + $active_plugins = ! empty($active_plugins) ? maybe_unserialize($active_plugins) : []; + + if (! is_array($active_plugins)) { + $active_plugins = []; + } + + $active_plugins[ $plugin ] = time(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $existing = $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM $sitemeta_table WHERE site_id = 1 AND meta_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + 'active_sitewide_plugins' + ) + ); + + if ($existing) { + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $result = $wpdb->update( + $sitemeta_table, + ['meta_value' => maybe_serialize($active_plugins)], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + [ + 'site_id' => 1, + 'meta_key' => 'active_sitewide_plugins', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + ] + ); + } else { + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $result = $wpdb->insert( + $sitemeta_table, + [ + 'site_id' => 1, + 'meta_key' => 'active_sitewide_plugins', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => maybe_serialize($active_plugins), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ] + ); + } + + if (false === $result) { + throw new \Exception(esc_html__('Failed to network-activate Ultimate Multisite.', 'ultimate-multisite')); + } + } +} diff --git a/inc/installers/class-recommended-plugins-installer.php b/inc/installers/class-recommended-plugins-installer.php index b464a009..28648b3e 100644 --- a/inc/installers/class-recommended-plugins-installer.php +++ b/inc/installers/class-recommended-plugins-installer.php @@ -77,7 +77,7 @@ public function get_steps() { * @param bool|\WP_Error $status Current status passed through the filter chain. * @param string $installer The installer slug (e.g. `install_plugin_user-switching`). * @param object $wizard Wizard page instance. - * @return void + * @return bool|\WP_Error */ public function handle($status, $installer, $wizard) { @@ -88,13 +88,15 @@ public function handle($status, $installer, $wizard) { $result = $this->install_wporg_plugin($plugin_slug); if (is_wp_error($result)) { - return; + return $result; } } catch (\Throwable $e) { wu_log_add(\WP_Ultimo::LOG_HANDLE, $e->getMessage(), LogLevel::ERROR); + + return new \WP_Error($installer, $e->getMessage()); } - return; + return $status; } if (str_starts_with($installer, 'activate_plugin_')) { @@ -104,14 +106,18 @@ public function handle($status, $installer, $wizard) { $result = $this->activate_plugin($plugin_slug); if (is_wp_error($result)) { - return; + return $result; } } catch (\Throwable $e) { wu_log_add(\WP_Ultimo::LOG_HANDLE, $e->getMessage(), LogLevel::ERROR); + + return new \WP_Error($installer, $e->getMessage()); } - return; + return $status; } + + return $status; } /** diff --git a/views/wizards/multisite-setup/welcome.php b/views/wizards/multisite-setup/welcome.php new file mode 100644 index 00000000..10da4d78 --- /dev/null +++ b/views/wizards/multisite-setup/welcome.php @@ -0,0 +1,50 @@ + +
+

+ +

+
    +
  • +
  • +
  • +
  • +
+
+ +
+

+ +

+

+ +

+
    +
  1. +
  2. +
  3. +
  4. +
+
+ +
+
+
+ +
+
+

+ +

+

+ +

+
+
+
From 4c32e7637bf5c33aa1f7e93413a2a0954fa57f3b Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 12 Feb 2026 10:41:36 -0700 Subject: [PATCH 8/9] Get network wiz working fully --- .wiki/README.md | 2 +- .../class-multisite-setup-admin-page.php | 139 ++++---------- .../class-setup-wizard-admin-page.php | 56 ------ inc/admin-pages/class-wizard-admin-page.php | 44 ++++- .../class-signup-field-order-bump.php | 2 +- .../class-signup-field-order-summary.php | 2 +- .../class-signup-field-pricing-table.php | 2 +- .../class-signup-field-steps.php | 2 +- .../class-signup-field-template-selection.php | 2 +- inc/class-documentation.php | 84 ++++++-- inc/class-wp-ultimo.php | 32 ++-- inc/functions/checkout.php | 4 +- .../class-multisite-network-installer.php | 180 ++---------------- inc/ui/class-template-switching-element.php | 3 +- views/base/wizard/submit-box.php | 11 +- .../checkout/partials/pricing-table-list.php | 2 +- .../templates/pricing-table/legacy.php | 2 +- .../templates/template-selection/clean.php | 2 +- .../templates/template-selection/legacy.php | 2 +- 19 files changed, 202 insertions(+), 371 deletions(-) diff --git a/.wiki/README.md b/.wiki/README.md index b39f1339..b1a6f200 100644 --- a/.wiki/README.md +++ b/.wiki/README.md @@ -18,7 +18,7 @@ The documentation is organized into the following categories: ## How to Use This Documentation -You can browse the documentation directly on GitHub by visiting the [Wiki](https://github.com/superdav42/wp-multisite-waas/wiki). +You can browse the documentation at [ultimatemultisite.com/docs](https://ultimatemultisite.com/docs/). ### Automatic Sync diff --git a/inc/admin-pages/class-multisite-setup-admin-page.php b/inc/admin-pages/class-multisite-setup-admin-page.php index c8a8ad1d..845e2c26 100644 --- a/inc/admin-pages/class-multisite-setup-admin-page.php +++ b/inc/admin-pages/class-multisite-setup-admin-page.php @@ -15,6 +15,7 @@ // Exit if accessed directly defined('ABSPATH') || exit; +use WP_Ultimo\Installers\Core_Installer; use WP_Ultimo\Installers\Multisite_Network_Installer; /** @@ -82,7 +83,10 @@ public function __construct() { parent::__construct(); add_action('admin_enqueue_scripts', [$this, 'register_scripts']); - add_action('wp_ajax_wu_multisite_install', [$this, 'setup_install']); + /** + * Same route as main setup wiz, but we run first to use different caps + */ + add_action('wp_ajax_wu_setup_install', [$this, 'setup_install'], 5); } /** @@ -141,10 +145,15 @@ public function get_sections() { 'install' => [ 'title' => __('Installing Network', 'multisite-ultimate'), 'description' => __('Setting up your WordPress Multisite network...', 'multisite-ultimate'), - 'view' => [$this, 'section_install'], - 'next_label' => __('Install', 'multisite-ultimate'), + 'next_label' => Core_Installer::get_instance()->all_done() ? __('Begin Ultimate Multisite Setup →', 'ultimate-multisite') : __('Install', 'ultimate-multisite'), 'disable_next' => true, 'back' => false, + 'fields' => [ + 'terms' => [ + 'type' => 'note', + 'desc' => fn() => $this->render_installation_steps(Multisite_Network_Installer::get_instance()->get_steps(), false), + ], + ], ], 'complete' => [ 'title' => __('Setup Complete', 'multisite-ultimate'), @@ -284,78 +293,6 @@ public function handle_configure(): void { exit; } - /** - * Renders the install section with AJAX-driven installation steps. - * - * @since 2.0.0 - * @return void - */ - public function section_install(): void { - - $installer = Multisite_Network_Installer::get_instance(); - $steps = $installer->get_steps(); - - wp_localize_script('wu-setup-wizard', 'wu_setup', $steps); - - wp_localize_script( - 'wu-setup-wizard', - 'wu_setup_settings', - [ - 'dry_run' => false, - 'ajax_action' => 'wu_multisite_install', - 'generic_error_message' => __('A server error happened while processing this item.', 'ultimate-multisite'), - ] - ); - - wp_enqueue_script('wu-setup-wizard'); - - echo wu_get_template_contents( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - 'wizards/setup/installation_steps', - [ - 'page' => $this, - 'steps' => $steps, - 'checks' => false, - ] - ); - - ?> - -
-
- -
-
- - handle(true, $installer_slug, $this); - - if (is_wp_error($result)) { - wp_send_json_error($result); - } - - wp_send_json_success(); - } - /** * Completion section view. * @@ -518,6 +455,36 @@ protected function display_manual_instructions(): void { get_steps(); + if ( ! isset($steps[ $installer ])) { + return; + } + + $status = $multisite_network_installer->handle(true, $installer, $this); + + if (is_wp_error($status)) { + wp_send_json_error($status); + } + + wp_send_json_success(); + } + /** * Register page scripts and styles. * @@ -535,27 +502,5 @@ public function register_scripts(): void { wp_enqueue_script('wu-setup-wizard-extra', wu_get_asset('setup-wizard-extra.js', 'js'), ['jquery'], wu_get_version(), true); wp_register_script('wu-setup-wizard', wu_get_asset('setup-wizard.js', 'js'), ['jquery'], wu_get_version(), true); - - wp_add_inline_script( - 'jquery', - ' - // Copy to clipboard functionality - document.addEventListener("DOMContentLoaded", function() { - document.querySelectorAll("button[onclick*=\'navigator.clipboard.writeText\']").forEach(function(button) { - button.addEventListener("click", function() { - var textarea = this.nextElementSibling; - if (textarea && textarea.tagName === "TEXTAREA") { - navigator.clipboard.writeText(textarea.value).then(function() { - button.textContent = "Copied!"; - setTimeout(function() { - button.textContent = "Copy to clipboard"; - }, 2000); - }); - } - }); - }); - }); - ' - ); } } diff --git a/inc/admin-pages/class-setup-wizard-admin-page.php b/inc/admin-pages/class-setup-wizard-admin-page.php index 930452f9..4eea8a2a 100644 --- a/inc/admin-pages/class-setup-wizard-admin-page.php +++ b/inc/admin-pages/class-setup-wizard-admin-page.php @@ -141,30 +141,8 @@ public function __construct() { add_filter('wu_handle_ajax_installers', [Default_Content_Installer::get_instance(), 'handle'], 10, 3); add_filter('wu_handle_ajax_installers', [Recommended_Plugins_Installer::get_instance(), 'handle'], 10, 3); add_filter('wu_handle_ajax_installers', [Migrator::get_instance(), 'handle'], 10, 3); - add_filter('wu_handle_ajax_installers', [Multisite_Network_Installer::get_instance(), 'handle'], 10, 3); add_action('admin_init', [$this, 'alert_incomplete_installation']); - add_action('admin_init', [$this, 'redirect_multisite_setup_to_setup_wizard']); - } - - /** - * Redirects requests to the old multisite setup page to this setup wizard. - * - * Once multisite is enabled, the Multisite_Setup_Admin_Page is no longer - * registered. Bookmarks or login redirects pointing to its URL would 403. - * This catches those requests and redirects to the main setup wizard. - * - * @since 2.0.0 - * @return void - */ - public function redirect_multisite_setup_to_setup_wizard(): void { - - if (wu_request('page') !== 'wp-ultimo-multisite-setup' || ! is_multisite()) { - return; - } - - wp_safe_redirect(wu_network_admin_url('wp-ultimo-setup')); - exit; } /** @@ -589,40 +567,6 @@ public function get_payment_settings() { return apply_filters('wu_setup_get_payment_settings', $fields); } - /** - * Render the installation steps table. - * - * @since 2.0.0 - * - * @param array $steps The list of steps. - * @param boolean $checks If we should add the checkbox for selection or not. - * @return string - */ - public function render_installation_steps($steps, $checks = true) { - - wp_localize_script('wu-setup-wizard', 'wu_setup', $steps); - - wp_localize_script( - 'wu-setup-wizard', - 'wu_setup_settings', - [ - 'dry_run' => wu_request('dry-run', true), - 'generic_error_message' => __('A server error happened while processing this item.', 'ultimate-multisite'), - ] - ); - - wp_enqueue_script('wu-setup-wizard'); - - return wu_get_template_contents( - 'wizards/setup/installation_steps', - [ - 'page' => $this, - 'steps' => $steps, - 'checks' => $checks, - ] - ); - } - /** * Renders the terms of support. * diff --git a/inc/admin-pages/class-wizard-admin-page.php b/inc/admin-pages/class-wizard-admin-page.php index 34228733..1f65435d 100644 --- a/inc/admin-pages/class-wizard-admin-page.php +++ b/inc/admin-pages/class-wizard-admin-page.php @@ -288,7 +288,15 @@ public function get_prev_section_link() { $keys = array_keys($sections); - return add_query_arg($this->section_slug, $keys[ array_search($current_section, array_keys($sections), true) - 1 ]); + $current_section_idx = array_search($current_section, array_keys($sections), true); + + if (false === $current_section_idx) { + return ''; + } + if (empty($keys[ $current_section_idx - 1 ])) { + return ''; + } + return add_query_arg($this->section_slug, $keys[ $current_section_idx - 1 ]); } /** @@ -384,6 +392,40 @@ public function render_submit_box(): void { ); } + /** + * Render the installation steps table. + * + * @since 2.0.0 + * + * @param array $steps The list of steps. + * @param boolean $checks If we should add the checkbox for selection or not. + * @return string + */ + public function render_installation_steps($steps, $checks = true) { + + wp_localize_script('wu-setup-wizard', 'wu_setup', $steps); + + wp_localize_script( + 'wu-setup-wizard', + 'wu_setup_settings', + [ + 'dry_run' => wu_request('dry-run', true), + 'generic_error_message' => __('A server error happened while processing this item.', 'ultimate-multisite'), + ] + ); + + wp_enqueue_script('wu-setup-wizard'); + + return wu_get_template_contents( + 'wizards/setup/installation_steps', + [ + 'page' => $this, + 'steps' => $steps, + 'checks' => $checks, + ] + ); + } + /** * Wizard classes should implement a method that returns an array of sections and subsections. * diff --git a/inc/checkout/signup-fields/class-signup-field-order-bump.php b/inc/checkout/signup-fields/class-signup-field-order-bump.php index 376235ec..d990a6ba 100644 --- a/inc/checkout/signup-fields/class-signup-field-order-bump.php +++ b/inc/checkout/signup-fields/class-signup-field-order-bump.php @@ -220,7 +220,7 @@ public function get_fields() { // 'order' => 99, // 'wrapper_classes' => 'sm:wu-p-0 sm:wu-block', // 'classes' => '', - // 'desc' => sprintf('
%s
', __('Want to add customized order bump templates?
See how you can do that here.', 'ultimate-multisite')), + // 'desc' => sprintf('
%s
', sprintf(__('Want to add customized order bump templates?
See how you can do that here.', 'ultimate-multisite'), esc_url(wu_get_documentation_url('wp-ultimo-checkout-forms')))), // ); // phpcs:enable diff --git a/inc/checkout/signup-fields/class-signup-field-order-summary.php b/inc/checkout/signup-fields/class-signup-field-order-summary.php index 5e393060..9429e70f 100644 --- a/inc/checkout/signup-fields/class-signup-field-order-summary.php +++ b/inc/checkout/signup-fields/class-signup-field-order-summary.php @@ -195,7 +195,7 @@ public function get_fields() { // 'order' => 99, // 'wrapper_classes' => 'sm:wu-p-0 sm:wu-block', // 'classes' => '', - // 'desc' => sprintf('
%s
', __('Want to add customized order summary templates?
See how you can do that here.', 'ultimate-multisite')), + // 'desc' => sprintf('
%s
', sprintf(__('Want to add customized order summary templates?
See how you can do that here.', 'ultimate-multisite'), esc_url(wu_get_documentation_url('wp-ultimo-checkout-forms')))), // ); // phpcs:enable diff --git a/inc/checkout/signup-fields/class-signup-field-pricing-table.php b/inc/checkout/signup-fields/class-signup-field-pricing-table.php index d16c4210..77cd5c1b 100644 --- a/inc/checkout/signup-fields/class-signup-field-pricing-table.php +++ b/inc/checkout/signup-fields/class-signup-field-pricing-table.php @@ -231,7 +231,7 @@ public function get_fields() { // 'order' => 99, // 'wrapper_classes' => 'sm:wu-p-0 sm:wu-block', // 'classes' => '', - // 'desc' => sprintf('
%s
', __('Want to add customized pricing table templates?
See how you can do that here.', 'ultimate-multisite')), + // 'desc' => sprintf('
%s
', sprintf(__('Want to add customized pricing table templates?
See how you can do that here.', 'ultimate-multisite'), esc_url(wu_get_documentation_url('wp-ultimo-checkout-forms')))), // ); // phpcs:enable diff --git a/inc/checkout/signup-fields/class-signup-field-steps.php b/inc/checkout/signup-fields/class-signup-field-steps.php index fb853d17..87429cdd 100644 --- a/inc/checkout/signup-fields/class-signup-field-steps.php +++ b/inc/checkout/signup-fields/class-signup-field-steps.php @@ -181,7 +181,7 @@ public function get_fields() { // 'order' => 99, // 'wrapper_classes' => 'sm:wu-p-0 sm:wu-block', // 'classes' => '', - // 'desc' => sprintf('
%s
', __('Want to add customized steps templates?
See how you can do that here.', 'ultimate-multisite')), + // 'desc' => sprintf('
%s
', sprintf(__('Want to add customized steps templates?
See how you can do that here.', 'ultimate-multisite'), esc_url(wu_get_documentation_url('wp-ultimo-checkout-forms')))), // ); // phpcs:enable diff --git a/inc/checkout/signup-fields/class-signup-field-template-selection.php b/inc/checkout/signup-fields/class-signup-field-template-selection.php index 01abb10f..8ce1d9a9 100644 --- a/inc/checkout/signup-fields/class-signup-field-template-selection.php +++ b/inc/checkout/signup-fields/class-signup-field-template-selection.php @@ -266,7 +266,7 @@ public function get_fields() { // 'order' => 99, // 'wrapper_classes' => 'sm:wu-p-0 sm:wu-block', // 'classes' => '', - // 'desc' => sprintf('
%s
', __('Want to add customized template selection templates?
See how you can do that here.', 'ultimate-multisite')), + // 'desc' => sprintf('
%s
', sprintf(__('Want to add customized template selection templates?
See how you can do that here.', 'ultimate-multisite'), esc_url(wu_get_documentation_url('wp-ultimo-checkout-forms')))), // ); // phpcs:enable diff --git a/inc/class-documentation.php b/inc/class-documentation.php index 6204cbea..10025082 100644 --- a/inc/class-documentation.php +++ b/inc/class-documentation.php @@ -35,7 +35,25 @@ class Documentation implements \WP_Ultimo\Interfaces\Singleton { * * @var string */ - protected $default_link = 'https://github.com/superdav42/wp-multisite-waas/wiki'; + protected $default_link = 'https://ultimatemultisite.com/docs/'; + + /** + * Map of WordPress locale prefixes to Docusaurus locale codes. + * + * @var array + */ + protected static array $locale_map = [ + 'es' => 'es', + 'fr' => 'fr', + 'de' => 'de', + 'pt_BR' => 'pt-BR', + 'ja' => 'ja', + 'zh_CN' => 'zh-Hans', + 'ru' => 'ru', + 'it' => 'it', + 'ko' => 'ko', + 'nl' => 'nl', + ]; /** * Set the default links. @@ -45,47 +63,79 @@ class Documentation implements \WP_Ultimo\Interfaces\Singleton { */ public function init(): void { + $base = $this->get_docs_base_url(); + + $this->default_link = $base; + $links = []; // Ultimate Multisite Dashboard - $links['wp-ultimo'] = 'https://github.com/superdav42/wp-multisite-waas/wiki'; + $links['wp-ultimo'] = $base; // Settings Page - $links['wp-ultimo-settings'] = 'https://github.com/superdav42/wp-multisite-waas/wiki'; + $links['wp-ultimo-settings'] = $base; // Checkout Pages - $links['wp-ultimo-checkout-forms'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/checkout-forms'; - $links['wp-ultimo-edit-checkout-form'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/checkout-forms'; - $links['wp-ultimo-populate-site-template'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/Pre-populate-Site-Template'; + $links['wp-ultimo-checkout-forms'] = $base . 'user-guide/configuration/checkout-forms'; + $links['wp-ultimo-edit-checkout-form'] = $base . 'user-guide/configuration/checkout-forms'; + $links['wp-ultimo-populate-site-template'] = $base . 'user-guide/configuration/site-templates'; // Products - $links['wp-ultimo-products'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/creating-your-first-subscription-product-v2'; - $links['wp-ultimo-edit-product'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/creating-your-first-subscription-product-v2'; + $links['wp-ultimo-products'] = $base . 'user-guide/configuration/creating-your-first-subscription-product'; + $links['wp-ultimo-edit-product'] = $base . 'user-guide/configuration/creating-your-first-subscription-product'; // Memberships - $links['wp-ultimo-memberships'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/managing-memberships-v2'; - $links['wp-ultimo-edit-membership'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/managing-memberships-v2'; + $links['wp-ultimo-memberships'] = $base . 'user-guide/administration/managing-memberships'; + $links['wp-ultimo-edit-membership'] = $base . 'user-guide/administration/managing-memberships'; // Payments - $links['wp-ultimo-payments'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/managing-payments-and-invoices'; - $links['wp-ultimo-edit-payment'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/managing-payments-and-invoices'; + $links['wp-ultimo-payments'] = $base . 'user-guide/administration/managing-payments-and-invoices'; + $links['wp-ultimo-edit-payment'] = $base . 'user-guide/administration/managing-payments-and-invoices'; // WP Config Closte Instructions - $links['wp-ultimo-closte-config'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/Closte-Integration'; + $links['wp-ultimo-closte-config'] = $base . 'user-guide/host-integrations/closte'; // Requirements - $links['wp-ultimo-requirements'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/wp-ultimo-requirements'; + $links['wp-ultimo-requirements'] = $base . 'user-guide/getting-started/requirements'; // Installer - Migrator - $links['installation-errors'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/error-installing-the-sunrise-file'; - $links['migration-errors'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/migrating-from-v1'; + $links['installation-errors'] = $base . 'user-guide/troubleshooting/sunrise-file-error'; + $links['migration-errors'] = $base . 'user-guide/migration/migrating-from-v1'; // Multiple Accounts - $links['multiple-accounts'] = 'https://github.com/superdav42/wp-multisite-waas/wiki/Multiple-Accounts'; + $links['multiple-accounts'] = $base . 'user-guide/configuration/customizing-your-registration-form'; $this->links = apply_filters('wu_documentation_links_list', $links); } + /** + * Get the locale-aware base URL for the documentation site. + * + * @since 2.3.0 + * @return string + */ + protected function get_docs_base_url(): string { + + $base = 'https://ultimatemultisite.com/docs/'; + + $wp_locale = determine_locale(); + + // Try exact match first (e.g., pt_BR) + if (isset(self::$locale_map[ $wp_locale ])) { + return $base . self::$locale_map[ $wp_locale ] . '/'; + } + + // Try language-only match (e.g., es_ES -> es, fr_FR -> fr) + $lang = substr($wp_locale, 0, 2); + + if (isset(self::$locale_map[ $lang ])) { + return $base . self::$locale_map[ $lang ] . '/'; + } + + // Default to English (no locale prefix) + return $base; + } + /** * Checks if a link exists. * diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php index 56b897e6..70c3a50f 100644 --- a/inc/class-wp-ultimo.php +++ b/inc/class-wp-ultimo.php @@ -145,25 +145,6 @@ public function init(): void { */ $this->load_public_apis(); - add_action( - 'init', - function () { - new WP_Ultimo\Admin_Pages\Setup_Wizard_Admin_Page(); - } - ); - - /* - * Multisite Setup page. On non-multisite installs, this shows the setup - * wizard. On multisite, it just registers a redirect to the main setup - * wizard so old URLs (bookmarks, login redirects) don't 403. - */ - add_action( - 'init', - function () { - new WP_Ultimo\Admin_Pages\Multisite_Setup_Admin_Page(); - } - ); - /* * Loads the Ultimate Multisite settings helper class. */ @@ -179,7 +160,16 @@ function () { * Everything we need to run our setup install needs top be loaded before this * and have no dependencies outside of the classes loaded so far. */ - if (WP_Ultimo\Requirements::met() === false || WP_Ultimo\Requirements::run_setup() === false) { + if (WP_Ultimo\Requirements::met() === false || WP_Ultimo\Requirements::run_setup() === false || ($_GET['page'] ?? '') === 'wp-ultimo-multisite-setup') { // phpcs:ignore WordPress.Security + // Use wizard to setup multisite. + add_action( + 'init', + function () { + new WP_Ultimo\Admin_Pages\Setup_Wizard_Admin_Page(); + new WP_Ultimo\Admin_Pages\Multisite_Setup_Admin_Page(); + } + ); + return; } @@ -810,6 +800,8 @@ protected function load_admin_only_pages(): void { new WP_Ultimo\Tax\Dashboard_Taxes_Tab(); new WP_Ultimo\Admin_Pages\Addons_Admin_Page(); + + new WP_Ultimo\Admin_Pages\Setup_Wizard_Admin_Page(); } /** diff --git a/inc/functions/checkout.php b/inc/functions/checkout.php index 026ab652..029854c0 100644 --- a/inc/functions/checkout.php +++ b/inc/functions/checkout.php @@ -246,7 +246,7 @@ function wu_get_days_in_cycle($duration_unit, $duration) { * Field types are types of field (duh!) that can be * added to the checkout flow and other forms inside Ultimate Multisite. * - * @see https://github.com/superdav42/wp-multisite-waas/wiki/Add-Custom-Field-Types + * @see https://ultimatemultisite.com/docs/developer/hooks/Filters/wu_checkout_field_types * * @since 2.0.0 * @@ -274,7 +274,7 @@ function ($field_types) use ($field_type_id, $field_type_class_name) { * Ultimate Multisite to be used as the final representation of a given * checkout field. * - * @see https://github.com/superdav42/wp-multisite-waas/wiki/Customize-Checkout-Flow + * @see https://ultimatemultisite.com/docs/user-guide/configuration/customizing-your-registration-form * * @since 2.0.0 * diff --git a/inc/installers/class-multisite-network-installer.php b/inc/installers/class-multisite-network-installer.php index 3a9dbc9e..a5ea01b6 100644 --- a/inc/installers/class-multisite-network-installer.php +++ b/inc/installers/class-multisite-network-installer.php @@ -40,8 +40,6 @@ class Multisite_Network_Installer extends Base_Installer { */ public function get_steps() { - global $wpdb; - $has_multisite_constant = defined('WP_ALLOW_MULTISITE') && WP_ALLOW_MULTISITE; $has_network_tables = $this->check_network_tables_exist(); $has_wp_config_updated = defined('MULTISITE') && MULTISITE; // @phpstan-ignore phpstanWP.wpConstant.fetch @@ -74,17 +72,8 @@ public function get_steps() { 'success' => __('Success!', 'ultimate-multisite'), 'help' => '', ], - 'cookie_fix' => [ - 'done' => $has_wp_config_updated, - 'title' => __('Fix Cookies', 'ultimate-multisite'), - 'description' => __('Ensures site URL is correct to prevent cookie issues after activation.', 'ultimate-multisite'), - 'pending' => __('Pending', 'ultimate-multisite'), - 'installing' => __('Fixing cookies...', 'ultimate-multisite'), - 'success' => __('Success!', 'ultimate-multisite'), - 'help' => '', - ], 'network_activate' => [ - 'done' => $this->is_network_activated(), + 'done' => is_plugin_active_for_network(WP_ULTIMO_PLUGIN_BASENAME), 'title' => __('Network Activate Plugin', 'ultimate-multisite'), 'description' => __('Network-activates Ultimate Multisite so it runs across the entire network.', 'ultimate-multisite'), 'pending' => __('Pending', 'ultimate-multisite'), @@ -95,47 +84,6 @@ public function get_steps() { ]; } - /** - * Checks whether Ultimate Multisite is network-activated. - * - * Uses direct DB query because this may run before multisite - * is active in the current PHP process. - * - * @since 2.0.0 - * @return bool - */ - protected function is_network_activated(): bool { - - global $wpdb; - - $sitemeta_table = $wpdb->base_prefix . 'sitemeta'; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $table_exists = $wpdb->get_var( - $wpdb->prepare('SHOW TABLES LIKE %s', $sitemeta_table) - ); - - if ($table_exists !== $sitemeta_table) { - return false; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $active_plugins = $wpdb->get_var( - $wpdb->prepare( - "SELECT meta_value FROM $sitemeta_table WHERE site_id = 1 AND meta_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - 'active_sitewide_plugins' - ) - ); - - if (empty($active_plugins)) { - return false; - } - - $active_plugins = maybe_unserialize($active_plugins); - - return is_array($active_plugins) && isset($active_plugins['ultimate-multisite/ultimate-multisite.php']); - } - /** * Checks whether the multisite network tables exist. * @@ -248,15 +196,14 @@ public function _install_create_network(): void { // phpcs:ignore PSR2.Methods.M } // Fix siteurl trailing slash to prevent cookie hash change. - // Is this really needed? - // $wpdb->update( - // $wpdb->sitemeta, - // ['meta_value' => untrailingslashit(get_option('siteurl'))], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value - // [ - // 'site_id' => 1, - // 'meta_key' => 'siteurl', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - // ] - // ); + $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->sitemeta, + ['meta_value' => untrailingslashit(get_option('siteurl'))], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + [ + 'site_id' => 1, + 'meta_key' => 'siteurl', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + ] + ); } /** @@ -292,44 +239,7 @@ public function _install_update_wp_config(): void { // phpcs:ignore PSR2.Methods throw new \Exception(esc_html($result->get_error_message())); } } - } - - /** - * Step 4: Verify and fix the siteurl trailing slash in sitemeta. - * - * This is an idempotent safety check to ensure the cookie hash - * remains consistent after multisite activation. - * - * @since 2.0.0 - * @throws \Exception When the fix fails. - * @return void - */ - public function _install_cookie_fix(): void { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore - - global $wpdb; - - $sitemeta_table = $wpdb->base_prefix . 'sitemeta'; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $table_exists = $wpdb->get_var( - $wpdb->prepare('SHOW TABLES LIKE %s', $sitemeta_table) - ); - - if ($table_exists !== $sitemeta_table) { - return; - } - - $siteurl = get_option('siteurl'); - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $sitemeta_table, - ['meta_value' => untrailingslashit($siteurl)], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value - [ - 'site_id' => 1, - 'meta_key' => 'siteurl', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - ] - ); + wp_cache_flush(); } /** @@ -343,73 +253,17 @@ public function _install_cookie_fix(): void { // phpcs:ignore PSR2.Methods.Metho * @return void */ public function _install_network_activate(): void { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore - - if ($this->is_network_activated()) { + // If already active, succeed early. + if (is_plugin_active(WP_ULTIMO_PLUGIN_FILE)) { return; } - global $wpdb; - - $sitemeta_table = $wpdb->base_prefix . 'sitemeta'; - $plugin = 'ultimate-multisite/ultimate-multisite.php'; + // Activate the plugin. + $result = activate_plugin(WP_ULTIMO_PLUGIN_FILE, '', true); - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $table_exists = $wpdb->get_var( - $wpdb->prepare('SHOW TABLES LIKE %s', $sitemeta_table) - ); - - if ($table_exists !== $sitemeta_table) { - throw new \Exception(esc_html__('The sitemeta table does not exist. Network tables must be created first.', 'ultimate-multisite')); - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $active_plugins = $wpdb->get_var( - $wpdb->prepare( - "SELECT meta_value FROM $sitemeta_table WHERE site_id = 1 AND meta_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - 'active_sitewide_plugins' - ) - ); - - $active_plugins = ! empty($active_plugins) ? maybe_unserialize($active_plugins) : []; - - if (! is_array($active_plugins)) { - $active_plugins = []; - } - - $active_plugins[ $plugin ] = time(); - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $existing = $wpdb->get_var( - $wpdb->prepare( - "SELECT COUNT(*) FROM $sitemeta_table WHERE site_id = 1 AND meta_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - 'active_sitewide_plugins' - ) - ); - - if ($existing) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $result = $wpdb->update( - $sitemeta_table, - ['meta_value' => maybe_serialize($active_plugins)], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value - [ - 'site_id' => 1, - 'meta_key' => 'active_sitewide_plugins', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - ] - ); - } else { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $result = $wpdb->insert( - $sitemeta_table, - [ - 'site_id' => 1, - 'meta_key' => 'active_sitewide_plugins', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - 'meta_value' => maybe_serialize($active_plugins), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value - ] - ); - } - - if (false === $result) { - throw new \Exception(esc_html__('Failed to network-activate Ultimate Multisite.', 'ultimate-multisite')); + if (is_wp_error($result)) { + // translators: %s full error message. + throw new \Exception(sprintf(esc_html__('Failed to network-activate Ultimate Multisite: %s', 'ultimate-multisite'), esc_html($result->get_error_message()))); } } } diff --git a/inc/ui/class-template-switching-element.php b/inc/ui/class-template-switching-element.php index fba02104..43bd38fa 100644 --- a/inc/ui/class-template-switching-element.php +++ b/inc/ui/class-template-switching-element.php @@ -169,7 +169,8 @@ public function fields() { 'order' => 99, 'wrapper_classes' => 'sm:wu-p-0 sm:wu-block', 'classes' => '', - 'desc' => sprintf('
%s
', __('Want to add customized template selection templates?
See how you can do that here.', 'ultimate-multisite')), + // translators: %s the doc url + 'desc' => sprintf('
%s
', sprintf(__('Want to add customized template selection templates?
See how you can do that here.', 'ultimate-multisite'), esc_url(wu_get_documentation_url('wp-ultimo-checkout-forms')))), ]; return $fields; diff --git a/views/base/wizard/submit-box.php b/views/base/wizard/submit-box.php index ce4bef47..bf96d559 100644 --- a/views/base/wizard/submit-box.php +++ b/views/base/wizard/submit-box.php @@ -5,14 +5,17 @@ * @since 2.0.0 */ defined('ABSPATH') || exit; - +/** @var \WP_Ultimo\Admin_Pages\Wizard_Admin_Page $page */ +$back_url = $page->get_prev_section_link(); ?>
- - - + + + + + diff --git a/views/checkout/partials/pricing-table-list.php b/views/checkout/partials/pricing-table-list.php index 94cdce3b..78d31fef 100644 --- a/views/checkout/partials/pricing-table-list.php +++ b/views/checkout/partials/pricing-table-list.php @@ -5,7 +5,7 @@ * To see what methods are available on the product variable, @see inc/models/class-producs.php. * * This template can also be overrid using template overrides. - * See more here: https://github.com/superdav42/wp-multisite-waas/wiki/Template-Overrides. + * See more here: https://ultimatemultisite.com/docs/user-guide/miscellaneous/frequently-asked-questions * * @since 2.0.0 * @param array $products List of product objects. diff --git a/views/checkout/templates/pricing-table/legacy.php b/views/checkout/templates/pricing-table/legacy.php index d2768bc7..0f0a7d17 100644 --- a/views/checkout/templates/pricing-table/legacy.php +++ b/views/checkout/templates/pricing-table/legacy.php @@ -5,7 +5,7 @@ * To see what methods are available on the product variable, @see inc/models/class-products.php. * * This template can also be override using template overrides. - * See more here: https://github.com/superdav42/wp-multisite-waas/wiki/Template-Overrides. + * See more here: https://ultimatemultisite.com/docs/user-guide/miscellaneous/frequently-asked-questions * * @since 2.0.0 * @param array $products List of product objects. diff --git a/views/checkout/templates/template-selection/clean.php b/views/checkout/templates/template-selection/clean.php index de56f0ea..d28ff20f 100644 --- a/views/checkout/templates/template-selection/clean.php +++ b/views/checkout/templates/template-selection/clean.php @@ -5,7 +5,7 @@ * To see what methods are available on the product variable, @see inc/models/class-products.php. * * This template can also be overridden using template overrides. - * See more here: https://github.com/superdav42/wp-multisite-waas/wiki/Template-Overrides. + * See more here: https://ultimatemultisite.com/docs/user-guide/miscellaneous/frequently-asked-questions * * @since 2.0.0 * @package WP_Ultimo/Views diff --git a/views/checkout/templates/template-selection/legacy.php b/views/checkout/templates/template-selection/legacy.php index fb62a412..4895fd22 100644 --- a/views/checkout/templates/template-selection/legacy.php +++ b/views/checkout/templates/template-selection/legacy.php @@ -10,7 +10,7 @@ * @see inc/models/class-products.php. * * This template can also be overridden using template overrides. - * See more here: https://github.com/superdav42/wp-multisite-waas/wiki/Template-Overrides. + * See more here: https://ultimatemultisite.com/docs/user-guide/miscellaneous/frequently-asked-questions * * @since 2.0.0 * @package WP_Ultimo/Views From d790a28d4c4613f04532edd4228b5df812706119 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 12 Feb 2026 11:51:57 -0700 Subject: [PATCH 9/9] Get network wiz working fully --- .phpcs.xml.dist | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index a702c05b..6ec0c206 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -17,10 +17,6 @@ - - - -