Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
439 changes: 439 additions & 0 deletions inc/apis/class-settings-endpoint.php

Large diffs are not rendered by default.

21 changes: 15 additions & 6 deletions inc/checkout/class-checkout-pages.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,28 @@ public function init(): void {

add_filter('lostpassword_redirect', [$this, 'filter_lost_password_redirect']);

$use_custom_login = wu_get_setting('enable_custom_login_page', false);

/*
* Login URL filters need to run on ALL sites (including subsites)
* so that password reset and login links redirect to the main site's
* custom login page instead of wp-login.php (which may be obfuscated).
*
* @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/291
*/
if ($use_custom_login) {
add_filter('login_url', [$this, 'filter_login_url'], 10, 3);

add_filter('lostpassword_url', [$this, 'filter_login_url'], 10, 3);
}

if (is_main_site()) {
add_action('before_signup_header', [$this, 'redirect_to_registration_page']);

$use_custom_login = wu_get_setting('enable_custom_login_page', false);

if ( ! $use_custom_login) {
return;
}

add_filter('login_url', [$this, 'filter_login_url'], 10, 3);

add_filter('lostpassword_url', [$this, 'filter_login_url'], 10, 3);

add_filter('retrieve_password_message', [$this, 'replace_reset_password_link'], 10, 4);

add_filter('network_site_url', [$this, 'maybe_change_wp_login_on_urls']);
Expand Down
8 changes: 7 additions & 1 deletion inc/class-addon-repository.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<?php
/**
* Handle access to addons.
*/

namespace WP_Ultimo;

use Psr\Log\LogLevel;

/**
* Addon Repository class for handling addon downloads and updates.
*
Expand Down Expand Up @@ -141,7 +146,8 @@ public function get_user_data(): array {
$code = wp_remote_retrieve_response_code($request);
$message = wp_remote_retrieve_response_message($request);
if (is_wp_error($request)) {
throw new \Exception(esc_html($request->get_error_message()), (int) $request->get_error_code());
wu_log_add('api-calls', $request->get_error_message(), LogLevel::ERROR);
$this->delete_tokens();
}
if (200 === absint($code) && 'OK' === $message) {
$user = json_decode($body, true);
Expand Down
31 changes: 16 additions & 15 deletions inc/class-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,22 @@ public function default_sections(): void {
120
);

$this->add_field(
'general',
'enable_error_reporting',
[
'title' => __('Help Improve Ultimate Multisite', 'ultimate-multisite'),
'desc' => sprintf(
/* translators: %s is a link to the privacy policy */
__('Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. <a href="%s" target="_blank">Learn more</a>.', 'ultimate-multisite'),
'https://ultimatemultisite.com/privacy-policy/'
),
Comment on lines +711 to +720
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add rel="noopener noreferrer" to the privacy‑policy link

Line 718 opens a new tab without rel, which exposes a tabnabbing risk. Consider also wrapping the URL with esc_url() for safety.

🛠️ Proposed fix
-				'desc'    => sprintf(
+				'desc'    => sprintf(
 				/* translators: %s is a link to the privacy policy */
-					__('Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. <a href="%s" target="_blank">Learn more</a>.', 'ultimate-multisite'),
-					'https://ultimatemultisite.com/privacy-policy/'
+					__('Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. <a href="%s" target="_blank" rel="noopener noreferrer">Learn more</a>.', 'ultimate-multisite'),
+					esc_url('https://ultimatemultisite.com/privacy-policy/')
 				),
🤖 Prompt for AI Agents
In `@inc/class-settings.php` around lines 711 - 720, The privacy-policy link in
the description passed to add_field for 'enable_error_reporting' lacks
rel="noopener noreferrer" and the URL isn't escaped; update the sprintf call
building the description so the <a href="%s" ...> uses esc_url(
'https://ultimatemultisite.com/privacy-policy/' ) and add rel="noopener
noreferrer" to the anchor (i.e., modify the description string for
add_field('general','enable_error_reporting', ...) to include rel="noopener
noreferrer" and wrap the URL with esc_url()).

'type' => 'toggle',
'default' => 0,
],
130
);

