Conversation
WalkthroughAdds a new Multisite Setup admin wizard, wires it into initialization, and updates redirect logic to route non-multisite installs to this wizard. Introduces multisite creation/configuration routines, completion views, and manual instructions. Also includes a small docblock fix and a multisite guard in a default content installer. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Admin
participant WP as WP Admin
participant SW as Setup_Wizard_Admin_Page
participant MS as Multisite_Setup_Admin_Page
Admin->>WP: Access admin
WP->>SW: redirect_to_wizard()
alt Multisite disabled
SW-->>WP: wp_redirect(admin.php?page=wp-ultimo-multisite-setup)
WP->>MS: Load wizard (welcome)
else Multisite enabled AND setup not finished
SW-->>WP: wp_redirect(wu_network_admin_url('wp-ultimo-setup'))
else Multisite enabled AND setup finished
SW-->>WP: No redirect
end
sequenceDiagram
autonumber
actor Admin
participant MS as Multisite_Setup_Admin_Page
participant FS as Filesystem
participant WP as WordPress (Install/Network)
Admin->>MS: Configure (structure, sitename, email) + Submit
MS->>MS: Validate & store transients
MS->>FS: modify_wp_config() (initial multisite constants)
alt Write OK
MS->>WP: create_network(subdomain?, sitename, email)
alt Network created
MS->>FS: add_final_multisite_constants()
MS-->>Admin: Redirect to Complete
else Creation failed
MS-->>Admin: Show manual instructions
end
else Write failed
MS-->>Admin: Show manual instructions
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
inc/class-wp-ultimo.php (1)
143-147: Gate Multisite Setup page instantiation to non‑multisite to avoid duplicate top‑level menusRight now both Setup Wizard and Multisite Setup pages register as top-level in single-site, causing duplicate “Multisite Ultimate” menus. Instantiate the Multisite Setup page only when multisite is disabled.
- /* - * Multisite Setup for non-multisite installations - */ - new WP_Ultimo\Admin_Pages\Multisite_Setup_Admin_Page(); + /* + * Multisite Setup for non-multisite installations + */ + if (! is_multisite()) { + new WP_Ultimo\Admin_Pages\Multisite_Setup_Admin_Page(); + }inc/admin-pages/class-multisite-setup-admin-page.php (2)
382-394: Subdomain vs subdirectory .htaccess rules differProvide rules based on selected structure; current rules are subdirectory-only.
- $htaccess_rules = 'RewriteEngine On + $htaccess_rules = $subdomain_install ? 'RewriteEngine On +RewriteBase / +RewriteRule ^index\.php$ - [L] + +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]' : '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]';Please verify these match WordPress Network Setup output for your server environment.
Also applies to: 368-375
74-83: Optional: Auto-redirect away if multisite is already enabledPrevents showing this page after success.
public function __construct() { + if (is_multisite()) { + wp_safe_redirect(wu_network_admin_url('wp-ultimo-setup')); + exit; + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
inc/admin-pages/class-base-admin-page.php(1 hunks)inc/admin-pages/class-multisite-setup-admin-page.php(1 hunks)inc/admin-pages/class-setup-wizard-admin-page.php(1 hunks)inc/class-wp-ultimo.php(1 hunks)inc/installers/class-default-content-installer.php(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
inc/class-wp-ultimo.php (1)
inc/admin-pages/class-multisite-setup-admin-page.php (1)
Multisite_Setup_Admin_Page(21-626)
inc/admin-pages/class-setup-wizard-admin-page.php (2)
inc/class-requirements.php (1)
run_setup(101-120)inc/functions/helper.php (1)
wu_request(132-137)
inc/admin-pages/class-multisite-setup-admin-page.php (4)
inc/admin-pages/class-wizard-admin-page.php (3)
Wizard_Admin_Page(23-392)render_submit_box(373-383)get_next_section_link(266-275)inc/admin-pages/class-base-admin-page.php (4)
__construct(173-205)get_title(718-718)get_menu_title(726-726)register_scripts(694-694)inc/functions/helper.php (1)
wu_request(132-137)inc/functions/url.php (1)
wu_network_admin_url(53-60)
inc/installers/class-default-content-installer.php (1)
inc/class-requirements.php (1)
is_multisite(228-237)
🔇 Additional comments (3)
inc/admin-pages/class-base-admin-page.php (1)
219-224: Docblock fix looks goodAccurate and clarifies the method purpose.
inc/installers/class-default-content-installer.php (1)
55-57: Good guard to avoid fatal in single-siteEarly return prevents calling domain_exists() outside multisite. Nice.
inc/admin-pages/class-setup-wizard-admin-page.php (1)
224-236: Redirect split is correct and avoids loopsClear separation of single-site vs network flows, with exits. LGTM.
| return [ | ||
| 'welcome' => [ | ||
| 'title' => __('Multisite Required', 'multisite-ultimate'), | ||
| 'description' => implode( | ||
| '<br><br>', | ||
| [ | ||
| __('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, | ||
| ], | ||
| ]; |
There was a problem hiding this comment.
🧹 Nitpick
Welcome view defined but not used
section_welcome() won’t run because the 'welcome' section lacks a 'view' entry. Wire it up.
- 'welcome' => [
+ 'welcome' => [
'title' => __('Multisite Required', 'multisite-ultimate'),
'description' => implode(
'<br><br>',
[
__('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'],
],Also applies to: 160-208
🤖 Prompt for AI Agents
inc/admin-pages/class-multisite-setup-admin-page.php around lines 123-151 (and
similarly 160-208): the "welcome" section currently has no 'view' entry so the
section_welcome() callback is never invoked; add a 'view' => [$this,
'section_welcome'] key to the welcome array, and review other section arrays in
the 160-208 range to ensure each section that has a corresponding section_*
method is wired with a 'view' => [$this, 'section_<name>'] entry (or remove
unused section_* methods) so the defined view callbacks are actually used.
| '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'), '<code>' . esc_html($base_domain) . '/site1</code>'), | ||
|
|
||
| 'sub1' => sprintf(__('Sites will use sub-domains like %s (Requires wildcard DNS)', 'multisite-ultimate'), '<code>site1.' . esc_html($base_domain) . '</code>'), | ||
|
|
||
| ], | ||
| 'default' => '0', | ||
| ], |
There was a problem hiding this comment.
Radio values → boolean conversion bug (always true)
Options use 'sub0'/'sub1' but the code casts the string directly to bool, making both truthy. Use '0'/'1' (or map explicitly) and compare string values.
- 'subdomain_install' => [
+ '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'), '<code>' . esc_html($base_domain) . '/site1</code>'),
-
- 'sub1' => sprintf(__('Sites will use sub-domains like %s (Requires wildcard DNS)', 'multisite-ultimate'), '<code>site1.' . esc_html($base_domain) . '</code>'),
+ '0' => sprintf(__('Sites will use sub-directories like %s (Recommended)', 'multisite-ultimate'), '<code>' . esc_html($base_domain) . '/site1</code>'),
+ '1' => sprintf(__('Sites will use sub-domains like %s (Requires wildcard DNS)', 'multisite-ultimate'), '<code>site1.' . esc_html($base_domain) . '</code>'),
],
- 'default' => '0',
+ 'default' => '0',
],- $subdomain_install = (bool) wu_request('subdomain_install', 0);
+ $subdomain_install = wu_request('subdomain_install', '0') === '1';Also applies to: 287-290
🤖 Prompt for AI Agents
inc/admin-pages/class-multisite-setup-admin-page.php lines ~228-239 and
~287-290: the radio options currently use keys 'sub0'/'sub1' but elsewhere the
code casts the selected value to boolean (making both keys truthy), so selection
is always treated as true; change the option keys to simple '0' and '1' (and
keep 'default' => '0'), or explicitly map 'sub0'/'sub1' to boolean values and
update any checks to compare the string value (use strict equality to '1' or
'0') instead of casting to bool; apply the same fix to the block at lines
~287-290 so selection logic works correctly.
| $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 WP_ALLOW_MULTISITE is already defined | ||
| if (strpos($config_content, 'WP_ALLOW_MULTISITE') !== false) { | ||
| 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 ($insert_position === false) { | ||
| // 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) { | ||
| 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; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Support wp-config.php one level above ABSPATH
Many installs place wp-config.php in the parent directory. Add a fallback search before giving up.
- $wp_config_path = ABSPATH . 'wp-config.php';
-
- if (! file_exists($wp_config_path) || ! is_writable($wp_config_path)) {
+ $paths = [
+ ABSPATH . 'wp-config.php',
+ trailingslashit(dirname(ABSPATH)) . 'wp-config.php',
+ ];
+ $wp_config_path = null;
+ foreach ($paths as $candidate) {
+ if (file_exists($candidate) && is_writable($candidate)) {
+ $wp_config_path = $candidate;
+ break;
+ }
+ }
+ if ($wp_config_path === null) {
return false;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $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 WP_ALLOW_MULTISITE is already defined | |
| if (strpos($config_content, 'WP_ALLOW_MULTISITE') !== false) { | |
| 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 ($insert_position === false) { | |
| // 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) { | |
| 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; | |
| } | |
| $paths = [ | |
| ABSPATH . 'wp-config.php', | |
| trailingslashit(dirname(ABSPATH)) . 'wp-config.php', | |
| ]; | |
| $wp_config_path = null; | |
| foreach ($paths as $candidate) { | |
| if (file_exists($candidate) && is_writable($candidate)) { | |
| $wp_config_path = $candidate; | |
| break; | |
| } | |
| } | |
| if ($wp_config_path === null) { | |
| return false; | |
| } | |
| $config_content = file_get_contents($wp_config_path); | |
| if ($config_content === false) { | |
| return false; | |
| } | |
| // Check if WP_ALLOW_MULTISITE is already defined | |
| if (strpos($config_content, 'WP_ALLOW_MULTISITE') !== false) { | |
| 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 ($insert_position === false) { | |
| // 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) { | |
| 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; | |
| } |
| // Add final multisite constants to wp-config.php | ||
| $this->add_final_multisite_constants($subdomain_install, $domain); | ||
|
|
||
| return true; |
There was a problem hiding this comment.
Don’t ignore failure to write MULTISITE constants; add insertion fallback
- add_final_multisite_constants() returns false if the exact WP_ALLOW_MULTISITE line isn’t found. Provide a fallback insertion point.
- In create_network(), treat a failure to add constants as a failure overall.
- // Add final multisite constants to wp-config.php
- $this->add_final_multisite_constants($subdomain_install, $domain);
-
- return true;
+ // Add final multisite constants to wp-config.php
+ $constants_ok = $this->add_final_multisite_constants($subdomain_install, $domain);
+ return (bool) $constants_ok;- // 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;
+ // Prefer inserting after WP_ALLOW_MULTISITE, otherwise before "That's all" or wp-settings
+ $anchors = [
+ "define( 'WP_ALLOW_MULTISITE', true );",
+ "/* That's all, stop editing! Happy publishing. */",
+ "require_once ABSPATH . 'wp-settings.php';",
+ ];
+ foreach ($anchors as $anchor) {
+ $pos = strpos($config_content, $anchor);
+ if ($pos !== false) {
+ $pos += ($anchor === $anchors[0]) ? strlen($anchor) : 0;
+ $new_content = substr_replace($config_content, $constants_to_add, $pos, 0);
+ return file_put_contents($wp_config_path, $new_content) !== false;
+ }
+ }
+ return false;Also applies to: 579-590
🤖 Prompt for AI Agents
inc/admin-pages/class-multisite-setup-admin-page.php lines 535-538 and 579-590:
add_final_multisite_constants() can return false when the exact
WP_ALLOW_MULTISITE anchor isn’t found, so update create_network() to check its
return value and treat a false as an overall failure (return false and surface
an error). If add_final_multisite_constants() fails, implement a fallback
insertion strategy: attempt to insert the MULTISITE constants at a secondary
anchor (for example before "/* That's all, stop editing! */" or immediately
before the require_once for wp-settings.php), and only if both primary and
fallback insertions fail return false; ensure proper logging of the failure and
that create_network() propagates that failure instead of returning true.
| return true; | ||
| } catch (Exception $e) { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Namespace issue: catching Exception will fatal
Use the global Exception class or import it. Otherwise PHP looks for WP_Ultimo\Admin_Pages\Exception.
- } catch (Exception $e) {
+ } catch (\Exception $e) {
return false;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return true; | |
| } catch (Exception $e) { | |
| return false; | |
| } | |
| return true; | |
| } catch (\Exception $e) { | |
| return false; | |
| } |
🤖 Prompt for AI Agents
In inc/admin-pages/class-multisite-setup-admin-page.php around lines 538 to 541,
the catch block currently references Exception without qualification which makes
PHP look for WP_Ultimo\Admin_Pages\Exception and will fatal; change the catch to
use the global Exception class (e.g. catch (\Exception $e)) or add a top-level
import (use Exception;) at the file namespace header so the global Exception is
correctly referenced; update the catch signature accordingly and keep existing
handling (return false).
| 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); | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
| ' | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick
Inline script attached to a non‑existent handle
wp_add_inline_script('wp-admin', ...) won’t run. Attach to a registered/enqueued handle (e.g., 'jquery') or enqueue a small handle for this page and attach to it.
- wp_add_inline_script(
- 'wp-admin',
+ wp_add_inline_script(
+ 'jquery',
'
// Copy to clipboard functionality
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll("button[onclick*=\'navigator.clipboard.writeText\']").forEach(function(button) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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); | |
| }); | |
| } | |
| }); | |
| }); | |
| }); | |
| ' | |
| ); | |
| } | |
| if (get_current_screen()->id !== 'toplevel_page_wp-ultimo-multisite-setup') { | |
| return; | |
| } | |
| 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); | |
| }); | |
| } | |
| }); | |
| }); | |
| }); | |
| ' | |
| ); | |
| } |
Summary by CodeRabbit