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 @@ + +
+

+ +

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

+ +

+

+ +

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

+ +

+

+ +

+
+
+