/*
* Login & Registration
* This section holds the Login & Registration settings of the Ultimate Multisite Plugin.
Expand Down Expand Up @@ -1730,21 +1746,6 @@ public function default_sections(): void {
]
);

$this->add_field(
'other',
'enable_error_reporting',
[
'title' => __('Help Improve Ultimate Multisite', 'ultimate-multisite'),
'desc' => sprintf(
/* translators: %s is a link to the privacy policy */
__('Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. <a href="%s" target="_blank">Learn more</a>.', 'ultimate-multisite'),
'https://ultimatemultisite.com/privacy-policy/'
),
'type' => 'toggle',
'default' => 0,
]
);

$this->add_field(
'other',
'advanced_header',
Expand Down
5 changes: 5 additions & 0 deletions inc/class-wp-ultimo.php
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,11 @@ protected function load_extra_components(): void {
*/
\WP_Ultimo\API\Register_Endpoint::get_instance();

/*
* Loads API settings endpoint.
*/
\WP_Ultimo\API\Settings_Endpoint::get_instance();

/*
* Loads Documentation
*/
Expand Down
20 changes: 15 additions & 5 deletions inc/functions/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function wu_slugify($term) {
*/
function wu_path($dir): string {

return WP_ULTIMO_PLUGIN_DIR . $dir; // @phpstan-ignore-line
return WP_ULTIMO_PLUGIN_DIR . $dir;
}

/**
Expand All @@ -117,7 +117,7 @@ function wu_path($dir): string {
*/
function wu_url($dir) {

return apply_filters('wp_ultimo_url', WP_ULTIMO_PLUGIN_URL . $dir); // @phpstan-ignore-line
return apply_filters('wp_ultimo_url', WP_ULTIMO_PLUGIN_URL . $dir);
}

/**
Expand Down Expand Up @@ -294,8 +294,7 @@ function wu_ignore_errors($func, $log = false) { // phpcs:ignore Generic.CodeAna

try {
call_user_func($func);
} catch (\Throwable $exception) {

} catch (\Throwable $exception) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// Ignore it or log it.
}
}
Expand Down Expand Up @@ -496,7 +495,7 @@ function wu_kses_allowed_html(): array {
'template' => true,
];

return [
$allowed_tags = [
'svg' => $svg_attributes + [
'width' => true,
'height' => true,
Expand Down Expand Up @@ -613,4 +612,15 @@ function wu_kses_allowed_html(): array {
'height' => true,
],
] + array_merge_recursive($allowed_html, array_fill_keys(array_keys($allowed_html) + ['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'b', 'i', 'ul', 'ol', 'li', 'a', 'img', 'input', 'textarea'], $vue_and_data_attributes));

/**
* Filters the allowed HTML tags and attributes.
*
* Allows addons to extend the allowed HTML elements for wp_kses sanitization.
*
* @since 2.5.0
*
* @param array $allowed_tags The allowed HTML tags and attributes.
*/
return apply_filters('wu_kses_allowed_html', $allowed_tags);
}
6 changes: 6 additions & 0 deletions inc/ui/class-login-form-element.php
Original file line number Diff line number Diff line change
Expand Up @@ -771,12 +771,18 @@ public function output($atts, $content = null): void {
'title' => $atts['label_username'],
'placeholder' => $atts['placeholder_username'],
'tooltip' => '',
'html_attr' => [
'autocomplete' => 'username',
],
],
'pwd' => [
'type' => 'password',
'title' => $atts['label_password'],
'placeholder' => $atts['placeholder_password'],
'tooltip' => '',
'html_attr' => [
'autocomplete' => 'current-password',
],
],
];

Expand Down
7 changes: 7 additions & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -240,16 +240,23 @@ We recommend running this in a staging environment before updating your producti

== Changelog ==

Version [2.4.10] - Released on 2026-XX-XX
- New: Settings API
- Fix: Problems with choosing country and state


Version [2.4.10] - Released on 2026-01-23
- New: Configurable minimum password strength setting with Medium, Strong, and Super Strong options.
Comment on lines +243 to 249
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Changelog duplicates 2.4.10 and uses a placeholder date

Lines 243–248 introduce a second 2.4.10 header with “2026‑XX‑XX”. Please merge into a single 2.4.10 entry and use a concrete release date (e.g., 2026‑01‑25 if releasing today) or mark as “Unreleased”.

🤖 Prompt for AI Agents
In `@readme.txt` around lines 243 - 249, The changelog contains duplicate "Version
[2.4.10]" entries (the headers "Version [2.4.10]" and the block dated
"2026-XX-XX"); merge the two blocks into a single 2.4.10 entry combining both
bullet points, and replace the placeholder date "2026-XX-XX" with a concrete
release date (e.g., "2026-01-25") or label the entry "Unreleased" if it isn't
finalized; update the single "Version [2.4.10]" header and remove the redundant
header/block so only one consolidated 2.4.10 section remains.

