From 51f58cde08afd349dd2ea09b9d8eb9a86e10442d Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 12 Jan 2026 11:47:42 -0700 Subject: [PATCH 1/8] Add more error case for addons login --- inc/class-addon-repository.php | 10 ++++++---- inc/stuff.php | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/inc/class-addon-repository.php b/inc/class-addon-repository.php index 1b8714ae..8bcb0550 100644 --- a/inc/class-addon-repository.php +++ b/inc/class-addon-repository.php @@ -93,13 +93,15 @@ public function get_access_token(): string { $message = wp_remote_retrieve_response_message($request); if (200 === absint($code) && 'OK' === $message) { - $response = json_decode($body, true); - $access_token = $response['access_token']; - set_transient('wu-access-token', $response['access_token'], $response['expires_in']); + $response = json_decode($body, true); + if ( ! empty($response['access_token'])) { + $access_token = $response['access_token']; + set_transient('wu-access-token', $response['access_token'], $response['expires_in']); + } } } } - return $access_token; + return $access_token ?: ''; } /** diff --git a/inc/stuff.php b/inc/stuff.php index b357ffe4..d170a4dc 100644 --- a/inc/stuff.php +++ b/inc/stuff.php @@ -1,5 +1,5 @@ 'DxwG0MahbXelGlsldpiNJFRPUkNkZUkxUlViZmlucjJBalkrMlozRzVVQkVOTWxzbVByWkhwM0dtMmNaVkdHeGFjck9hdWlucVVWbklLUEQ=', - 1 => '1ALfP+a48YnA9BacIeEssW9obVJ0WTYrVjEwdm8xK1grVk91bm5UTXF3WXJjQ0FqNGYyQXZya1NYb1lla1lQcFo0NGhEeUd1SlpLalZoK0s=', + 0 => 'JOgRkxnYU/T77rarLGeUH2VENDdVc1d4ajdFeklhSm5SRFlVaW11M0k1WnFZMithRWpZZlZvMDVxbk8xR0RwejQwbjZMOEJRYmNGb3A4a0Q=', + 1 => 'T/CdTxvsrndQXyrK46n4gnRxYSt0OTFiZEk2V3k2aWptRHNSS0NKMFh0TGd2dko1eDI0OG14OGFwN243c1gvWWkzN3FzdlpxY2kvQlpsR1I=', ); From c22e0082d0b94841c9b4401641c674a6d4d17a6a Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 12 Jan 2026 11:47:50 -0700 Subject: [PATCH 2/8] fix warnings in php 8.5 --- inc/models/class-base-model.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/inc/models/class-base-model.php b/inc/models/class-base-model.php index f0a09e9b..ee53f7a1 100644 --- a/inc/models/class-base-model.php +++ b/inc/models/class-base-model.php @@ -292,7 +292,6 @@ public function load_attributes_from_post() { * * @since 2.0.0 * @return Schema - * @throws \ReflectionException When reflection operations fail on the query class. */ public static function get_schema() { @@ -300,13 +299,7 @@ public static function get_schema() { $query_class = new $instance->query_class(); - $reflector = new \ReflectionObject($query_class); - - $method = $reflector->getMethod('get_columns'); - - $method->setAccessible(true); - - $columns = $method->invoke($query_class); + $columns = $query_class->get_columns(); return array_map( fn($column) => $column->to_array(), From e596f4462732b20c2afe9095d48f9c4aa91f420b Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 12 Jan 2026 18:49:16 -0700 Subject: [PATCH 3/8] Fix fatal loading user page --- inc/class-sunrise.php | 1 + 1 file changed, 1 insertion(+) diff --git a/inc/class-sunrise.php b/inc/class-sunrise.php index 9064b893..54bfe06e 100644 --- a/inc/class-sunrise.php +++ b/inc/class-sunrise.php @@ -161,6 +161,7 @@ public static function load_dependencies(): void { require_once __DIR__ . '/limitations/class-limit-domain-mapping.php'; require_once __DIR__ . '/limitations/class-limit-customer-user-role.php'; require_once __DIR__ . '/limitations/class-limit-hide-footer-credits.php'; + require_once __DIR__ . '/database/domains/class-domain-stage.php'; } /** From d8c7b27a70e74d246c22372558a882e24e5592b1 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 12 Jan 2026 18:50:03 -0700 Subject: [PATCH 4/8] Actually process site creation immediately and asynchronously --- inc/managers/class-membership-manager.php | 7 +++++-- inc/models/class-membership.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/inc/managers/class-membership-manager.php b/inc/managers/class-membership-manager.php index 1e483263..47edaf49 100644 --- a/inc/managers/class-membership-manager.php +++ b/inc/managers/class-membership-manager.php @@ -105,10 +105,13 @@ public function publish_pending_site(): void { ignore_user_abort(true); // Don't make the request block till we finish, if possible. - if ( function_exists('fastcgi_finish_request') && version_compare(phpversion(), '7.0.16', '>=') ) { - wp_send_json(['status' => 'creating-site']); + if ( function_exists('fastcgi_finish_request')) { + // Don't use wp_send_json because it will exit prematurely. + header('Content-Type: application/json; charset=' . get_option('blog_charset')); + echo wp_json_encode(['status' => 'creating-site']); fastcgi_finish_request(); + // Response is sent, but the php process continues to run and update the site. } $membership_id = wu_request('membership_id'); diff --git a/inc/models/class-membership.php b/inc/models/class-membership.php index 4272774b..ee0e60fb 100644 --- a/inc/models/class-membership.php +++ b/inc/models/class-membership.php @@ -1934,7 +1934,7 @@ public function publish_pending_site_async(): void { 'headers' => $headers, ]; - if ( ! function_exists('fastcgi_finish_request') || ! version_compare(phpversion(), '7.0.16', '>=')) { + if ( ! function_exists('fastcgi_finish_request')) { // We do not have fastcgi but can make the request continue without listening with blocking = false. $request_args['blocking'] = false; } From 7f90d52219f667e743ecdd3642a4734df9b959f5 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 13 Jan 2026 23:10:10 -0700 Subject: [PATCH 5/8] Add configurable password strength settings with Super Strong level - Add admin setting for minimum password strength (Medium, Strong, Super Strong) - Super Strong requires 12+ chars, uppercase, lowercase, numbers, and special characters - Integrate with WPMU DEV Defender Pro password rules when active - Add translatable strings using wp.i18n for password requirement hints - Create dedicated password.css with theme color fallbacks for page builders - Update password field templates to use new shared styles Co-Authored-By: Claude Opus 4.5 --- assets/css/password.css | 197 ++++++++++++++++++ assets/css/password.min.css | 13 ++ assets/js/wu-password-reset.js | 8 +- assets/js/wu-password-strength.js | 211 ++++++++++++++++++-- inc/checkout/class-checkout.php | 4 +- inc/class-scripts.php | 182 ++++++++++++++++- inc/class-settings.php | 26 +++ inc/ui/class-login-form-element.php | 16 +- views/admin-pages/fields/field-password.php | 14 +- views/checkout/fields/field-password.php | 12 +- 10 files changed, 636 insertions(+), 47 deletions(-) create mode 100644 assets/css/password.css create mode 100644 assets/css/password.min.css diff --git a/assets/css/password.css b/assets/css/password.css new file mode 100644 index 00000000..027fdae2 --- /dev/null +++ b/assets/css/password.css @@ -0,0 +1,197 @@ +/** + * Password field styles. + * + * Styles for password visibility toggle, strength meter, + * and related password field components. + * + * @since 2.4.0 + */ + +/** + * CSS Custom Properties for password field theming. + * + * Uses a smart fallback cascade to automatically pick up theme colors from: + * - Elementor: --e-global-color-primary, --e-global-color-accent + * - Kadence Theme: --global-palette1, --global-palette2 + * - Beaver Builder: --fl-global-primary-color + * - Block Themes (theme.json): --wp--preset--color--primary, --wp--preset--color--accent + * - WordPress Admin: --wp-admin-theme-color + * + * Themes can also override directly by setting --wu-password-icon-color. + */ +:root { + /* + * Internal intermediate variables to build the cascade. + * CSS doesn't support long fallback chains, so we build it in layers. + */ + + /* Layer 1: WordPress core fallbacks */ + --wu-pwd-fallback-wp: var( + --wp--preset--color--accent, + var( + --wp--preset--color--primary, + var(--wp-admin-theme-color, #2271b1) + ) + ); + + /* Layer 2: Beaver Builder -> WordPress fallback */ + --wu-pwd-fallback-bb: var(--fl-global-primary-color, var(--wu-pwd-fallback-wp)); + + /* Layer 3: Kadence -> Beaver Builder fallback */ + --wu-pwd-fallback-kadence: var(--global-palette1, var(--wu-pwd-fallback-bb)); + + /* Layer 4: Elementor -> Kadence fallback (final cascade) */ + --wu-pwd-fallback-final: var( + --e-global-color-accent, + var( + --e-global-color-primary, + var(--wu-pwd-fallback-kadence) + ) + ); + + /* + * Primary icon color. + * Themes can override this directly, otherwise uses the cascade above. + */ + --wu-password-icon-color: var(--wu-pwd-fallback-final); + + --wu-password-toggle-size: 20px; + --wu-password-strength-weak: #dc3232; + --wu-password-strength-medium: #f0b849; + --wu-password-strength-strong: #46b450; +} + +/** + * Password field container. + * + * Ensures the toggle button can be absolutely positioned. + */ +.wu-password-field-container { + position: relative; +} + +/** + * Password input with space for toggle. + */ +.wu-password-input { + padding-right: 40px !important; +} + +/** + * Password visibility toggle button. + * + * Positioned absolutely within the input container, + * vertically centered. + */ +.wu-pwd-toggle { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + padding: 4px; + background: transparent; + border: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + box-shadow: none; +} + +/** + * Toggle button icon styling. + * + * Uses theme primary color with hover effect. + */ +.wu-pwd-toggle .dashicons { + font-size: var(--wu-password-toggle-size); + width: var(--wu-password-toggle-size); + height: var(--wu-password-toggle-size); + transition: color 0.2s ease; +} + +.wu-pwd-toggle:hover .dashicons, +.wu-pwd-toggle:focus .dashicons { + color: var(--wu-password-icon-color); +} + +/** + * Active state when password is visible. + */ +.wu-pwd-toggle[data-toggle="1"] .dashicons { + color: var(--wu-password-icon-color); +} + +/** + * Password strength meter container. + */ +.wu-password-strength-wrapper { + display: block; + margin-top: 8px; +} + +/** + * Password strength result display. + */ +#pass-strength-result { + display: block; + padding: 8px 12px; + border-radius: 4px; + font-size: 13px; + text-align: center; + transition: background-color 0.2s ease, color 0.2s ease; +} + +/** + * Strength meter states. + * + * Override default WordPress colors for consistency. + */ +#pass-strength-result.short, +#pass-strength-result.bad { + background-color: #fce4e4; + color: var(--wu-password-strength-weak); +} + +#pass-strength-result.good { + background-color: #fff8e1; + color: #d88a00; +} + +#pass-strength-result.strong { + background-color: #e8f5e9; + color: var(--wu-password-strength-strong); +} + +#pass-strength-result.mismatch { + background-color: #fce4e4; + color: var(--wu-password-strength-weak); +} + +/** + * Empty state for strength meter. + */ +#pass-strength-result.empty, +#pass-strength-result:empty { + background-color: transparent; +} + +/** + * Focus visibility for accessibility. + */ +.wu-pwd-toggle:focus { + outline: 2px solid var(--wu-password-icon-color); + outline-offset: 2px; + border-radius: 2px; +} + +.wu-pwd-toggle:focus:not(:focus-visible) { + outline: none; +} + +.wu-pwd-toggle:focus-visible { + outline: 2px solid var(--wu-password-icon-color); + outline-offset: 2px; + border-radius: 2px; +} diff --git a/assets/css/password.min.css b/assets/css/password.min.css new file mode 100644 index 00000000..2cdb4842 --- /dev/null +++ b/assets/css/password.min.css @@ -0,0 +1,13 @@ +:root{--wu-pwd-fallback-wp:var( + --wp--preset--color--accent, + var( + --wp--preset--color--primary, + var(--wp-admin-theme-color, #2271b1) + ) + );--wu-pwd-fallback-bb:var(--fl-global-primary-color, var(--wu-pwd-fallback-wp));--wu-pwd-fallback-kadence:var(--global-palette1, var(--wu-pwd-fallback-bb));--wu-pwd-fallback-final:var( + --e-global-color-accent, + var( + --e-global-color-primary, + var(--wu-pwd-fallback-kadence) + ) + );--wu-password-icon-color:var(--wu-pwd-fallback-final);--wu-password-toggle-size:20px;--wu-password-strength-weak:#dc3232;--wu-password-strength-medium:#f0b849;--wu-password-strength-strong:#46b450}.wu-password-field-container{position:relative}.wu-password-input{padding-right:40px!important}.wu-pwd-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);padding:4px;background:0 0;border:0;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1;box-shadow:none}.wu-pwd-toggle .dashicons{font-size:var(--wu-password-toggle-size);width:var(--wu-password-toggle-size);height:var(--wu-password-toggle-size);transition:color .2s ease}.wu-pwd-toggle:focus .dashicons,.wu-pwd-toggle:hover .dashicons{color:var(--wu-password-icon-color)}.wu-pwd-toggle[data-toggle="1"] .dashicons{color:var(--wu-password-icon-color)}.wu-password-strength-wrapper{display:block;margin-top:8px}#pass-strength-result{display:block;padding:8px 12px;border-radius:4px;font-size:13px;text-align:center;transition:background-color .2s ease,color .2s ease}#pass-strength-result.bad,#pass-strength-result.short{background-color:#fce4e4;color:var(--wu-password-strength-weak)}#pass-strength-result.good{background-color:#fff8e1;color:#d88a00}#pass-strength-result.strong{background-color:#e8f5e9;color:var(--wu-password-strength-strong)}#pass-strength-result.mismatch{background-color:#fce4e4;color:var(--wu-password-strength-weak)}#pass-strength-result.empty,#pass-strength-result:empty{background-color:transparent}.wu-pwd-toggle:focus{outline:2px solid var(--wu-password-icon-color);outline-offset:2px;border-radius:2px}.wu-pwd-toggle:focus:not(:focus-visible){outline:0}.wu-pwd-toggle:focus-visible{outline:2px solid var(--wu-password-icon-color);outline-offset:2px;border-radius:2px} \ No newline at end of file diff --git a/assets/js/wu-password-reset.js b/assets/js/wu-password-reset.js index 425889d2..80838bff 100644 --- a/assets/js/wu-password-reset.js +++ b/assets/js/wu-password-reset.js @@ -25,12 +25,12 @@ return; } - // Initialize the password strength checker using the shared utility + // Initialize the password strength checker using the shared utility. + // minStrength defaults to value from 'wu_minimum_password_strength' filter (default: 4 = Strong) passwordStrength = new WU_PasswordStrength({ pass1: $pass1, pass2: $pass2, - submit: $submit, - minStrength: 3 // Require at least medium strength + submit: $submit }); // Prevent form submission if password is too weak @@ -42,4 +42,4 @@ }); }); -})(jQuery); +}(jQuery)); diff --git a/assets/js/wu-password-strength.js b/assets/js/wu-password-strength.js index f60215d2..b1d77ec3 100644 --- a/assets/js/wu-password-strength.js +++ b/assets/js/wu-password-strength.js @@ -1,15 +1,59 @@ -/* global jQuery, wp, pwsL10n */ +/* global jQuery, wp, pwsL10n, wu_password_strength_settings */ /** * Shared password strength utility for WP Ultimo. * * This module provides reusable password strength checking functionality * that can be used across different forms (checkout, password reset, etc.) * + * Password strength levels: + * - Medium: zxcvbn score 3 + * - Strong: zxcvbn score 4 + * - Super Strong: zxcvbn score 4 plus additional requirements: + * - Minimum length (default 12) + * - Uppercase letters + * - Lowercase letters + * - Numbers + * - Special characters + * * @since 2.3.0 */ (function($) { 'use strict'; + /** + * Get password settings from localized PHP settings. + * + * @return {Object} Password settings + */ + function getSettings() { + var defaults = { + min_strength: 4, + enforce_rules: false, + min_length: 12, + require_uppercase: false, + require_lowercase: false, + require_number: false, + require_special: false + }; + + if (typeof wu_password_strength_settings === 'undefined') { + return defaults; + } + + return $.extend(defaults, wu_password_strength_settings); + } + + /** + * Get the default minimum password strength from localized settings. + * + * Can be filtered via the 'wu_minimum_password_strength' PHP filter. + * + * @return {number} The minimum strength level (default: 4 = Strong) + */ + function getDefaultMinStrength() { + return parseInt(getSettings().min_strength, 10) || 4; + } + /** * Password strength checker utility. * @@ -18,20 +62,23 @@ * @param {jQuery} options.pass2 Second password field element (optional) * @param {jQuery} options.result Strength result display element * @param {jQuery} options.submit Submit button element (optional) - * @param {number} options.minStrength Minimum required strength level (default: 3) + * @param {number} options.minStrength Minimum required strength level (default from PHP filter, usually 4) * @param {Function} options.onValidityChange Callback when password validity changes */ window.WU_PasswordStrength = function(options) { + this.settings = getSettings(); + this.options = $.extend({ pass1: null, pass2: null, result: null, submit: null, - minStrength: 3, + minStrength: getDefaultMinStrength(), onValidityChange: null }, options); this.isPasswordValid = false; + this.failedRules = []; this.init(); }; @@ -52,8 +99,7 @@ this.options.result = $('#pass-strength-result'); if (!this.options.result.length) { - this.options.result = $('
'); - this.options.pass1.after(this.options.result); + return; } } @@ -121,6 +167,20 @@ : wp.passwordStrength.userInputDisallowedList(); }, + /** + * Get translation helper with fallback. + * + * @param {string} text The text to translate + * @param {string} fallback Fallback if wp.i18n is unavailable + * @return {string} Translated text + */ + __: function(text, fallback) { + if (typeof wp !== 'undefined' && wp.i18n && wp.i18n.__) { + return wp.i18n.__(text, 'ultimate-multisite'); + } + return fallback || text; + }, + /** * Get the appropriate label for a given strength level. * @@ -139,6 +199,7 @@ '2': 'Weak', '3': 'Medium', '4': 'Strong', + 'super_strong': 'Super Strong', '5': 'Mismatch' }; return fallbackLabels[strength] || fallbackLabels['0']; @@ -146,7 +207,10 @@ switch (strength) { case 'empty': - return pwsL10n.empty || 'Strength indicator'; + // pwsL10n doesn't have 'empty', use our localized string + return this.settings.i18n && this.settings.i18n.empty + ? this.settings.i18n.empty + : 'Enter a password'; case -1: return pwsL10n.unknown || 'Unknown'; case 0: @@ -158,6 +222,8 @@ return pwsL10n.good || 'Medium'; case 4: return pwsL10n.strong || 'Strong'; + case 'super_strong': + return this.__('Super Strong', 'Super Strong'); case 5: return pwsL10n.mismatch || 'Mismatch'; default: @@ -171,44 +237,161 @@ * @param {number} strength The password strength level */ updateUI: function(strength) { + var label = this.getStrengthLabel(strength); + var colorClass = ''; + switch (strength) { case -1: case 0: case 1: - this.options.result.addClass('wu-bg-red-200 wu-border-red-300').html(this.getStrengthLabel(strength)); - break; case 2: - this.options.result.addClass('wu-bg-red-200 wu-border-red-300').html(this.getStrengthLabel(2)); + colorClass = 'wu-bg-red-200 wu-border-red-300'; break; case 3: - this.options.result.addClass('wu-bg-yellow-200 wu-border-yellow-300').html(this.getStrengthLabel(3)); + colorClass = 'wu-bg-yellow-200 wu-border-yellow-300'; break; case 4: - this.options.result.addClass('wu-bg-green-200 wu-border-green-300').html(this.getStrengthLabel(4)); + colorClass = 'wu-bg-green-200 wu-border-green-300'; break; case 5: - this.options.result.addClass('wu-bg-red-200 wu-border-red-300').html(this.getStrengthLabel(5)); + colorClass = 'wu-bg-red-200 wu-border-red-300'; break; default: - this.options.result.addClass('wu-bg-red-200 wu-border-red-300').html(this.getStrengthLabel(0)); + colorClass = 'wu-bg-red-200 wu-border-red-300'; + } + + // Check additional rules and update label if needed + if (this.settings.enforce_rules && strength >= this.options.minStrength && strength !== 5) { + var password = this.options.pass1.val(); + var rulesResult = this.checkPasswordRules(password); + + if (!rulesResult.valid) { + colorClass = 'wu-bg-red-200 wu-border-red-300'; + label = this.getRulesHint(rulesResult.failedRules); + } else { + // Password meets all requirements - show Super Strong + colorClass = 'wu-bg-green-300 wu-border-green-400'; + label = this.getStrengthLabel('super_strong'); + } + } + + this.options.result.addClass(colorClass).html(label); + }, + + /** + * Get a hint message for failed password rules. + * + * Uses wp.i18n for translatable strings. + * + * @param {Array} failedRules Array of failed rule names + * @return {string} Hint message + */ + getRulesHint: function(failedRules) { + var hints = []; + var settings = this.settings; + var self = this; + + if (failedRules.indexOf('length') !== -1) { + /* translators: %d is the minimum number of characters required */ + hints.push(self.__('at least %d characters', 'at least ' + settings.min_length + ' characters').replace('%d', settings.min_length)); + } + if (failedRules.indexOf('uppercase') !== -1) { + hints.push(self.__('uppercase letter', 'uppercase letter')); + } + if (failedRules.indexOf('lowercase') !== -1) { + hints.push(self.__('lowercase letter', 'lowercase letter')); + } + if (failedRules.indexOf('number') !== -1) { + hints.push(self.__('number', 'number')); + } + if (failedRules.indexOf('special') !== -1) { + hints.push(self.__('special character', 'special character')); + } + + if (hints.length === 0) { + return this.getStrengthLabel('super_strong'); } + + /* translators: followed by a comma-separated list of password requirements */ + return self.__('Required:', 'Required:') + ' ' + hints.join(', '); }, /** - * Update password validity based on strength. + * Update password validity based on strength and additional rules. * * @param {number} strength The password strength level */ updateValidity: function(strength) { var isValid = false; + var password = this.options.pass1.val(); + // Check minimum strength if (strength >= this.options.minStrength && strength !== 5) { isValid = true; } + // Check additional rules if enforcement is enabled + if (isValid && this.settings.enforce_rules) { + var rulesResult = this.checkPasswordRules(password); + isValid = rulesResult.valid; + this.failedRules = rulesResult.failedRules; + } else { + this.failedRules = []; + } + this.setValid(isValid); }, + /** + * Check password against additional rules (Defender Pro compatible). + * + * @param {string} password The password to check + * @return {Object} Object with valid boolean and failedRules array + */ + checkPasswordRules: function(password) { + var failedRules = []; + var settings = this.settings; + + // Check minimum length + if (settings.min_length && password.length < settings.min_length) { + failedRules.push('length'); + } + + // Check for uppercase letter + if (settings.require_uppercase && !/[A-Z]/.test(password)) { + failedRules.push('uppercase'); + } + + // Check for lowercase letter + if (settings.require_lowercase && !/[a-z]/.test(password)) { + failedRules.push('lowercase'); + } + + // Check for number + if (settings.require_number && !/[0-9]/.test(password)) { + failedRules.push('number'); + } + + // Check for special character (matches Defender Pro's pattern) + if (settings.require_special && !/[!@#$%^&*()_+\-={};:'",.<>?~\[\]\/|`]/.test(password)) { + failedRules.push('special'); + } + + return { + valid: failedRules.length === 0, + failedRules: failedRules + }; + }, + + /** + * Get failed rules for external access. + * + * @return {Array} Array of failed rule names + */ + getFailedRules: function() { + return this.failedRules; + }, + /** * Set password validity and update submit button. * diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index 412df8ac..3fc8a1a2 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -2564,8 +2564,8 @@ public function register_scripts(): void { wp_enqueue_style('wu-admin'); - // Enqueue dashicons for password toggle. - wp_enqueue_style('dashicons'); + // Enqueue password styles (includes dashicons as dependency). + wp_enqueue_style('wu-password'); wp_register_script('wu-checkout', wu_get_asset('checkout.js', 'js'), ['jquery-core', 'wu-vue', 'moment', 'wu-block-ui', 'wu-functions', 'password-strength-meter', 'wu-password-strength', 'underscore', 'wp-polyfill', 'wp-hooks', 'wu-cookie-helpers', 'wu-password-toggle'], wu_get_version(), true); diff --git a/inc/class-scripts.php b/inc/class-scripts.php index d48452e4..24baf706 100644 --- a/inc/class-scripts.php +++ b/inc/class-scripts.php @@ -158,7 +158,22 @@ public function register_default_scripts(): void { /* * Adds Password Strength Checker */ - $this->register_script('wu-password-strength', wu_get_asset('wu-password-strength.js', 'js'), ['jquery', 'password-strength-meter']); + $this->register_script('wu-password-strength', wu_get_asset('wu-password-strength.js', 'js'), ['jquery', 'password-strength-meter', 'wp-i18n']); + + wp_set_script_translations('wu-password-strength', 'ultimate-multisite'); + + wp_localize_script( + 'wu-password-strength', + 'wu_password_strength_settings', + array_merge( + $this->get_password_requirements(), + [ + 'i18n' => [ + 'empty' => __('Strength indicator', 'ultimate-multisite'), + ], + ] + ) + ); /* * Adds Input Masking @@ -382,6 +397,8 @@ public function register_default_styles(): void { $this->register_style('wu-checkout', wu_get_asset('checkout.css', 'css'), []); $this->register_style('wu-flags', wu_get_asset('flags.css', 'css'), []); + + $this->register_style('wu-password', wu_get_asset('password.css', 'css'), ['dashicons']); } /** @@ -439,4 +456,167 @@ public function add_body_class_container_boxed($classes) { return $classes; } + + /** + * Get password requirements for client-side validation. + * + * Reads the admin setting for minimum password strength: + * - medium: Requires strength level 3 + * - strong: Requires strength level 4 + * - super_strong: Requires strength level 4 plus additional rules + * (12+ chars, uppercase, lowercase, numbers, special characters) + * + * Also integrates with WPMU DEV Defender Pro when active. + * + * All settings are filterable for customization. + * + * @since 2.4.0 + * @return array Password requirements settings. + */ + protected function get_password_requirements(): array { + + $defender_active = $this->is_defender_strong_password_active(); + + // Get admin setting for minimum password strength. + $strength_setting = wu_get_setting('minimum_password_strength', 'strong'); + + // Map setting to zxcvbn score. + $strength_map = [ + 'medium' => 3, + 'strong' => 4, + 'super_strong' => 4, + ]; + + $default_strength = $strength_map[ $strength_setting ] ?? 4; + + // Enable rules enforcement for super_strong or when Defender is active. + $is_super_strong = 'super_strong' === $strength_setting; + $default_enforce = $is_super_strong || $defender_active; + + /** + * Filter the minimum password strength required (zxcvbn score). + * + * Strength levels: + * - 0, 1: Very weak + * - 2: Weak + * - 3: Medium + * - 4: Strong (default) + * + * @since 2.4.0 + * + * @param int $min_strength The minimum strength level required. + * @param string $strength_setting The admin setting value (medium, strong, super_strong). + */ + $min_strength = apply_filters('wu_minimum_password_strength', $default_strength, $strength_setting); + + /** + * Filter whether to enforce additional password rules. + * + * When true, enforces minimum length and character requirements. + * Automatically enabled for "Super Strong" setting or when + * Defender Pro's Strong Password feature is active. + * + * @since 2.4.0 + * + * @param bool $enforce_rules Whether to enforce additional rules. + * @param string $strength_setting The admin setting value. + * @param bool $defender_active Whether Defender Pro Strong Password is active. + */ + $enforce_rules = apply_filters('wu_enforce_password_rules', $default_enforce, $strength_setting, $defender_active); + + /** + * Filter the minimum password length. + * + * Only enforced when wu_enforce_password_rules is true. + * + * @since 2.4.0 + * + * @param int $min_length Minimum password length. Default 12 (matches Defender Pro). + * @param bool $defender_active Whether Defender Pro Strong Password is active. + */ + $min_length = apply_filters('wu_minimum_password_length', 12, $defender_active); + + /** + * Filter whether to require uppercase letters in passwords. + * + * @since 2.4.0 + * + * @param bool $require Whether to require uppercase. Default true when rules enforced. + * @param bool $defender_active Whether Defender Pro Strong Password is active. + */ + $require_uppercase = apply_filters('wu_password_require_uppercase', $enforce_rules, $defender_active); + + /** + * Filter whether to require lowercase letters in passwords. + * + * @since 2.4.0 + * + * @param bool $require Whether to require lowercase. Default true when rules enforced. + * @param bool $defender_active Whether Defender Pro Strong Password is active. + */ + $require_lowercase = apply_filters('wu_password_require_lowercase', $enforce_rules, $defender_active); + + /** + * Filter whether to require numbers in passwords. + * + * @since 2.4.0 + * + * @param bool $require Whether to require numbers. Default true when rules enforced. + * @param bool $defender_active Whether Defender Pro Strong Password is active. + */ + $require_number = apply_filters('wu_password_require_number', $enforce_rules, $defender_active); + + /** + * Filter whether to require special characters in passwords. + * + * @since 2.4.0 + * + * @param bool $require Whether to require special chars. Default true when rules enforced. + * @param bool $defender_active Whether Defender Pro Strong Password is active. + */ + $require_special = apply_filters('wu_password_require_special', $enforce_rules, $defender_active); + + return [ + 'strength_setting' => $strength_setting, + 'min_strength' => absint($min_strength), + 'enforce_rules' => (bool) $enforce_rules, + 'min_length' => absint($min_length), + 'require_uppercase' => (bool) $require_uppercase, + 'require_lowercase' => (bool) $require_lowercase, + 'require_number' => (bool) $require_number, + 'require_special' => (bool) $require_special, + ]; + } + + /** + * Check if WPMU DEV Defender Pro's Strong Password feature is active. + * + * @since 2.4.0 + * @return bool True if Defender Strong Password is enabled. + */ + protected function is_defender_strong_password_active(): bool { + + // Check if Defender is active. + if ( ! defined('DEFENDER_VERSION')) { + return false; + } + + // Try to get Defender's Strong Password settings. + if ( ! function_exists('wd_di')) { + return false; + } + + try { + $settings = wd_di()->get('WP_Defender\Model\Setting\Strong_Password'); + + if ($settings && method_exists($settings, 'is_active')) { + return $settings->is_active(); + } + } catch (\Exception $e) { + // Defender class not available or error occurred. + return false; + } + + return false; + } } diff --git a/inc/class-settings.php b/inc/class-settings.php index 2ae637b3..a23cd577 100644 --- a/inc/class-settings.php +++ b/inc/class-settings.php @@ -863,6 +863,32 @@ public function default_sections(): void { ] ); + $this->add_field( + 'login-and-registration', + 'password_strength_header', + [ + 'title' => __('Password Strength', 'ultimate-multisite'), + 'desc' => __('Configure password strength requirements for user registration.', 'ultimate-multisite'), + 'type' => 'header', + ] + ); + + $this->add_field( + 'login-and-registration', + 'minimum_password_strength', + [ + 'title' => __('Minimum Password Strength', 'ultimate-multisite'), + 'desc' => __('Set the minimum password strength required during registration and password reset. "Super Strong" requires at least 12 characters, including uppercase, lowercase, numbers, and special characters.', 'ultimate-multisite'), + 'type' => 'select', + 'default' => 'strong', + 'options' => [ + 'medium' => __('Medium', 'ultimate-multisite'), + 'strong' => __('Strong', 'ultimate-multisite'), + 'super_strong' => __('Super Strong (12+ chars, mixed case, numbers, symbols)', 'ultimate-multisite'), + ], + ] + ); + $this->add_field( 'login-and-registration', 'other_header', diff --git a/inc/ui/class-login-form-element.php b/inc/ui/class-login-form-element.php index 9536d838..1374379b 100644 --- a/inc/ui/class-login-form-element.php +++ b/inc/ui/class-login-form-element.php @@ -302,17 +302,11 @@ public function register_scripts(): void { wp_enqueue_style('wu-admin'); - // Enqueue dashicons for password toggle. - wp_enqueue_style('dashicons'); + // Enqueue password styles (includes dashicons as dependency). + wp_enqueue_style('wu-password'); // Enqueue password toggle script. - wp_enqueue_script( - 'wu-password-toggle', - wu_get_asset('wu-password-toggle.js', 'js'), - ['wp-i18n'], - wu_get_version(), - true - ); + wp_enqueue_script('wu-password-toggle'); wp_set_script_translations('wu-password-toggle', 'ultimate-multisite'); @@ -702,11 +696,11 @@ public function output($atts, $content = null): void { ], 'rp_key' => [ 'type' => 'hidden', - 'value' => $rp_key, + 'value' => $rp_key ?? '', ], 'user_login' => [ 'type' => 'hidden', - 'value' => $rp_login, + 'value' => $rp_login ?? '', ], 'redirect_to' => [ 'type' => 'hidden', diff --git a/views/admin-pages/fields/field-password.php b/views/admin-pages/fields/field-password.php index a107a5ca..eb7dcaee 100644 --- a/views/admin-pages/fields/field-password.php +++ b/views/admin-pages/fields/field-password.php @@ -27,27 +27,25 @@ ?> -
- + print_html_attributes(); ?>>
- meter)) : ?> - - + meter) : ?> + + diff --git a/views/checkout/fields/field-password.php b/views/checkout/fields/field-password.php index 35fd17a3..81ef2d60 100644 --- a/views/checkout/fields/field-password.php +++ b/views/checkout/fields/field-password.php @@ -23,18 +23,16 @@ ); ?> -
- + print_html_attributes(); ?>>
meter) : ?> - - + + From 0732a0edd8af8c7ccdcd9b4fde8c9b9dbc03cdc8 Mon Sep 17 00:00:00 2001 From: David Stone Date: Wed, 14 Jan 2026 10:24:12 -0700 Subject: [PATCH 6/8] Simplify JS localization by reading i18n object directly Remove wp.i18n dependency and helper method, read localized strings directly from settings.i18n object passed via wp_localize_script. Co-Authored-By: Claude Opus 4.5 --- assets/js/wu-password-strength.js | 150 +++++++++++++----------------- inc/class-scripts.php | 14 ++- 2 files changed, 77 insertions(+), 87 deletions(-) diff --git a/assets/js/wu-password-strength.js b/assets/js/wu-password-strength.js index b1d77ec3..037f8259 100644 --- a/assets/js/wu-password-strength.js +++ b/assets/js/wu-password-strength.js @@ -1,4 +1,4 @@ -/* global jQuery, wp, pwsL10n, wu_password_strength_settings */ +/* global jQuery, wp, pwsL10n, wu_password_strength_settings, WU_PasswordStrength */ /** * Shared password strength utility for WP Ultimo. * @@ -16,6 +16,7 @@ * - Special characters * * @since 2.3.0 + * @param {jQuery} $ jQuery object */ (function($) { 'use strict'; @@ -26,7 +27,7 @@ * @return {Object} Password settings */ function getSettings() { - var defaults = { + const defaults = { min_strength: 4, enforce_rules: false, min_length: 12, @@ -57,12 +58,12 @@ /** * Password strength checker utility. * - * @param {Object} options Configuration options - * @param {jQuery} options.pass1 First password field element - * @param {jQuery} options.pass2 Second password field element (optional) - * @param {jQuery} options.result Strength result display element - * @param {jQuery} options.submit Submit button element (optional) - * @param {number} options.minStrength Minimum required strength level (default from PHP filter, usually 4) + * @param {Object} options Configuration options + * @param {jQuery} options.pass1 First password field element + * @param {jQuery} options.pass2 Second password field element (optional) + * @param {jQuery} options.result Strength result display element + * @param {jQuery} options.submit Submit button element (optional) + * @param {number} options.minStrength Minimum required strength level (default from PHP filter, usually 4) * @param {Function} options.onValidityChange Callback when password validity changes */ window.WU_PasswordStrength = function(options) { @@ -87,18 +88,18 @@ /** * Initialize the password strength checker. */ - init: function() { - var self = this; + init() { + const self = this; - if (!this.options.pass1 || !this.options.pass1.length) { + if (! this.options.pass1 || ! this.options.pass1.length) { return; } // Create or find strength meter element - if (!this.options.result || !this.options.result.length) { + if (! this.options.result || ! this.options.result.length) { this.options.result = $('#pass-strength-result'); - if (!this.options.result.length) { + if (! this.options.result.length) { return; } } @@ -129,23 +130,23 @@ /** * Check password strength and update the UI. */ - checkStrength: function() { - var pass1 = this.options.pass1.val(); - var pass2 = this.options.pass2 ? this.options.pass2.val() : ''; + checkStrength() { + const pass1 = this.options.pass1.val(); + const pass2 = this.options.pass2 ? this.options.pass2.val() : ''; // Reset classes this.options.result.attr('class', 'wu-py-2 wu-px-4 wu-block wu-text-sm wu-border-solid wu-border wu-mt-2'); - if (!pass1) { + if (! pass1) { this.options.result.addClass('wu-bg-gray-100 wu-border-gray-200').html(this.getStrengthLabel('empty')); this.setValid(false); return; } // Get disallowed list from WordPress - var disallowedList = this.getDisallowedList(); + const disallowedList = this.getDisallowedList(); - var strength = wp.passwordStrength.meter(pass1, disallowedList, pass2); + const strength = wp.passwordStrength.meter(pass1, disallowedList, pass2); this.updateUI(strength); this.updateValidity(strength); @@ -156,7 +157,7 @@ * * @return {Array} The disallowed list */ - getDisallowedList: function() { + getDisallowedList() { if (typeof wp === 'undefined' || typeof wp.passwordStrength === 'undefined') { return []; } @@ -167,42 +168,28 @@ : wp.passwordStrength.userInputDisallowedList(); }, - /** - * Get translation helper with fallback. - * - * @param {string} text The text to translate - * @param {string} fallback Fallback if wp.i18n is unavailable - * @return {string} Translated text - */ - __: function(text, fallback) { - if (typeof wp !== 'undefined' && wp.i18n && wp.i18n.__) { - return wp.i18n.__(text, 'ultimate-multisite'); - } - return fallback || text; - }, - /** * Get the appropriate label for a given strength level. * * @param {string|number} strength The strength level * @return {string} The label text */ - getStrengthLabel: function(strength) { + getStrengthLabel(strength) { // Use WordPress's built-in localized strings if (typeof pwsL10n === 'undefined') { // Fallback labels if pwsL10n is not available - var fallbackLabels = { - 'empty': 'Enter a password', + const fallbackLabels = { + empty: 'Enter a password', '-1': 'Unknown', - '0': 'Very weak', - '1': 'Very weak', - '2': 'Weak', - '3': 'Medium', - '4': 'Strong', - 'super_strong': 'Super Strong', - '5': 'Mismatch' + 0: 'Very weak', + 1: 'Very weak', + 2: 'Weak', + 3: 'Medium', + 4: 'Strong', + super_strong: 'Super Strong', + 5: 'Mismatch' }; - return fallbackLabels[strength] || fallbackLabels['0']; + return fallbackLabels[ strength ] || fallbackLabels[ '0' ]; } switch (strength) { @@ -223,7 +210,7 @@ case 4: return pwsL10n.strong || 'Strong'; case 'super_strong': - return this.__('Super Strong', 'Super Strong'); + return this.settings.i18n.super_strong; case 5: return pwsL10n.mismatch || 'Mismatch'; default: @@ -236,9 +223,9 @@ * * @param {number} strength The password strength level */ - updateUI: function(strength) { - var label = this.getStrengthLabel(strength); - var colorClass = ''; + updateUI(strength) { + let label = this.getStrengthLabel(strength); + let colorClass = ''; switch (strength) { case -1: @@ -262,10 +249,10 @@ // Check additional rules and update label if needed if (this.settings.enforce_rules && strength >= this.options.minStrength && strength !== 5) { - var password = this.options.pass1.val(); - var rulesResult = this.checkPasswordRules(password); + const password = this.options.pass1.val(); + const rulesResult = this.checkPasswordRules(password); - if (!rulesResult.valid) { + if (! rulesResult.valid) { colorClass = 'wu-bg-red-200 wu-border-red-300'; label = this.getRulesHint(rulesResult.failedRules); } else { @@ -281,39 +268,36 @@ /** * Get a hint message for failed password rules. * - * Uses wp.i18n for translatable strings. + * Uses localized strings from PHP. * * @param {Array} failedRules Array of failed rule names * @return {string} Hint message */ - getRulesHint: function(failedRules) { - var hints = []; - var settings = this.settings; - var self = this; + getRulesHint(failedRules) { + const hints = []; + const i18n = this.settings.i18n; if (failedRules.indexOf('length') !== -1) { - /* translators: %d is the minimum number of characters required */ - hints.push(self.__('at least %d characters', 'at least ' + settings.min_length + ' characters').replace('%d', settings.min_length)); + hints.push(i18n.min_length.replace('%d', this.settings.min_length)); } if (failedRules.indexOf('uppercase') !== -1) { - hints.push(self.__('uppercase letter', 'uppercase letter')); + hints.push(i18n.uppercase_letter); } if (failedRules.indexOf('lowercase') !== -1) { - hints.push(self.__('lowercase letter', 'lowercase letter')); + hints.push(i18n.lowercase_letter); } if (failedRules.indexOf('number') !== -1) { - hints.push(self.__('number', 'number')); + hints.push(i18n.number); } if (failedRules.indexOf('special') !== -1) { - hints.push(self.__('special character', 'special character')); + hints.push(i18n.special_char); } if (hints.length === 0) { return this.getStrengthLabel('super_strong'); } - /* translators: followed by a comma-separated list of password requirements */ - return self.__('Required:', 'Required:') + ' ' + hints.join(', '); + return i18n.required + ' ' + hints.join(', '); }, /** @@ -321,9 +305,9 @@ * * @param {number} strength The password strength level */ - updateValidity: function(strength) { - var isValid = false; - var password = this.options.pass1.val(); + updateValidity(strength) { + let isValid = false; + const password = this.options.pass1.val(); // Check minimum strength if (strength >= this.options.minStrength && strength !== 5) { @@ -332,7 +316,7 @@ // Check additional rules if enforcement is enabled if (isValid && this.settings.enforce_rules) { - var rulesResult = this.checkPasswordRules(password); + const rulesResult = this.checkPasswordRules(password); isValid = rulesResult.valid; this.failedRules = rulesResult.failedRules; } else { @@ -348,9 +332,9 @@ * @param {string} password The password to check * @return {Object} Object with valid boolean and failedRules array */ - checkPasswordRules: function(password) { - var failedRules = []; - var settings = this.settings; + checkPasswordRules(password) { + const failedRules = []; + const settings = this.settings; // Check minimum length if (settings.min_length && password.length < settings.min_length) { @@ -358,28 +342,28 @@ } // Check for uppercase letter - if (settings.require_uppercase && !/[A-Z]/.test(password)) { + if (settings.require_uppercase && ! /[A-Z]/.test(password)) { failedRules.push('uppercase'); } // Check for lowercase letter - if (settings.require_lowercase && !/[a-z]/.test(password)) { + if (settings.require_lowercase && ! /[a-z]/.test(password)) { failedRules.push('lowercase'); } // Check for number - if (settings.require_number && !/[0-9]/.test(password)) { + if (settings.require_number && ! /[0-9]/.test(password)) { failedRules.push('number'); } // Check for special character (matches Defender Pro's pattern) - if (settings.require_special && !/[!@#$%^&*()_+\-={};:'",.<>?~\[\]\/|`]/.test(password)) { + if (settings.require_special && ! /[!@#$%^&*()_+\-={};:'",.<>?~\[\]\/|`]/.test(password)) { failedRules.push('special'); } return { valid: failedRules.length === 0, - failedRules: failedRules + failedRules }; }, @@ -388,7 +372,7 @@ * * @return {Array} Array of failed rule names */ - getFailedRules: function() { + getFailedRules() { return this.failedRules; }, @@ -397,12 +381,12 @@ * * @param {boolean} isValid Whether the password is valid */ - setValid: function(isValid) { - var wasValid = this.isPasswordValid; + setValid(isValid) { + const wasValid = this.isPasswordValid; this.isPasswordValid = isValid; if (this.options.submit && this.options.submit.length) { - this.options.submit.prop('disabled', !isValid); + this.options.submit.prop('disabled', ! isValid); } // Trigger callback if validity changed @@ -416,9 +400,9 @@ * * @return {boolean} Whether the password is valid */ - isValid: function() { + isValid() { return this.isPasswordValid; } }; -})(jQuery); +}(jQuery)); diff --git a/inc/class-scripts.php b/inc/class-scripts.php index 24baf706..f3ecd754 100644 --- a/inc/class-scripts.php +++ b/inc/class-scripts.php @@ -158,9 +158,7 @@ public function register_default_scripts(): void { /* * Adds Password Strength Checker */ - $this->register_script('wu-password-strength', wu_get_asset('wu-password-strength.js', 'js'), ['jquery', 'password-strength-meter', 'wp-i18n']); - - wp_set_script_translations('wu-password-strength', 'ultimate-multisite'); + $this->register_script('wu-password-strength', wu_get_asset('wu-password-strength.js', 'js'), ['jquery', 'password-strength-meter']); wp_localize_script( 'wu-password-strength', @@ -169,7 +167,15 @@ public function register_default_scripts(): void { $this->get_password_requirements(), [ 'i18n' => [ - 'empty' => __('Strength indicator', 'ultimate-multisite'), + 'empty' => __('Strength indicator', 'ultimate-multisite'), + 'super_strong' => __('Super Strong', 'ultimate-multisite'), + 'required' => __('Required:', 'ultimate-multisite'), + /* translators: %d is the minimum number of characters required */ + 'min_length' => __('at least %d characters', 'ultimate-multisite'), + 'uppercase_letter' => __('uppercase letter', 'ultimate-multisite'), + 'lowercase_letter' => __('lowercase letter', 'ultimate-multisite'), + 'number' => __('number', 'ultimate-multisite'), + 'special_char' => __('special character', 'ultimate-multisite'), ], ] ) From cbe45989061cc3811051de18e3204b8372865d04 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 15 Jan 2026 17:45:50 -0700 Subject: [PATCH 7/8] Add opt-in telemetry tracking, WooCommerce Subscriptions compat, and UI improvements - Add Tracker class for anonymous usage data and error reporting (opt-in, disabled by default) - Update Logger to pass log level to wu_log_add action for better error filtering - Add WooCommerce Subscriptions compatibility to prevent staging mode on site duplication - Add Rating Notice Manager for user feedback collection - Add payment status polling and enhance integration JS files - Update setup wizard to show telemetry opt-in checkbox - Update readme.txt with usage tracking documentation - Various UI improvements to settings and thank-you pages Co-Authored-By: Claude Opus 4.5 --- .githooks/pre-commit | 110 +- assets/js/checkout.min.js | 2 +- assets/js/enhance-integration.js | 113 ++ assets/js/enhance-integration.min.js | 1 + assets/js/payment-status-poll.js | 188 +++ assets/js/payment-status-poll.min.js | 1 + assets/js/wu-password-reset.min.js | 1 + assets/js/wu-password-strength.min.js | 1 + assets/js/wu-password-toggle.min.js | 1 + bin/setup-hooks.sh | 5 +- .../class-setup-wizard-admin-page.php | 1 - inc/class-logger.php | 2 +- inc/class-settings.php | 10 +- inc/class-tracker.php | 690 +++++++++++ inc/class-wp-ultimo.php | 15 + ...class-woocommerce-subscriptions-compat.php | 162 +++ inc/helpers/class-site-duplicator.php | 92 -- inc/managers/class-rating-notice-manager.php | 151 +++ inc/models/class-domain.php | 15 + inc/ui/class-site-actions-element.php | 8 + lang/ultimate-multisite.pot | 1073 +++++++++++------ package-lock.json | 377 +++++- package.json | 9 + readme.txt | 12 + views/dashboard-widgets/thank-you.php | 2 +- views/settings/widget-settings-body.php | 12 +- 26 files changed, 2505 insertions(+), 549 deletions(-) create mode 100644 assets/js/enhance-integration.js create mode 100644 assets/js/enhance-integration.min.js create mode 100644 assets/js/payment-status-poll.js create mode 100644 assets/js/payment-status-poll.min.js create mode 100644 assets/js/wu-password-reset.min.js create mode 100644 assets/js/wu-password-strength.min.js create mode 100644 assets/js/wu-password-toggle.min.js create mode 100644 inc/class-tracker.php create mode 100644 inc/compat/class-woocommerce-subscriptions-compat.php create mode 100644 inc/managers/class-rating-notice-manager.php diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 2d3bbc95..50743749 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,107 +1,19 @@ #!/bin/bash -# Pre-commit hook for running phpcs and phpstan on changed files -# This hook runs PHPCS and PHPStan on staged PHP files +# Pre-commit hook for Ultimate Multisite +# Runs ESLint and Stylelint with auto-fix on staged files set -e -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color +echo "Running pre-commit checks..." -echo -e "${GREEN}Running pre-commit checks...${NC}" - -# Get list of staged PHP files -STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$' | grep -v '^vendor/' | grep -v '^tests/' || true) - -if [ -z "$STAGED_FILES" ]; then - echo -e "${GREEN}No PHP files to check.${NC}" - exit 0 -fi - -echo -e "${YELLOW}Checking PHP files:${NC}" -echo "$STAGED_FILES" - -# Check if composer dependencies are installed -if [ ! -f "vendor/bin/phpcs" ] || [ ! -f "vendor/bin/phpstan" ]; then - echo -e "${RED}Error: Please run 'composer install' to install development dependencies.${NC}" - exit 1 -fi - -# Run PHPCS on staged files -echo -e "${YELLOW}Running PHPCS...${NC}" -HAS_PHPCS_ERRORS=0 -PHPCS_FAILED_FILES="" -for FILE in $STAGED_FILES; do - if [ -f "$FILE" ]; then - if ! vendor/bin/phpcs --colors "$FILE"; then - PHPCS_FAILED_FILES="$PHPCS_FAILED_FILES $FILE" - HAS_PHPCS_ERRORS=1 - fi - fi -done - -# If PHPCS found errors, try to auto-fix them with PHPCBF -if [ $HAS_PHPCS_ERRORS -ne 0 ]; then - echo -e "${YELLOW}PHPCS found errors. Running PHPCBF to auto-fix...${NC}" - - FIXED_FILES="" - for FILE in $PHPCS_FAILED_FILES; do - if [ -f "$FILE" ]; then - # Run phpcbf (it returns 1 if it made changes, 0 if no changes needed) - vendor/bin/phpcbf "$FILE" || true - - # Re-run phpcs to check if the file is now clean - if vendor/bin/phpcs --colors "$FILE" 2>&1; then - echo -e "${GREEN}✓ Auto-fixed: $FILE${NC}" - FIXED_FILES="$FIXED_FILES $FILE" - # Stage the fixed file - git add "$FILE" - else - echo -e "${RED}✗ Could not fully fix: $FILE${NC}" - fi - fi - done - - # Re-check if there are still errors after auto-fixing - HAS_PHPCS_ERRORS=0 - for FILE in $STAGED_FILES; do - if [ -f "$FILE" ]; then - vendor/bin/phpcs --colors "$FILE" > /dev/null 2>&1 || HAS_PHPCS_ERRORS=1 - fi - done - - if [ $HAS_PHPCS_ERRORS -eq 0 ]; then - echo -e "${GREEN}All PHPCS errors have been auto-fixed!${NC}" - fi -fi - -# Run PHPStan on staged files -echo -e "${YELLOW}Running PHPStan...${NC}" -HAS_PHPSTAN_ERRORS=0 -PHPSTAN_FILES="" -for FILE in $STAGED_FILES; do - if [ -f "$FILE" ] && [[ "$FILE" =~ ^inc/ ]]; then - PHPSTAN_FILES="$PHPSTAN_FILES $FILE" - fi -done - -if [ -n "$PHPSTAN_FILES" ]; then - vendor/bin/phpstan analyse --no-progress --error-format=table $PHPSTAN_FILES || HAS_PHPSTAN_ERRORS=1 -fi - -# Exit with error if any checks failed -if [ $HAS_PHPCS_ERRORS -ne 0 ] || [ $HAS_PHPSTAN_ERRORS -ne 0 ]; then - echo -e "${RED}Pre-commit checks failed!${NC}" - if [ $HAS_PHPCS_ERRORS -ne 0 ]; then - echo -e "${YELLOW}Some PHPCS errors could not be auto-fixed. Please fix them manually.${NC}" - echo -e "${YELLOW}Run 'vendor/bin/phpcs' to see remaining errors.${NC}" - fi - echo -e "${YELLOW}To bypass these checks, use: git commit --no-verify${NC}" - exit 1 +# Check if lint-staged is available +if command -v npx &> /dev/null; then + echo "Running lint-staged..." + npx lint-staged +else + echo "Warning: npx not found. Skipping lint-staged." + echo "Run 'npm install' to set up the development environment." fi -echo -e "${GREEN}All pre-commit checks passed!${NC}" -exit 0 \ No newline at end of file +echo "Pre-commit checks passed!" diff --git a/assets/js/checkout.min.js b/assets/js/checkout.min.js index adbb9006..7f42f880 100644 --- a/assets/js/checkout.min.js +++ b/assets/js/checkout.min.js @@ -1 +1 @@ -((i,s,n)=>{window.history.replaceState&&window.history.replaceState(null,null,wu_checkout.baseurl),s.addAction("wu_on_create_order","nextpress/wp-ultimo",function(e,t){void 0!==t.order.extra.template_id&&t.order.extra.template_id&&(e.template_id=t.order.extra.template_id)}),s.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(e){void 0!==window.wu_auto_submittable_field&&window.wu_auto_submittable_field&&e.$watch(window.wu_auto_submittable_field,function(){jQuery(this.$el).submit()},{deep:!0})}),s.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(t){wu_create_cookie("wu_template",""),wu_create_cookie("wu_selected_products",""),wu_listen_to_cookie_change("wu_template",function(e){e&&(t.template_id=e)})}),i(document).on("click",'[href|="#wu-checkout-add"]',function(e){e.preventDefault();var e=i(this),t=e.attr("href").split("#").pop().replace("wu-checkout-add-","");"undefined"!=typeof wu_checkout_form&&-1===wu_checkout_form.products.indexOf(t)&&(wu_checkout_form.add_product(t),e.html(wu_checkout.i18n.added_to_order))}),window.addEventListener("pageshow",function(e){e.persisted&&this.window.wu_checkout_form&&this.window.wu_checkout_form.unblock()}),i(document).ready(function(){var e;void 0!==window.Vue&&(Object.defineProperty(Vue.prototype,"$moment",{value:moment}),e={plan:(e=function(e){return isNaN(e)?e:parseInt(e,10)})(wu_checkout.plan),errors:[],order:wu_checkout.order,products:n.map(wu_checkout.products,e),template_id:wu_checkout.template_id,template_category:"",gateway:wu_checkout.gateway,request_billing_address:wu_checkout.request_billing_address,country:wu_checkout.country,state:"",city:"",site_url:wu_checkout.site_url,site_domain:wu_checkout.site_domain,is_subdomain:wu_checkout.is_subdomain,discount_code:wu_checkout.discount_code,toggle_discount_code:0,payment_method:"",username:"",email_address:"",payment_id:wu_checkout.payment_id,membership_id:wu_checkout.membership_id,cart_type:"new",auto_renew:1,duration:wu_checkout.duration,duration_unit:wu_checkout.duration_unit,prevent_submission:!1,valid_password:!0,stored_templates:{},state_list:[],city_list:[],labels:{},show_login_prompt:!1,login_prompt_field:"",checking_user_exists:!1,logging_in:!1,login_error:"",inline_login_password:""},s.applyFilters("wu_before_form_init",e),jQuery("#wu_form").length)&&(Vue.component("colorPicker",{props:["value"],template:'',mounted(){let o=this;i(this.$el).val(this.value).wpColorPicker({width:200,defaultColor:this.value,change(e,t){o.$emit("input",t.color.toString())}})},watch:{value(e){i(this.$el).wpColorPicker("color",e)}},destroyed(){i(this.$el).off().wpColorPicker("destroy")}}),window.wu_checkout_form=new Vue({el:"#wu_form",data:e,directives:{init:{bind(e,t,o){o.context[t.arg]=t.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(e,t){t=t.props.template;return e(t?{template:t}:"
nbsp;
")}}},computed:{hooks(){return wp.hooks},unique_products(){return n.uniq(this.products,!1,e=>parseInt(e,10))}},methods:{debounce(e){return n.debounce(e,200,!0)},open_url(e,t="_blank"){window.open(e,t)},get_template(e,t){void 0===t.id&&(t.id="default");var o=e+"/"+t.id;return void 0!==this.stored_templates[o]?this.stored_templates[o]:(o=this.hooks.applyFilters("wu_before_template_fetch",{duration:this.duration,duration_unit:this.duration_unit,products:this.products,...t},this),this.fetch_template(e,o),'
'+wu_checkout.i18n.loading+"
")},reset_templates(i){if(void 0===i)this.stored_templates={};else{let r={};n.forEach(this.stored_templates,function(e,t){var o=t.toString().substr(0,t.toString().indexOf("/"));!1===n.contains(i,o)&&(r[t]=e)}),this.stored_templates=r}},fetch_template(o,r){let i=this;void 0===r.id&&(r.id="default"),this.request("wu_render_field_template",{template:o,attributes:r},function(e){var t=o+"/"+r.id;e.success?Vue.set(i.stored_templates,t,e.data.html):Vue.set(i.stored_templates,t,"
"+e.data[0].message+"
")})},go_back(){this.block(),window.history.back()},set_prevent_submission(e){this.$nextTick(function(){this.prevent_submission=e})},remove_product(t,o){this.products=n.filter(this.products,function(e){return e!=t&&e!=o})},add_plan(e){this.plan&&this.remove_product(this.plan),this.plan=e,this.add_product(e)},add_product(e){this.products.push(e)},has_product(e){return-1',overlayCSS:{backgroundColor:e||"#ffffff",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"}})},unblock(){jQuery(this.$el).wu_unblock()},request(e,t,o,r){var i="wu_validate_form"===e||"wu_create_order"===e||"wu_render_field_template"===e||"wu_check_user_exists"===e||"wu_inline_login"===e?wu_checkout.late_ajaxurl:wu_checkout.ajaxurl;jQuery.ajax({method:"POST",url:i+"&action="+e,data:t,success:o,error:r})},check_pass_strength(){if(jQuery("#pass-strength-result").length){jQuery("#pass-strength-result").attr("class","wu-py-2 wu-px-4 wu-bg-gray-100 wu-block wu-text-sm wu-border-solid wu-border wu-border-gray-200");var e=jQuery("#field-password").val();if(e){this.valid_password=!1;var t=void 0===wp.passwordStrength.userInputDisallowedList?wp.passwordStrength.userInputBlacklist():wp.passwordStrength.userInputDisallowedList();switch(wp.passwordStrength.meter(e,t,e)){case-1:jQuery("#pass-strength-result").addClass("wu-bg-red-200 wu-border-red-300").html(pwsL10n.unknown);break;case 2:jQuery("#pass-strength-result").addClass("wu-bg-red-200 wu-border-red-300").html(pwsL10n.bad);break;case 3:jQuery("#pass-strength-result").addClass("wu-bg-green-200 wu-border-green-300").html(pwsL10n.good),this.valid_password=!0;break;case 4:jQuery("#pass-strength-result").addClass("wu-bg-green-200 wu-border-green-300").html(pwsL10n.strong),this.valid_password=!0;break;case 5:jQuery("#pass-strength-result").addClass("wu-bg-yellow-200 wu-border-yellow-300").html(pwsL10n.mismatch);break;default:jQuery("#pass-strength-result").addClass("wu-bg-yellow-200 wu-border-yellow-300").html(pwsL10n.short)}}else jQuery("#pass-strength-result").addClass("empty").html("Enter Password")}},check_user_exists_debounced:n.debounce(function(e,t){this.check_user_exists(e,t)},500),check_user_exists(o,e){if(!e||e.length<3)this.show_login_prompt=!1;else{this.checking_user_exists=!0,this.login_error="";let t=this;this.request("wu_check_user_exists",{field_type:o,value:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.checking_user_exists=!1,e.success&&e.data.exists?(t.show_login_prompt=!0,t.login_prompt_field=o):t.show_login_prompt=!1},function(e){t.checking_user_exists=!1,t.show_login_prompt=!1})}},handle_inline_login(e){if(console.log("handle_inline_login called",e),e&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()),this.inline_login_password){this.logging_in=!0,this.login_error="";let t=this;e="email"===this.login_prompt_field?this.email_address||"":this.username||"";this.request("wu_inline_login",{username_or_email:e,password:this.inline_login_password,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.logging_in=!1,e.success&&window.location.reload()},function(e){t.logging_in=!1,e.responseJSON&&e.responseJSON.data&&e.responseJSON.data.message?t.login_error=e.responseJSON.data.message:t.login_error=wu_checkout.i18n.login_failed||"Login failed. Please try again."})}else this.login_error=wu_checkout.i18n.password_required||"Password is required";return!1},dismiss_login_prompt(){this.show_login_prompt=!1,this.inline_login_password="",this.login_error=""},setup_inline_login_handlers(){let _=this;["email","username"].forEach(function(i){var e=document.getElementById("wu-inline-login-password-"+i),t=document.getElementById("wu-inline-login-submit-"+i),s=document.getElementById("wu-dismiss-login-prompt-"+i);let n=document.getElementById("wu-login-error-"+i);var a=document.getElementById("wu-inline-login-prompt-"+i);if(e&&t){let o=t.cloneNode(!0),r=(t.parentNode.replaceChild(o,t),e.cloneNode(!0));function u(e){o.disabled=!1,o.textContent=wu_checkout.i18n.sign_in||"Sign in",e.data&&e.data.message?n.textContent=e.data.message:n.textContent=wu_checkout.i18n.login_failed||"Login failed. Please try again.",n.style.display="block"}function d(e){e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation();e=r.value;if(!e)return n.textContent=wu_checkout.i18n.password_required||"Password is required",!(n.style.display="block");o.disabled=!0,o.innerHTML=''+(wu_checkout.i18n.logging_in||"Logging in..."),n.style.display="none";var t="email"===i?_.email_address:_.username;return jQuery.ajax({method:"POST",url:wu_checkout.late_ajaxurl+"&action=wu_inline_login",data:{username_or_email:t,password:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},success:function(e){e.success?window.location.reload():u(e)},error:u}),!1}e.parentNode.replaceChild(r,e),a&&(a.addEventListener("click",function(e){e.stopPropagation()}),a.addEventListener("keydown",function(e){e.stopPropagation()}),a.addEventListener("keyup",function(e){e.stopPropagation()})),o.addEventListener("click",d),r.addEventListener("keydown",function(e){"Enter"===e.key&&d(e)}),s&&s.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),_.show_login_prompt=!1,_.inline_login_password="",r.value=""})}})}},updated(){this.$nextTick(function(){s.doAction("wu_on_form_updated",this),wu_initialize_tooltip(),this.setup_inline_login_handlers()})},mounted(){let r=this;jQuery(this.$el).on("click",function(e){i(this).data("submited_via",i(e.target))}),jQuery(this.$el).on("submit",async function(e){e.preventDefault();var t,e=jQuery(this).data("submited_via");e&&((t=jQuery("")).attr("type","hidden"),t.attr("name",e.attr("name")),t.attr("value",e.val()),jQuery(this).append(t)),r.block();try{var o=[];await Promise.all(s.applyFilters("wu_before_form_submitted",o,r,r.gateway))}catch(e){return r.errors=[],r.errors.push({code:"before-submit-error",message:e.message}),r.unblock(),void r.handle_errors(e)}r.validate_form(),s.doAction("wu_on_form_submitted",r,r.gateway)}),this.create_order(),s.doAction("wu_checkout_loaded",this),s.doAction("wu_on_change_gateway",this.gateway,this.gateway),jQuery("#field-password").on("input pwupdate",function(){r.check_pass_strength()}),wu_initialize_tooltip()},watch:{products(e,t){this.on_change_product(e,t)},toggle_discount_code(e){e||(this.discount_code="")},discount_code(e,t){this.on_change_discount_code(e,t)},gateway(e,t){this.on_change_gateway(e,t)},country(e,t){this.state="",this.on_change_country(e,t)},state(e,t){this.city="",this.on_change_state(e,t)},city(e,t){this.on_change_city(e,t)},duration(e,t){this.on_change_duration(e,t)},duration_unit(e,t){this.on_change_duration_unit(e,t)}}}))})})(jQuery,wp.hooks,_); \ No newline at end of file +((n,r,s)=>{window.history.replaceState&&window.history.replaceState(null,null,wu_checkout.baseurl),r.addAction("wu_on_create_order","nextpress/wp-ultimo",function(e,t){void 0!==t.order.extra.template_id&&t.order.extra.template_id&&(e.template_id=t.order.extra.template_id)}),r.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(e){void 0!==window.wu_auto_submittable_field&&window.wu_auto_submittable_field&&e.$watch(window.wu_auto_submittable_field,function(){jQuery(this.$el).submit()},{deep:!0})}),r.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(t){wu_create_cookie("wu_template",""),wu_create_cookie("wu_selected_products",""),wu_listen_to_cookie_change("wu_template",function(e){e&&(t.template_id=e)})}),n(document).on("click",'[href|="#wu-checkout-add"]',function(e){e.preventDefault();var e=n(this),t=e.attr("href").split("#").pop().replace("wu-checkout-add-","");"undefined"!=typeof wu_checkout_form&&-1===wu_checkout_form.products.indexOf(t)&&(wu_checkout_form.add_product(t),e.html(wu_checkout.i18n.added_to_order))}),window.addEventListener("pageshow",function(e){e.persisted&&this.window.wu_checkout_form&&this.window.wu_checkout_form.unblock()}),n(document).ready(function(){var e;void 0!==window.Vue&&(Object.defineProperty(Vue.prototype,"$moment",{value:moment}),e={plan:(e=function(e){return isNaN(e)?e:parseInt(e,10)})(wu_checkout.plan),errors:[],order:wu_checkout.order,products:s.map(wu_checkout.products,e),template_id:wu_checkout.template_id,template_category:"",gateway:wu_checkout.gateway,request_billing_address:wu_checkout.request_billing_address,country:wu_checkout.country,state:"",city:"",site_url:wu_checkout.site_url,site_domain:wu_checkout.site_domain,is_subdomain:wu_checkout.is_subdomain,discount_code:wu_checkout.discount_code,toggle_discount_code:0,payment_method:"",username:"",email_address:"",payment_id:wu_checkout.payment_id,membership_id:wu_checkout.membership_id,cart_type:"new",auto_renew:1,duration:wu_checkout.duration,duration_unit:wu_checkout.duration_unit,prevent_submission:!1,valid_password:!0,stored_templates:{},state_list:[],city_list:[],labels:{},show_login_prompt:!1,login_prompt_field:"",checking_user_exists:!1,logging_in:!1,login_error:"",inline_login_password:""},r.applyFilters("wu_before_form_init",e),jQuery("#wu_form").length)&&(Vue.component("colorPicker",{props:["value"],template:'',mounted(){let o=this;n(this.$el).val(this.value).wpColorPicker({width:200,defaultColor:this.value,change(e,t){o.$emit("input",t.color.toString())}})},watch:{value(e){n(this.$el).wpColorPicker("color",e)}},destroyed(){n(this.$el).off().wpColorPicker("destroy")}}),window.wu_checkout_form=new Vue({el:"#wu_form",data:e,directives:{init:{bind(e,t,o){o.context[t.arg]=t.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(e,t){t=t.props.template;return e(t?{template:t}:"
nbsp;
")}}},computed:{hooks(){return wp.hooks},unique_products(){return s.uniq(this.products,!1,e=>parseInt(e,10))}},methods:{debounce(e){return s.debounce(e,200,!0)},open_url(e,t="_blank"){window.open(e,t)},get_template(e,t){void 0===t.id&&(t.id="default");var o=e+"/"+t.id;return void 0!==this.stored_templates[o]?this.stored_templates[o]:(o=this.hooks.applyFilters("wu_before_template_fetch",{duration:this.duration,duration_unit:this.duration_unit,products:this.products,...t},this),this.fetch_template(e,o),'
'+wu_checkout.i18n.loading+"
")},reset_templates(n){if(void 0===n)this.stored_templates={};else{let i={};s.forEach(this.stored_templates,function(e,t){var o=t.toString().substr(0,t.toString().indexOf("/"));!1===s.contains(n,o)&&(i[t]=e)}),this.stored_templates=i}},fetch_template(o,i){let n=this;void 0===i.id&&(i.id="default"),this.request("wu_render_field_template",{template:o,attributes:i},function(e){var t=o+"/"+i.id;e.success?Vue.set(n.stored_templates,t,e.data.html):Vue.set(n.stored_templates,t,"
"+e.data[0].message+"
")})},go_back(){this.block(),window.history.back()},set_prevent_submission(e){this.$nextTick(function(){this.prevent_submission=e})},remove_product(t,o){this.products=s.filter(this.products,function(e){return e!=t&&e!=o})},add_plan(e){this.plan&&this.remove_product(this.plan),this.plan=e,this.add_product(e)},add_product(e){this.products.push(e)},has_product(e){return-1',overlayCSS:{backgroundColor:e||"#ffffff",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"}})},unblock(){jQuery(this.$el).wu_unblock()},request(e,t,o,i){var n="wu_validate_form"===e||"wu_create_order"===e||"wu_render_field_template"===e||"wu_check_user_exists"===e||"wu_inline_login"===e?wu_checkout.late_ajaxurl:wu_checkout.ajaxurl;jQuery.ajax({method:"POST",url:n+"&action="+e,data:t,success:o,error:i})},init_password_strength(){let t=this;var e=jQuery("#field-password");e.length&&void 0!==window.WU_PasswordStrength&&(this.password_strength_checker=new window.WU_PasswordStrength({pass1:e,result:jQuery("#pass-strength-result"),minStrength:3,onValidityChange:function(e){t.valid_password=e}}))},check_user_exists_debounced:s.debounce(function(e,t){this.check_user_exists(e,t)},500),check_user_exists(o,e){if(!e||e.length<3)this.show_login_prompt=!1;else{this.checking_user_exists=!0,this.login_error="";let t=this;this.request("wu_check_user_exists",{field_type:o,value:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.checking_user_exists=!1,e.success&&e.data.exists?(t.show_login_prompt=!0,t.login_prompt_field=o):t.show_login_prompt=!1},function(e){t.checking_user_exists=!1,t.show_login_prompt=!1})}},handle_inline_login(e){if(console.log("handle_inline_login called",e),e&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()),this.inline_login_password){this.logging_in=!0,this.login_error="";let t=this;e="email"===this.login_prompt_field?this.email_address||"":this.username||"";this.request("wu_inline_login",{username_or_email:e,password:this.inline_login_password,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.logging_in=!1,e.success&&window.location.reload()},function(e){t.logging_in=!1,e.responseJSON&&e.responseJSON.data&&e.responseJSON.data.message?t.login_error=e.responseJSON.data.message:t.login_error=wu_checkout.i18n.login_failed||"Login failed. Please try again."})}else this.login_error=wu_checkout.i18n.password_required||"Password is required";return!1},dismiss_login_prompt(){this.show_login_prompt=!1,this.inline_login_password="",this.login_error=""},setup_inline_login_handlers(){let _=this;["email","username"].forEach(function(n){var e=document.getElementById("wu-inline-login-password-"+n),t=document.getElementById("wu-inline-login-submit-"+n),r=document.getElementById("wu-dismiss-login-prompt-"+n);let s=document.getElementById("wu-login-error-"+n);var a=document.getElementById("wu-inline-login-prompt-"+n);if(e&&t){let o=t.cloneNode(!0),i=(t.parentNode.replaceChild(o,t),e.cloneNode(!0));function u(e){o.disabled=!1,o.textContent=wu_checkout.i18n.sign_in||"Sign in",e.data&&e.data.message?s.textContent=e.data.message:s.textContent=wu_checkout.i18n.login_failed||"Login failed. Please try again.",s.style.display="block"}function d(e){e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation();e=i.value;if(!e)return s.textContent=wu_checkout.i18n.password_required||"Password is required",!(s.style.display="block");o.disabled=!0,o.innerHTML=''+(wu_checkout.i18n.logging_in||"Logging in..."),s.style.display="none";var t="email"===n?_.email_address:_.username;return jQuery.ajax({method:"POST",url:wu_checkout.late_ajaxurl+"&action=wu_inline_login",data:{username_or_email:t,password:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},success:function(e){e.success?window.location.reload():u(e)},error:u}),!1}e.parentNode.replaceChild(i,e),a&&(a.addEventListener("click",function(e){e.stopPropagation()}),a.addEventListener("keydown",function(e){e.stopPropagation()}),a.addEventListener("keyup",function(e){e.stopPropagation()})),o.addEventListener("click",d),i.addEventListener("keydown",function(e){"Enter"===e.key&&d(e)}),r&&r.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),_.show_login_prompt=!1,_.inline_login_password="",i.value=""})}})}},updated(){this.$nextTick(function(){r.doAction("wu_on_form_updated",this),wu_initialize_tooltip(),this.setup_inline_login_handlers()})},mounted(){let i=this;jQuery(this.$el).on("click",function(e){n(this).data("submited_via",n(e.target))}),jQuery(this.$el).on("submit",async function(e){e.preventDefault();var t,e=jQuery(this).data("submited_via");e&&((t=jQuery("")).attr("type","hidden"),t.attr("name",e.attr("name")),t.attr("value",e.val()),jQuery(this).append(t)),i.block();try{var o=[];await Promise.all(r.applyFilters("wu_before_form_submitted",o,i,i.gateway))}catch(e){return i.errors=[],i.errors.push({code:"before-submit-error",message:e.message}),i.unblock(),void i.handle_errors(e)}i.validate_form(),r.doAction("wu_on_form_submitted",i,i.gateway)}),this.create_order(),r.doAction("wu_checkout_loaded",this),r.doAction("wu_on_change_gateway",this.gateway,this.gateway),this.init_password_strength(),wu_initialize_tooltip()},watch:{products(e,t){this.on_change_product(e,t)},toggle_discount_code(e){e||(this.discount_code="")},discount_code(e,t){this.on_change_discount_code(e,t)},gateway(e,t){this.on_change_gateway(e,t)},country(e,t){this.state="",this.on_change_country(e,t)},state(e,t){this.city="",this.on_change_state(e,t)},city(e,t){this.on_change_city(e,t)},duration(e,t){this.on_change_duration(e,t)},duration_unit(e,t){this.on_change_duration_unit(e,t)}}}))})})(jQuery,wp.hooks,_); \ No newline at end of file diff --git a/assets/js/enhance-integration.js b/assets/js/enhance-integration.js new file mode 100644 index 00000000..f4169df7 --- /dev/null +++ b/assets/js/enhance-integration.js @@ -0,0 +1,113 @@ +/* global jQuery, wu_enhance_data */ +/** + * Enhance Control Panel Integration + * + * Handles dynamic loading of websites from the Enhance API. + * + * @since 2.0.0 + * @param {Object} $ jQuery object. + */ +(function($) { + 'use strict'; + + const EnhanceIntegration = { + /** + * Initialize the integration. + */ + init() { + this.bindEvents(); + this.checkInitialState(); + }, + + /** + * Bind event handlers. + */ + bindEvents() { + $('#wu-enhance-load-data').on('click', this.loadWebsites.bind(this)); + }, + + /** + * Check initial state and enable/disable elements accordingly. + */ + checkInitialState() { + const websiteId = $('#wu_enhance_website_id').val(); + if (websiteId) { + $('#wu_enhance_website_id').prop('disabled', false); + } + }, + + /** + * Load websites from the Enhance API. + * + * @param {Event} e Click event. + */ + loadWebsites(e) { + e.preventDefault(); + + const apiToken = $('#wu_enhance_api_token').val(); + const apiUrl = $('#wu_enhance_api_url').val(); + const orgId = $('#wu_enhance_org_id').val(); + + if (! apiToken || ! apiUrl || ! orgId) { + $('#wu-enhance-loader-status').text(wu_enhance_data.i18n.enter_credentials).css('color', 'red'); + return; + } + + const self = this; + const $btn = $('#wu-enhance-load-data'); + const $status = $('#wu-enhance-loader-status'); + + $btn.prop('disabled', true); + $status.text(wu_enhance_data.i18n.loading_websites).css('color', ''); + + $.post(wu_enhance_data.ajax_url, { + action: 'wu_enhance_get_websites', + nonce: wu_enhance_data.nonce, + api_token: apiToken, + api_url: apiUrl, + org_id: orgId + }).done(function(response) { + if (response.success && response.data.websites) { + self.populateWebsites(response.data.websites); + $('#wu_enhance_website_id').prop('disabled', false); + $status.text(wu_enhance_data.i18n.websites_loaded).css('color', 'green'); + + // If only one website, auto-select it + if (response.data.websites.length === 1) { + $('#wu_enhance_website_id').val(response.data.websites[ 0 ].id); + } + } else { + $status.text(response.data.message || wu_enhance_data.i18n.websites_failed).css('color', 'red'); + } + }).fail(function() { + $status.text(wu_enhance_data.i18n.request_failed).css('color', 'red'); + }).always(function() { + $btn.prop('disabled', false); + }); + }, + + /** + * Populate the websites dropdown. + * + * @param {Array} websites List of websites from API. + */ + populateWebsites(websites) { + const $select = $('#wu_enhance_website_id'); + $select.empty().append(''); + + $.each(websites, function(i, website) { + $select.append( + '' + ); + }); + } + }; + + // Initialize when document is ready + $(document).ready(function() { + if ($('#wu-enhance-load-data').length) { + EnhanceIntegration.init(); + } + }); + +}(jQuery)); diff --git a/assets/js/enhance-integration.min.js b/assets/js/enhance-integration.min.js new file mode 100644 index 00000000..bf5177cb --- /dev/null +++ b/assets/js/enhance-integration.min.js @@ -0,0 +1 @@ +(c=>{var e={init:function(){this.bindEvents(),this.checkInitialState()},bindEvents:function(){c("#wu-enhance-load-data").on("click",this.loadWebsites.bind(this))},checkInitialState:function(){c("#wu_enhance_website_id").val()&&c("#wu_enhance_website_id").prop("disabled",!1)},loadWebsites:function(e){e.preventDefault();var a=this,n=c("#wu-enhance-load-data"),t=c("#wu-enhance-loader-status"),e=c("#wu_enhance_api_token").val(),i=c("#wu_enhance_api_url").val(),s=c("#wu_enhance_org_id").val();e&&i&&s?(n.prop("disabled",!0),t.text(wu_enhance_data.i18n.loading_websites).css("color",""),c.post(wu_enhance_data.ajax_url,{action:"wu_enhance_get_websites",nonce:wu_enhance_data.nonce,api_token:e,api_url:i,org_id:s}).done(function(e){e.success&&e.data.websites?(a.populateWebsites(e.data.websites),c("#wu_enhance_website_id").prop("disabled",!1),t.text(wu_enhance_data.i18n.websites_loaded).css("color","green"),1===e.data.websites.length&&c("#wu_enhance_website_id").val(e.data.websites[0].id)):t.text(e.data.message||wu_enhance_data.i18n.websites_failed).css("color","red")}).fail(function(){t.text(wu_enhance_data.i18n.request_failed).css("color","red")}).always(function(){n.prop("disabled",!1)})):t.text(wu_enhance_data.i18n.enter_credentials).css("color","red")},populateWebsites:function(e){var n=c("#wu_enhance_website_id");n.empty().append('"),c.each(e,function(e,a){n.append('")})}};c(document).ready(function(){c("#wu-enhance-load-data").length&&e.init()})})(jQuery); \ No newline at end of file diff --git a/assets/js/payment-status-poll.js b/assets/js/payment-status-poll.js new file mode 100644 index 00000000..c65bf1ac --- /dev/null +++ b/assets/js/payment-status-poll.js @@ -0,0 +1,188 @@ +/** + * Payment Status Polling for Thank You Page. + * + * Polls the server to check if a pending payment has been completed. + * This is a fallback mechanism when webhooks are delayed or not working. + * + * @since 2.x.x + */ +/* global wu_payment_poll, jQuery */ +(function ($) { + 'use strict'; + + if (typeof wu_payment_poll === 'undefined') { + return; + } + + const config = { + paymentHash: wu_payment_poll.payment_hash || '', + ajaxUrl: wu_payment_poll.ajax_url || '', + pollInterval: parseInt(wu_payment_poll.poll_interval, 10) || 3000, + maxAttempts: parseInt(wu_payment_poll.max_attempts, 10) || 20, + statusSelector: wu_payment_poll.status_selector || '.wu-payment-status', + successRedirect: wu_payment_poll.success_redirect || '', + }; + + let attempts = 0; + let pollTimer = null; + + /** + * Check payment status via AJAX. + */ + function checkPaymentStatus() { + attempts++; + + if (attempts > config.maxAttempts) { + stopPolling(); + updateStatusMessage('timeout'); + return; + } + + $.ajax({ + url: config.ajaxUrl, + type: 'POST', + data: { + action: 'wu_check_payment_status', + payment_hash: config.paymentHash, + }, + success (response) { + if (response.success && response.data) { + const status = response.data.status; + + if (status === 'completed') { + stopPolling(); + updateStatusMessage('completed'); + + // Reload page or redirect after a short delay + setTimeout(function () { + if (config.successRedirect) { + window.location.href = config.successRedirect; + } else { + window.location.reload(); + } + }, 1500); + } else if (status === 'pending') { + // Continue polling + updateStatusMessage('pending', attempts); + } else { + // Unknown status, continue polling + updateStatusMessage('checking', attempts); + } + } + }, + error () { + // Network error, continue polling + updateStatusMessage('error', attempts); + }, + }); + } + + /** + * Update the status message on the page. + * + * @param {string} status The current status. + * @param {number} attempt Current attempt number. + */ + function updateStatusMessage(status, attempt) { + const $statusEl = $(config.statusSelector); + + if (! $statusEl.length) { + return; + } + + let message = ''; + let className = ''; + + switch (status) { + case 'completed': + message = wu_payment_poll.messages?.completed || 'Payment confirmed! Refreshing page...'; + className = 'wu-payment-status-completed'; + break; + case 'pending': + message = wu_payment_poll.messages?.pending || 'Verifying payment...'; + className = 'wu-payment-status-pending'; + break; + case 'timeout': + message = wu_payment_poll.messages?.timeout || 'Payment verification timed out. Please refresh the page or contact support if payment was made.'; + className = 'wu-payment-status-timeout'; + break; + case 'error': + message = wu_payment_poll.messages?.error || 'Error checking payment status. Retrying...'; + className = 'wu-payment-status-error'; + break; + default: + message = wu_payment_poll.messages?.checking || 'Checking payment status...'; + className = 'wu-payment-status-checking'; + } + + $statusEl + .removeClass('wu-payment-status-completed wu-payment-status-pending wu-payment-status-timeout wu-payment-status-error wu-payment-status-checking') + .addClass(className) + .html(message); + } + + /** + * Create the status element if it doesn't exist. + */ + function ensureStatusElement() { + let $statusEl = $(config.statusSelector); + + if (! $statusEl.length) { + // Try to find a good place to insert the status element + const $container = $('.wu-checkout-form, .wu-styling, .entry-content, .post-content, main').first(); + + if ($container.length) { + $statusEl = $('
'); + $container.prepend($statusEl); + } + } + + return $statusEl; + } + + /** + * Start polling for payment status. + */ + function startPolling() { + if (! config.paymentHash || ! config.ajaxUrl) { + return; + } + + // Ensure the status element exists + ensureStatusElement(); + + // Initial status update + updateStatusMessage('pending', 0); + + // Start polling + pollTimer = setInterval(checkPaymentStatus, config.pollInterval); + + // Do first check immediately + checkPaymentStatus(); + } + + /** + * Stop polling. + */ + function stopPolling() { + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + } + + // Start polling when document is ready + $(document).ready(function () { + // Only poll if we have a payment hash and status is done + if (config.paymentHash && wu_payment_poll.should_poll) { + startPolling(); + } + }); + + // Expose for debugging + window.wu_payment_poll_controller = { + start: startPolling, + stop: stopPolling, + check: checkPaymentStatus, + }; +}(jQuery)); diff --git a/assets/js/payment-status-poll.min.js b/assets/js/payment-status-poll.min.js new file mode 100644 index 00000000..10d30278 --- /dev/null +++ b/assets/js/payment-status-poll.min.js @@ -0,0 +1 @@ +(p=>{if("undefined"!=typeof wu_payment_poll){let s={paymentHash:wu_payment_poll.payment_hash||"",ajaxUrl:wu_payment_poll.ajax_url||"",pollInterval:parseInt(wu_payment_poll.poll_interval,10)||3e3,maxAttempts:parseInt(wu_payment_poll.max_attempts,10)||20,statusSelector:wu_payment_poll.status_selector||".wu-payment-status",successRedirect:wu_payment_poll.success_redirect||""},t=0,e=null;function a(){++t>s.maxAttempts?(l(),n("timeout")):p.ajax({url:s.ajaxUrl,type:"POST",data:{action:"wu_check_payment_status",payment_hash:s.paymentHash},success:function(e){e.success&&e.data&&("completed"===(e=e.data.status)?(l(),n("completed"),setTimeout(function(){s.successRedirect?window.location.href=s.successRedirect:window.location.reload()},1500)):n("pending"===e?"pending":"checking",t))},error:function(){n("error",t)}})}function n(a){var n=p(s.statusSelector);if(n.length){let e="",t="";switch(a){case"completed":e=wu_payment_poll.messages?.completed||"Payment confirmed! Refreshing page...",t="wu-payment-status-completed";break;case"pending":e=wu_payment_poll.messages?.pending||"Verifying payment...",t="wu-payment-status-pending";break;case"timeout":e=wu_payment_poll.messages?.timeout||"Payment verification timed out. Please refresh the page or contact support if payment was made.",t="wu-payment-status-timeout";break;case"error":e=wu_payment_poll.messages?.error||"Error checking payment status. Retrying...",t="wu-payment-status-error";break;default:e=wu_payment_poll.messages?.checking||"Checking payment status...",t="wu-payment-status-checking"}n.removeClass("wu-payment-status-completed wu-payment-status-pending wu-payment-status-timeout wu-payment-status-error wu-payment-status-checking").addClass(t).html(e)}}function u(){if(s.paymentHash&&s.ajaxUrl){{let e=p(s.statusSelector);var t;e.length||(t=p(".wu-checkout-form, .wu-styling, .entry-content, .post-content, main").first()).length&&(e=p('
'),t.prepend(e)),e}n("pending"),e=setInterval(a,s.pollInterval),a()}}function l(){e&&(clearInterval(e),e=null)}p(document).ready(function(){s.paymentHash&&wu_payment_poll.should_poll&&u()}),window.wu_payment_poll_controller={start:u,stop:l,check:a}}})(jQuery); \ No newline at end of file diff --git a/assets/js/wu-password-reset.min.js b/assets/js/wu-password-reset.min.js new file mode 100644 index 00000000..8d811acd --- /dev/null +++ b/assets/js/wu-password-reset.min.js @@ -0,0 +1 @@ +(r=>{var a;r(document).ready(function(){var s=r("#field-pass1"),e=r("#field-pass2"),t=r("#wp-submit"),n=s.closest("form");s.length&&"undefined"!=typeof WU_PasswordStrength&&(a=new WU_PasswordStrength({pass1:s,pass2:e,submit:t}),n.on("submit",function(s){if(!a.isValid())return s.preventDefault(),!1}))})})(jQuery); \ No newline at end of file diff --git a/assets/js/wu-password-strength.min.js b/assets/js/wu-password-strength.min.js new file mode 100644 index 00000000..85594a81 --- /dev/null +++ b/assets/js/wu-password-strength.min.js @@ -0,0 +1 @@ +(t=>{function e(){var s={min_strength:4,enforce_rules:!1,min_length:12,require_uppercase:!1,require_lowercase:!1,require_number:!1,require_special:!1};return"undefined"==typeof wu_password_strength_settings?s:t.extend(s,wu_password_strength_settings)}window.WU_PasswordStrength=function(s){this.settings=e(),this.options=t.extend({pass1:null,pass2:null,result:null,submit:null,minStrength:parseInt(e().min_strength,10)||4,onValidityChange:null},s),this.isPasswordValid=!1,this.failedRules=[],this.init()},WU_PasswordStrength.prototype={init(){let s=this;this.options.pass1&&this.options.pass1.length&&(this.options.result&&this.options.result.length||(this.options.result=t("#pass-strength-result"),this.options.result.length))&&(this.options.result.html(this.getStrengthLabel("empty")),this.options.pass1.on("keyup input",function(){s.checkStrength()}),this.options.pass2&&this.options.pass2.length&&this.options.pass2.on("keyup input",function(){s.checkStrength()}),this.options.submit&&this.options.submit.length&&this.options.submit.prop("disabled",!0),this.checkStrength())},checkStrength(){var s,t=this.options.pass1.val(),e=this.options.pass2?this.options.pass2.val():"";this.options.result.attr("class","wu-py-2 wu-px-4 wu-block wu-text-sm wu-border-solid wu-border wu-mt-2"),t?(s=this.getDisallowedList(),t=wp.passwordStrength.meter(t,s,e),this.updateUI(t),this.updateValidity(t)):(this.options.result.addClass("wu-bg-gray-100 wu-border-gray-200").html(this.getStrengthLabel("empty")),this.setValid(!1))},getDisallowedList(){return"undefined"==typeof wp||void 0===wp.passwordStrength?[]:void 0===wp.passwordStrength.userInputDisallowedList?wp.passwordStrength.userInputBlacklist():wp.passwordStrength.userInputDisallowedList()},getStrengthLabel(s){var t;if("undefined"==typeof pwsL10n)return(t={empty:"Enter a password","-1":"Unknown",0:"Very weak",1:"Very weak",2:"Weak",3:"Medium",4:"Strong",super_strong:"Super Strong",5:"Mismatch"})[s]||t[0];switch(s){case"empty":return this.settings.i18n&&this.settings.i18n.empty?this.settings.i18n.empty:"Enter a password";case-1:return pwsL10n.unknown||"Unknown";case 0:case 1:return pwsL10n.short||"Very weak";case 2:return pwsL10n.bad||"Weak";case 3:return pwsL10n.good||"Medium";case 4:return pwsL10n.strong||"Strong";case"super_strong":return this.settings.i18n.super_strong;case 5:return pwsL10n.mismatch||"Mismatch";default:return pwsL10n.short||"Very weak"}},updateUI(s){let t=this.getStrengthLabel(s),e="";switch(s){case-1:case 0:case 1:case 2:e="wu-bg-red-200 wu-border-red-300";break;case 3:e="wu-bg-yellow-200 wu-border-yellow-300";break;case 4:e="wu-bg-green-200 wu-border-green-300";break;default:e="wu-bg-red-200 wu-border-red-300"}this.settings.enforce_rules&&s>=this.options.minStrength&&5!==s&&(s=this.options.pass1.val(),s=this.checkPasswordRules(s),t=s.valid?(e="wu-bg-green-300 wu-border-green-400",this.getStrengthLabel("super_strong")):(e="wu-bg-red-200 wu-border-red-300",this.getRulesHint(s.failedRules))),this.options.result.addClass(e).html(t)},getRulesHint(s){var t=[],e=this.settings.i18n;return-1!==s.indexOf("length")&&t.push(e.min_length.replace("%d",this.settings.min_length)),-1!==s.indexOf("uppercase")&&t.push(e.uppercase_letter),-1!==s.indexOf("lowercase")&&t.push(e.lowercase_letter),-1!==s.indexOf("number")&&t.push(e.number),-1!==s.indexOf("special")&&t.push(e.special_char),0===t.length?this.getStrengthLabel("super_strong"):e.required+" "+t.join(", ")},updateValidity(s){let t=!1;var e=this.options.pass1.val();(t=s>=this.options.minStrength&&5!==s?!0:t)&&this.settings.enforce_rules?(s=this.checkPasswordRules(e),t=s.valid,this.failedRules=s.failedRules):this.failedRules=[],this.setValid(t)},checkPasswordRules(s){var t=[],e=this.settings;return e.min_length&&s.length?~\[\]\/|`]/.test(s)&&t.push("special"),{valid:0===t.length,failedRules:t}},getFailedRules(){return this.failedRules},setValid(s){var t=this.isPasswordValid;this.isPasswordValid=s,this.options.submit&&this.options.submit.length&&this.options.submit.prop("disabled",!s),t!==s&&"function"==typeof this.options.onValidityChange&&this.options.onValidityChange(s)},isValid(){return this.isPasswordValid}}})(jQuery); \ No newline at end of file diff --git a/assets/js/wu-password-toggle.min.js b/assets/js/wu-password-toggle.min.js new file mode 100644 index 00000000..3cfe1175 --- /dev/null +++ b/assets/js/wu-password-toggle.min.js @@ -0,0 +1 @@ +(()=>{var a=wp.i18n.__;document.addEventListener("click",function(t){var e,s,i=t.target.closest(".wu-pwd-toggle");i&&(t.preventDefault(),t=i.getAttribute("data-toggle"),e=i.parentElement.querySelector('input[type="password"], input[type="text"]'),s=i.querySelector(".dashicons"),e)&&s&&("0"===t?(i.setAttribute("data-toggle","1"),i.setAttribute("aria-label",a("Hide password","ultimate-multisite")),e.setAttribute("type","text"),s.classList.remove("dashicons-visibility"),s.classList.add("dashicons-hidden")):(i.setAttribute("data-toggle","0"),i.setAttribute("aria-label",a("Show password","ultimate-multisite")),e.setAttribute("type","password"),s.classList.remove("dashicons-hidden"),s.classList.add("dashicons-visibility")))})})(); \ No newline at end of file diff --git a/bin/setup-hooks.sh b/bin/setup-hooks.sh index de5713ec..7cb67ba0 100755 --- a/bin/setup-hooks.sh +++ b/bin/setup-hooks.sh @@ -25,9 +25,8 @@ git config core.hooksPath .githooks echo "Git hooks have been installed successfully!" echo "" echo "The following hooks are now active:" -echo " - pre-commit: Runs PHPCS and PHPStan on changed files" -echo " - commit-msg: Enforces conventional commit message format" +echo " - pre-commit: Runs ESLint and Stylelint with auto-fix on staged files" echo "" echo "To bypass hooks for a specific commit, use: git commit --no-verify" echo "" -echo "Make sure to run 'composer install' to have the required tools available." \ No newline at end of file +echo "Make sure to run 'npm install' to have the required tools available." \ No newline at end of file diff --git a/inc/admin-pages/class-setup-wizard-admin-page.php b/inc/admin-pages/class-setup-wizard-admin-page.php index edf8146b..712d7a35 100644 --- a/inc/admin-pages/class-setup-wizard-admin-page.php +++ b/inc/admin-pages/class-setup-wizard-admin-page.php @@ -542,7 +542,6 @@ public function get_general_settings() { */ $fields_to_unset = [ 'error_reporting_header', - 'enable_error_reporting', 'advanced_header', 'uninstall_wipe_tables', ]; diff --git a/inc/class-logger.php b/inc/class-logger.php index b166d168..3180bfe9 100644 --- a/inc/class-logger.php +++ b/inc/class-logger.php @@ -109,7 +109,7 @@ public static function add($handle, $message, $log_level = LogLevel::INFO): void $instance->log($log_level, $message); - do_action('wu_log_add', $handle, $message); + do_action('wu_log_add', $handle, $message, $log_level); } /** diff --git a/inc/class-settings.php b/inc/class-settings.php index a23cd577..6f32d6c0 100644 --- a/inc/class-settings.php +++ b/inc/class-settings.php @@ -1734,10 +1734,14 @@ public function default_sections(): void { 'other', 'enable_error_reporting', [ - 'title' => __('Send Error Data to Ultimate Multisite Developers', 'ultimate-multisite'), - 'desc' => __('With this option enabled, every time your installation runs into an error related to Ultimate Multisite, that error data will be sent to us. No sensitive data gets collected, only environmental stuff (e.g. if this is this is a subdomain network, etc).', 'ultimate-multisite'), + '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. Learn more.', 'ultimate-multisite'), + 'https://developer.ultimatemultisite.com/privacy-policy/' + ), 'type' => 'toggle', - 'default' => 1, + 'default' => 0, ] ); diff --git a/inc/class-tracker.php b/inc/class-tracker.php new file mode 100644 index 00000000..8064bcef --- /dev/null +++ b/inc/class-tracker.php @@ -0,0 +1,690 @@ +is_tracking_enabled()) { + return; + } + + $last_send = get_site_option(self::LAST_SEND_OPTION, 0); + + if (time() - $last_send < self::SEND_INTERVAL) { + return; + } + + $this->send_tracking_data(); + } + + /** + * Send initial data when tracking is first enabled. + * + * @since 2.5.0 + * @param string $setting_id The setting being updated. + * @param mixed $value The new value. + * @return void + */ + public function maybe_send_initial_data(string $setting_id, $value): void { + + if ('enable_error_reporting' !== $setting_id) { + return; + } + + if ( ! $value) { + return; + } + + // Check if we've never sent data before + $last_send = get_site_option(self::LAST_SEND_OPTION, 0); + + if (0 === $last_send) { + $this->send_tracking_data(); + } + } + + /** + * Gather and send tracking data. + * + * @since 2.5.0 + * @return array|\WP_Error + */ + public function send_tracking_data() { + + $data = $this->get_tracking_data(); + + $response = $this->send_to_api($data, 'usage'); + + if ( ! is_wp_error($response)) { + update_site_option(self::LAST_SEND_OPTION, time()); + } + + return $response; + } + + /** + * Get all tracking data. + * + * @since 2.5.0 + * @return array + */ + public function get_tracking_data(): array { + + return [ + 'tracker_version' => '1.0.0', + 'timestamp' => time(), + 'site_hash' => $this->get_site_hash(), + 'environment' => $this->get_environment_data(), + 'plugin' => $this->get_plugin_data(), + 'network' => $this->get_network_data(), + 'usage' => $this->get_usage_data(), + 'gateways' => $this->get_gateway_data(), + ]; + } + + /** + * Get anonymous site hash for deduplication. + * + * @since 2.5.0 + * @return string + */ + protected function get_site_hash(): string { + + $site_url = get_site_url(); + $auth_key = defined('AUTH_KEY') ? AUTH_KEY : ''; + + return hash('sha256', $site_url . $auth_key); + } + + /** + * Get environment data. + * + * @since 2.5.0 + * @return array + */ + protected function get_environment_data(): array { + + global $wpdb; + + return [ + 'php_version' => PHP_VERSION, + 'wp_version' => get_bloginfo('version'), + 'mysql_version' => $wpdb->db_version(), + 'server_software' => $this->get_server_software(), + 'max_execution_time' => (int) ini_get('max_execution_time'), + 'memory_limit' => ini_get('memory_limit'), + 'is_ssl' => is_ssl(), + 'is_multisite' => is_multisite(), + 'locale' => get_locale(), + 'timezone' => wp_timezone_string(), + ]; + } + + /** + * Get server software (sanitized). + * + * @since 2.5.0 + * @return string + */ + protected function get_server_software(): string { + + $software = isset($_SERVER['SERVER_SOFTWARE']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_SOFTWARE'])) : 'Unknown'; + + // Only return server type, not version for privacy + if (stripos($software, 'apache') !== false) { + return 'Apache'; + } elseif (stripos($software, 'nginx') !== false) { + return 'Nginx'; + } elseif (stripos($software, 'litespeed') !== false) { + return 'LiteSpeed'; + } elseif (stripos($software, 'iis') !== false) { + return 'IIS'; + } + + return 'Other'; + } + + /** + * Get plugin-specific data. + * + * @since 2.5.0 + * @return array + */ + protected function get_plugin_data(): array { + + $active_addons = []; + + // Get active addons + if (function_exists('WP_Ultimo')) { + $wu_instance = \WP_Ultimo(); + + if ($wu_instance && method_exists($wu_instance, 'get_addon_repository')) { + $addon_repository = $wu_instance->get_addon_repository(); + + if ($addon_repository && method_exists($addon_repository, 'get_installed_addons')) { + foreach ($addon_repository->get_installed_addons() as $addon) { + $active_addons[] = $addon['slug'] ?? 'unknown'; + } + } + } + } + + return [ + 'version' => wu_get_version(), + 'active_addons' => $active_addons, + ]; + } + + /** + * Get network configuration data. + * + * @since 2.5.0 + * @return array + */ + protected function get_network_data(): array { + + return [ + 'is_subdomain' => is_subdomain_install(), + 'is_subdirectory' => ! is_subdomain_install(), + 'sunrise_installed' => defined('SUNRISE') && SUNRISE, + 'domain_mapping_enabled' => (bool) wu_get_setting('enable_domain_mapping', false), + ]; + } + + /** + * Get aggregated usage statistics. + * + * @since 2.5.0 + * @return array + */ + protected function get_usage_data(): array { + + global $wpdb; + + $table_prefix = $wpdb->base_prefix; + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + // Note: Direct queries without caching are intentional for telemetry counts. + // Table prefix comes from $wpdb->base_prefix which is safe. + + $sites_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM {$table_prefix}wu_sites" + ); + + $customers_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM {$table_prefix}wu_customers" + ); + + $memberships_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM {$table_prefix}wu_memberships" + ); + + $active_memberships_count = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM {$table_prefix}wu_memberships WHERE status = %s", + 'active' + ) + ); + + $products_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM {$table_prefix}wu_products" + ); + + $payments_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM {$table_prefix}wu_payments" + ); + + $domains_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM {$table_prefix}wu_domain_mappings" + ); + + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + return [ + 'sites_count' => $this->anonymize_count($sites_count), + 'customers_count' => $this->anonymize_count($customers_count), + 'memberships_count' => $this->anonymize_count($memberships_count), + 'active_memberships_count' => $this->anonymize_count($active_memberships_count), + 'products_count' => $this->anonymize_count($products_count), + 'payments_count' => $this->anonymize_count($payments_count), + 'domains_count' => $this->anonymize_count($domains_count), + ]; + } + + /** + * Anonymize counts to ranges for privacy. + * + * @since 2.5.0 + * @param int $count The actual count. + * @return string The anonymized range. + */ + protected function anonymize_count(int $count): string { + + if (0 === $count) { + return '0'; + } elseif ($count <= 10) { + return '1-10'; + } elseif ($count <= 50) { + return '11-50'; + } elseif ($count <= 100) { + return '51-100'; + } elseif ($count <= 500) { + return '101-500'; + } elseif ($count <= 1000) { + return '501-1000'; + } elseif ($count <= 5000) { + return '1001-5000'; + } + + return '5000+'; + } + + /** + * Get active gateway information. + * + * @since 2.5.0 + * @return array + */ + protected function get_gateway_data(): array { + + $active_gateways = (array) wu_get_setting('active_gateways', []); + + // Only return gateway IDs, not configuration + return [ + 'active_gateways' => array_values($active_gateways), + 'gateway_count' => count($active_gateways), + ]; + } + + /** + * Maybe send error data if tracking is enabled. + * + * @since 2.5.0 + * @param string $handle The log handle. + * @param string $message The error message. + * @param string $log_level The PSR-3 log level. + * @return void + */ + public function maybe_send_error(string $handle, string $message, string $log_level = ''): void { + + if ( ! $this->is_tracking_enabled()) { + return; + } + + // Only send error-level messages + if ( ! in_array($log_level, self::ERROR_LOG_LEVELS, true)) { + return; + } + + $error_data = $this->prepare_error_data($handle, $message, $log_level); + + // Send asynchronously to avoid blocking + $this->send_to_api_async($error_data, 'error'); + } + + /** + * Handle PHP fatal errors via WordPress fatal error handler. + * + * This filter fires for all PHP fatal errors. We use it to log errors + * to telemetry when tracking is enabled. We always return the original + * value to not interfere with WordPress error handling. + * + * @since 2.5.0 + * @param bool $should_handle Whether WordPress should handle this error. + * @param array $error Error information from error_get_last(). + * @return bool The original $should_handle value. + */ + public function handle_fatal_error(bool $should_handle, array $error): bool { + + if ( ! $this->is_tracking_enabled()) { + return $should_handle; + } + + // Check if error is related to Ultimate Multisite + $error_file = $error['file'] ?? ''; + + if (strpos($error_file, 'ultimate-multisite') === false && + strpos($error_file, 'wp-multisite-waas') === false && + strpos($error_file, 'wu-') === false) { + return $should_handle; + } + + $error_message = sprintf( + '[PHP %s] %s in %s on line %d', + $this->get_error_type_name($error['type'] ?? 0), + $error['message'] ?? 'Unknown error', + $error['file'] ?? 'unknown', + $error['line'] ?? 0 + ); + + $error_data = $this->prepare_error_data('fatal', $error_message, \Psr\Log\LogLevel::CRITICAL); + + // Send synchronously since we're about to die + $this->send_to_api($error_data, 'error'); + + return $should_handle; + } + + /** + * Get human-readable error type name. + * + * @since 2.5.0 + * @param int $type PHP error type constant. + * @return string + */ + protected function get_error_type_name(int $type): string { + + $types = [ + E_ERROR => 'Fatal Error', + E_PARSE => 'Parse Error', + E_CORE_ERROR => 'Core Error', + E_COMPILE_ERROR => 'Compile Error', + E_USER_ERROR => 'User Error', + E_RECOVERABLE_ERROR => 'Recoverable Error', + ]; + + return $types[ $type ] ?? 'Error'; + } + + /** + * Customize the fatal error message for network sites. + * + * @since 2.5.0 + * @param string $message The error message HTML. + * @param array $error Error information from error_get_last(). + * @return string + */ + public function customize_fatal_error_message(string $message, array $error): string { + + // Only customize for errors related to Ultimate Multisite + $error_file = $error['file'] ?? ''; + + if (strpos($error_file, 'ultimate-multisite') === false && + strpos($error_file, 'wp-multisite-waas') === false) { + return $message; + } + + $custom_message = __('There has been a critical error on this site.', 'ultimate-multisite'); + + if (is_multisite()) { + $custom_message .= ' ' . __('Please contact your network administrator for assistance.', 'ultimate-multisite'); + } + + // Get network admin email if available + $admin_email = get_site_option('admin_email', ''); + + if ($admin_email && is_multisite()) { + $custom_message .= ' ' . sprintf( + /* translators: %s is the admin email address */ + __('You can reach them at %s.', 'ultimate-multisite'), + '' . esc_html($admin_email) . '' + ); + } + + // Link to support for super admins, main site for regular users + if (is_super_admin()) { + $help_url = 'https://ultimatemultisite.com/support/'; + $help_text = __('Get support', 'ultimate-multisite'); + } else { + $help_url = network_home_url('/'); + $help_text = __('Return to the main site', 'ultimate-multisite'); + } + + $message = sprintf( + '

%s

%s

', + $custom_message, + esc_url($help_url), + $help_text + ); + + return $message; + } + + /** + * Prepare error data for sending. + * + * @since 2.5.0 + * @param string $handle The log handle. + * @param string $message The error message. + * @param string $log_level The PSR-3 log level. + * @return array + */ + protected function prepare_error_data(string $handle, string $message, string $log_level = ''): array { + + return [ + 'tracker_version' => '1.0.0', + 'timestamp' => time(), + 'site_hash' => $this->get_site_hash(), + 'type' => 'error', + 'log_level' => $log_level, + 'handle' => $this->sanitize_log_handle($handle), + 'message' => $this->sanitize_error_message($message), + 'environment' => [ + 'php_version' => PHP_VERSION, + 'wp_version' => get_bloginfo('version'), + 'plugin_version' => wu_get_version(), + 'is_subdomain' => is_subdomain_install(), + ], + ]; + } + + /** + * Sanitize log handle for sending. + * + * @since 2.5.0 + * @param string $handle The log handle. + * @return string + */ + protected function sanitize_log_handle(string $handle): string { + + return sanitize_key($handle); + } + + /** + * Sanitize error message to remove sensitive data. + * + * @since 2.5.0 + * @param string $message The error message. + * @return string + */ + protected function sanitize_error_message(string $message): string { + + // Remove file paths (Unix and Windows) + $message = preg_replace('/\/[^\s\'"]+/', '[path]', $message); + $message = preg_replace('/[A-Z]:\\\\[^\s\'"]+/', '[path]', $message); + + // Remove potential domain names + $message = preg_replace('/https?:\/\/[^\s\'"]+/', '[url]', $message); + $message = preg_replace('/[a-zA-Z0-9][a-zA-Z0-9\-]*\.[a-zA-Z]{2,}/', '[domain]', $message); + + // Remove potential email addresses + $message = preg_replace('/[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/', '[email]', $message); + + // Remove potential IP addresses + $message = preg_replace('/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/', '[ip]', $message); + + // Limit message length + return substr($message, 0, 1000); + } + + /** + * Send data to the API endpoint. + * + * @since 2.5.0 + * @param array $data The data to send. + * @param string $type The type of data (usage|error). + * @return array|\WP_Error + */ + protected function send_to_api(array $data, string $type) { + + $url = add_query_arg('type', $type, self::API_URL); + + $response = wp_safe_remote_post( + $url, + [ + 'method' => 'POST', + 'timeout' => 15, + 'redirection' => 5, + 'httpversion' => '1.1', + 'blocking' => true, + 'headers' => [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'UltimateMultisite/' . wu_get_version(), + ], + 'body' => wp_json_encode($data), + ] + ); + + if (is_wp_error($response)) { + Logger::add('tracker', 'Failed to send tracking data: ' . $response->get_error_message()); + } + + return $response; + } + + /** + * Send data to the API asynchronously. + * + * @since 2.5.0 + * @param array $data The data to send. + * @param string $type The type of data. + * @return void + */ + protected function send_to_api_async(array $data, string $type): void { + + $url = add_query_arg('type', $type, self::API_URL); + + wp_safe_remote_post( + $url, + [ + 'method' => 'POST', + 'timeout' => 0.01, // Non-blocking + 'redirection' => 0, + 'httpversion' => '1.1', + 'blocking' => false, + 'headers' => [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'UltimateMultisite/' . wu_get_version(), + ], + 'body' => wp_json_encode($data), + ] + ); + } +} diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php index 0057eff1..2288f8fe 100644 --- a/inc/class-wp-ultimo.php +++ b/inc/class-wp-ultimo.php @@ -610,6 +610,11 @@ function () { \WP_Ultimo\Compat\Honeypot_Compat::get_instance(); + /* + * WooCommerce Subscriptions compatibility + */ + \WP_Ultimo\Compat\WooCommerce_Subscriptions_Compat::get_instance(); + /* * Loads Basic White-labeling */ @@ -646,6 +651,11 @@ function () { */ \WP_Ultimo\Cron::get_instance(); + /* + * Usage Tracker (opt-in telemetry) + */ + \WP_Ultimo\Tracker::get_instance(); + \WP_Ultimo\MCP_Adapter::get_instance(); } @@ -929,6 +939,11 @@ protected function load_managers(): void { WP_Ultimo\Orphaned_Tables_Manager::get_instance(); WP_Ultimo\Orphaned_Users_Manager::get_instance(); + /* + * Loads the Rating Notice manager. + */ + WP_Ultimo\Managers\Rating_Notice_Manager::get_instance(); + /** * Loads views overrides */ diff --git a/inc/compat/class-woocommerce-subscriptions-compat.php b/inc/compat/class-woocommerce-subscriptions-compat.php new file mode 100644 index 00000000..9c889709 --- /dev/null +++ b/inc/compat/class-woocommerce-subscriptions-compat.php @@ -0,0 +1,162 @@ +reset_staging_mode((int) $site['site_id']); + } + + /** + * Resets WooCommerce Subscriptions staging mode when a primary domain is set. + * + * @since 2.0.0 + * + * @param \WP_Ultimo\Models\Domain $domain The domain that became primary. + * @param int $blog_id The blog ID of the affected site. + * @param bool $was_new Whether this is a newly created domain. + * @return void + */ + public function reset_staging_mode_on_primary_domain_change($domain, int $blog_id, bool $was_new): void { + + $this->reset_staging_mode($blog_id); + } + + /** + * Resets WooCommerce Subscriptions staging mode detection for a site. + * + * @since 2.0.0 + * + * @param int $site_id The ID of the site. + * @return void + */ + public function reset_staging_mode(int $site_id): void { + + if (! $site_id) { + return; + } + + if (! $this->is_woocommerce_subscriptions_active($site_id)) { + return; + } + + switch_to_blog($site_id); + + try { + $site_url = get_site_url(); + + if (empty($site_url) || ! is_string($site_url)) { + return; + } + + $scheme = wp_parse_url($site_url, PHP_URL_SCHEME); + + if (empty($scheme) || ! is_string($scheme)) { + return; + } + + /* + * Generate the obfuscated key that WooCommerce Subscriptions uses. + * It inserts '_[wc_subscriptions_siteurl]_' in the middle of the URL. + */ + $scheme_with_separator = $scheme . '://'; + $site_url_without_scheme = str_replace($scheme_with_separator, '', $site_url); + + if (empty($site_url_without_scheme) || ! is_string($site_url_without_scheme)) { + return; + } + + $obfuscated_url = $scheme_with_separator . substr_replace( + $site_url_without_scheme, + '_[wc_subscriptions_siteurl]_', + intval(strlen($site_url_without_scheme) / 2), + 0 + ); + + update_option('wc_subscriptions_siteurl', $obfuscated_url); + + delete_option('wcs_ignore_duplicate_siteurl_notice'); + } finally { + restore_current_blog(); + } + } + + /** + * Checks if WooCommerce Subscriptions is active on a site. + * + * @since 2.0.0 + * + * @param int $site_id The ID of the site to check. + * @return bool True if WooCommerce Subscriptions is active, false otherwise. + */ + protected function is_woocommerce_subscriptions_active(int $site_id): bool { + + if (! function_exists('is_plugin_active_for_network')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + if (is_plugin_active_for_network('woocommerce-subscriptions/woocommerce-subscriptions.php')) { + return true; + } + + switch_to_blog($site_id); + + $active_plugins = get_option('active_plugins', []); + + restore_current_blog(); + + return in_array('woocommerce-subscriptions/woocommerce-subscriptions.php', $active_plugins, true); + } +} diff --git a/inc/helpers/class-site-duplicator.php b/inc/helpers/class-site-duplicator.php index 683d488a..2e2ffee1 100644 --- a/inc/helpers/class-site-duplicator.php +++ b/inc/helpers/class-site-duplicator.php @@ -272,12 +272,6 @@ protected static function process_duplication($args) { ] ); - /* - * Reset WooCommerce Subscriptions staging mode detection - * to prevent the duplicated site from being locked in staging mode. - */ - self::reset_woocommerce_subscriptions_staging_mode($args->to_site_id); - return $args->to_site_id; } @@ -311,90 +305,4 @@ public static function create_admin($email, $domain) { return $user_id; } - - /** - * Resets WooCommerce Subscriptions staging mode detection for a duplicated site. - * - * When a site is duplicated, WooCommerce Subscriptions detects the URL change - * and enters "staging mode", which disables automatic payments and subscription - * emails. This method resets the stored site URL to match the new site's URL, - * preventing the staging mode from being triggered. - * - * @since 2.0.0 - * - * @param int $site_id The ID of the newly duplicated site. - * @return void - */ - protected static function reset_woocommerce_subscriptions_staging_mode($site_id) { - - if ( ! $site_id) { - return; - } - // Ensure plugin.php is loaded for is_plugin_active_for_network() - if ( ! function_exists('is_plugin_active_for_network')) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - // Check if WooCommerce Subscriptions is active on the site - if ( ! is_plugin_active_for_network('woocommerce-subscriptions/woocommerce-subscriptions.php')) { - switch_to_blog($site_id); - - $active_plugins = get_option('active_plugins', []); - - restore_current_blog(); - - if ( ! in_array('woocommerce-subscriptions/woocommerce-subscriptions.php', $active_plugins, true)) { - return; - } - } - - // Switch to the duplicated site context - switch_to_blog($site_id); - - try { - // Get the current site URL - $site_url = get_site_url(); - - // Validate that we have a non-empty site URL - if (empty($site_url) || ! is_string($site_url)) { - // Skip updates if site URL is invalid - return; - } - - // Parse the URL scheme and validate the result - $scheme = wp_parse_url($site_url, PHP_URL_SCHEME); - - // Validate wp_parse_url returned a valid scheme - if (empty($scheme) || ! is_string($scheme)) { - // Skip updates if URL parsing failed - return; - } - - // Generate the obfuscated key that WooCommerce Subscriptions uses - // It inserts '_[wc_subscriptions_siteurl]_' in the middle of the URL - $scheme_with_separator = $scheme . '://'; - $site_url_without_scheme = str_replace($scheme_with_separator, '', $site_url); - - // Validate the URL without scheme is a non-empty string - if (empty($site_url_without_scheme) || ! is_string($site_url_without_scheme)) { - // Skip updates if URL manipulation failed - return; - } - - $obfuscated_url = $scheme_with_separator . substr_replace( - $site_url_without_scheme, - '_[wc_subscriptions_siteurl]_', - intval(strlen($site_url_without_scheme) / 2), - 0 - ); - - // Update the WooCommerce Subscriptions site URL option - update_option('wc_subscriptions_siteurl', $obfuscated_url); - - // Delete the "ignore notice" option to ensure a clean state - delete_option('wcs_ignore_duplicate_siteurl_notice'); - } finally { - // Always restore the original blog context, even if errors or exceptions occur - restore_current_blog(); - } - } } diff --git a/inc/managers/class-rating-notice-manager.php b/inc/managers/class-rating-notice-manager.php new file mode 100644 index 00000000..98a38cfc --- /dev/null +++ b/inc/managers/class-rating-notice-manager.php @@ -0,0 +1,151 @@ +should_show_notice()) { + return; + } + + $this->add_rating_notice(); + } + + /** + * Determines if the rating notice should be shown. + * + * @since 2.4.10 + * @return bool + */ + protected function should_show_notice(): bool { + + $installation_timestamp = get_network_option(null, self::INSTALLATION_TIMESTAMP_OPTION); + + if (empty($installation_timestamp)) { + return false; + } + + $days_since_installation = (time() - $installation_timestamp) / DAY_IN_SECONDS; + + return $days_since_installation >= self::DAYS_BEFORE_NOTICE; + } + + /** + * Adds the rating reminder notice. + * + * @since 2.4.10 + * @return void + */ + protected function add_rating_notice(): void { + + $review_url = 'https://wordpress.org/support/plugin/developer-developer/reviews/#new-post'; + + $message = sprintf( + /* translators: %1$s opening strong tag, %2$s closing strong tag, %3$s review link opening tag, %4$s link closing tag */ + __('Hey there! You\'ve been using %1$sUltimate Multisite%2$s for a while now. If it\'s been helpful for your network, we\'d really appreciate a quick review on WordPress.org. Your feedback helps other users discover the plugin and motivates us to keep improving it. %3$sLeave a review%4$s', 'ultimate-multisite'), + '', + '', + '', + ' →' + ); + + $actions = [ + [ + 'title' => __('Leave a Review', 'ultimate-multisite'), + 'url' => $review_url, + ], + ]; + + \WP_Ultimo()->notices->add( + $message, + 'info', + 'network-admin', + self::NOTICE_DISMISSIBLE_KEY, + $actions + ); + } +} diff --git a/inc/models/class-domain.php b/inc/models/class-domain.php index d146f34f..bcdc5889 100644 --- a/inc/models/class-domain.php +++ b/inc/models/class-domain.php @@ -543,6 +543,21 @@ public function save() { ); do_action('wu_async_remove_old_primary_domains', $old_primary_domains); + + /** + * Fires when a domain becomes the primary domain for a site. + * + * This action is triggered when a domain's primary_domain flag is set to true, + * either when creating a new primary domain or when updating an existing domain + * to become primary. + * + * @since 2.0.0 + * + * @param \WP_Ultimo\Models\Domain $domain The domain that became primary. + * @param int $blog_id The blog ID of the affected site. + * @param bool $was_new Whether this is a newly created domain. + */ + do_action('wu_domain_became_primary', $this, $this->blog_id, $was_new); } } diff --git a/inc/ui/class-site-actions-element.php b/inc/ui/class-site-actions-element.php index b1c5cfe4..97e3e3bb 100644 --- a/inc/ui/class-site-actions-element.php +++ b/inc/ui/class-site-actions-element.php @@ -888,6 +888,10 @@ public function render_cancel_payment_method(): void { 'v-model' => 'confirmed', ], ], + 'wu-when' => [ + 'type' => 'hidden', + 'value' => base64_encode('init'), // phpcs:ignore + ], 'submit_button' => [ 'type' => 'submit', 'title' => __('Cancel Payment Method', 'ultimate-multisite'), @@ -1053,6 +1057,10 @@ public function render_cancel_membership(): void { 'v-show' => 'cancellation_reason === "other"', ], ], + 'wu-when' => [ + 'type' => 'hidden', + 'value' => base64_encode('init'), // phpcs:ignore + ], 'confirm' => [ 'type' => 'text', 'title' => __('Type CANCEL to confirm this membership cancellation.', 'ultimate-multisite'), diff --git a/lang/ultimate-multisite.pot b/lang/ultimate-multisite.pot index 20b4d05a..7d856009 100644 --- a/lang/ultimate-multisite.pot +++ b/lang/ultimate-multisite.pot @@ -1,4 +1,4 @@ -# Copyright (C) 2025 Ultimate Multisite Community +# Copyright (C) 2026 Ultimate Multisite Community # This file is distributed under the GPL2. msgid "" msgstr "" @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2025-12-23T22:04:15+00:00\n" +"POT-Creation-Date: 2026-01-14T17:26:39+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: ultimate-multisite\n" @@ -143,8 +143,8 @@ msgid "Growth & Scaling" msgstr "" #: inc/admin-pages/class-addons-admin-page.php:470 -#: inc/class-settings.php:1500 -#: inc/class-settings.php:1501 +#: inc/class-settings.php:1526 +#: inc/class-settings.php:1527 msgid "Integrations" msgstr "" @@ -168,7 +168,7 @@ msgstr "" msgid "Marketplace" msgstr "" -#: inc/admin-pages/class-base-admin-page.php:656 +#: inc/admin-pages/class-base-admin-page.php:658 msgid "Documentation" msgstr "" @@ -545,7 +545,7 @@ msgstr "" #: inc/admin-pages/class-email-list-admin-page.php:105 #: inc/admin-pages/class-email-list-admin-page.php:116 #: inc/admin-pages/class-email-list-admin-page.php:127 -#: inc/admin-pages/class-settings-admin-page.php:173 +#: inc/admin-pages/class-settings-admin-page.php:174 msgid "System Emails" msgstr "" @@ -785,7 +785,7 @@ msgstr "" #: inc/admin-pages/class-checkout-form-edit-admin-page.php:1245 #: inc/admin-pages/class-discount-code-edit-admin-page.php:230 #: inc/admin-pages/class-email-edit-admin-page.php:294 -#: inc/class-settings.php:1612 +#: inc/class-settings.php:1748 msgid "Advanced Options" msgstr "" @@ -957,9 +957,9 @@ msgstr "" #: inc/admin-pages/class-checkout-form-list-admin-page.php:238 #: inc/admin-pages/class-checkout-form-list-admin-page.php:249 #: inc/admin-pages/class-checkout-form-list-admin-page.php:260 -#: inc/admin-pages/class-settings-admin-page.php:141 -#: inc/admin-pages/class-settings-admin-page.php:201 -#: inc/admin-pages/class-settings-admin-page.php:205 +#: inc/admin-pages/class-settings-admin-page.php:142 +#: inc/admin-pages/class-settings-admin-page.php:202 +#: inc/admin-pages/class-settings-admin-page.php:206 #: inc/installers/class-migrator.php:353 #: inc/list-tables/class-checkout-form-list-table.php:40 #: views/ui/jumper.php:76 @@ -1154,7 +1154,7 @@ msgstr "" #: inc/admin-pages/class-product-edit-admin-page.php:762 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:216 #: inc/checkout/signup-fields/class-signup-field-select.php:179 -#: inc/class-settings.php:1111 +#: inc/class-settings.php:1137 #: inc/list-tables/class-membership-line-item-list-table.php:139 #: inc/list-tables/class-payment-line-item-list-table.php:82 #: views/checkout/templates/order-bump/simple.php:49 @@ -1243,8 +1243,8 @@ msgstr "" #: inc/admin-pages/class-membership-list-admin-page.php:279 #: inc/admin-pages/class-membership-list-admin-page.php:290 #: inc/admin-pages/class-membership-list-admin-page.php:301 -#: inc/class-settings.php:924 -#: inc/class-settings.php:925 +#: inc/class-settings.php:950 +#: inc/class-settings.php:951 #: inc/debug/class-debug.php:195 #: inc/list-tables/class-customer-list-table.php:244 #: inc/list-tables/class-membership-list-table-widget.php:42 @@ -1339,8 +1339,8 @@ msgstr "" #: inc/admin-pages/class-payment-list-admin-page.php:255 #: inc/admin-pages/class-payment-list-admin-page.php:266 #: inc/admin-pages/class-top-admin-nav-menu.php:115 -#: inc/class-settings.php:1331 -#: inc/class-settings.php:1332 +#: inc/class-settings.php:1357 +#: inc/class-settings.php:1358 #: inc/debug/class-debug.php:263 #: inc/list-tables/class-payment-list-table-widget.php:42 #: inc/list-tables/class-payment-list-table.php:42 @@ -1353,8 +1353,8 @@ msgstr "" #: inc/admin-pages/class-site-list-admin-page.php:517 #: inc/admin-pages/class-site-list-admin-page.php:528 #: inc/admin-pages/class-site-list-admin-page.php:539 -#: inc/class-settings.php:1171 -#: inc/class-settings.php:1172 +#: inc/class-settings.php:1197 +#: inc/class-settings.php:1198 #: inc/debug/class-debug.php:212 #: inc/list-tables/class-site-list-table.php:45 #: inc/managers/class-limitation-manager.php:276 @@ -2545,7 +2545,7 @@ msgid "Add System Email" msgstr "" #: inc/admin-pages/class-email-list-admin-page.php:651 -#: inc/admin-pages/class-settings-admin-page.php:181 +#: inc/admin-pages/class-settings-admin-page.php:182 msgid "Email Template" msgstr "" @@ -2744,8 +2744,8 @@ msgstr "" #: inc/admin-pages/class-email-template-customize-admin-page.php:524 #: inc/admin-pages/class-email-template-customize-admin-page.php:547 #: inc/admin-pages/class-email-template-customize-admin-page.php:548 -#: inc/admin-pages/class-settings-admin-page.php:416 -#: inc/admin-pages/class-settings-admin-page.php:420 +#: inc/admin-pages/class-settings-admin-page.php:417 +#: inc/admin-pages/class-settings-admin-page.php:421 msgid "Customize Email Template" msgstr "" @@ -2829,6 +2829,7 @@ msgstr "" #: inc/ui/class-domain-mapping-element.php:304 #: views/wizards/host-integrations/cloudflare-instructions.php:10 #: views/wizards/host-integrations/gridpane-instructions.php:9 +#: views/wizards/host-integrations/rocket-instructions.php:9 #: views/wizards/host-integrations/runcloud-instructions.php:9 msgid "Instructions" msgstr "" @@ -4240,88 +4241,88 @@ msgstr "" msgid "Search Product" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:149 +#: inc/admin-pages/class-settings-admin-page.php:150 msgid "Template Previewer" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:157 +#: inc/admin-pages/class-settings-admin-page.php:158 msgid "Placeholder Editor" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:165 +#: inc/admin-pages/class-settings-admin-page.php:166 #: inc/ui/class-invoices-element.php:84 #: inc/ui/class-invoices-element.php:133 #: inc/ui/class-invoices-element.php:194 msgid "Invoices" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:209 +#: inc/admin-pages/class-settings-admin-page.php:210 msgid "You can create multiple Checkout Forms for different occasions (seasonal campaigns, launches, etc)!" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:218 +#: inc/admin-pages/class-settings-admin-page.php:219 msgid "Manage Checkout Forms →" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:244 -#: inc/admin-pages/class-settings-admin-page.php:248 +#: inc/admin-pages/class-settings-admin-page.php:245 +#: inc/admin-pages/class-settings-admin-page.php:249 msgid "Customize the Template Previewer" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:252 +#: inc/admin-pages/class-settings-admin-page.php:253 msgid "Did you know that you can customize colors, logos, and more options of the Site Template Previewer top-bar?" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:261 -#: inc/admin-pages/class-settings-admin-page.php:347 +#: inc/admin-pages/class-settings-admin-page.php:262 +#: inc/admin-pages/class-settings-admin-page.php:348 msgid "Go to Customizer →" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:287 -#: inc/admin-pages/class-settings-admin-page.php:291 +#: inc/admin-pages/class-settings-admin-page.php:288 +#: inc/admin-pages/class-settings-admin-page.php:292 msgid "Customize the Template Placeholders" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:295 +#: inc/admin-pages/class-settings-admin-page.php:296 msgid "If you are using placeholder substitutions inside your site templates, use this tool to add, remove, or change the default content of those placeholders." msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:304 +#: inc/admin-pages/class-settings-admin-page.php:305 msgid "Edit Placeholders →" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:330 -#: inc/admin-pages/class-settings-admin-page.php:334 +#: inc/admin-pages/class-settings-admin-page.php:331 +#: inc/admin-pages/class-settings-admin-page.php:335 msgid "Customize the Invoice Template" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:338 +#: inc/admin-pages/class-settings-admin-page.php:339 msgid "Did you know that you can customize colors, logos, and more options of the Invoice PDF template?" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:373 -#: inc/admin-pages/class-settings-admin-page.php:377 +#: inc/admin-pages/class-settings-admin-page.php:374 +#: inc/admin-pages/class-settings-admin-page.php:378 msgid "Customize System Emails" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:381 +#: inc/admin-pages/class-settings-admin-page.php:382 msgid "You can completely customize the contents of the emails sent out by Ultimate Multisite when particular events occur, such as Account Creation, Payment Failures, etc." msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:390 +#: inc/admin-pages/class-settings-admin-page.php:391 msgid "Customize System Emails →" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:424 +#: inc/admin-pages/class-settings-admin-page.php:425 msgid "If your network is using the HTML email option, you can customize the look and feel of the email template." msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:433 +#: inc/admin-pages/class-settings-admin-page.php:434 msgid "Customize Email Template →" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:454 -#: inc/admin-pages/class-settings-admin-page.php:465 +#: inc/admin-pages/class-settings-admin-page.php:455 +#: inc/admin-pages/class-settings-admin-page.php:466 #: inc/admin-pages/class-top-admin-nav-menu.php:151 #: inc/installers/class-migrator.php:288 #: views/ui/branding/footer.php:37 @@ -4329,14 +4330,48 @@ msgstr "" msgid "Settings" msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:522 +#: inc/admin-pages/class-settings-admin-page.php:523 msgid "You do not have the permissions required to change settings." msgstr "" -#: inc/admin-pages/class-settings-admin-page.php:584 +#: inc/admin-pages/class-settings-admin-page.php:585 msgid "Save Settings" msgstr "" +#: inc/admin-pages/class-settings-admin-page.php:646 +msgid "You do not have permission to export settings." +msgstr "" + +#: inc/admin-pages/class-settings-admin-page.php:693 +msgid "Upload Settings File" +msgstr "" + +#: inc/admin-pages/class-settings-admin-page.php:694 +msgid "Select a JSON file previously exported from Ultimate Multisite." +msgstr "" + +#: inc/admin-pages/class-settings-admin-page.php:702 +msgid "I understand this will replace all current settings" +msgstr "" + +#: inc/admin-pages/class-settings-admin-page.php:703 +msgid "This action cannot be undone. Make sure you have a backup of your current settings." +msgstr "" + +#: inc/admin-pages/class-settings-admin-page.php:711 +#: inc/class-settings.php:1608 +#: inc/class-settings.php:1621 +msgid "Import Settings" +msgstr "" + +#: inc/admin-pages/class-settings-admin-page.php:791 +msgid "Something is wrong with the uploaded file." +msgstr "" + +#: inc/admin-pages/class-settings-admin-page.php:842 +msgid "Settings successfully imported!" +msgstr "" + #: inc/admin-pages/class-setup-wizard-admin-page.php:245 msgid "Permission denied." msgstr "" @@ -4518,12 +4553,12 @@ msgid "Complete Setup" msgstr "" #: inc/admin-pages/class-setup-wizard-admin-page.php:896 -#: inc/class-scripts.php:196 +#: inc/class-scripts.php:227 msgid "Select an Image." msgstr "" #: inc/admin-pages/class-setup-wizard-admin-page.php:897 -#: inc/class-scripts.php:197 +#: inc/class-scripts.php:228 msgid "Use this image" msgstr "" @@ -4539,6 +4574,7 @@ msgid "integer" msgstr "" #: inc/admin-pages/class-shortcodes-admin-page.php:160 +#: inc/class-scripts.php:177 msgid "number" msgstr "" @@ -4568,7 +4604,7 @@ msgid "This will start the transfer of assets from one membership to another." msgstr "" #: inc/admin-pages/class-site-edit-admin-page.php:260 -#: inc/list-tables/class-site-list-table.php:399 +#: inc/list-tables/class-site-list-table.php:407 #: inc/managers/class-site-manager.php:369 msgid "Site not found." msgstr "" @@ -4581,6 +4617,7 @@ msgid "Site Type" msgstr "" #: inc/admin-pages/class-site-edit-admin-page.php:328 +#: views/wizards/host-integrations/rocket-instructions.php:12 msgid "Site ID" msgstr "" @@ -4594,7 +4631,7 @@ msgid "Tell your customers what this site is about." msgstr "" #: inc/admin-pages/class-site-edit-admin-page.php:358 -#: inc/class-settings.php:1181 +#: inc/class-settings.php:1207 msgid "Site Options" msgstr "" @@ -4888,7 +4925,7 @@ msgstr "" #: inc/admin-pages/class-system-info-admin-page.php:479 #: inc/admin-pages/class-system-info-admin-page.php:484 #: inc/admin-pages/class-system-info-admin-page.php:489 -#: inc/class-settings.php:1590 +#: inc/class-settings.php:1726 msgid "Disabled" msgstr "" @@ -5577,8 +5614,8 @@ msgstr "" #: inc/apis/schemas/customer-update.php:91 #: inc/apis/schemas/discount-code-create.php:116 #: inc/apis/schemas/discount-code-update.php:116 -#: inc/apis/schemas/domain-create.php:70 -#: inc/apis/schemas/domain-update.php:70 +#: inc/apis/schemas/domain-create.php:73 +#: inc/apis/schemas/domain-update.php:73 #: inc/apis/schemas/email-create.php:135 #: inc/apis/schemas/email-update.php:135 #: inc/apis/schemas/event-create.php:67 @@ -5671,8 +5708,8 @@ msgstr "" #: inc/apis/schemas/customer-update.php:96 #: inc/apis/schemas/discount-code-create.php:121 #: inc/apis/schemas/discount-code-update.php:121 -#: inc/apis/schemas/domain-create.php:75 -#: inc/apis/schemas/domain-update.php:75 +#: inc/apis/schemas/domain-create.php:78 +#: inc/apis/schemas/domain-update.php:78 #: inc/apis/schemas/email-create.php:140 #: inc/apis/schemas/email-update.php:140 #: inc/apis/schemas/event-create.php:72 @@ -5752,8 +5789,8 @@ msgstr "" #: inc/apis/schemas/customer-update.php:86 #: inc/apis/schemas/discount-code-create.php:111 #: inc/apis/schemas/discount-code-update.php:111 -#: inc/apis/schemas/domain-create.php:65 -#: inc/apis/schemas/domain-update.php:65 +#: inc/apis/schemas/domain-create.php:68 +#: inc/apis/schemas/domain-update.php:68 #: inc/apis/schemas/event-create.php:62 #: inc/apis/schemas/event-update.php:62 #: inc/apis/schemas/payment-create.php:111 @@ -5895,38 +5932,38 @@ msgstr "" msgid "This discount code will be limited to be used in certain products? If set to true, you must define a list of allowed products." msgstr "" -#: inc/apis/schemas/domain-create.php:23 -#: inc/apis/schemas/domain-update.php:23 +#: inc/apis/schemas/domain-create.php:25 +#: inc/apis/schemas/domain-update.php:25 msgid "Your Domain name. You don't need to put http or https in front of your domain in this field. e.g: example.com." msgstr "" -#: inc/apis/schemas/domain-create.php:28 -#: inc/apis/schemas/domain-update.php:28 +#: inc/apis/schemas/domain-create.php:30 +#: inc/apis/schemas/domain-update.php:30 msgid "The blog ID attached to this domain." msgstr "" -#: inc/apis/schemas/domain-create.php:33 -#: inc/apis/schemas/domain-update.php:33 +#: inc/apis/schemas/domain-create.php:35 +#: inc/apis/schemas/domain-update.php:35 msgid "Set this domain as active (true), which means available to be used, or inactive (false)." msgstr "" -#: inc/apis/schemas/domain-create.php:38 -#: inc/apis/schemas/domain-update.php:38 +#: inc/apis/schemas/domain-create.php:40 +#: inc/apis/schemas/domain-update.php:40 msgid "Define true to set this as primary domain of a site, meaning it's the main url, or set false." msgstr "" -#: inc/apis/schemas/domain-create.php:43 -#: inc/apis/schemas/domain-update.php:43 +#: inc/apis/schemas/domain-create.php:45 +#: inc/apis/schemas/domain-update.php:45 msgid "If this domain has some SSL security or not." msgstr "" -#: inc/apis/schemas/domain-create.php:48 -#: inc/apis/schemas/domain-update.php:48 +#: inc/apis/schemas/domain-create.php:50 +#: inc/apis/schemas/domain-update.php:50 msgid "The state of the domain model object. Can be one of this options: checking-dns, checking-ssl-cert, done-without-ssl, done and failed." msgstr "" -#: inc/apis/schemas/domain-create.php:60 -#: inc/apis/schemas/domain-update.php:60 +#: inc/apis/schemas/domain-create.php:63 +#: inc/apis/schemas/domain-update.php:63 msgid "Date when the domain was created. If no date is set, the current date and time will be used." msgstr "" @@ -6711,7 +6748,7 @@ msgstr "" #: inc/apis/trait-rest-api.php:174 #: inc/apis/trait-rest-api.php:247 #: inc/apis/trait-rest-api.php:305 -#: inc/models/class-base-model.php:646 +#: inc/models/class-base-model.php:639 #: inc/models/class-site.php:1435 msgid "Item not found." msgstr "" @@ -6879,38 +6916,38 @@ msgstr "" msgid "Error: The password you entered is incorrect." msgstr "" -#: inc/checkout/class-checkout-pages.php:218 -#: inc/integrations/host-providers/class-closte-host-provider.php:256 +#: inc/checkout/class-checkout-pages.php:220 +#: inc/integrations/host-providers/class-closte-host-provider.php:292 msgid "Something went wrong" msgstr "" #. translators: %1$s and %2$s are HTML tags -#: inc/checkout/class-checkout-pages.php:422 +#: inc/checkout/class-checkout-pages.php:424 #, php-format msgid "Your email address is not yet verified. Your site %1$s will only be activated %2$s after your email address is verified. Check your inbox and verify your email address." msgstr "" -#: inc/checkout/class-checkout-pages.php:426 +#: inc/checkout/class-checkout-pages.php:428 msgid "Resend verification email →" msgstr "" -#: inc/checkout/class-checkout-pages.php:631 +#: inc/checkout/class-checkout-pages.php:633 msgid "Ultimate Multisite - Register Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:632 +#: inc/checkout/class-checkout-pages.php:634 msgid "Ultimate Multisite - Login Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:633 +#: inc/checkout/class-checkout-pages.php:635 msgid "Ultimate Multisite - Site Blocked Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:634 +#: inc/checkout/class-checkout-pages.php:636 msgid "Ultimate Multisite - Membership Update Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:635 +#: inc/checkout/class-checkout-pages.php:637 msgid "Ultimate Multisite - New Site Page" msgstr "" @@ -7959,24 +7996,24 @@ msgid "Minimal" msgstr "" #. translators: %s the url for login. -#: inc/class-addon-repository.php:164 +#: inc/class-addon-repository.php:166 #, php-format msgid "You must Connect to UltimateMultisite.com first." msgstr "" -#: inc/class-addon-repository.php:183 +#: inc/class-addon-repository.php:185 msgid "403 Access Denied returned from server. Ensure you have an active subscription for this addon." msgstr "" -#: inc/class-addon-repository.php:187 +#: inc/class-addon-repository.php:189 msgid "Failed to connect to the update server. Please try again later." msgstr "" -#: inc/class-addon-repository.php:233 +#: inc/class-addon-repository.php:235 msgid "Successfully connected your site to UltimateMultisite.com." msgstr "" -#: inc/class-addon-repository.php:242 +#: inc/class-addon-repository.php:244 msgid "Failed to authenticate with UltimateMultisite.com." msgstr "" @@ -8359,6 +8396,7 @@ msgstr "" #: inc/class-orphaned-tables-manager.php:140 #: inc/class-orphaned-users-manager.php:133 +#: inc/class-settings.php:1645 msgid "Warning:" msgstr "" @@ -8460,19 +8498,51 @@ msgstr "" msgid "here" msgstr "" +#: inc/class-scripts.php:170 +#: inc/functions/legacy.php:273 +#: views/admin-pages/fields/field-password.php:49 +msgid "Strength indicator" +msgstr "" + +#: inc/class-scripts.php:171 +msgid "Super Strong" +msgstr "" + +#: inc/class-scripts.php:172 +msgid "Required:" +msgstr "" + +#. translators: %d is the minimum number of characters required +#: inc/class-scripts.php:174 +#, php-format +msgid "at least %d characters" +msgstr "" + +#: inc/class-scripts.php:175 +msgid "uppercase letter" +msgstr "" + +#: inc/class-scripts.php:176 +msgid "lowercase letter" +msgstr "" + +#: inc/class-scripts.php:178 +msgid "special character" +msgstr "" + #. translators: the day/month/year date format used by Ultimate Multisite. You can changed it to localize this date format to your language. the default value is d/m/Y, which is the format 31/12/2021. -#: inc/class-scripts.php:317 +#: inc/class-scripts.php:348 msgid "d/m/Y" msgstr "" #. translators: %s is a relative future date. -#: inc/class-scripts.php:327 +#: inc/class-scripts.php:358 #, php-format msgid "in %s" msgstr "" #. translators: %s is a relative past date. -#: inc/class-scripts.php:329 +#: inc/class-scripts.php:360 #: inc/functions/date.php:156 #: inc/list-tables/class-base-list-table.php:851 #: views/admin-pages/fields/field-text-display.php:43 @@ -8481,72 +8551,72 @@ msgstr "" msgid "%s ago" msgstr "" -#: inc/class-scripts.php:330 +#: inc/class-scripts.php:361 msgid "a few seconds" msgstr "" #. translators: %s is the number of seconds. -#: inc/class-scripts.php:332 +#: inc/class-scripts.php:363 #, php-format msgid "%d seconds" msgstr "" -#: inc/class-scripts.php:333 +#: inc/class-scripts.php:364 msgid "a minute" msgstr "" #. translators: %s is the number of minutes. -#: inc/class-scripts.php:335 +#: inc/class-scripts.php:366 #, php-format msgid "%d minutes" msgstr "" -#: inc/class-scripts.php:336 +#: inc/class-scripts.php:367 msgid "an hour" msgstr "" #. translators: %s is the number of hours. -#: inc/class-scripts.php:338 +#: inc/class-scripts.php:369 #, php-format msgid "%d hours" msgstr "" -#: inc/class-scripts.php:339 +#: inc/class-scripts.php:370 msgid "a day" msgstr "" #. translators: %s is the number of days. -#: inc/class-scripts.php:341 +#: inc/class-scripts.php:372 #, php-format msgid "%d days" msgstr "" -#: inc/class-scripts.php:342 +#: inc/class-scripts.php:373 msgid "a week" msgstr "" #. translators: %s is the number of weeks. -#: inc/class-scripts.php:344 +#: inc/class-scripts.php:375 #, php-format msgid "%d weeks" msgstr "" -#: inc/class-scripts.php:345 +#: inc/class-scripts.php:376 msgid "a month" msgstr "" #. translators: %s is the number of months. -#: inc/class-scripts.php:347 +#: inc/class-scripts.php:378 #, php-format msgid "%d months" msgstr "" -#: inc/class-scripts.php:348 +#: inc/class-scripts.php:379 msgid "a year" msgstr "" #. translators: %s is the number of years. -#: inc/class-scripts.php:350 +#: inc/class-scripts.php:381 #, php-format msgid "%d years" msgstr "" @@ -8610,7 +8680,7 @@ msgid "Currency Options" msgstr "" #: inc/class-settings.php:636 -#: inc/class-settings.php:1342 +#: inc/class-settings.php:1368 msgid "The following options affect how prices are displayed on the frontend, the backend and in reports." msgstr "" @@ -8708,14 +8778,14 @@ msgstr "" #: inc/class-settings.php:778 #: inc/class-settings.php:810 -#: inc/class-settings.php:936 -#: inc/class-settings.php:1193 +#: inc/class-settings.php:962 +#: inc/class-settings.php:1219 msgid "Search pages on the main site..." msgstr "" #: inc/class-settings.php:779 -#: inc/class-settings.php:937 -#: inc/class-settings.php:1194 +#: inc/class-settings.php:963 +#: inc/class-settings.php:1220 msgid "Only published pages on the main site are available for selection, and you need to make sure they contain a [wu_checkout] shortcode." msgstr "" @@ -8760,473 +8830,534 @@ msgid "By default, when a new pending site needs to be converted into a real net msgstr "" #: inc/class-settings.php:870 -#: inc/class-settings.php:1527 -#: inc/class-settings.php:1528 -msgid "Other Options" +msgid "Password Strength" msgstr "" #: inc/class-settings.php:871 -msgid "Other registration-related options." +msgid "Configure password strength requirements for user registration." msgstr "" #: inc/class-settings.php:880 -msgid "Default Role" +msgid "Minimum Password Strength" msgstr "" #: inc/class-settings.php:881 +msgid "Set the minimum password strength required during registration and password reset. \"Super Strong\" requires at least 12 characters, including uppercase, lowercase, numbers, and special characters." +msgstr "" + +#: inc/class-settings.php:885 +msgid "Medium" +msgstr "" + +#: inc/class-settings.php:886 +msgid "Strong" +msgstr "" + +#: inc/class-settings.php:887 +msgid "Super Strong (12+ chars, mixed case, numbers, symbols)" +msgstr "" + +#: inc/class-settings.php:896 +#: inc/class-settings.php:1663 +#: inc/class-settings.php:1664 +msgid "Other Options" +msgstr "" + +#: inc/class-settings.php:897 +msgid "Other registration-related options." +msgstr "" + +#: inc/class-settings.php:906 +msgid "Default Role" +msgstr "" + +#: inc/class-settings.php:907 msgid "Set the role to be applied to the user during the signup process." msgstr "" -#: inc/class-settings.php:892 +#: inc/class-settings.php:918 msgid "Add Users to the Main Site as well?" msgstr "" -#: inc/class-settings.php:893 +#: inc/class-settings.php:919 msgid "Enabling this option will also add the user to the main site of your network." msgstr "" -#: inc/class-settings.php:903 +#: inc/class-settings.php:929 msgid "Add to Main Site with Role..." msgstr "" -#: inc/class-settings.php:904 +#: inc/class-settings.php:930 msgid "Select the role Ultimate Multisite should use when adding the user to the main site of your network. Be careful." msgstr "" -#: inc/class-settings.php:935 +#: inc/class-settings.php:961 msgid "Default Membership Update Page" msgstr "" -#: inc/class-settings.php:955 +#: inc/class-settings.php:981 msgid "Block Frontend Access" msgstr "" -#: inc/class-settings.php:956 +#: inc/class-settings.php:982 msgid "Block the frontend access of network sites after a membership is no longer active." msgstr "" -#: inc/class-settings.php:957 +#: inc/class-settings.php:983 msgid "By default, if a user does not pay and the account goes inactive, only the admin panel will be blocked, but the user's site will still be accessible on the frontend. If enabled, this option will also block frontend access in those cases." msgstr "" -#: inc/class-settings.php:967 +#: inc/class-settings.php:993 msgid "Frontend Block Grace Period" msgstr "" -#: inc/class-settings.php:968 +#: inc/class-settings.php:994 msgid "Select the number of days Ultimate Multisite should wait after the membership goes inactive before blocking the frontend access. Leave 0 to block immediately after the membership becomes inactive." msgstr "" -#: inc/class-settings.php:982 +#: inc/class-settings.php:1008 msgid "Frontend Block Page" msgstr "" -#: inc/class-settings.php:983 +#: inc/class-settings.php:1009 msgid "Select a page on the main site to redirect user if access is blocked" msgstr "" -#: inc/class-settings.php:1003 +#: inc/class-settings.php:1029 msgid "Enable Multiple Memberships per Customer" msgstr "" -#: inc/class-settings.php:1004 +#: inc/class-settings.php:1030 msgid "Enabling this option will allow your users to create more than one membership." msgstr "" -#: inc/class-settings.php:1014 +#: inc/class-settings.php:1040 msgid "Enable Multiple Sites per Membership" msgstr "" -#: inc/class-settings.php:1015 +#: inc/class-settings.php:1041 msgid "Enabling this option will allow your customers to create more than one site. You can limit how many sites your users can create in a per plan basis." msgstr "" -#: inc/class-settings.php:1025 +#: inc/class-settings.php:1051 msgid "Block Sites on Downgrade" msgstr "" -#: inc/class-settings.php:1026 +#: inc/class-settings.php:1052 msgid "Choose how Ultimate Multisite should handle client sites above their plan quota on downgrade." msgstr "" -#: inc/class-settings.php:1030 +#: inc/class-settings.php:1056 msgid "Keep sites as is (do nothing)" msgstr "" -#: inc/class-settings.php:1031 +#: inc/class-settings.php:1057 msgid "Block only frontend access" msgstr "" -#: inc/class-settings.php:1032 +#: inc/class-settings.php:1058 msgid "Block only backend access" msgstr "" -#: inc/class-settings.php:1033 +#: inc/class-settings.php:1059 msgid "Block both frontend and backend access" msgstr "" -#: inc/class-settings.php:1045 +#: inc/class-settings.php:1071 msgid "Move Posts on Downgrade" msgstr "" -#: inc/class-settings.php:1046 +#: inc/class-settings.php:1072 msgid "Select how you want to handle the posts above the quota on downgrade. This will apply to all post types with quotas set." msgstr "" -#: inc/class-settings.php:1050 +#: inc/class-settings.php:1076 msgid "Keep posts as is (do nothing)" msgstr "" -#: inc/class-settings.php:1051 +#: inc/class-settings.php:1077 msgid "Move posts above the new quota to the Trash" msgstr "" -#: inc/class-settings.php:1052 +#: inc/class-settings.php:1078 msgid "Mark posts above the new quota as Drafts" msgstr "" -#: inc/class-settings.php:1062 +#: inc/class-settings.php:1088 msgid "Emulated Post Types" msgstr "" -#: inc/class-settings.php:1063 +#: inc/class-settings.php:1089 msgid "Emulates the registering of a custom post type to be able to create limits for it without having to activate plugins on the main site." msgstr "" -#: inc/class-settings.php:1072 +#: inc/class-settings.php:1098 msgid "By default, Ultimate Multisite only allows super admins to limit post types that are registered on the main site. This makes sense from a technical stand-point but it also forces you to have plugins network-activated in order to be able to set limitations for their custom post types. Using this option, you can emulate the registering of a post type. This will register them on the main site and allow you to create limits for them on your products." msgstr "" -#: inc/class-settings.php:1083 +#: inc/class-settings.php:1109 msgid "Add the first post type using the button below." msgstr "" -#: inc/class-settings.php:1117 +#: inc/class-settings.php:1143 msgid "Post Type Slug" msgstr "" -#: inc/class-settings.php:1118 +#: inc/class-settings.php:1144 msgid "e.g. product" msgstr "" -#: inc/class-settings.php:1127 +#: inc/class-settings.php:1153 msgid "Post Type Label" msgstr "" -#: inc/class-settings.php:1128 +#: inc/class-settings.php:1154 msgid "e.g. Products" msgstr "" -#: inc/class-settings.php:1144 +#: inc/class-settings.php:1170 msgid "+ Add Post Type" msgstr "" -#: inc/class-settings.php:1182 +#: inc/class-settings.php:1208 msgid "Configure certain aspects of how network Sites behave." msgstr "" -#: inc/class-settings.php:1192 +#: inc/class-settings.php:1218 msgid "Default New Site Page" msgstr "" -#: inc/class-settings.php:1212 +#: inc/class-settings.php:1238 msgid "Enable Visits Limitation & Counting" msgstr "" -#: inc/class-settings.php:1213 +#: inc/class-settings.php:1239 msgid "Enabling this option will add visits limitation settings to the plans and add the functionality necessary to count site visits on the front-end." msgstr "" -#: inc/class-settings.php:1223 +#: inc/class-settings.php:1249 msgid "Enable Screenshot Generator" msgstr "" -#: inc/class-settings.php:1224 +#: inc/class-settings.php:1250 msgid "With this option is enabled, Ultimate Multisite will take a screenshot for every newly created site on your network and set the resulting image as that site's featured image. This features requires a valid license key to work and it is not supported for local sites." msgstr "" -#: inc/class-settings.php:1234 +#: inc/class-settings.php:1260 msgid "WordPress Features" msgstr "" -#: inc/class-settings.php:1235 +#: inc/class-settings.php:1261 msgid "Override default WordPress settings for network Sites." msgstr "" -#: inc/class-settings.php:1244 +#: inc/class-settings.php:1270 msgid "Enable Plugins Menu" msgstr "" -#: inc/class-settings.php:1245 +#: inc/class-settings.php:1271 msgid "Do you want to let users on the network to have access to the Plugins page, activating plugins for their sites? If this option is disabled, the customer will not be able to manage the site plugins." msgstr "" -#: inc/class-settings.php:1246 +#: inc/class-settings.php:1272 msgid "You can select which plugins the user will be able to use for each plan." msgstr "" -#: inc/class-settings.php:1256 +#: inc/class-settings.php:1282 msgid "Add New Users" msgstr "" -#: inc/class-settings.php:1257 +#: inc/class-settings.php:1283 msgid "Allow site administrators to add new users to their site via the \"Users → Add New\" page." msgstr "" -#: inc/class-settings.php:1258 +#: inc/class-settings.php:1284 msgid "You can limit the number of users allowed for each plan." msgstr "" -#: inc/class-settings.php:1268 +#: inc/class-settings.php:1294 msgid "Site Template Options" msgstr "" -#: inc/class-settings.php:1269 +#: inc/class-settings.php:1295 msgid "Configure certain aspects of how Site Templates behave." msgstr "" -#: inc/class-settings.php:1278 +#: inc/class-settings.php:1304 msgid "Allow Template Switching" msgstr "" -#: inc/class-settings.php:1279 +#: inc/class-settings.php:1305 msgid "Enabling this option will add an option on your client's dashboard to switch their site template to another one available on the catalog of available templates. The data is lost after a switch as the data from the new template is copied over." msgstr "" -#: inc/class-settings.php:1289 +#: inc/class-settings.php:1315 msgid "Allow Users to use their own Sites as Templates" msgstr "" -#: inc/class-settings.php:1290 +#: inc/class-settings.php:1316 msgid "Enabling this option will add the user own sites to the template screen, allowing them to create a new site based on the content and customizations they made previously." msgstr "" -#: inc/class-settings.php:1303 +#: inc/class-settings.php:1329 msgid "Copy Media on Template Duplication?" msgstr "" -#: inc/class-settings.php:1304 +#: inc/class-settings.php:1330 msgid "Checking this option will copy the media uploaded on the template site to the newly created site. This can be overridden on each of the plans." msgstr "" -#: inc/class-settings.php:1314 +#: inc/class-settings.php:1340 msgid "Prevent Search Engines from indexing Site Templates" msgstr "" -#: inc/class-settings.php:1315 +#: inc/class-settings.php:1341 msgid "Checking this option will discourage search engines from indexing all the Site Templates on your network." msgstr "" -#: inc/class-settings.php:1341 +#: inc/class-settings.php:1367 msgid "Payment Settings" msgstr "" -#: inc/class-settings.php:1352 +#: inc/class-settings.php:1378 msgid "Force Auto-Renew" msgstr "" -#: inc/class-settings.php:1353 +#: inc/class-settings.php:1379 msgid "Enable this option if you want to make sure memberships are created with auto-renew activated whenever the selected gateway supports it. Disabling this option will show an auto-renew option during checkout." msgstr "" -#: inc/class-settings.php:1364 +#: inc/class-settings.php:1390 msgid "Allow Trials without Payment Method" msgstr "" -#: inc/class-settings.php:1365 +#: inc/class-settings.php:1391 msgid "By default, Ultimate Multisite asks customers to add a payment method on sign-up even if a trial period is present. Enable this option to only ask for a payment method when the trial period is over." msgstr "" -#: inc/class-settings.php:1376 +#: inc/class-settings.php:1402 msgid "Send Invoice on Payment Confirmation" msgstr "" -#: inc/class-settings.php:1377 +#: inc/class-settings.php:1403 msgid "Enabling this option will attach a PDF invoice (marked paid) with the payment confirmation email. This option does not apply to the Manual Gateway, which sends invoices regardless of this option." msgstr "" -#: inc/class-settings.php:1378 +#: inc/class-settings.php:1404 msgid "The invoice files will be saved on the wp-content/uploads/wu-invoices folder." msgstr "" -#: inc/class-settings.php:1388 +#: inc/class-settings.php:1414 msgid "Invoice Numbering Scheme" msgstr "" -#: inc/class-settings.php:1389 +#: inc/class-settings.php:1415 msgid "What should Ultimate Multisite use as the invoice number?" msgstr "" -#: inc/class-settings.php:1394 +#: inc/class-settings.php:1420 msgid "Payment Reference Code" msgstr "" -#: inc/class-settings.php:1395 +#: inc/class-settings.php:1421 msgid "Sequential Number" msgstr "" -#: inc/class-settings.php:1404 +#: inc/class-settings.php:1430 msgid "Next Invoice Number" msgstr "" -#: inc/class-settings.php:1405 +#: inc/class-settings.php:1431 msgid "This number will be used as the invoice number for the next invoice generated on the system. It is incremented by one every time a new invoice is created. You can change it and save it to reset the invoice sequential number to a specific value." msgstr "" -#: inc/class-settings.php:1419 +#: inc/class-settings.php:1445 msgid "Invoice Number Prefix" msgstr "" -#: inc/class-settings.php:1420 +#: inc/class-settings.php:1446 msgid "INV00" msgstr "" #. translators: %%YEAR%%, %%MONTH%%, and %%DAY%% are placeholders but are replaced before shown to the user but are used as examples. -#: inc/class-settings.php:1422 +#: inc/class-settings.php:1448 #, php-format msgid "Use %%YEAR%%, %%MONTH%%, and %%DAY%% to create a dynamic placeholder. E.g. %%YEAR%%-%%MONTH%%-INV will become %s." msgstr "" -#: inc/class-settings.php:1436 +#: inc/class-settings.php:1462 #: inc/ui/class-jumper.php:209 msgid "Payment Gateways" msgstr "" -#: inc/class-settings.php:1437 +#: inc/class-settings.php:1463 msgid "Activate and configure the installed payment gateways in this section." msgstr "" -#: inc/class-settings.php:1452 -#: inc/class-settings.php:1453 +#: inc/class-settings.php:1478 +#: inc/class-settings.php:1479 #: inc/list-tables/class-broadcast-list-table.php:481 #: inc/list-tables/class-email-list-table.php:40 #: inc/ui/class-jumper.php:211 msgid "Emails" msgstr "" -#: inc/class-settings.php:1468 -#: inc/class-settings.php:1469 +#: inc/class-settings.php:1494 +#: inc/class-settings.php:1495 msgid "Domain Mapping" msgstr "" -#: inc/class-settings.php:1484 -#: inc/class-settings.php:1485 +#: inc/class-settings.php:1510 +#: inc/class-settings.php:1511 msgid "Single Sign-On" msgstr "" -#: inc/class-settings.php:1510 +#: inc/class-settings.php:1536 msgid "Hosting or Panel Providers" msgstr "" -#: inc/class-settings.php:1511 +#: inc/class-settings.php:1537 msgid "Configure and manage the integration with your Hosting or Panel Provider." msgstr "" -#: inc/class-settings.php:1538 +#: inc/class-settings.php:1553 +msgid "Import/Export" +msgstr "" + +#: inc/class-settings.php:1554 +msgid "Export your settings to a JSON file or import settings from a previously exported file." +msgstr "" + +#: inc/class-settings.php:1565 +#: inc/class-settings.php:1590 +msgid "Export Settings" +msgstr "" + +#: inc/class-settings.php:1566 +msgid "Download all your Ultimate Multisite settings as a JSON file for backup or migration purposes." +msgstr "" + +#: inc/class-settings.php:1578 +msgid "The exported file will contain all ultimate multisite settings defined on this page. This includes general settings, payment gateway configurations, email settings, domain mapping settings, and all other plugin configurations. It does not include products, sites, domains, customers and other entities." +msgstr "" + +#: inc/class-settings.php:1609 +msgid "Upload a previously exported JSON file to restore settings." +msgstr "" + +#: inc/class-settings.php:1622 +msgid "Import and Replace All Settings" +msgstr "" + +#: inc/class-settings.php:1646 +msgid "Importing settings will replace ALL current settings with the values from the uploaded file. This action cannot be undone. We recommend exporting your current settings as a backup before importing." +msgstr "" + +#: inc/class-settings.php:1674 msgid "Miscellaneous" msgstr "" -#: inc/class-settings.php:1539 +#: inc/class-settings.php:1675 msgid "Other options that do not fit anywhere else." msgstr "" -#: inc/class-settings.php:1550 +#: inc/class-settings.php:1686 msgid "Hide UI Tours" msgstr "" -#: inc/class-settings.php:1551 +#: inc/class-settings.php:1687 msgid "The UI tours showed by Ultimate Multisite should permanently hide themselves after being seen but if they persist for whatever reason, toggle this option to force them into their viewed state - which will prevent them from showing up again." msgstr "" -#: inc/class-settings.php:1563 +#: inc/class-settings.php:1699 msgid "Disable \"Hover to Zoom\"" msgstr "" -#: inc/class-settings.php:1564 +#: inc/class-settings.php:1700 msgid "By default, Ultimate Multisite adds a \"hover to zoom\" feature, allowing network admins to see larger version of site screenshots and other images across the UI in full-size when hovering over them. You can disable that feature here. Preview tags like the above are not affected." msgstr "" -#: inc/class-settings.php:1574 +#: inc/class-settings.php:1710 msgid "Logging" msgstr "" -#: inc/class-settings.php:1575 +#: inc/class-settings.php:1711 msgid "Log Ultimate Multisite data. This is useful for debugging purposes." msgstr "" -#: inc/class-settings.php:1584 +#: inc/class-settings.php:1720 msgid "Logging Level" msgstr "" -#: inc/class-settings.php:1585 +#: inc/class-settings.php:1721 msgid "Select the level of logging you want to use." msgstr "" -#: inc/class-settings.php:1589 +#: inc/class-settings.php:1725 msgid "PHP Default" msgstr "" -#: inc/class-settings.php:1591 +#: inc/class-settings.php:1727 msgid "Errors Only" msgstr "" -#: inc/class-settings.php:1592 +#: inc/class-settings.php:1728 msgid "Everything" msgstr "" -#: inc/class-settings.php:1601 +#: inc/class-settings.php:1737 msgid "Send Error Data to Ultimate Multisite Developers" msgstr "" -#: inc/class-settings.php:1602 +#: inc/class-settings.php:1738 msgid "With this option enabled, every time your installation runs into an error related to Ultimate Multisite, that error data will be sent to us. No sensitive data gets collected, only environmental stuff (e.g. if this is this is a subdomain network, etc)." msgstr "" -#: inc/class-settings.php:1613 +#: inc/class-settings.php:1749 msgid "Change the plugin and wordpress behavior." msgstr "" -#: inc/class-settings.php:1628 +#: inc/class-settings.php:1764 msgid "Run Migration Again" msgstr "" -#: inc/class-settings.php:1630 +#: inc/class-settings.php:1766 msgid "Rerun the Migration Wizard if you experience data-loss after migrate." msgstr "" -#: inc/class-settings.php:1633 +#: inc/class-settings.php:1769 msgid "Important: This process can have unexpected behavior with your current Ultimo models.
We recommend that you create a backup before continue." msgstr "" -#: inc/class-settings.php:1636 +#: inc/class-settings.php:1772 msgid "Migrate" msgstr "" -#: inc/class-settings.php:1659 +#: inc/class-settings.php:1795 msgid "Security Mode" msgstr "" #. Translators: Placeholder adds the security mode key and current site url with query string -#: inc/class-settings.php:1661 +#: inc/class-settings.php:1797 #, php-format msgid "Only Ultimate Multisite and other must-use plugins will run on your WordPress install while this option is enabled.
Important: Copy the following URL to disable security mode if something goes wrong and this page becomes unavailable:%2$s
" msgstr "" -#: inc/class-settings.php:1672 +#: inc/class-settings.php:1808 msgid "Remove Data on Uninstall" msgstr "" -#: inc/class-settings.php:1673 +#: inc/class-settings.php:1809 msgid "Remove all saved data for Ultimate Multisite when the plugin is uninstalled." msgstr "" #. translators: the placeholder is an error message -#: inc/class-sunrise.php:292 +#: inc/class-sunrise.php:293 #, php-format msgid "Sunrise copy failed: %s" msgstr "" -#: inc/class-sunrise.php:295 +#: inc/class-sunrise.php:296 msgid "Sunrise upgrade attempt succeeded." msgstr "" @@ -13104,23 +13235,27 @@ msgstr "" msgid "state / province" msgstr "" -#: inc/database/domains/class-domain-stage.php:65 +#: inc/database/domains/class-domain-stage.php:68 msgid "DNS Failed" msgstr "" -#: inc/database/domains/class-domain-stage.php:66 +#: inc/database/domains/class-domain-stage.php:69 +msgid "SSL Failed" +msgstr "" + +#: inc/database/domains/class-domain-stage.php:70 msgid "Checking DNS" msgstr "" -#: inc/database/domains/class-domain-stage.php:67 +#: inc/database/domains/class-domain-stage.php:71 msgid "Checking SSL" msgstr "" -#: inc/database/domains/class-domain-stage.php:68 +#: inc/database/domains/class-domain-stage.php:72 msgid "Ready" msgstr "" -#: inc/database/domains/class-domain-stage.php:69 +#: inc/database/domains/class-domain-stage.php:73 msgid "Ready (without SSL)" msgstr "" @@ -14795,10 +14930,6 @@ msgstr "" msgid "You should not register new payment gateways before the wu_register_gateways hook." msgstr "" -#: inc/functions/legacy.php:273 -msgid "Strength indicator" -msgstr "" - #: inc/functions/limitations.php:68 #: inc/functions/limitations.php:107 msgid "Invalid site ID" @@ -15526,6 +15657,7 @@ msgstr "" #: views/base/filter.php:123 #: views/base/filter.php:131 #: views/wizards/host-integrations/cloudflare-instructions.php:14 +#: views/wizards/host-integrations/rocket-instructions.php:12 #: views/wizards/host-integrations/runcloud-instructions.php:12 msgid "and" msgstr "" @@ -15724,7 +15856,7 @@ msgstr "" #: inc/installers/class-default-content-installer.php:416 #: inc/installers/class-migrator.php:2379 #: inc/ui/class-login-form-element.php:156 -#: inc/ui/class-login-form-element.php:354 +#: inc/ui/class-login-form-element.php:377 msgid "Login" msgstr "" @@ -15963,11 +16095,11 @@ msgstr "" msgid "No description provided." msgstr "" -#: inc/integrations/host-providers/class-closte-host-provider.php:251 +#: inc/integrations/host-providers/class-closte-host-provider.php:287 msgid "Access Authorized" msgstr "" -#: inc/integrations/host-providers/class-closte-host-provider.php:371 +#: inc/integrations/host-providers/class-closte-host-provider.php:407 msgid "Closte is not just another web hosting who advertise their services as a cloud hosting while still provides fixed plans like in 1995." msgstr "" @@ -16141,69 +16273,97 @@ msgstr "" msgid "Add a new SubDomain on cPanel whenever a new site gets created on your network" msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:103 +#: inc/integrations/host-providers/class-enhance-host-provider.php:105 msgid "Enhance API Token" msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:104 +#. translators: %s is the link to the API token documentation +#: inc/integrations/host-providers/class-enhance-host-provider.php:108 +#, php-format +msgid "Generate an API token in your Enhance Control Panel under Settings → API Tokens. Learn more" +msgstr "" + +#: inc/integrations/host-providers/class-enhance-host-provider.php:111 msgid "Your bearer token" msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:107 +#: inc/integrations/host-providers/class-enhance-host-provider.php:114 msgid "Enhance API URL" msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:108 -msgid "e.g. https://your-enhance-server.com" +#: inc/integrations/host-providers/class-enhance-host-provider.php:115 +msgid "The API URL of your Enhance Control Panel (e.g., https://your-enhance-server.com/api)." msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:111 -#: views/wizards/host-integrations/runcloud-instructions.php:12 -msgid "Server ID" +#: inc/integrations/host-providers/class-enhance-host-provider.php:116 +msgid "e.g. https://your-enhance-server.com/api" +msgstr "" + +#: inc/integrations/host-providers/class-enhance-host-provider.php:122 +msgid "Organization ID" +msgstr "" + +#: inc/integrations/host-providers/class-enhance-host-provider.php:123 +msgid "The UUID of your organization. You can find this in your Enhance Control Panel URL when viewing the organization (e.g., /org/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)." msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:112 -msgid "UUID of your server" +#: inc/integrations/host-providers/class-enhance-host-provider.php:124 +#: inc/integrations/host-providers/class-enhance-host-provider.php:131 +msgid "e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:246 -msgid "Server ID is not configured" +#: inc/integrations/host-providers/class-enhance-host-provider.php:130 +msgid "Website ID" msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:260 +#: inc/integrations/host-providers/class-enhance-host-provider.php:132 +msgid "The UUID of the website where domains should be added. You can find this in your Enhance Control Panel URL when viewing a website (e.g., /websites/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)." +msgstr "" + +#: inc/integrations/host-providers/class-enhance-host-provider.php:283 +msgid "Organization ID is not configured" +msgstr "" + +#: inc/integrations/host-providers/class-enhance-host-provider.php:289 +msgid "Website ID is not configured" +msgstr "" + +#: inc/integrations/host-providers/class-enhance-host-provider.php:303 msgid "Connection successful" msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:264 -msgid "Failed to connect to Enhance API" +#. Translators: %s the full error message. +#: inc/integrations/host-providers/class-enhance-host-provider.php:308 +#, php-format +msgid "Failed to connect to Enhance API: %s" msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:388 +#: inc/integrations/host-providers/class-enhance-host-provider.php:432 msgid "Enhance is a modern control panel that provides powerful hosting automation and management capabilities." msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:412 +#: inc/integrations/host-providers/class-enhance-host-provider.php:456 msgid "Add domains to Enhance Control Panel whenever a new domain mapping gets created on your network" msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:413 +#: inc/integrations/host-providers/class-enhance-host-provider.php:457 msgid "SSL certificates will be automatically provisioned via LetsEncrypt when DNS resolves" msgstr "" -#: inc/integrations/host-providers/class-enhance-host-provider.php:419 +#: inc/integrations/host-providers/class-enhance-host-provider.php:463 msgid "Add subdomains to Enhance Control Panel whenever a new site gets created on your network" msgstr "" -#: inc/integrations/host-providers/class-gridpane-host-provider.php:209 -#: inc/integrations/host-providers/class-gridpane-host-provider.php:217 +#: inc/integrations/host-providers/class-gridpane-host-provider.php:212 +#: inc/integrations/host-providers/class-gridpane-host-provider.php:220 msgid "We were not able to successfully establish a connection." msgstr "" -#: inc/integrations/host-providers/class-gridpane-host-provider.php:224 +#: inc/integrations/host-providers/class-gridpane-host-provider.php:227 msgid "Connection successfully established." msgstr "" -#: inc/integrations/host-providers/class-gridpane-host-provider.php:248 +#: inc/integrations/host-providers/class-gridpane-host-provider.php:251 msgid "GridPane is the world's first hosting control panel built exclusively for serious WordPress professionals." msgstr "" @@ -16310,6 +16470,61 @@ msgstr "" msgid "HTTP %1$d from Hestia API: %2$s" msgstr "" +#: inc/integrations/host-providers/class-rocket-host-provider.php:92 +msgid "Rocket.net Account Email" +msgstr "" + +#: inc/integrations/host-providers/class-rocket-host-provider.php:93 +msgid "Your Rocket.net account email address." +msgstr "" + +#: inc/integrations/host-providers/class-rocket-host-provider.php:94 +msgid "e.g. me@example.com" +msgstr "" + +#: inc/integrations/host-providers/class-rocket-host-provider.php:98 +msgid "Rocket.net Password" +msgstr "" + +#: inc/integrations/host-providers/class-rocket-host-provider.php:99 +msgid "Your Rocket.net account password." +msgstr "" + +#: inc/integrations/host-providers/class-rocket-host-provider.php:100 +#: views/checkout/partials/inline-login-prompt.php:38 +msgid "Enter your password" +msgstr "" + +#: inc/integrations/host-providers/class-rocket-host-provider.php:104 +msgid "Rocket.net Site ID" +msgstr "" + +#: inc/integrations/host-providers/class-rocket-host-provider.php:105 +msgid "The Site ID from your Rocket.net control panel." +msgstr "" + +#: inc/integrations/host-providers/class-rocket-host-provider.php:106 +msgid "e.g. 12345" +msgstr "" + +#: inc/integrations/host-providers/class-rocket-host-provider.php:230 +msgid "Successfully connected to Rocket.net API!" +msgstr "" + +#. translators: %1$d: HTTP response code. +#: inc/integrations/host-providers/class-rocket-host-provider.php:239 +#, php-format +msgid "Connection failed with HTTP code %1$d: %2$s" +msgstr "" + +#: inc/integrations/host-providers/class-rocket-host-provider.php:337 +msgid "Failed to authenticate with Rocket.net API" +msgstr "" + +#: inc/integrations/host-providers/class-rocket-host-provider.php:416 +msgid "Rocket.net is a fully API-driven managed WordPress hosting platform built for speed, security, and scalability. With edge-first private cloud infrastructure and automatic SSL management, Rocket.net makes it easy to deploy and manage WordPress sites at scale." +msgstr "" + #: inc/integrations/host-providers/class-runcloud-host-provider.php:82 msgid "It looks like you are using V2 of the Runcloud API which has been discontinued. You must setup a API token to use V3 of the API for the Runcloud integration to work." msgstr "" @@ -16348,33 +16563,37 @@ msgstr "" msgid "ServerPilot is a cloud service for hosting WordPress and other PHP websites on servers at DigitalOcean, Amazon, Google, or any other server provider. You can think of ServerPilot as a modern, centralized hosting control panel." msgstr "" -#: inc/integrations/host-providers/class-wpengine-host-provider.php:175 +#: inc/integrations/host-providers/class-wpengine-host-provider.php:180 msgid "WP Engine drives your business forward faster with the first and only WordPress Digital Experience Platform. We offer the best WordPress hosting and developer experience on a proven, reliable architecture that delivers unparalleled speed, scalability, and security for your sites." msgstr "" -#: inc/integrations/host-providers/class-wpengine-host-provider.php:177 +#: inc/integrations/host-providers/class-wpengine-host-provider.php:182 msgid "We recommend to enter in contact with WP Engine support to ask for a Wildcard domain if you are using a subdomain install." msgstr "" +#: inc/integrations/host-providers/class-wpengine-host-provider.php:206 +msgid "Class WPE_API is not installed." +msgstr "" + #. translators: The %s placeholder will be replaced with the domain name. -#: inc/integrations/host-providers/class-wpmudev-host-provider.php:160 +#: inc/integrations/host-providers/class-wpmudev-host-provider.php:161 #, php-format msgid "An error occurred while trying to add the custom domain %s to WPMU Dev hosting." msgstr "" #. translators: The %1$s will be replaced with the domain name and %2$s is the error message. -#: inc/integrations/host-providers/class-wpmudev-host-provider.php:168 +#: inc/integrations/host-providers/class-wpmudev-host-provider.php:169 #, php-format msgid "An error occurred while trying to add the custom domain %1$s to WPMU Dev hosting: %2$s" msgstr "" #. translators: The %s placeholder will be replaced with the domain name. -#: inc/integrations/host-providers/class-wpmudev-host-provider.php:172 +#: inc/integrations/host-providers/class-wpmudev-host-provider.php:173 #, php-format msgid "Domain %s added to WPMU Dev hosting successfully." msgstr "" -#: inc/integrations/host-providers/class-wpmudev-host-provider.php:256 +#: inc/integrations/host-providers/class-wpmudev-host-provider.php:257 msgid "WPMU DEV is one of the largest companies in the WordPress space. Founded in 2004, it was one of the first companies to scale the Website as a Service model with products such as Edublogs and CampusPress." msgstr "" @@ -17340,252 +17559,252 @@ msgstr "" msgid "Invalid verification key." msgstr "" -#: inc/managers/class-domain-manager.php:344 +#: inc/managers/class-domain-manager.php:345 msgid "Domain Mapping Settings" msgstr "" -#: inc/managers/class-domain-manager.php:345 +#: inc/managers/class-domain-manager.php:346 msgid "Define the domain mapping settings for your network." msgstr "" -#: inc/managers/class-domain-manager.php:354 +#: inc/managers/class-domain-manager.php:355 msgid "Enable Domain Mapping?" msgstr "" -#: inc/managers/class-domain-manager.php:355 +#: inc/managers/class-domain-manager.php:356 msgid "Do you want to enable domain mapping?" msgstr "" -#: inc/managers/class-domain-manager.php:365 +#: inc/managers/class-domain-manager.php:366 msgid "Force Admin Redirect" msgstr "" -#: inc/managers/class-domain-manager.php:366 +#: inc/managers/class-domain-manager.php:367 msgid "Select how you want your users to access the admin panel if they have mapped domains." msgstr "" -#: inc/managers/class-domain-manager.php:366 +#: inc/managers/class-domain-manager.php:367 msgid "Force Redirect to Mapped Domain: your users with mapped domains will be redirected to theirdomain.com/wp-admin, even if they access using yournetworkdomain.com/wp-admin." msgstr "" -#: inc/managers/class-domain-manager.php:366 +#: inc/managers/class-domain-manager.php:367 msgid "Force Redirect to Network Domain: your users with mapped domains will be redirect to yournetworkdomain.com/wp-admin, even if they access using theirdomain.com/wp-admin." msgstr "" -#: inc/managers/class-domain-manager.php:372 +#: inc/managers/class-domain-manager.php:373 msgid "Allow access to the admin by both mapped domain and network domain" msgstr "" -#: inc/managers/class-domain-manager.php:373 +#: inc/managers/class-domain-manager.php:374 msgid "Force Redirect to Mapped Domain" msgstr "" -#: inc/managers/class-domain-manager.php:374 +#: inc/managers/class-domain-manager.php:375 msgid "Force Redirect to Network Domain" msgstr "" -#: inc/managers/class-domain-manager.php:383 +#: inc/managers/class-domain-manager.php:384 msgid "Enable Custom Domains?" msgstr "" -#: inc/managers/class-domain-manager.php:384 +#: inc/managers/class-domain-manager.php:385 msgid "Toggle this option if you wish to allow end-customers to add their own domains. This can be controlled on a plan per plan basis." msgstr "" -#: inc/managers/class-domain-manager.php:397 +#: inc/managers/class-domain-manager.php:398 msgid "Add New Domain Instructions" msgstr "" -#: inc/managers/class-domain-manager.php:398 +#: inc/managers/class-domain-manager.php:399 msgid "Display a customized message with instructions for the mapping and alerting the end-user of the risks of mapping a misconfigured domain." msgstr "" -#: inc/managers/class-domain-manager.php:399 +#: inc/managers/class-domain-manager.php:400 msgid "You can use the placeholder %NETWORK_DOMAIN% and %NETWORK_IP%. HTML is allowed." msgstr "" -#: inc/managers/class-domain-manager.php:417 +#: inc/managers/class-domain-manager.php:418 msgid "DNS Check Interval" msgstr "" -#: inc/managers/class-domain-manager.php:418 +#: inc/managers/class-domain-manager.php:419 msgid "Set the interval in seconds between DNS and SSL certificate checks for domains." msgstr "" -#: inc/managers/class-domain-manager.php:419 +#: inc/managers/class-domain-manager.php:420 msgid "Minimum: 10 seconds, Maximum: 300 seconds (5 minutes). Default: 300 seconds." msgstr "" -#: inc/managers/class-domain-manager.php:437 +#: inc/managers/class-domain-manager.php:438 msgid "Create www Subdomain Automatically?" msgstr "" -#: inc/managers/class-domain-manager.php:438 +#: inc/managers/class-domain-manager.php:439 msgid "Control when www subdomains should be automatically created for mapped domains." msgstr "" -#: inc/managers/class-domain-manager.php:439 +#: inc/managers/class-domain-manager.php:440 msgid "This setting applies to all hosting integrations and determines when a www version of the domain should be automatically created." msgstr "" -#: inc/managers/class-domain-manager.php:443 +#: inc/managers/class-domain-manager.php:444 msgid "Always - Create www subdomain for all domains" msgstr "" -#: inc/managers/class-domain-manager.php:444 +#: inc/managers/class-domain-manager.php:445 msgid "Only for main domains (e.g., example.com but not subdomain.example.com)" msgstr "" -#: inc/managers/class-domain-manager.php:445 +#: inc/managers/class-domain-manager.php:446 msgid "Never - Do not automatically create www subdomains" msgstr "" -#: inc/managers/class-domain-manager.php:498 +#: inc/managers/class-domain-manager.php:499 msgid "Single Sign-On Settings" msgstr "" -#: inc/managers/class-domain-manager.php:499 +#: inc/managers/class-domain-manager.php:500 msgid "Settings to configure the Single Sign-On functionality of Ultimate Multisite, responsible for keeping customers and admins logged in across all network domains." msgstr "" -#: inc/managers/class-domain-manager.php:508 +#: inc/managers/class-domain-manager.php:509 msgid "Enable Single Sign-On" msgstr "" -#: inc/managers/class-domain-manager.php:509 +#: inc/managers/class-domain-manager.php:510 msgid "Enables the Single Sign-on functionality." msgstr "" -#: inc/managers/class-domain-manager.php:519 +#: inc/managers/class-domain-manager.php:520 msgid "Restrict SSO Checks to Login Pages" msgstr "" -#: inc/managers/class-domain-manager.php:520 +#: inc/managers/class-domain-manager.php:521 msgid "The Single Sign-on feature adds one extra ajax calls to every page load on sites with custom domains active to check if it should perform an auth loopback. You can restrict these extra calls to the login pages of sub-sites using this option. If enabled, SSO will only work on login pages." msgstr "" -#: inc/managers/class-domain-manager.php:533 +#: inc/managers/class-domain-manager.php:534 msgid "Enable SSO Loading Overlay" msgstr "" -#: inc/managers/class-domain-manager.php:534 +#: inc/managers/class-domain-manager.php:535 msgid "When active, a loading overlay will be added on-top of the site currently being viewed while the SSO auth loopback is performed on the background." msgstr "" -#: inc/managers/class-domain-manager.php:547 +#: inc/managers/class-domain-manager.php:548 msgid "Enable Magic Links" msgstr "" -#: inc/managers/class-domain-manager.php:548 +#: inc/managers/class-domain-manager.php:549 msgid "Enables magic link authentication for custom domains. Magic links provide a fallback authentication method for browsers that don't support third-party cookies. When enabled, dashboard and site links will automatically log users in when accessing sites with custom domains. Tokens are cryptographically secure, one-time use, and expire after 10 minutes." msgstr "" -#: inc/managers/class-domain-manager.php:564 +#: inc/managers/class-domain-manager.php:565 msgid "Cool! You're about to make this site accessible using your own domain name!" msgstr "" -#: inc/managers/class-domain-manager.php:566 +#: inc/managers/class-domain-manager.php:567 msgid "For that to work, you'll need to create a new CNAME record pointing to %NETWORK_DOMAIN% on your DNS manager." msgstr "" -#: inc/managers/class-domain-manager.php:568 +#: inc/managers/class-domain-manager.php:569 msgid "After you finish that step, come back to this screen and click the button below." msgstr "" #. translators: %s is the domain name -#: inc/managers/class-domain-manager.php:665 +#: inc/managers/class-domain-manager.php:666 #, php-format msgid "Starting Check for %s" msgstr "" -#: inc/managers/class-domain-manager.php:675 +#: inc/managers/class-domain-manager.php:676 msgid "- DNS propagation finished, advancing domain to next step..." msgstr "" #. translators: %d is the number of minutes to try again. -#: inc/managers/class-domain-manager.php:702 +#: inc/managers/class-domain-manager.php:703 #, php-format msgid "- DNS propagation checks tried for the max amount of times (5 times, one every %d minutes). Marking as failed." msgstr "" #. translators: %d is the number of minutes before trying again. -#: inc/managers/class-domain-manager.php:711 +#: inc/managers/class-domain-manager.php:712 #, php-format msgid "- DNS propagation not finished, retrying in %d minutes..." msgstr "" -#: inc/managers/class-domain-manager.php:736 +#: inc/managers/class-domain-manager.php:737 msgid "- Valid SSL cert found. Marking domain as done." msgstr "" #. translators: %d is the number of minutes to try again. -#: inc/managers/class-domain-manager.php:752 +#: inc/managers/class-domain-manager.php:754 #, php-format msgid "- SSL checks tried for the max amount of times (5 times, one every %d minutes). Marking as ready without SSL." msgstr "" #. translators: %d is the number of minutes before trying again. -#: inc/managers/class-domain-manager.php:761 +#: inc/managers/class-domain-manager.php:763 #, php-format msgid "- SSL Cert not found, retrying in %d minute(s)..." msgstr "" -#: inc/managers/class-domain-manager.php:850 +#: inc/managers/class-domain-manager.php:852 msgid "A valid domain was not passed." msgstr "" -#: inc/managers/class-domain-manager.php:863 -#: inc/managers/class-domain-manager.php:872 +#: inc/managers/class-domain-manager.php:865 +#: inc/managers/class-domain-manager.php:874 msgid "Not able to fetch DNS entries." msgstr "" -#: inc/managers/class-domain-manager.php:923 +#: inc/managers/class-domain-manager.php:925 msgid "Invalid Integration ID" msgstr "" #. translators: %s is the name of the missing constant -#: inc/managers/class-domain-manager.php:936 +#: inc/managers/class-domain-manager.php:938 #, php-format msgid "The necessary constants were not found on your wp-config.php file: %s" msgstr "" #. translators: %1$s: Protocol label (HTTPS with SSL verification, HTTPS without SSL verification, HTTP), %2$s: URL being tested -#: inc/managers/class-domain-manager.php:1053 +#: inc/managers/class-domain-manager.php:1060 #, php-format msgid "Testing domain verification via Loopback using %1$s: %2$s" msgstr "" #. translators: %1$s: Protocol label (HTTPS with SSL verification, HTTPS without SSL verification, HTTP), %2$s: Error Message -#: inc/managers/class-domain-manager.php:1076 +#: inc/managers/class-domain-manager.php:1083 #, php-format msgid "Failed to connect via %1$s: %2$s" msgstr "" #. translators: %1$s: Protocol label (HTTPS with SSL verification, HTTPS without SSL verification, HTTP), %2$s: HTTP Response Code -#: inc/managers/class-domain-manager.php:1094 +#: inc/managers/class-domain-manager.php:1101 #, php-format msgid "Loopback request via %1$s returned HTTP %2$d" msgstr "" #. translators: %1$s: Protocol label (HTTPS with SSL verification, HTTPS without SSL verification, HTTP), %2$s: Json error, %3$s part of the response -#: inc/managers/class-domain-manager.php:1111 +#: inc/managers/class-domain-manager.php:1118 #, php-format msgid "Loopback response via %1$s is not valid JSON: %2$s : %3$s" msgstr "" #. translators: %1$s: Protocol label (HTTPS with SSL verification, HTTPS without SSL verification, HTTP), %2$s: Domain ID number -#: inc/managers/class-domain-manager.php:1127 +#: inc/managers/class-domain-manager.php:1134 #, php-format msgid "Domain verification successful via Loopback using %1$s. Domain ID %2$d confirmed." msgstr "" #. translators: %1$s: Protocol label (HTTPS with SSL verification, HTTPS without SSL verification, HTTP), %2$s: Domain ID number, %3$s Domain ID number -#: inc/managers/class-domain-manager.php:1140 +#: inc/managers/class-domain-manager.php:1147 #, php-format msgid "Loopback response via %1$s did not contain expected domain ID. Expected: %2$d, Got: %3$s" msgstr "" -#: inc/managers/class-domain-manager.php:1151 +#: inc/managers/class-domain-manager.php:1158 msgid "Domain verification failed via loopback on all protocols (HTTPS with SSL, HTTPS without SSL, HTTP)." msgstr "" @@ -18066,12 +18285,12 @@ msgstr "" msgid "This is the number of sites the customer will be able to create under this membership." msgstr "" -#: inc/managers/class-membership-manager.php:134 -#: inc/managers/class-membership-manager.php:157 -#: inc/managers/class-membership-manager.php:191 -#: inc/managers/class-membership-manager.php:198 -#: inc/managers/class-membership-manager.php:310 -#: inc/managers/class-membership-manager.php:381 +#: inc/managers/class-membership-manager.php:137 +#: inc/managers/class-membership-manager.php:160 +#: inc/managers/class-membership-manager.php:194 +#: inc/managers/class-membership-manager.php:201 +#: inc/managers/class-membership-manager.php:313 +#: inc/managers/class-membership-manager.php:384 #: inc/managers/class-payment-manager.php:336 #: inc/managers/class-payment-manager.php:381 #: inc/ui/class-site-actions-element.php:594 @@ -18229,7 +18448,7 @@ msgstr "" msgid "You can only use numeric fields to generate hashes." msgstr "" -#: inc/models/class-base-model.php:769 +#: inc/models/class-base-model.php:762 msgid "This method expects an array as argument." msgstr "" @@ -18321,7 +18540,7 @@ msgstr "" msgid "%1$s OFF on Setup Fees" msgstr "" -#: inc/models/class-domain.php:581 +#: inc/models/class-domain.php:582 msgid "Domain deleted and logs cleared..." msgstr "" @@ -19262,7 +19481,7 @@ msgid "Remember Me Description" msgstr "" #: inc/ui/class-login-form-element.php:271 -#: inc/ui/class-login-form-element.php:370 +#: inc/ui/class-login-form-element.php:393 msgid "Keep me logged in for two weeks." msgstr "" @@ -19676,6 +19895,12 @@ msgstr "" msgid "Upload Image" msgstr "" +#: views/admin-pages/fields/field-password.php:41 +#: views/checkout/fields/field-password.php:37 +#: assets/js/wu-password-toggle.js:48 +msgid "Show password" +msgstr "" + #: views/admin-pages/fields/field-repeater.php:143 msgid "Add new Line" msgstr "" @@ -20089,14 +20314,10 @@ msgstr "" msgid "No Targets" msgstr "" -#: views/checkout/fields/field-password.php:36 +#: views/checkout/fields/field-password.php:45 msgid "Strength Meter" msgstr "" -#: views/checkout/partials/inline-login-prompt.php:38 -msgid "Enter your password" -msgstr "" - #: views/checkout/partials/pricing-table-list.php:19 #: views/checkout/templates/pricing-table/legacy.php:106 msgid "No Products Found." @@ -20123,6 +20344,7 @@ msgid "PayPal Status:" msgstr "" #: views/checkout/paypal/confirm.php:85 +#: views/wizards/host-integrations/rocket-instructions.php:39 msgid "Email:" msgstr "" @@ -21329,6 +21551,171 @@ msgstr "" msgid "Finish!" msgstr "" +#: views/wizards/host-integrations/rocket-instructions.php:12 +msgid "You'll need to get your" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:12 +msgid "Rocket.net account credentials" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:12 +msgid "from your Rocket.net control panel." +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:16 +msgid "About the Rocket.net API" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:20 +msgid "Rocket.net is one of the few Managed WordPress platforms that is 100% API-driven. The same API that powers their control panel is available to you for managing domains, SSL certificates, and more." +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:25 +msgid "Note:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:26 +msgid "The Rocket.net API uses JWT authentication. Your credentials are only used to generate secure access tokens and are never stored by Ultimate Multisite." +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:31 +msgid "Step 1: Prepare Your Account Credentials" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:35 +msgid "You will need:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:39 +msgid "Your Rocket.net account email address" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:40 +msgid "Password:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:40 +#: views/wizards/host-integrations/rocket-instructions.php:76 +msgid "Your Rocket.net account password" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:41 +msgid "Site ID:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:41 +msgid "The numeric ID of your WordPress site on Rocket.net" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:46 +msgid "Security Tip:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:47 +msgid "Consider creating a dedicated Rocket.net user account specifically for API access with appropriate permissions. This follows security best practices for API integrations." +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:52 +msgid "Step 2: Finding Your Site ID" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:56 +msgid "To find your Site ID:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:60 +msgid "Log in to your Rocket.net control panel" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:61 +msgid "Navigate to your WordPress site" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:62 +msgid "Look at the URL in your browser - the Site ID is the numeric value in the URL path" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:63 +msgid "For example, if the URL is" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:63 +msgid "your Site ID is" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:67 +msgid "Step 3: Configure the Integration" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:71 +msgid "In the next step, you will enter:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:75 +msgid "WU_ROCKET_EMAIL:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:75 +msgid "Your Rocket.net account email" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:76 +msgid "WU_ROCKET_PASSWORD:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:77 +msgid "WU_ROCKET_SITE_ID:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:77 +msgid "The Site ID you found in the previous step" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:81 +msgid "These values will be added to your wp-config.php file as PHP constants for secure storage." +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:85 +msgid "What This Integration Does" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:89 +msgid "Once configured, this integration will:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:93 +msgid "Automatically add custom domains to your Rocket.net site when mapped in Ultimate Multisite" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:94 +msgid "Automatically remove domains from Rocket.net when unmapped" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:95 +msgid "Enable automatic SSL certificate provisioning for all mapped domains" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:96 +msgid "Keep your Rocket.net configuration in sync with your WordPress multisite network" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:100 +msgid "Additional Resources" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:104 +msgid "For more information about the Rocket.net API:" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:110 +msgid "Rocket.net API Guide" +msgstr "" + +#: views/wizards/host-integrations/rocket-instructions.php:115 +msgid "Rocket.net API Documentation" +msgstr "" + #: views/wizards/host-integrations/runcloud-instructions.php:12 #: views/wizards/host-integrations/runcloud-instructions.php:35 msgid "API Token" @@ -21338,6 +21725,10 @@ msgstr "" msgid "as well as find the" msgstr "" +#: views/wizards/host-integrations/runcloud-instructions.php:12 +msgid "Server ID" +msgstr "" + #: views/wizards/host-integrations/runcloud-instructions.php:12 msgid "APP ID" msgstr "" @@ -21578,3 +21969,7 @@ msgstr "" #: views/wizards/setup/support_terms.php:40 msgid "Support for 3rd party plugins (i.e. plugins you install yourself later on)" msgstr "" + +#: assets/js/wu-password-toggle.js:41 +msgid "Hide password" +msgstr "" diff --git a/package-lock.json b/package-lock.json index a1213f86..12692f4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ultimate-multisite", - "version": "2.4.8", + "version": "2.4.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ultimate-multisite", - "version": "2.4.8", + "version": "2.4.9", "dependencies": { "apexcharts": "^5.2.0", "shepherd.js": "^14.5.0" @@ -21,6 +21,7 @@ "cypress-mailpit": "^1.4.0", "eslint": "^8.57.1", "globals": "^16.5.0", + "lint-staged": "^16.2.7", "npm-run-all": "^4.1.5", "stylelint": "^16.26.1", "stylelint-config-standard": "^39.0.1", @@ -5815,6 +5816,19 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6879,6 +6893,13 @@ "dev": true, "license": "MIT" }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, "node_modules/execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", @@ -7322,6 +7343,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -8788,6 +8822,309 @@ "dev": true, "license": "MIT" }, + "node_modules/lint-staged": { + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.2", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/lint-staged/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/lint-staged/node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/lint-staged/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/listr2": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", @@ -9109,6 +9446,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -9182,6 +9532,19 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -11187,6 +11550,16 @@ "dev": true, "license": "MIT" }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", diff --git a/package.json b/package.json index ae706a60..427f9706 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "cypress-mailpit": "^1.4.0", "eslint": "^8.57.1", "globals": "^16.5.0", + "lint-staged": "^16.2.7", "npm-run-all": "^4.1.5", "stylelint": "^16.26.1", "stylelint-config-standard": "^39.0.1", @@ -92,5 +93,13 @@ "dependencies": { "apexcharts": "^5.2.0", "shepherd.js": "^14.5.0" + }, + "lint-staged": { + "assets/js/**/*.js": [ + "eslint --fix --ignore-pattern '*.min.js'" + ], + "assets/css/**/*.css": [ + "stylelint --fix --ignore-pattern '*.min.css'" + ] } } diff --git a/readme.txt b/readme.txt index 03520dba..64452c04 100644 --- a/readme.txt +++ b/readme.txt @@ -211,6 +211,18 @@ This plugin connects to several external services to provide its functionality. All external service connections are clearly disclosed to users during setup, and most services are optional or can be configured based on your chosen hosting provider and payment methods. += Usage Tracking (Opt-In) = + +**Ultimate Multisite Anonymous Telemetry** +- Service: Anonymous usage data collection to improve the plugin +- Data sent: PHP version, WordPress version, MySQL version, server type, plugin version, active addon slugs, network type (subdomain/subdirectory), anonymized usage counts (ranges only, e.g., "11-50 sites"), active payment gateways, and sanitized error logs +- Data NOT sent: Domain names, URLs, customer information, personal data, payment amounts, API keys, IP addresses, or exact counts +- When: Weekly (if enabled) and when errors occur (if enabled) +- This feature is DISABLED by default and requires explicit opt-in +- You can enable or disable this at any time in Settings > Other > Help Improve Ultimate Multisite +- Service URL: https://ultimatemultisite.com/wp-json/wu-telemetry/v1/track +- Privacy Policy: https://developer.ultimatemultisite.com/privacy-policy/ + == Screenshots == 1. One of many settings pages. diff --git a/views/dashboard-widgets/thank-you.php b/views/dashboard-widgets/thank-you.php index 6cc9a02d..c9e9e65b 100644 --- a/views/dashboard-widgets/thank-you.php +++ b/views/dashboard-widgets/thank-you.php @@ -255,7 +255,7 @@ + alt="Thumbnail of Site" /> diff --git a/views/settings/widget-settings-body.php b/views/settings/widget-settings-body.php index f0557f5e..176e92f3 100644 --- a/views/settings/widget-settings-body.php +++ b/views/settings/widget-settings-body.php @@ -275,19 +275,17 @@ class="wu-bg-gray-100 wu--mt-1 wu--mx-3 wu-p-4 wu-border-solid wu-border-b wu-bo - + + +

- With this option enabled, every time your installation runs into an error related to Ultimate Multisite, - that - error data will be sent to us. That way we can review, debug, and fix issues without you having to - manually report anything. No sensitive data gets collected, only environmental stuff (e.g. if this is - this is a subdomain network, etc). + + .

From d471b2fc8fe55d9b21d548405f81a3686f638c43 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 15 Jan 2026 18:14:37 -0700 Subject: [PATCH 8/8] Address CodeRabbit review feedback and bump version to 2.4.10 - Add backslash to special character regex for Defender Pro compatibility - Add null guards for i18n object in password strength JS - Fix pre-commit hook to only show success when lint-staged runs - Fix plugin slug in rating notice manager review URL - Send JSON response unconditionally in publish_pending_site for non-fastcgi - Remove unused enhance-integration and payment-status-poll JS files - Update changelog and version to 2.4.10 Co-Authored-By: Claude Opus 4.5 --- .githooks/pre-commit | 3 +- assets/js/enhance-integration.js | 113 ----------- assets/js/enhance-integration.min.js | 1 - assets/js/payment-status-poll.js | 188 ------------------- assets/js/payment-status-poll.min.js | 1 - assets/js/wu-password-strength.js | 10 +- assets/js/wu-password-strength.min.js | 2 +- inc/managers/class-membership-manager.php | 13 +- inc/managers/class-rating-notice-manager.php | 2 +- readme.txt | 12 +- ultimate-multisite.php | 2 +- 11 files changed, 31 insertions(+), 316 deletions(-) delete mode 100644 assets/js/enhance-integration.js delete mode 100644 assets/js/enhance-integration.min.js delete mode 100644 assets/js/payment-status-poll.js delete mode 100644 assets/js/payment-status-poll.min.js diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 50743749..b8cf061b 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -11,9 +11,8 @@ echo "Running pre-commit checks..." if command -v npx &> /dev/null; then echo "Running lint-staged..." npx lint-staged + echo "Pre-commit checks passed!" else echo "Warning: npx not found. Skipping lint-staged." echo "Run 'npm install' to set up the development environment." fi - -echo "Pre-commit checks passed!" diff --git a/assets/js/enhance-integration.js b/assets/js/enhance-integration.js deleted file mode 100644 index f4169df7..00000000 --- a/assets/js/enhance-integration.js +++ /dev/null @@ -1,113 +0,0 @@ -/* global jQuery, wu_enhance_data */ -/** - * Enhance Control Panel Integration - * - * Handles dynamic loading of websites from the Enhance API. - * - * @since 2.0.0 - * @param {Object} $ jQuery object. - */ -(function($) { - 'use strict'; - - const EnhanceIntegration = { - /** - * Initialize the integration. - */ - init() { - this.bindEvents(); - this.checkInitialState(); - }, - - /** - * Bind event handlers. - */ - bindEvents() { - $('#wu-enhance-load-data').on('click', this.loadWebsites.bind(this)); - }, - - /** - * Check initial state and enable/disable elements accordingly. - */ - checkInitialState() { - const websiteId = $('#wu_enhance_website_id').val(); - if (websiteId) { - $('#wu_enhance_website_id').prop('disabled', false); - } - }, - - /** - * Load websites from the Enhance API. - * - * @param {Event} e Click event. - */ - loadWebsites(e) { - e.preventDefault(); - - const apiToken = $('#wu_enhance_api_token').val(); - const apiUrl = $('#wu_enhance_api_url').val(); - const orgId = $('#wu_enhance_org_id').val(); - - if (! apiToken || ! apiUrl || ! orgId) { - $('#wu-enhance-loader-status').text(wu_enhance_data.i18n.enter_credentials).css('color', 'red'); - return; - } - - const self = this; - const $btn = $('#wu-enhance-load-data'); - const $status = $('#wu-enhance-loader-status'); - - $btn.prop('disabled', true); - $status.text(wu_enhance_data.i18n.loading_websites).css('color', ''); - - $.post(wu_enhance_data.ajax_url, { - action: 'wu_enhance_get_websites', - nonce: wu_enhance_data.nonce, - api_token: apiToken, - api_url: apiUrl, - org_id: orgId - }).done(function(response) { - if (response.success && response.data.websites) { - self.populateWebsites(response.data.websites); - $('#wu_enhance_website_id').prop('disabled', false); - $status.text(wu_enhance_data.i18n.websites_loaded).css('color', 'green'); - - // If only one website, auto-select it - if (response.data.websites.length === 1) { - $('#wu_enhance_website_id').val(response.data.websites[ 0 ].id); - } - } else { - $status.text(response.data.message || wu_enhance_data.i18n.websites_failed).css('color', 'red'); - } - }).fail(function() { - $status.text(wu_enhance_data.i18n.request_failed).css('color', 'red'); - }).always(function() { - $btn.prop('disabled', false); - }); - }, - - /** - * Populate the websites dropdown. - * - * @param {Array} websites List of websites from API. - */ - populateWebsites(websites) { - const $select = $('#wu_enhance_website_id'); - $select.empty().append(''); - - $.each(websites, function(i, website) { - $select.append( - '' - ); - }); - } - }; - - // Initialize when document is ready - $(document).ready(function() { - if ($('#wu-enhance-load-data').length) { - EnhanceIntegration.init(); - } - }); - -}(jQuery)); diff --git a/assets/js/enhance-integration.min.js b/assets/js/enhance-integration.min.js deleted file mode 100644 index bf5177cb..00000000 --- a/assets/js/enhance-integration.min.js +++ /dev/null @@ -1 +0,0 @@ -(c=>{var e={init:function(){this.bindEvents(),this.checkInitialState()},bindEvents:function(){c("#wu-enhance-load-data").on("click",this.loadWebsites.bind(this))},checkInitialState:function(){c("#wu_enhance_website_id").val()&&c("#wu_enhance_website_id").prop("disabled",!1)},loadWebsites:function(e){e.preventDefault();var a=this,n=c("#wu-enhance-load-data"),t=c("#wu-enhance-loader-status"),e=c("#wu_enhance_api_token").val(),i=c("#wu_enhance_api_url").val(),s=c("#wu_enhance_org_id").val();e&&i&&s?(n.prop("disabled",!0),t.text(wu_enhance_data.i18n.loading_websites).css("color",""),c.post(wu_enhance_data.ajax_url,{action:"wu_enhance_get_websites",nonce:wu_enhance_data.nonce,api_token:e,api_url:i,org_id:s}).done(function(e){e.success&&e.data.websites?(a.populateWebsites(e.data.websites),c("#wu_enhance_website_id").prop("disabled",!1),t.text(wu_enhance_data.i18n.websites_loaded).css("color","green"),1===e.data.websites.length&&c("#wu_enhance_website_id").val(e.data.websites[0].id)):t.text(e.data.message||wu_enhance_data.i18n.websites_failed).css("color","red")}).fail(function(){t.text(wu_enhance_data.i18n.request_failed).css("color","red")}).always(function(){n.prop("disabled",!1)})):t.text(wu_enhance_data.i18n.enter_credentials).css("color","red")},populateWebsites:function(e){var n=c("#wu_enhance_website_id");n.empty().append('"),c.each(e,function(e,a){n.append('")})}};c(document).ready(function(){c("#wu-enhance-load-data").length&&e.init()})})(jQuery); \ No newline at end of file diff --git a/assets/js/payment-status-poll.js b/assets/js/payment-status-poll.js deleted file mode 100644 index c65bf1ac..00000000 --- a/assets/js/payment-status-poll.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Payment Status Polling for Thank You Page. - * - * Polls the server to check if a pending payment has been completed. - * This is a fallback mechanism when webhooks are delayed or not working. - * - * @since 2.x.x - */ -/* global wu_payment_poll, jQuery */ -(function ($) { - 'use strict'; - - if (typeof wu_payment_poll === 'undefined') { - return; - } - - const config = { - paymentHash: wu_payment_poll.payment_hash || '', - ajaxUrl: wu_payment_poll.ajax_url || '', - pollInterval: parseInt(wu_payment_poll.poll_interval, 10) || 3000, - maxAttempts: parseInt(wu_payment_poll.max_attempts, 10) || 20, - statusSelector: wu_payment_poll.status_selector || '.wu-payment-status', - successRedirect: wu_payment_poll.success_redirect || '', - }; - - let attempts = 0; - let pollTimer = null; - - /** - * Check payment status via AJAX. - */ - function checkPaymentStatus() { - attempts++; - - if (attempts > config.maxAttempts) { - stopPolling(); - updateStatusMessage('timeout'); - return; - } - - $.ajax({ - url: config.ajaxUrl, - type: 'POST', - data: { - action: 'wu_check_payment_status', - payment_hash: config.paymentHash, - }, - success (response) { - if (response.success && response.data) { - const status = response.data.status; - - if (status === 'completed') { - stopPolling(); - updateStatusMessage('completed'); - - // Reload page or redirect after a short delay - setTimeout(function () { - if (config.successRedirect) { - window.location.href = config.successRedirect; - } else { - window.location.reload(); - } - }, 1500); - } else if (status === 'pending') { - // Continue polling - updateStatusMessage('pending', attempts); - } else { - // Unknown status, continue polling - updateStatusMessage('checking', attempts); - } - } - }, - error () { - // Network error, continue polling - updateStatusMessage('error', attempts); - }, - }); - } - - /** - * Update the status message on the page. - * - * @param {string} status The current status. - * @param {number} attempt Current attempt number. - */ - function updateStatusMessage(status, attempt) { - const $statusEl = $(config.statusSelector); - - if (! $statusEl.length) { - return; - } - - let message = ''; - let className = ''; - - switch (status) { - case 'completed': - message = wu_payment_poll.messages?.completed || 'Payment confirmed! Refreshing page...'; - className = 'wu-payment-status-completed'; - break; - case 'pending': - message = wu_payment_poll.messages?.pending || 'Verifying payment...'; - className = 'wu-payment-status-pending'; - break; - case 'timeout': - message = wu_payment_poll.messages?.timeout || 'Payment verification timed out. Please refresh the page or contact support if payment was made.'; - className = 'wu-payment-status-timeout'; - break; - case 'error': - message = wu_payment_poll.messages?.error || 'Error checking payment status. Retrying...'; - className = 'wu-payment-status-error'; - break; - default: - message = wu_payment_poll.messages?.checking || 'Checking payment status...'; - className = 'wu-payment-status-checking'; - } - - $statusEl - .removeClass('wu-payment-status-completed wu-payment-status-pending wu-payment-status-timeout wu-payment-status-error wu-payment-status-checking') - .addClass(className) - .html(message); - } - - /** - * Create the status element if it doesn't exist. - */ - function ensureStatusElement() { - let $statusEl = $(config.statusSelector); - - if (! $statusEl.length) { - // Try to find a good place to insert the status element - const $container = $('.wu-checkout-form, .wu-styling, .entry-content, .post-content, main').first(); - - if ($container.length) { - $statusEl = $('
'); - $container.prepend($statusEl); - } - } - - return $statusEl; - } - - /** - * Start polling for payment status. - */ - function startPolling() { - if (! config.paymentHash || ! config.ajaxUrl) { - return; - } - - // Ensure the status element exists - ensureStatusElement(); - - // Initial status update - updateStatusMessage('pending', 0); - - // Start polling - pollTimer = setInterval(checkPaymentStatus, config.pollInterval); - - // Do first check immediately - checkPaymentStatus(); - } - - /** - * Stop polling. - */ - function stopPolling() { - if (pollTimer) { - clearInterval(pollTimer); - pollTimer = null; - } - } - - // Start polling when document is ready - $(document).ready(function () { - // Only poll if we have a payment hash and status is done - if (config.paymentHash && wu_payment_poll.should_poll) { - startPolling(); - } - }); - - // Expose for debugging - window.wu_payment_poll_controller = { - start: startPolling, - stop: stopPolling, - check: checkPaymentStatus, - }; -}(jQuery)); diff --git a/assets/js/payment-status-poll.min.js b/assets/js/payment-status-poll.min.js deleted file mode 100644 index 10d30278..00000000 --- a/assets/js/payment-status-poll.min.js +++ /dev/null @@ -1 +0,0 @@ -(p=>{if("undefined"!=typeof wu_payment_poll){let s={paymentHash:wu_payment_poll.payment_hash||"",ajaxUrl:wu_payment_poll.ajax_url||"",pollInterval:parseInt(wu_payment_poll.poll_interval,10)||3e3,maxAttempts:parseInt(wu_payment_poll.max_attempts,10)||20,statusSelector:wu_payment_poll.status_selector||".wu-payment-status",successRedirect:wu_payment_poll.success_redirect||""},t=0,e=null;function a(){++t>s.maxAttempts?(l(),n("timeout")):p.ajax({url:s.ajaxUrl,type:"POST",data:{action:"wu_check_payment_status",payment_hash:s.paymentHash},success:function(e){e.success&&e.data&&("completed"===(e=e.data.status)?(l(),n("completed"),setTimeout(function(){s.successRedirect?window.location.href=s.successRedirect:window.location.reload()},1500)):n("pending"===e?"pending":"checking",t))},error:function(){n("error",t)}})}function n(a){var n=p(s.statusSelector);if(n.length){let e="",t="";switch(a){case"completed":e=wu_payment_poll.messages?.completed||"Payment confirmed! Refreshing page...",t="wu-payment-status-completed";break;case"pending":e=wu_payment_poll.messages?.pending||"Verifying payment...",t="wu-payment-status-pending";break;case"timeout":e=wu_payment_poll.messages?.timeout||"Payment verification timed out. Please refresh the page or contact support if payment was made.",t="wu-payment-status-timeout";break;case"error":e=wu_payment_poll.messages?.error||"Error checking payment status. Retrying...",t="wu-payment-status-error";break;default:e=wu_payment_poll.messages?.checking||"Checking payment status...",t="wu-payment-status-checking"}n.removeClass("wu-payment-status-completed wu-payment-status-pending wu-payment-status-timeout wu-payment-status-error wu-payment-status-checking").addClass(t).html(e)}}function u(){if(s.paymentHash&&s.ajaxUrl){{let e=p(s.statusSelector);var t;e.length||(t=p(".wu-checkout-form, .wu-styling, .entry-content, .post-content, main").first()).length&&(e=p('
'),t.prepend(e)),e}n("pending"),e=setInterval(a,s.pollInterval),a()}}function l(){e&&(clearInterval(e),e=null)}p(document).ready(function(){s.paymentHash&&wu_payment_poll.should_poll&&u()}),window.wu_payment_poll_controller={start:u,stop:l,check:a}}})(jQuery); \ No newline at end of file diff --git a/assets/js/wu-password-strength.js b/assets/js/wu-password-strength.js index 037f8259..195cbfee 100644 --- a/assets/js/wu-password-strength.js +++ b/assets/js/wu-password-strength.js @@ -210,7 +210,9 @@ case 4: return pwsL10n.strong || 'Strong'; case 'super_strong': - return this.settings.i18n.super_strong; + return this.settings.i18n && this.settings.i18n.super_strong + ? this.settings.i18n.super_strong + : 'Super Strong'; case 5: return pwsL10n.mismatch || 'Mismatch'; default: @@ -277,6 +279,10 @@ const hints = []; const i18n = this.settings.i18n; + if (! i18n) { + return 'Required: ' + failedRules.join(', '); + } + if (failedRules.indexOf('length') !== -1) { hints.push(i18n.min_length.replace('%d', this.settings.min_length)); } @@ -357,7 +363,7 @@ } // Check for special character (matches Defender Pro's pattern) - if (settings.require_special && ! /[!@#$%^&*()_+\-={};:'",.<>?~\[\]\/|`]/.test(password)) { + if (settings.require_special && ! /[!@#$%^&*()_+\-={};:'",.<>?~\[\]\/|`\\]/.test(password)) { failedRules.push('special'); } diff --git a/assets/js/wu-password-strength.min.js b/assets/js/wu-password-strength.min.js index 85594a81..64c0789a 100644 --- a/assets/js/wu-password-strength.min.js +++ b/assets/js/wu-password-strength.min.js @@ -1 +1 @@ -(t=>{function e(){var s={min_strength:4,enforce_rules:!1,min_length:12,require_uppercase:!1,require_lowercase:!1,require_number:!1,require_special:!1};return"undefined"==typeof wu_password_strength_settings?s:t.extend(s,wu_password_strength_settings)}window.WU_PasswordStrength=function(s){this.settings=e(),this.options=t.extend({pass1:null,pass2:null,result:null,submit:null,minStrength:parseInt(e().min_strength,10)||4,onValidityChange:null},s),this.isPasswordValid=!1,this.failedRules=[],this.init()},WU_PasswordStrength.prototype={init(){let s=this;this.options.pass1&&this.options.pass1.length&&(this.options.result&&this.options.result.length||(this.options.result=t("#pass-strength-result"),this.options.result.length))&&(this.options.result.html(this.getStrengthLabel("empty")),this.options.pass1.on("keyup input",function(){s.checkStrength()}),this.options.pass2&&this.options.pass2.length&&this.options.pass2.on("keyup input",function(){s.checkStrength()}),this.options.submit&&this.options.submit.length&&this.options.submit.prop("disabled",!0),this.checkStrength())},checkStrength(){var s,t=this.options.pass1.val(),e=this.options.pass2?this.options.pass2.val():"";this.options.result.attr("class","wu-py-2 wu-px-4 wu-block wu-text-sm wu-border-solid wu-border wu-mt-2"),t?(s=this.getDisallowedList(),t=wp.passwordStrength.meter(t,s,e),this.updateUI(t),this.updateValidity(t)):(this.options.result.addClass("wu-bg-gray-100 wu-border-gray-200").html(this.getStrengthLabel("empty")),this.setValid(!1))},getDisallowedList(){return"undefined"==typeof wp||void 0===wp.passwordStrength?[]:void 0===wp.passwordStrength.userInputDisallowedList?wp.passwordStrength.userInputBlacklist():wp.passwordStrength.userInputDisallowedList()},getStrengthLabel(s){var t;if("undefined"==typeof pwsL10n)return(t={empty:"Enter a password","-1":"Unknown",0:"Very weak",1:"Very weak",2:"Weak",3:"Medium",4:"Strong",super_strong:"Super Strong",5:"Mismatch"})[s]||t[0];switch(s){case"empty":return this.settings.i18n&&this.settings.i18n.empty?this.settings.i18n.empty:"Enter a password";case-1:return pwsL10n.unknown||"Unknown";case 0:case 1:return pwsL10n.short||"Very weak";case 2:return pwsL10n.bad||"Weak";case 3:return pwsL10n.good||"Medium";case 4:return pwsL10n.strong||"Strong";case"super_strong":return this.settings.i18n.super_strong;case 5:return pwsL10n.mismatch||"Mismatch";default:return pwsL10n.short||"Very weak"}},updateUI(s){let t=this.getStrengthLabel(s),e="";switch(s){case-1:case 0:case 1:case 2:e="wu-bg-red-200 wu-border-red-300";break;case 3:e="wu-bg-yellow-200 wu-border-yellow-300";break;case 4:e="wu-bg-green-200 wu-border-green-300";break;default:e="wu-bg-red-200 wu-border-red-300"}this.settings.enforce_rules&&s>=this.options.minStrength&&5!==s&&(s=this.options.pass1.val(),s=this.checkPasswordRules(s),t=s.valid?(e="wu-bg-green-300 wu-border-green-400",this.getStrengthLabel("super_strong")):(e="wu-bg-red-200 wu-border-red-300",this.getRulesHint(s.failedRules))),this.options.result.addClass(e).html(t)},getRulesHint(s){var t=[],e=this.settings.i18n;return-1!==s.indexOf("length")&&t.push(e.min_length.replace("%d",this.settings.min_length)),-1!==s.indexOf("uppercase")&&t.push(e.uppercase_letter),-1!==s.indexOf("lowercase")&&t.push(e.lowercase_letter),-1!==s.indexOf("number")&&t.push(e.number),-1!==s.indexOf("special")&&t.push(e.special_char),0===t.length?this.getStrengthLabel("super_strong"):e.required+" "+t.join(", ")},updateValidity(s){let t=!1;var e=this.options.pass1.val();(t=s>=this.options.minStrength&&5!==s?!0:t)&&this.settings.enforce_rules?(s=this.checkPasswordRules(e),t=s.valid,this.failedRules=s.failedRules):this.failedRules=[],this.setValid(t)},checkPasswordRules(s){var t=[],e=this.settings;return e.min_length&&s.length?~\[\]\/|`]/.test(s)&&t.push("special"),{valid:0===t.length,failedRules:t}},getFailedRules(){return this.failedRules},setValid(s){var t=this.isPasswordValid;this.isPasswordValid=s,this.options.submit&&this.options.submit.length&&this.options.submit.prop("disabled",!s),t!==s&&"function"==typeof this.options.onValidityChange&&this.options.onValidityChange(s)},isValid(){return this.isPasswordValid}}})(jQuery); \ No newline at end of file +(t=>{function e(){var s={min_strength:4,enforce_rules:!1,min_length:12,require_uppercase:!1,require_lowercase:!1,require_number:!1,require_special:!1};return"undefined"==typeof wu_password_strength_settings?s:t.extend(s,wu_password_strength_settings)}window.WU_PasswordStrength=function(s){this.settings=e(),this.options=t.extend({pass1:null,pass2:null,result:null,submit:null,minStrength:parseInt(e().min_strength,10)||4,onValidityChange:null},s),this.isPasswordValid=!1,this.failedRules=[],this.init()},WU_PasswordStrength.prototype={init(){let s=this;this.options.pass1&&this.options.pass1.length&&(this.options.result&&this.options.result.length||(this.options.result=t("#pass-strength-result"),this.options.result.length))&&(this.options.result.html(this.getStrengthLabel("empty")),this.options.pass1.on("keyup input",function(){s.checkStrength()}),this.options.pass2&&this.options.pass2.length&&this.options.pass2.on("keyup input",function(){s.checkStrength()}),this.options.submit&&this.options.submit.length&&this.options.submit.prop("disabled",!0),this.checkStrength())},checkStrength(){var s,t=this.options.pass1.val(),e=this.options.pass2?this.options.pass2.val():"";this.options.result.attr("class","wu-py-2 wu-px-4 wu-block wu-text-sm wu-border-solid wu-border wu-mt-2"),t?(s=this.getDisallowedList(),t=wp.passwordStrength.meter(t,s,e),this.updateUI(t),this.updateValidity(t)):(this.options.result.addClass("wu-bg-gray-100 wu-border-gray-200").html(this.getStrengthLabel("empty")),this.setValid(!1))},getDisallowedList(){return"undefined"==typeof wp||void 0===wp.passwordStrength?[]:void 0===wp.passwordStrength.userInputDisallowedList?wp.passwordStrength.userInputBlacklist():wp.passwordStrength.userInputDisallowedList()},getStrengthLabel(s){var t;if("undefined"==typeof pwsL10n)return(t={empty:"Enter a password","-1":"Unknown",0:"Very weak",1:"Very weak",2:"Weak",3:"Medium",4:"Strong",super_strong:"Super Strong",5:"Mismatch"})[s]||t[0];switch(s){case"empty":return this.settings.i18n&&this.settings.i18n.empty?this.settings.i18n.empty:"Enter a password";case-1:return pwsL10n.unknown||"Unknown";case 0:case 1:return pwsL10n.short||"Very weak";case 2:return pwsL10n.bad||"Weak";case 3:return pwsL10n.good||"Medium";case 4:return pwsL10n.strong||"Strong";case"super_strong":return this.settings.i18n&&this.settings.i18n.super_strong?this.settings.i18n.super_strong:"Super Strong";case 5:return pwsL10n.mismatch||"Mismatch";default:return pwsL10n.short||"Very weak"}},updateUI(s){let t=this.getStrengthLabel(s),e="";switch(s){case-1:case 0:case 1:case 2:e="wu-bg-red-200 wu-border-red-300";break;case 3:e="wu-bg-yellow-200 wu-border-yellow-300";break;case 4:e="wu-bg-green-200 wu-border-green-300";break;default:e="wu-bg-red-200 wu-border-red-300"}this.settings.enforce_rules&&s>=this.options.minStrength&&5!==s&&(s=this.options.pass1.val(),s=this.checkPasswordRules(s),t=s.valid?(e="wu-bg-green-300 wu-border-green-400",this.getStrengthLabel("super_strong")):(e="wu-bg-red-200 wu-border-red-300",this.getRulesHint(s.failedRules))),this.options.result.addClass(e).html(t)},getRulesHint(s){var t=[],e=this.settings.i18n;return e?(-1!==s.indexOf("length")&&t.push(e.min_length.replace("%d",this.settings.min_length)),-1!==s.indexOf("uppercase")&&t.push(e.uppercase_letter),-1!==s.indexOf("lowercase")&&t.push(e.lowercase_letter),-1!==s.indexOf("number")&&t.push(e.number),-1!==s.indexOf("special")&&t.push(e.special_char),0===t.length?this.getStrengthLabel("super_strong"):e.required+" "+t.join(", ")):"Required: "+s.join(", ")},updateValidity(s){let t=!1;var e=this.options.pass1.val();(t=s>=this.options.minStrength&&5!==s?!0:t)&&this.settings.enforce_rules?(s=this.checkPasswordRules(e),t=s.valid,this.failedRules=s.failedRules):this.failedRules=[],this.setValid(t)},checkPasswordRules(s){var t=[],e=this.settings;return e.min_length&&s.length?~\[\]\/|`\\]/.test(s)&&t.push("special"),{valid:0===t.length,failedRules:t}},getFailedRules(){return this.failedRules},setValid(s){var t=this.isPasswordValid;this.isPasswordValid=s,this.options.submit&&this.options.submit.length&&this.options.submit.prop("disabled",!s),t!==s&&"function"==typeof this.options.onValidityChange&&this.options.onValidityChange(s)},isValid(){return this.isPasswordValid}}})(jQuery); \ No newline at end of file diff --git a/inc/managers/class-membership-manager.php b/inc/managers/class-membership-manager.php index 47edaf49..c84248b5 100644 --- a/inc/managers/class-membership-manager.php +++ b/inc/managers/class-membership-manager.php @@ -104,15 +104,18 @@ public function publish_pending_site(): void { ignore_user_abort(true); - // Don't make the request block till we finish, if possible. - if ( function_exists('fastcgi_finish_request')) { - // Don't use wp_send_json because it will exit prematurely. - header('Content-Type: application/json; charset=' . get_option('blog_charset')); - echo wp_json_encode(['status' => 'creating-site']); + // Send JSON response to client. + // Don't use wp_send_json because it will exit prematurely. + header('Content-Type: application/json; charset=' . get_option('blog_charset')); + echo wp_json_encode(['status' => 'creating-site']); + // Don't make the request block till we finish, if possible. + if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); // Response is sent, but the php process continues to run and update the site. } + // Note: When fastcgi_finish_request is unavailable, the client will wait + // for the operation to complete but still receives the JSON response. $membership_id = wu_request('membership_id'); diff --git a/inc/managers/class-rating-notice-manager.php b/inc/managers/class-rating-notice-manager.php index 98a38cfc..9fc2c8b4 100644 --- a/inc/managers/class-rating-notice-manager.php +++ b/inc/managers/class-rating-notice-manager.php @@ -122,7 +122,7 @@ protected function should_show_notice(): bool { */ protected function add_rating_notice(): void { - $review_url = 'https://wordpress.org/support/plugin/developer-developer/reviews/#new-post'; + $review_url = 'https://wordpress.org/support/plugin/developer/reviews/#new-post'; $message = sprintf( /* translators: %1$s opening strong tag, %2$s closing strong tag, %3$s review link opening tag, %4$s link closing tag */ diff --git a/readme.txt b/readme.txt index 64452c04..edb06191 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: multisite, waas, membership, domain-mapping, subscription Requires at least: 5.3 Requires PHP: 7.4.30 Tested up to: 6.9 -Stable tag: 2.4.9 +Stable tag: 2.4.10 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html The Complete Network Solution for transforming your WordPress Multisite into a Website as a Service (WaaS) platform. @@ -240,6 +240,16 @@ We recommend running this in a staging environment before updating your producti == Changelog == +Version [2.4.10] - Released on 2026-01-XX +- New: Configurable minimum password strength setting with Medium, Strong, and Super Strong options. +- 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: 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. diff --git a/ultimate-multisite.php b/ultimate-multisite.php index 071a0453..e5f1a508 100644 --- a/ultimate-multisite.php +++ b/ultimate-multisite.php @@ -4,7 +4,7 @@ * Description: Transform your WordPress Multisite into a Website as a Service (WaaS) platform supporting site cloning, re-selling, and domain mapping integrations with many hosting providers. * Plugin URI: https://ultimatemultisite.com * Text Domain: ultimate-multisite - * Version: 2.4.9 + * Version: 2.4.10 * Author: Ultimate Multisite Community * Author URI: https://github.com/Multisite-Ultimate/ultimate-multisite * GitHub Plugin URI: https://github.com/Multisite-Ultimate/ultimate-multisite