diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist
index 22b0ace2..6ec0c206 100644
--- a/.phpcs.xml.dist
+++ b/.phpcs.xml.dist
@@ -17,7 +17,6 @@
-
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/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-base-admin-page.php b/inc/admin-pages/class-base-admin-page.php
index 31bbaac5..bf8009fe 100644
--- a/inc/admin-pages/class-base-admin-page.php
+++ b/inc/admin-pages/class-base-admin-page.php
@@ -239,7 +239,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..845e2c26
--- /dev/null
+++ b/inc/admin-pages/class-multisite-setup-admin-page.php
@@ -0,0 +1,506 @@
+ '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']);
+ /**
+ * 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);
+ }
+
+ /**
+ * 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'),
+ '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' => __('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'),
+ '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'),
+ '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 {
+
+ wu_get_template('wizards/multisite-setup/welcome');
+
+ $this->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 = wp_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' => 'select',
+ 'title' => __('Site Structure', 'multisite-ultimate'),
+ 'desc' => __('Choose between subdomains or subdirectories for your network sites.', 'multisite-ultimate'),
+ 'options' => [
+ '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' => '1',
+ ],
+ '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'),
+ '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'),
+ 'value' => $user->user_email,
+ ],
+ 'backup_warning' => [
+ 'type' => 'note',
+ 'desc' => '
+
+
+
+
+
+
' . 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') . '
+
+
+
',
+ ],
+ ];
+ }
+
+ /**
+ * 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
+ */
+ public function handle_configure(): void {
+
+ if (! current_user_can('manage_options')) {
+ wp_die(esc_html__('Permission denied.', 'multisite-ultimate'));
+ }
+
+ $subdomain_install = wu_request('subdomain_install', '0') === '1';
+ $sitename = sanitize_text_field(wu_request('sitename', ''));
+ $email = sanitize_email(wu_request('email', ''));
+
+ $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 ($port) {
+ $domain .= ':' . $port;
+ }
+
+ set_transient(
+ Multisite_Network_Installer::CONFIG_TRANSIENT,
+ [
+ 'subdomain_install' => $subdomain_install,
+ 'sitename' => $sitename,
+ 'email' => $email,
+ 'domain' => $domain,
+ 'base' => $base,
+ ],
+ HOUR_IN_SECONDS
+ );
+
+ wp_safe_redirect($this->get_next_section_link());
+ exit;
+ }
+
+ /**
+ * Completion section view.
+ *
+ * @since 2.0.0
+ * @return void
+ */
+ public function section_complete(): void {
+
+ $result = wu_request('result', ''); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ if ('success' === $result || is_multisite()) :
+ ?>
+
+
+
+ display_manual_instructions();
+ endif;
+ }
+
+ /**
+ * Display manual configuration instructions.
+ *
+ * @since 2.0.0
+ * @return void
+ */
+ protected function display_manual_instructions(): void {
+
+ $home_url = get_option('home');
+ $base_domain = wp_parse_url($home_url, PHP_URL_HOST);
+ $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 );
+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 );";
+
+ 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
+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. */'
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+ 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.
+ *
+ * @since 2.0.0
+ * @return void
+ */
+ public function register_scripts(): void {
+
+ if (get_current_screen()->id !== 'toplevel_page_wp-ultimo-multisite-setup') {
+ 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);
+ }
+}
diff --git a/inc/admin-pages/class-setup-wizard-admin-page.php b/inc/admin-pages/class-setup-wizard-admin-page.php
index 0890527e..4eea8a2a 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,15 +137,10 @@ 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_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_action('admin_init', [$this, 'alert_incomplete_installation']);
}
@@ -216,21 +211,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 ( ! 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.
*
@@ -587,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/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/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/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/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-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/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-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-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 4544f035..70c3a50f 100644
--- a/inc/class-wp-ultimo.php
+++ b/inc/class-wp-ultimo.php
@@ -145,11 +145,6 @@ public function init(): void {
*/
$this->load_public_apis();
- /*
- * Setup Wizard
- */
- new WP_Ultimo\Admin_Pages\Setup_Wizard_Admin_Page();
-
/*
* Loads the Ultimate Multisite settings helper class.
*/
@@ -165,7 +160,16 @@ public function init(): void {
* 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;
}
@@ -796,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/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..02e0d5ad 100644
--- a/inc/functions/site.php
+++ b/inc/functions/site.php
@@ -13,10 +13,14 @@
* Returns the current site.
*
* @since 2.0.0
- * @return \WP_Ultimo\Models\Site
+ * @return \WP_Ultimo\Models\Site|null
*/
function wu_get_current_site() {
+ if ( ! is_multisite()) {
+ return null;
+ }
+
static $sites = array();
$blog_id = get_current_blog_id();
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-default-content-installer.php b/inc/installers/class-default-content-installer.php
index d3559ece..92a8b792 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());
@@ -383,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..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) {
}
}
@@ -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
*/
diff --git a/inc/installers/class-multisite-network-installer.php b/inc/installers/class-multisite-network-installer.php
new file mode 100644
index 00000000..a5ea01b6
--- /dev/null
+++ b/inc/installers/class-multisite-network-installer.php
@@ -0,0 +1,269 @@
+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' => '',
+ ],
+ 'network_activate' => [
+ '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'),
+ 'installing' => __('Activating plugin...', 'ultimate-multisite'),
+ 'success' => __('Success!', 'ultimate-multisite'),
+ 'help' => '',
+ ],
+ ];
+ }
+
+ /**
+ * 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.
+ $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
+ ]
+ );
+ }
+
+ /**
+ * 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()));
+ }
+ }
+ wp_cache_flush();
+ }
+
+ /**
+ * 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 already active, succeed early.
+ if (is_plugin_active(WP_ULTIMO_PLUGIN_FILE)) {
+ return;
+ }
+
+ // Activate the plugin.
+ $result = activate_plugin(WP_ULTIMO_PLUGIN_FILE, '', true);
+
+ 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/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/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
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+