- New: Super Strong password requirements include 12+ characters, uppercase, lowercase, numbers, and special characters - compatible with WPMU DEV Defender Pro rules.
- New: Real-time password requirement hints during checkout with translatable strings.
- New: Themed password field styling with visibility toggle and color fallbacks for page builders (Elementor, Kadence, Beaver Builder).
- New: Opt-in anonymous usage tracking to help improve the plugin.
- New: Better error page for customers and admins.
- New: Rating reminder notice after 30 days of installation.
- New: WooCommerce Subscriptions compatibility layer for site duplication.
- Improved: JSON response handling for pending site creation in non-FastCGI environments.


Version [2.4.9] - Released on 2025-12-23
- New: Inline login prompt at checkout for existing users - returning customers can sign in directly without leaving the checkout flow.
- New: GitHub Actions workflow for PR builds with WordPress Playground testing - enables one-click browser-based testing of pull requests.
Expand Down
6 changes: 6 additions & 0 deletions views/checkout/fields/field-select.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@

?>

<?php
// Check if Vue is handling the name dynamically to avoid duplicate attributes
$has_vue_name = isset($field->html_attr['v-bind:name']);
?>
<select
class="form-control wu-w-full wu-my-1 <?php echo esc_attr(trim($field->classes)); ?>"
id="field-<?php echo esc_attr($field->id); ?>"
<?php if ( ! $has_vue_name) : ?>
name="<?php echo esc_attr($field->id); ?>"
<?php endif; ?>
value="<?php echo esc_attr($field->value); ?>"
<?php $field->print_html_attributes(); ?>
>
Expand Down
10 changes: 9 additions & 1 deletion views/checkout/fields/field-text.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@

<?php endif; ?>

<input class="form-control wu-w-full wu-my-1 <?php echo esc_attr(trim($field->classes)); ?>" id="field-<?php echo esc_attr($field->id); ?>" name="<?php echo esc_attr($field->id); ?>" type="<?php echo esc_attr($field->type); ?>" placeholder="<?php echo esc_attr($field->placeholder); ?>" value="<?php echo esc_attr($field->value); ?>" <?php $field->print_html_attributes(); ?>>
<?php
// Check if Vue is handling the name dynamically to avoid duplicate attributes
$has_vue_name = isset($field->html_attr['v-bind:name']);
?>
<input class="form-control wu-w-full wu-my-1 <?php echo esc_attr(trim($field->classes)); ?>" id="field-<?php echo esc_attr($field->id); ?>"
<?php
if ( ! $has_vue_name) :
?>
name="<?php echo esc_attr($field->id); ?>" <?php endif; ?>type="<?php echo esc_attr($field->type); ?>" placeholder="<?php echo esc_attr($field->placeholder); ?>" value="<?php echo esc_attr($field->value); ?>" <?php $field->print_html_attributes(); ?>>

<?php if ($field->suffix) : ?>

Expand Down
Loading