diff --git a/.githooks/pre-commit b/.githooks/pre-commit
index 2d3bbc95..23eef793 100755
--- a/.githooks/pre-commit
+++ b/.githooks/pre-commit
@@ -1,7 +1,8 @@
#!/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 PHPCS and PHPStan on staged PHP files
+# Runs ESLint and Stylelint on staged JS/CSS files
set -e
@@ -13,95 +14,133 @@ NC='\033[0m' # No Color
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)
+HAS_ERRORS=0
-if [ -z "$STAGED_FILES" ]; then
- echo -e "${GREEN}No PHP files to check.${NC}"
- exit 0
-fi
+# =============================================================================
+# PHP Checks (PHPCS and PHPStan)
+# =============================================================================
-echo -e "${YELLOW}Checking PHP files:${NC}"
-echo "$STAGED_FILES"
+# Get list of staged PHP files
+STAGED_PHP_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$' | grep -v '^vendor/' | grep -v '^tests/' || true)
-# 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
+if [ -n "$STAGED_PHP_FILES" ]; then
+ echo -e "${YELLOW}Checking PHP files:${NC}"
+ echo "$STAGED_PHP_FILES"
-# 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
+ # 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
-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
+ # Run PHPCS on staged files
+ echo -e "${YELLOW}Running PHPCS...${NC}"
+ HAS_PHPCS_ERRORS=0
+ PHPCS_FAILED_FILES=""
+ for FILE in $STAGED_PHP_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}"
+ if ! vendor/bin/phpcs --colors "$FILE"; then
+ PHPCS_FAILED_FILES="$PHPCS_FAILED_FILES $FILE"
+ HAS_PHPCS_ERRORS=1
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
+ # 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_PHP_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_PHP_FILES; do
+ if [ -f "$FILE" ] && [[ "$FILE" =~ ^inc/ ]]; then
+ PHPSTAN_FILES="$PHPSTAN_FILES $FILE"
fi
done
- if [ $HAS_PHPCS_ERRORS -eq 0 ]; then
- echo -e "${GREEN}All PHPCS errors have been auto-fixed!${NC}"
+ if [ -n "$PHPSTAN_FILES" ]; then
+ vendor/bin/phpstan analyse --no-progress --error-format=table $PHPSTAN_FILES || HAS_PHPSTAN_ERRORS=1
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"
+ # Track PHP errors
+ if [ $HAS_PHPCS_ERRORS -ne 0 ] || [ $HAS_PHPSTAN_ERRORS -ne 0 ]; then
+ HAS_ERRORS=1
+ 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
fi
-done
+else
+ echo -e "${GREEN}No PHP files to check.${NC}"
+fi
+
+# =============================================================================
+# JS/CSS Checks (ESLint and Stylelint via lint-staged)
+# =============================================================================
-if [ -n "$PHPSTAN_FILES" ]; then
- vendor/bin/phpstan analyse --no-progress --error-format=table $PHPSTAN_FILES || HAS_PHPSTAN_ERRORS=1
+# Get list of staged JS/CSS files
+STAGED_JS_CSS_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|css)$' | grep -v '\.min\.' || true)
+
+if [ -n "$STAGED_JS_CSS_FILES" ]; then
+ echo -e "${YELLOW}Checking JS/CSS files:${NC}"
+ echo "$STAGED_JS_CSS_FILES"
+
+ if command -v npx &> /dev/null; then
+ echo -e "${YELLOW}Running lint-staged...${NC}"
+ if ! npx lint-staged; then
+ HAS_ERRORS=1
+ echo -e "${RED}lint-staged found errors.${NC}"
+ fi
+ else
+ echo -e "${YELLOW}Warning: npx not found. Skipping JS/CSS linting.${NC}"
+ echo -e "${YELLOW}Run 'npm install' to set up the development environment.${NC}"
+ fi
+else
+ echo -e "${GREEN}No JS/CSS files to check.${NC}"
fi
-# Exit with error if any checks failed
-if [ $HAS_PHPCS_ERRORS -ne 0 ] || [ $HAS_PHPSTAN_ERRORS -ne 0 ]; then
+# =============================================================================
+# Final Result
+# =============================================================================
+
+if [ $HAS_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
fi
echo -e "${GREEN}All pre-commit checks passed!${NC}"
-exit 0
\ No newline at end of file
+exit 0
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/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/template-switching.js b/assets/js/template-switching.js
index c83690cd..b74b2898 100644
--- a/assets/js/template-switching.js
+++ b/assets/js/template-switching.js
@@ -1,219 +1,263 @@
/* global wu_template_switching_params, Vue, wu_create_cookie, wu_listen_to_cookie_change */
(function($, hooks) {
- /*
+ /*
* Sets up the cookie listener for template selection.
*/
- hooks.addAction('wu_checkout_loaded', 'nextpress/wp-ultimo', function() {
+ hooks.addAction('wu_checkout_loaded', 'nextpress/wp-ultimo', function() {
- /*
+ /*
* Resets the template selection cookie.
*/
- wu_create_cookie('wu_template', false);
+ wu_create_cookie('wu_template', false);
- /*
+ /*
* Listens for changes and set the template if one is detected.
*/
- wu_listen_to_cookie_change('wu_template', function(value) {
+ wu_listen_to_cookie_change('wu_template', function(value) {
- window.wu_template_switching.template_id = value;
+ window.wu_template_switching.template_id = value;
- });
+ });
- });
+ });
- $(document).ready(function() {
+ $(document).ready(function() {
- const dynamic = {
- functional: true,
- template: '#dynamic',
- props: ['template'],
- render(h, context) {
+ const dynamic = {
+ functional: true,
+ template: '#dynamic',
+ props: [ 'template' ],
+ render(h, context) {
- const template = context.props.template;
+ const template = context.props.template;
- const component = template ? { template } : 'nbsp;
';
+ const component = template ? { template } : 'nbsp;
';
- return h(component);
+ return h(component);
- },
- };
+ },
+ };
- hooks.doAction('wu_checkout_loaded');
+ hooks.doAction('wu_checkout_loaded');
- // eslint-disable-next-line no-unused-vars
- window.wu_template_switching = new Vue({
- el: '#wp-ultimo-form-wu-template-switching-form',
- data() {
+ // eslint-disable-next-line no-unused-vars
+ window.wu_template_switching = new Vue({
+ el: '#wp-ultimo-form-wu-template-switching-form',
+ data() {
- return {
- template_id: 0,
- original_template_id: -1,
- template_category: '',
- stored_templates: {},
- confirm_switch: 0,
- ready: false,
- };
+ return {
+ template_id: 0,
+ original_template_id: -1,
+ template_category: '',
+ stored_templates: {},
+ confirm_switch: 0,
+ ready: false,
+ };
- },
- directives: {
- init: {
- bind(el, binding, vnode) {
+ },
+ directives: {
+ init: {
+ bind(el, binding, vnode) {
- vnode.context[binding.arg] = binding.value;
+ vnode.context[ binding.arg ] = binding.value;
- },
- },
- },
- components: {
- dynamic,
- },
- watch: {
- ready() {
+ },
+ },
+ },
+ components: {
+ dynamic,
+ },
+ watch: {
+ ready() {
- const that = this;
+ const that = this;
- if (that.ready !== false) {
+ if (that.ready !== false) {
- that.switch_template();
+ that.switch_template();
- } // end if;
+ } // end if;
- },
- },
- methods: {
- get_template(template, data) {
+ },
+ },
+ methods: {
+ get_template(template, data) {
- if (typeof data.id === 'undefined') {
+ if (typeof data.id === 'undefined') {
- data.id = 'default';
+ data.id = 'default';
- } // end if;
+ } // end if;
- const template_name = template + '/' + data.id;
+ const template_name = template + '/' + data.id;
- if (typeof this.stored_templates[template_name] !== 'undefined') {
+ if (typeof this.stored_templates[ template_name ] !== 'undefined') {
- return this.stored_templates[template_name];
+ return this.stored_templates[ template_name ];
- } // end if;
+ } // end if;
- const template_data = {
- duration: this.duration,
- duration_unit: this.duration_unit,
- products: this.products,
- ...data,
- };
+ const template_data = {
+ duration: this.duration,
+ duration_unit: this.duration_unit,
+ products: this.products,
+ ...data,
+ };
- this.fetch_template(template, template_data);
+ this.fetch_template(template, template_data);
- return 'Loading
';
+ return 'Loading
';
- },
- fetch_template(template, data) {
+ },
+ fetch_template(template, data) {
- const that = this;
+ const that = this;
- if (typeof data.id === 'undefined') {
+ if (typeof data.id === 'undefined') {
- data.id = 'default';
+ data.id = 'default';
- } // end if;
+ } // end if;
- this.request('wu_render_field_template', {
- template,
- attributes: data,
- }, function(results) {
+ this.request('wu_render_field_template', {
+ template,
+ attributes: data,
+ }, function(results) {
- const template_name = template + '/' + data.id;
+ const template_name = template + '/' + data.id;
- if (results.success) {
+ if (results.success) {
- Vue.set(that.stored_templates, template_name, results.data.html);
+ Vue.set(that.stored_templates, template_name, results.data.html);
- } else {
+ } else {
- Vue.set(that.stored_templates, template_name, '' + results.data[0].message + '
');
+ Vue.set(that.stored_templates, template_name, '' + results.data[ 0 ].message + '
');
- } // end if;
+ } // end if;
- });
+ });
- },
- switch_template() {
+ },
+ switch_template() {
- const that = this;
+ const that = this;
- that.block();
+ that.block();
- this.request('wu_switch_template', {
- template_id: that.template_id,
- }, function(results) {
+ this.request('wu_switch_template', {
+ template_id: that.template_id,
+ }, function(results) {
- /*
+ /*
+ * Handle error responses.
+ */
+ if (results.success === false) {
+
+ that.unblock();
+
+ that.confirm_switch = false;
+
+ that.ready = false;
+
+ let errorMessage = 'An error occurred while switching templates.';
+
+ if (results.data && results.data.message) {
+
+ errorMessage = results.data.message;
+
+ } else if (results.data && Array.isArray(results.data) && results.data[ 0 ] && results.data[ 0 ].message) {
+
+ errorMessage = results.data[ 0 ].message;
+
+ }
+
+ // eslint-disable-next-line no-alert
+ alert(errorMessage);
+
+ return;
+
+ }
+
+ /*
* Redirect of we get a redirect URL back.
*/
- if (typeof results.data.redirect_url === 'string') {
+ if (typeof results.data.redirect_url === 'string') {
+
+ window.location.href = results.data.redirect_url;
+
+ } // end if;
+
+ }, function() {
+
+ /*
+ * Handle network errors.
+ */
+ that.unblock();
+
+ that.confirm_switch = false;
- window.location.href = results.data.redirect_url;
+ that.ready = false;
- } // end if;
+ // eslint-disable-next-line no-alert
+ alert('A network error occurred. Please try again.');
- });
+ });
- },
- block() {
+ },
+ block() {
- /*
+ /*
* Get the first bg color from a parent.
*/
- const bg_color = jQuery(this.$el).parents().filter(function() {
-
- return $(this).css('backgroundColor') !== 'rgba(0, 0, 0, 0)';
-
- }).first().css('backgroundColor');
-
- jQuery(this.$el).wu_block({
- message: '
',
- overlayCSS: {
- backgroundColor: bg_color ? bg_color : '#ffffff',
- opacity: 0.6,
- },
- css: {
- padding: 0,
- margin: 0,
- width: '50%',
- fontSize: '14px !important',
- top: '40%',
- left: '35%',
- textAlign: 'center',
- color: '#000',
- border: 'none',
- backgroundColor: 'none',
- cursor: 'wait',
- },
- });
-
- },
- unblock() {
-
- jQuery(this.$el).wu_unblock();
-
- },
- request(action, data, success_handler, error_handler) {
-
- jQuery.ajax({
- method: 'POST',
- url: wu_template_switching_params.ajaxurl + '&action=' + action,
- data,
- success: success_handler,
- error: error_handler,
- });
-
- },
- },
- });
-
- });
+ const bg_color = jQuery(this.$el).parents().filter(function() {
+
+ return $(this).css('backgroundColor') !== 'rgba(0, 0, 0, 0)';
+
+ }).first().css('backgroundColor');
+
+ jQuery(this.$el).wu_block({
+ message: '
',
+ overlayCSS: {
+ backgroundColor: bg_color ? bg_color : '#ffffff',
+ opacity: 0.6,
+ },
+ css: {
+ padding: 0,
+ margin: 0,
+ width: '50%',
+ fontSize: '14px !important',
+ top: '40%',
+ left: '35%',
+ textAlign: 'center',
+ color: '#000',
+ border: 'none',
+ backgroundColor: 'none',
+ cursor: 'wait',
+ },
+ });
+
+ },
+ unblock() {
+
+ jQuery(this.$el).wu_unblock();
+
+ },
+ request(action, data, success_handler, error_handler) {
+
+ jQuery.ajax({
+ method: 'POST',
+ url: wu_template_switching_params.ajaxurl + '&action=' + action,
+ data,
+ success: success_handler,
+ error: error_handler,
+ });
+
+ },
+ },
+ });
+
+ });
}(jQuery, wp.hooks));
diff --git a/assets/js/template-switching.min.js b/assets/js/template-switching.min.js
index 7cbb2748..712bba40 100644
--- a/assets/js/template-switching.min.js
+++ b/assets/js/template-switching.min.js
@@ -1 +1 @@
-((e,t)=>{t.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(){wu_create_cookie("wu_template",!1),wu_listen_to_cookie_change("wu_template",function(t){window.wu_template_switching.template_id=t})}),e(document).ready(function(){t.doAction("wu_checkout_loaded"),window.wu_template_switching=new Vue({el:"#wp-ultimo-form-wu-template-switching-form",data(){return{template_id:0,original_template_id:-1,template_category:"",stored_templates:{},confirm_switch:0,ready:!1}},directives:{init:{bind(t,e,i){i.context[e.arg]=e.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(t,e){e=e.props.template;return t(e?{template:e}:"nbsp;
")}}},watch:{ready(){!1!==this.ready&&this.switch_template()}},methods:{get_template(t,e){void 0===e.id&&(e.id="default");var i=t+"/"+e.id;return void 0!==this.stored_templates[i]?this.stored_templates[i]:(i={duration:this.duration,duration_unit:this.duration_unit,products:this.products,...e},this.fetch_template(t,i),'Loading
')},fetch_template(i,a){let o=this;void 0===a.id&&(a.id="default"),this.request("wu_render_field_template",{template:i,attributes:a},function(t){var e=i+"/"+a.id;t.success?Vue.set(o.stored_templates,e,t.data.html):Vue.set(o.stored_templates,e,""+t.data[0].message+"
")})},switch_template(){this.block(),this.request("wu_switch_template",{template_id:this.template_id},function(t){"string"==typeof t.data.redirect_url&&(window.location.href=t.data.redirect_url)})},block(){var t=jQuery(this.$el).parents().filter(function(){return"rgba(0, 0, 0, 0)"!==e(this).css("backgroundColor")}).first().css("backgroundColor");jQuery(this.$el).wu_block({message:'
',overlayCSS:{backgroundColor:t||"#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(t,e,i,a){jQuery.ajax({method:"POST",url:wu_template_switching_params.ajaxurl+"&action="+t,data:e,success:i,error:a})}}})})})(jQuery,wp.hooks);
\ No newline at end of file
+((e,t)=>{t.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(){wu_create_cookie("wu_template",!1),wu_listen_to_cookie_change("wu_template",function(t){window.wu_template_switching.template_id=t})}),e(document).ready(function(){t.doAction("wu_checkout_loaded"),window.wu_template_switching=new Vue({el:"#wp-ultimo-form-wu-template-switching-form",data(){return{template_id:0,original_template_id:-1,template_category:"",stored_templates:{},confirm_switch:0,ready:!1}},directives:{init:{bind(t,e,a){a.context[e.arg]=e.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(t,e){e=e.props.template;return t(e?{template:e}:"nbsp;
")}}},watch:{ready(){!1!==this.ready&&this.switch_template()}},methods:{get_template(t,e){void 0===e.id&&(e.id="default");var a=t+"/"+e.id;return void 0!==this.stored_templates[a]?this.stored_templates[a]:(a={duration:this.duration,duration_unit:this.duration_unit,products:this.products,...e},this.fetch_template(t,a),'Loading
')},fetch_template(a,i){let r=this;void 0===i.id&&(i.id="default"),this.request("wu_render_field_template",{template:a,attributes:i},function(t){var e=a+"/"+i.id;t.success?Vue.set(r.stored_templates,e,t.data.html):Vue.set(r.stored_templates,e,""+t.data[0].message+"
")})},switch_template(){let a=this;a.block(),this.request("wu_switch_template",{template_id:a.template_id},function(e){if(!1===e.success){a.unblock(),a.confirm_switch=!1,a.ready=!1;let t="An error occurred while switching templates.";e.data&&e.data.message?t=e.data.message:e.data&&Array.isArray(e.data)&&e.data[0]&&e.data[0].message&&(t=e.data[0].message),void alert(t)}else"string"==typeof e.data.redirect_url&&(window.location.href=e.data.redirect_url)},function(){a.unblock(),a.confirm_switch=!1,a.ready=!1,alert("A network error occurred. Please try again.")})},block(){var t=jQuery(this.$el).parents().filter(function(){return"rgba(0, 0, 0, 0)"!==e(this).css("backgroundColor")}).first().css("backgroundColor");jQuery(this.$el).wu_block({message:'
',overlayCSS:{backgroundColor:t||"#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(t,e,a,i){jQuery.ajax({method:"POST",url:wu_template_switching_params.ajaxurl+"&action="+t,data:e,success:a,error:i})}}})})})(jQuery,wp.hooks);
\ 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-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.js b/assets/js/wu-password-strength.js
index f60215d2..195cbfee 100644
--- a/assets/js/wu-password-strength.js
+++ b/assets/js/wu-password-strength.js
@@ -1,37 +1,85 @@
-/* global jQuery, wp, pwsL10n */
+/* global jQuery, wp, pwsL10n, wu_password_strength_settings, WU_PasswordStrength */
/**
* 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
+ * @param {jQuery} $ jQuery object
*/
(function($) {
'use strict';
+ /**
+ * Get password settings from localized PHP settings.
+ *
+ * @return {Object} Password settings
+ */
+ function getSettings() {
+ const 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.
*
- * @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: 3)
+ * @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) {
+ 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();
};
@@ -40,20 +88,19 @@
/**
* 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) {
- this.options.result = $('
');
- this.options.pass1.after(this.options.result);
+ if (! this.options.result.length) {
+ return;
}
}
@@ -83,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);
@@ -110,7 +157,7 @@
*
* @return {Array} The disallowed list
*/
- getDisallowedList: function() {
+ getDisallowedList() {
if (typeof wp === 'undefined' || typeof wp.passwordStrength === 'undefined') {
return [];
}
@@ -127,26 +174,30 @@
* @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',
- '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) {
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 +209,10 @@
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:
@@ -170,56 +225,174 @@
*
* @param {number} strength The password strength level
*/
- updateUI: function(strength) {
+ updateUI(strength) {
+ let label = this.getStrengthLabel(strength);
+ let 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) {
+ const password = this.options.pass1.val();
+ const 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);
},
/**
- * Update password validity based on strength.
+ * Get a hint message for failed password rules.
+ *
+ * Uses localized strings from PHP.
+ *
+ * @param {Array} failedRules Array of failed rule names
+ * @return {string} Hint message
+ */
+ getRulesHint(failedRules) {
+ 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));
+ }
+ if (failedRules.indexOf('uppercase') !== -1) {
+ hints.push(i18n.uppercase_letter);
+ }
+ if (failedRules.indexOf('lowercase') !== -1) {
+ hints.push(i18n.lowercase_letter);
+ }
+ if (failedRules.indexOf('number') !== -1) {
+ hints.push(i18n.number);
+ }
+ if (failedRules.indexOf('special') !== -1) {
+ hints.push(i18n.special_char);
+ }
+
+ if (hints.length === 0) {
+ return this.getStrengthLabel('super_strong');
+ }
+
+ return i18n.required + ' ' + hints.join(', ');
+ },
+
+ /**
+ * Update password validity based on strength and additional rules.
*
* @param {number} strength The password strength level
*/
- updateValidity: function(strength) {
- var isValid = false;
+ updateValidity(strength) {
+ let isValid = false;
+ const 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) {
+ const 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(password) {
+ const failedRules = [];
+ const 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
+ };
+ },
+
+ /**
+ * Get failed rules for external access.
+ *
+ * @return {Array} Array of failed rule names
+ */
+ getFailedRules() {
+ return this.failedRules;
+ },
+
/**
* Set password validity and update submit button.
*
* @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
@@ -233,9 +406,9 @@
*
* @return {boolean} Whether the password is valid
*/
- isValid: function() {
+ isValid() {
return this.isPasswordValid;
}
};
-})(jQuery);
+}(jQuery));
diff --git a/assets/js/wu-password-strength.min.js b/assets/js/wu-password-strength.min.js
new file mode 100644
index 00000000..64c0789a
--- /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&&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/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..bd2ce926 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 PHPCS, PHPStan, ESLint, and Stylelint 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 'composer install' and 'npm install' to have the required tools available."
\ No newline at end of file
diff --git a/inc/admin-pages/class-email-template-customize-admin-page.php b/inc/admin-pages/class-email-template-customize-admin-page.php
index f35cf297..7ece5ecc 100644
--- a/inc/admin-pages/class-email-template-customize-admin-page.php
+++ b/inc/admin-pages/class-email-template-customize-admin-page.php
@@ -159,13 +159,10 @@ public function email_template_preview(): void {
'subject' => __('Sample Subject', 'ultimate-multisite'),
'is_editor' => true,
'template_settings' => [
+ 'hide_logo' => wu_string_to_bool(wu_request('hide_logo', $first_request ? $object->get_setting('hide_logo', false) : false)),
'use_custom_logo' => wu_string_to_bool(wu_request('use_custom_logo', $first_request ? $object->get_setting('use_custom_logo', false) : false)),
'custom_logo' => wu_request('custom_logo', $object->get_setting('custom_logo', false)),
'background_color' => wu_request('background_color', $object->get_setting('background_color', '#f9f9f9')),
- 'title_color' => wu_request('title_color', $object->get_setting('title_color', '#000000')),
- 'title_size' => wu_request('title_size', $object->get_setting('title_size', 'h3')),
- 'title_align' => wu_request('title_align', $object->get_setting('title_align', 'center')),
- 'title_font' => wu_request('title_font', $object->get_setting('title_font', 'Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif')),
'content_color' => wu_request('content_color', $object->get_setting('content_color', '#000000')),
'content_align' => wu_request('content_align', $object->get_setting('content_align', 'left')),
'content_font' => wu_request('content_font', $object->get_setting('content_font', 'Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif')),
@@ -248,12 +245,24 @@ public function register_widgets(): void {
'footer' => __('Footer', 'ultimate-multisite'),
],
],
+ 'hide_logo' => [
+ 'type' => 'toggle',
+ 'title' => __('Hide Logo', 'ultimate-multisite'),
+ 'desc' => __('Toggle to hide the logo in the email header.', 'ultimate-multisite'),
+ 'wrapper_html_attr' => [
+ 'v-show' => 'require("tab", "header")',
+ 'v-cloak' => 1,
+ ],
+ 'html_attr' => [
+ 'v-model' => 'hide_logo',
+ ],
+ ],
'use_custom_logo' => [
'type' => 'toggle',
'title' => __('Use Custom Logo', 'ultimate-multisite'),
'desc' => __('You can set a different logo to be used on the system emails.', 'ultimate-multisite'),
'wrapper_html_attr' => [
- 'v-show' => 'require("tab", "header")',
+ 'v-show' => 'require("tab", "header") && require("hide_logo", false)',
'v-cloak' => 1,
],
'html_attr' => [
@@ -288,73 +297,6 @@ public function register_widgets(): void {
'v-model' => 'background_color',
],
],
- 'title_color' => [
- 'type' => 'color-picker',
- 'title' => __('Title Color', 'ultimate-multisite'),
- 'value' => '#00a1ff',
- 'wrapper_html_attr' => [
- 'v-show' => 'require("tab", "header")',
- 'v-cloak' => 1,
- ],
- 'html_attr' => [
- 'v-model' => 'title_color',
- ],
- ],
- 'title_size' => [
- 'type' => 'select',
- 'title' => __('Title Size', 'ultimate-multisite'),
- 'value' => wu_get_isset($settings, 'title_size'),
- 'options' => [
- 'h1' => __('h1', 'ultimate-multisite'),
- 'h2' => __('h2', 'ultimate-multisite'),
- 'h3' => __('h3', 'ultimate-multisite'),
- 'h4' => __('h4', 'ultimate-multisite'),
- 'h5' => __('h5', 'ultimate-multisite'),
- ],
- 'wrapper_html_attr' => [
- 'v-show' => 'require("tab", "header")',
- 'v-cloak' => 1,
- ],
- 'html_attr' => [
- 'v-model.lazy' => 'title_size',
- ],
- ],
- 'title_align' => [
- 'type' => 'select',
- 'title' => __('Title Align', 'ultimate-multisite'),
- 'tooltip' => __('Aligment of the font in the title.', 'ultimate-multisite'),
- 'value' => wu_get_isset($settings, 'title_align', ''),
- 'options' => [
- 'left' => __('Left', 'ultimate-multisite'),
- 'center' => __('Center', 'ultimate-multisite'),
- 'right' => __('Right', 'ultimate-multisite'),
- ],
- 'wrapper_html_attr' => [
- 'v-show' => 'require("tab", "header")',
- 'v-cloak' => 1,
- ],
- 'html_attr' => [
- 'v-model.lazy' => 'title_align',
- ],
- ],
- 'title_font' => [
- 'type' => 'select',
- 'title' => __('Title Font-Family', 'ultimate-multisite'),
- 'value' => wu_get_isset($settings, 'title_font', ''),
- 'options' => [
- 'Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif' => __('Helvetica', 'ultimate-multisite'),
- 'Arial, Helvetica, sans-serif' => __('Arial', 'ultimate-multisite'),
- 'Times New Roman, Times, serif' => __('Times New Roman', 'ultimate-multisite'),
- 'Lucida Console, Courier, monospace' => __('Lucida', 'ultimate-multisite'),
- ],
- 'wrapper_html_attr' => [
- 'v-show' => 'require("tab", "header")',
- 'v-cloak' => 1,
- ],
- 'html_attr' => [
- 'v-model.lazy' => 'title_font',
- ],
- ],
'content_color' => [
'type' => 'color-picker',
'title' => __('Content Color', 'ultimate-multisite'),
@@ -568,6 +510,7 @@ public function handle_save(): void {
$allowed_settings = [
'use_custom_logo',
'custom_logo',
+ 'hide_logo',
'display_company_address',
'background_color',
'title_color',
@@ -597,6 +540,7 @@ public function handle_save(): void {
$settings_to_save[ $setting ] = sanitize_hex_color($value);
break;
case 'use_custom_logo':
+ case 'hide_logo':
case 'display_company_address':
$settings_to_save[ $setting ] = wu_string_to_bool($value);
break;
@@ -670,6 +614,7 @@ public static function get_default_settings() {
return [
'use_custom_logo' => false,
'custom_logo' => false,
+ 'hide_logo' => false,
'display_company_address' => true,
'background_color' => '#f1f1f1',
'title_color' => '#000000',
diff --git a/inc/admin-pages/class-migration-alert-admin-page.php b/inc/admin-pages/class-migration-alert-admin-page.php
index ef23cbd6..6bd4b2be 100644
--- a/inc/admin-pages/class-migration-alert-admin-page.php
+++ b/inc/admin-pages/class-migration-alert-admin-page.php
@@ -145,7 +145,7 @@ public function section_alert(): void {
*/
public function handle_proceed(): void {
- delete_network_option(null, 'wu_setup_finished');
+ delete_network_option(null, \WP_Ultimo::NETWORK_OPTION_SETUP_FINISHED);
delete_network_option(null, 'wu_is_migration_done');
wp_safe_redirect(wu_network_admin_url('wp-ultimo-setup'));
diff --git a/inc/admin-pages/class-product-edit-admin-page.php b/inc/admin-pages/class-product-edit-admin-page.php
index de4dfec7..dd6b0637 100644
--- a/inc/admin-pages/class-product-edit-admin-page.php
+++ b/inc/admin-pages/class-product-edit-admin-page.php
@@ -1115,6 +1115,13 @@ public function handle_save(): void {
$_POST['price_variations'] = [];
}
+ /*
+ * Set available addons to empty array if not provided.
+ */
+ if ( ! wu_request('available_addons')) {
+ $_POST['available_addons'] = [];
+ }
+
/*
* Set the taxable value to zero if the toggle is disabled.
*/
diff --git a/inc/admin-pages/class-setup-wizard-admin-page.php b/inc/admin-pages/class-setup-wizard-admin-page.php
index edf8146b..0890527e 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',
];
@@ -703,7 +702,7 @@ public function renders_requirements_table() {
*/
public function section_ready(): void {
- update_network_option(null, 'wu_setup_finished', true);
+ update_network_option(null, \WP_Ultimo::NETWORK_OPTION_SETUP_FINISHED, time());
/**
* Mark the migration as done, if this was a migration.
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-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/class-dashboard-widgets.php b/inc/class-dashboard-widgets.php
index 6f984922..f1aa7203 100644
--- a/inc/class-dashboard-widgets.php
+++ b/inc/class-dashboard-widgets.php
@@ -178,7 +178,7 @@ public function register_widgets(): void {
*/
public function output_widget_first_steps(): void {
- $initial_setup_done = get_network_option(null, 'wu_setup_finished', false);
+ $initial_setup_done = get_network_option(null, \WP_Ultimo::NETWORK_OPTION_SETUP_FINISHED, false);
$steps = [
'inital-setup' => [
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-newsletter.php b/inc/class-newsletter.php
index 76360a6e..846882b4 100644
--- a/inc/class-newsletter.php
+++ b/inc/class-newsletter.php
@@ -1,4 +1,7 @@
register_script('wu-password-strength', wu_get_asset('wu-password-strength.js', 'js'), ['jquery', 'password-strength-meter']);
+ wp_localize_script(
+ 'wu-password-strength',
+ 'wu_password_strength_settings',
+ array_merge(
+ $this->get_password_requirements(),
+ [
+ 'i18n' => [
+ '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'),
+ ],
+ ]
+ )
+ );
+
/*
* Adds Input Masking
*/
@@ -382,6 +403,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 +462,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..f6e111cf 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',
@@ -1708,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://ultimatemultisite.com/privacy-policy/'
+ ),
'type' => 'toggle',
- 'default' => 1,
+ 'default' => 0,
]
);
diff --git a/inc/class-sunrise.php b/inc/class-sunrise.php
index 9064b893..4a6e4cac 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';
}
/**
@@ -230,7 +231,7 @@ public static function load(): void {
*/
add_filter('option_active_plugins', fn() => []);
- add_filter('site_option_active_sitewide_plugins', fn() => [basename(dirname(__DIR__)) . '/wp-ultimo.php' => 1], 10, 0);
+ add_filter('site_option_active_sitewide_plugins', fn() => [basename(dirname(__DIR__)) . '/ultimate-multisite.php' => 1], 10, 0);
}
}
}
diff --git a/inc/class-tracker.php b/inc/class-tracker.php
new file mode 100644
index 00000000..9b383919
--- /dev/null
+++ b/inc/class-tracker.php
@@ -0,0 +1,939 @@
+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($error_data, 'error', true);
+ }
+
+ /**
+ * 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 = wu_get_setting('company_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) . ' '
+ );
+ }
+
+ $error_details = $this->build_error_details($error);
+
+ // Link to support for super admins, main site for regular users
+ if (is_super_admin()) {
+ $support_url = $this->build_support_url($error_details, $admin_email);
+ $message = $this->build_admin_error_message($custom_message, $error_details, $support_url);
+ } else {
+ $home_url = network_home_url('/');
+ $message = $this->build_user_error_message($custom_message, $home_url);
+ }
+
+ if ($this->is_tracking_enabled() && str_contains($error_file, 'ultimate-multisite')) {
+ $error_data = $this->prepare_error_data('fatal', $error_details['full'], \Psr\Log\LogLevel::CRITICAL);
+
+ // Send synchronously since we're about to die
+ $this->send_to_api($error_data, 'error');
+ }
+ return $message;
+ }
+
+ /**
+ * Get normalized error context from an error array.
+ *
+ * @since 2.5.0
+ * @param array $error Error information from error_get_last().
+ * @return array Normalized error context with type, message, file, line, and environment.
+ */
+ protected function get_error_context(array $error): array {
+
+ $file = $error['file'] ?? '';
+
+ return [
+ 'type' => $this->get_error_type_name($error['type'] ?? 0),
+ 'message' => $error['message'] ?? __('Unknown error', 'ultimate-multisite'),
+ 'file' => $file ?: __('Unknown file', 'ultimate-multisite'),
+ 'line' => $error['line'] ?? 0,
+ 'trace' => $error['trace'] ?? [],
+ 'source_plugin' => $this->detect_plugin_from_path($file),
+ 'php' => PHP_VERSION,
+ 'wp' => get_bloginfo('version'),
+ 'plugin' => wu_get_version(),
+ 'multisite' => is_multisite() ? 'Yes' : 'No',
+ 'subdomain' => is_subdomain_install() ? 'Yes' : 'No',
+ ];
+ }
+
+ /**
+ * Detect the plugin or theme name from a file path.
+ *
+ * @since 2.5.0
+ * @param string $file_path The file path from the error.
+ * @return string The detected plugin/theme name or 'Unknown'.
+ */
+ protected function detect_plugin_from_path(string $file_path): string {
+
+ if (empty($file_path)) {
+ return __('Unknown', 'ultimate-multisite');
+ }
+
+ // Normalize path separators
+ $file_path = str_replace('\\', '/', $file_path);
+
+ // Check for plugins directory
+ if (preg_match('#/plugins/([^/]+)/#', $file_path, $matches)) {
+ return $this->format_plugin_name($matches[1]);
+ }
+
+ // Check for mu-plugins directory
+ if (preg_match('#/mu-plugins/([^/]+)#', $file_path, $matches)) {
+ $name = $matches[1];
+ // Handle single file mu-plugins
+ if (strpos($name, '.php') !== false) {
+ $name = basename($name, '.php');
+ }
+
+ return $this->format_plugin_name($name) . ' (mu-plugin)';
+ }
+
+ // Check for themes directory
+ if (preg_match('#/themes/([^/]+)/#', $file_path, $matches)) {
+ return $this->format_plugin_name($matches[1]) . ' (theme)';
+ }
+
+ // Check for wp-includes or wp-admin (WordPress core)
+ if (preg_match('#/wp-(includes|admin)/#', $file_path)) {
+ return 'WordPress Core';
+ }
+
+ return __('Unknown', 'ultimate-multisite');
+ }
+
+ /**
+ * Format a plugin slug into a readable name.
+ *
+ * @since 2.5.0
+ * @param string $slug The plugin slug/folder name.
+ * @return string The formatted name.
+ */
+ protected function format_plugin_name(string $slug): string {
+
+ // Replace hyphens and underscores with spaces, then title case
+ $name = str_replace(['-', '_'], ' ', $slug);
+
+ return ucwords($name);
+ }
+
+ /**
+ * Build the technical error details string.
+ *
+ * @since 2.5.0
+ * @param array $error Error information from error_get_last().
+ * @return array Contains 'summary' and 'full' keys.
+ */
+ protected function build_error_details(array $error): array {
+
+ $ctx = $this->get_error_context($error);
+
+ $summary = sprintf('%s: %s', $ctx['type'], $ctx['message']);
+
+ $full = sprintf(
+ "%s: %s\n\nSource: %s\nFile: %s\nLine: %d\n\nEnvironment:\n- PHP: %s\n- WordPress: %s\n- Ultimate Multisite: %s\n- Multisite: %s\n- Subdomain Install: %s",
+ $ctx['type'],
+ $ctx['message'],
+ $ctx['source_plugin'],
+ $ctx['file'],
+ $ctx['line'],
+ $ctx['php'],
+ $ctx['wp'],
+ $ctx['plugin'],
+ $ctx['multisite'],
+ $ctx['subdomain']
+ );
+
+ if ( ! empty($ctx['trace'])) {
+ $full .= "\n\nBacktrace:\n" . $this->format_backtrace($ctx['trace']);
+ }
+
+ return [
+ 'summary' => $summary,
+ 'type' => $ctx['type'],
+ 'full' => $full,
+ 'source_plugin' => $ctx['source_plugin'],
+ ];
+ }
+
+ /**
+ * Format a backtrace array into a readable string.
+ *
+ * @since 2.5.0
+ * @param array $trace The backtrace array.
+ * @return string
+ */
+ protected function format_backtrace(array $trace): string {
+
+ $lines = [];
+
+ foreach ($trace as $index => $frame) {
+ $file = $frame['file'] ?? '[internal]';
+ $line = $frame['line'] ?? 0;
+ $function = $frame['function'] ?? '[unknown]';
+ $class = $frame['class'] ?? '';
+ $type = $frame['type'] ?? '';
+
+ $call = $class ? "{$class}{$type}{$function}()" : "{$function}()";
+
+ $lines[] = sprintf('#%d %s:%d %s', $index, $file, $line, $call);
+ }
+
+ return implode("\n", $lines);
+ }
+
+ /**
+ * Sanitize error text for URL parameters to avoid WAF triggers.
+ *
+ * Uses Unicode lookalike characters to preserve readability while
+ * preventing Cloudflare and other WAFs from flagging the content
+ * as malicious payloads.
+ *
+ * @since 2.5.0
+ * @param string $text The error text to sanitize.
+ * @return string
+ */
+ protected function sanitize_error_for_url(string $text): string {
+
+ // Unicode lookalike replacements
+ $replacements = [
+ // Path separators - use division slash (U+2215) and reverse solidus operator (U+29F5)
+ '/' => "\u{2215}", // ∕ DIVISION SLASH
+ '\\' => "\u{29F5}", // ⧵ REVERSE SOLIDUS OPERATOR
+
+ // File extension dots - use fullwidth full stop (U+FF0E)
+ '.php' => "\u{FF0E}php", // .php
+ '.js' => "\u{FF0E}js",
+ '.sql' => "\u{FF0E}sql",
+ '.sh' => "\u{FF0E}sh",
+ '.exe' => "\u{FF0E}exe",
+ '.inc' => "\u{FF0E}inc",
+
+ // PHP tags - use fullwidth less/greater than (U+FF1C, U+FF1E)
+ ' "\u{FF1C}?php", // <?php
+ '' => "\u{FF1C}?",
+ '?>' => "?\u{FF1E}",
+
+ // Common dangerous function patterns - use fullwidth parentheses (U+FF08)
+ 'eval(' => "eval\u{FF08}", // eval(
+ 'exec(' => "exec\u{FF08}",
+ 'system(' => "system\u{FF08}",
+ 'shell_exec(' => "shell_exec\u{FF08}",
+ 'passthru(' => "passthru\u{FF08}",
+ 'popen(' => "popen\u{FF08}",
+ 'proc_open(' => "proc_open\u{FF08}",
+
+ // SQL injection patterns - use fullwidth semicolon (U+FF1B)
+ '; DROP' => "\u{FF1B} DROP",
+ '; SELECT' => "\u{FF1B} SELECT",
+ '; INSERT' => "\u{FF1B} INSERT",
+ '; UPDATE' => "\u{FF1B} UPDATE",
+ '; DELETE' => "\u{FF1B} DELETE",
+ "' OR '" => "\u{FF07} OR \u{FF07}", // fullwidth apostrophe
+ '" OR "' => "\u{FF02} OR \u{FF02}", // fullwidth quotation mark
+
+ // XSS patterns - use fullwidth less than (U+FF1C)
+ '
+HTML;
+ }
+
+ /**
+ * Build the error message HTML for regular users (non-admin).
+ *
+ * @since 2.5.0
+ * @param string $custom_message The main error message.
+ * @param string $home_url The network home URL.
+ * @return string
+ */
+ protected function build_user_error_message(string $custom_message, string $home_url): string {
+
+ $return_text = __('Return to the main site', 'ultimate-multisite');
+
+ return sprintf(
+ '%s
%s
',
+ $custom_message,
+ esc_url($home_url),
+ $return_text
+ );
+ }
+
+ /**
+ * 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 = str_replace(ABSPATH, 'ABSPATH', $message);
+ $message = str_replace(dirname(ABSPATH), '', $message);
+
+ // Remove potential domain names
+ $message = preg_replace('/https?:\/\/[^\s\'"]+/', '[url]', $message);
+ $message = preg_replace('/\b[a-zA-Z0-9][a-zA-Z0-9\-]*\.(?!(?:php|js|jsx|ts|tsx|css|scss|sass|less|html|htm|json|xml|txt|md|yml|yaml|ini|log|sql|sh|py|rb|go|vue|svelte|map|lock|twig|phtml|inc|mo|po|pot)\b)[a-zA-Z]{2,}\b/', '[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).
+ * @param bool $async Whether to send asynchronously.
+ * @return array|\WP_Error
+ */
+ protected function send_to_api(array $data, string $type, bool $async = false) {
+
+ $url = add_query_arg('type', $type, self::API_URL);
+
+ return wp_safe_remote_post(
+ $url,
+ [
+ 'method' => 'POST',
+ 'blocking' => ! $async,
+ '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..30349f47 100644
--- a/inc/class-wp-ultimo.php
+++ b/inc/class-wp-ultimo.php
@@ -41,6 +41,8 @@ final class WP_Ultimo {
*/
const LOG_HANDLE = 'ultimate-multisite-core';
+ const NETWORK_OPTION_SETUP_FINISHED = 'wu_setup_finished';
+
/**
* Version of the Plugin.
*
@@ -506,6 +508,7 @@ protected function load_extra_components(): void {
\WP_Ultimo\UI\Domain_Mapping_Element::get_instance();
\WP_Ultimo\UI\Site_Maintenance_Element::get_instance();
\WP_Ultimo\UI\Template_Switching_Element::get_instance();
+ \WP_Ultimo\UI\Magic_Link_Url_Element::get_instance();
/*
* Loads our Light Ajax implementation
@@ -610,6 +613,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 +654,11 @@ function () {
*/
\WP_Ultimo\Cron::get_instance();
+ /*
+ * Usage Tracker (opt-in telemetry)
+ */
+ \WP_Ultimo\Tracker::get_instance();
+
\WP_Ultimo\MCP_Adapter::get_instance();
}
@@ -676,6 +689,11 @@ protected function load_admin_pages(): void {
*/
\WP_Ultimo\SSO\Admin_Bar_Magic_Links::get_instance();
+ /*
+ * Initialize subsite links for nav menus.
+ */
+ \WP_Ultimo\SSO\Nav_Menu_Subsite_Links::get_instance();
+
/*
* Loads the Checkout Form admin page.
*/
@@ -929,6 +947,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..2dd65ec8
--- /dev/null
+++ b/inc/compat/class-woocommerce-subscriptions-compat.php
@@ -0,0 +1,155 @@
+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 { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+
+ $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;
+ }
+ switch_to_blog($site_id);
+
+ try {
+ $option_exists = get_option('wc_subscriptions_siteurl');
+
+ if (! $option_exists) {
+ return;
+ }
+ $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');
+ } catch (\Error $e) {
+ wu_log_add('site-duplication-errors', $e->getMessage(), LogLevel::ERROR);
+ } finally {
+ restore_current_blog();
+ }
+ }
+
+ /**
+ * Replace spaces with dots in WooCommerce customer usernames.
+ *
+ * WooCommerce allows spaces in usernames but they prevent future logins.
+ *
+ * @since 2.5.0
+ *
+ * @param string $username The username being created.
+ * @return string The username with spaces replaced by dots.
+ */
+ public function woocommerce_new_customer_username_no_space(string $username): string {
+ return preg_replace('/\\s/', '.', $username);
+ }
+}
diff --git a/inc/debug/class-debug.php b/inc/debug/class-debug.php
index 5d0def49..0a20d443 100644
--- a/inc/debug/class-debug.php
+++ b/inc/debug/class-debug.php
@@ -68,8 +68,8 @@ public function add_additional_hooks(): void {
public function add_debug_links(): void {
?>
-
@@ -77,7 +77,7 @@ class="wu-ml-2 wu-no-underline wu-text-gray-600"
-
-
- $should_use_prefix
*/
$options = [
- 'v2_settings' => true,
- 'debug_faker' => true,
- 'finished' => true,
- 'invoice_settings' => true,
- 'template_placeholders' => true,
- 'tax_rates' => true,
- 'top_bar_settings' => true,
- 'wu_setup_finished' => false,
- 'wu_activation' => false,
- 'wu_default_email_template' => false,
- 'wu_is_migration_done' => false,
- 'wu_host_integrations_enabled' => false,
+ 'v2_settings' => true,
+ 'debug_faker' => true,
+ 'finished' => true,
+ 'invoice_settings' => true,
+ 'template_placeholders' => true,
+ 'tax_rates' => true,
+ 'top_bar_settings' => true,
+ \WP_Ultimo::NETWORK_OPTION_SETUP_FINISHED => false,
+ 'wu_activation' => false,
+ 'wu_default_email_template' => false,
+ 'wu_is_migration_done' => false,
+ 'wu_host_integrations_enabled' => false,
];
foreach ($options as $option_name => $should_use_prefix) {
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/limits/class-site-template-limits.php b/inc/limits/class-site-template-limits.php
index cb6115dc..e62b7a02 100644
--- a/inc/limits/class-site-template-limits.php
+++ b/inc/limits/class-site-template-limits.php
@@ -87,8 +87,11 @@ public function maybe_filter_template_selection_options($attributes) {
} else {
$site_list = wu_get_isset($attributes, 'sites', explode(',', ($attributes['template_selection_sites'] ?? '')));
- $available_templates = $limits->site_templates->get_available_site_templates();
- $attributes['sites'] = array_intersect($site_list, $available_templates);
+ // Ensure consistent type comparison by casting to integers.
+ $site_list = array_map('intval', $site_list);
+ $available_templates = array_map('intval', $limits->site_templates->get_available_site_templates());
+
+ $attributes['sites'] = array_values(array_intersect($site_list, $available_templates));
}
}
diff --git a/inc/managers/class-membership-manager.php b/inc/managers/class-membership-manager.php
index 1e483263..c84248b5 100644
--- a/inc/managers/class-membership-manager.php
+++ b/inc/managers/class-membership-manager.php
@@ -104,12 +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') && version_compare(phpversion(), '7.0.16', '>=') ) {
- wp_send_json(['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
new file mode 100644
index 00000000..29460999
--- /dev/null
+++ b/inc/managers/class-rating-notice-manager.php
@@ -0,0 +1,124 @@
+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, \WP_Ultimo::NETWORK_OPTION_SETUP_FINISHED);
+
+ 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/ultimate-multisite/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 */
+ __('Hello! 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/managers/class-site-manager.php b/inc/managers/class-site-manager.php
index 7361dcfa..09a58dff 100644
--- a/inc/managers/class-site-manager.php
+++ b/inc/managers/class-site-manager.php
@@ -67,7 +67,7 @@ public function init(): void {
add_action('wu_async_take_screenshot', [$this, 'async_get_site_screenshot']);
- add_action('init', [$this, 'lock_site']);
+ add_action('wp', [$this, 'lock_site']);
add_action('admin_init', [$this, 'add_no_index_warning']);
@@ -269,7 +269,7 @@ public function handle_site_published($site, $membership): void {
*/
public function lock_site(): void {
- if (is_main_site() || is_admin() || wu_is_login_page() || wp_doing_ajax() || wu_request('wu-ajax')) {
+ if (is_main_site() || is_admin() || wu_is_login_page() || wp_doing_ajax() || wu_request('wu-ajax') || (function_exists('wp_is_rest_endpoint') && wp_is_rest_endpoint())) {
return;
}
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(),
diff --git a/inc/models/class-broadcast.php b/inc/models/class-broadcast.php
index 126226dd..75cd1e25 100644
--- a/inc/models/class-broadcast.php
+++ b/inc/models/class-broadcast.php
@@ -20,6 +20,22 @@
* @since 2.0.0
*/
class Broadcast extends Post_Base_Model {
+
+ /**
+ * Meta key for migrated from ID.
+ */
+ const META_MIGRATED_FROM_ID = 'migrated_from_id';
+
+ /**
+ * Meta key for notice type.
+ */
+ const META_NOTICE_TYPE = 'notice_type';
+
+ /**
+ * Meta key for message targets.
+ */
+ const META_MESSAGE_TARGETS = 'message_targets';
+
/**
* Post model.
*
@@ -126,7 +142,7 @@ public function validation_rules() {
public function get_migrated_from_id() {
if (null === $this->migrated_from_id) {
- $this->migrated_from_id = $this->get_meta('migrated_from_id', 0);
+ $this->migrated_from_id = $this->get_meta(self::META_MIGRATED_FROM_ID, 0);
}
return $this->migrated_from_id;
@@ -141,9 +157,9 @@ public function get_migrated_from_id() {
*/
public function set_migrated_from_id($migrated_from_id): void {
- $this->meta['migrated_from_id'] = $migrated_from_id;
+ $this->meta[ self::META_MIGRATED_FROM_ID ] = $migrated_from_id;
- $this->migrated_from_id = $this->meta['migrated_from_id'];
+ $this->migrated_from_id = $this->meta[ self::META_MIGRATED_FROM_ID ];
}
/**
@@ -177,7 +193,7 @@ public function get_title() {
public function get_notice_type() {
if (null === $this->notice_type) {
- $this->notice_type = $this->get_meta('notice_type', 'success');
+ $this->notice_type = $this->get_meta(self::META_NOTICE_TYPE, 'success');
}
return $this->notice_type;
@@ -191,7 +207,7 @@ public function get_notice_type() {
*/
public function get_message_targets() {
- return $this->get_meta('message_targets');
+ return $this->get_meta(self::META_MESSAGE_TARGETS);
}
/**
@@ -204,7 +220,7 @@ public function get_message_targets() {
*/
public function set_message_targets($message_targets): void {
- $this->meta['message_targets'] = $message_targets;
+ $this->meta[ self::META_MESSAGE_TARGETS ] = $message_targets;
}
/**
@@ -218,9 +234,9 @@ public function set_message_targets($message_targets): void {
*/
public function set_notice_type($notice_type): void {
- $this->meta['notice_type'] = $notice_type;
+ $this->meta[ self::META_NOTICE_TYPE ] = $notice_type;
- $this->notice_type = $this->meta['notice_type'];
+ $this->notice_type = $this->meta[ self::META_NOTICE_TYPE ];
}
/**
diff --git a/inc/models/class-checkout-form.php b/inc/models/class-checkout-form.php
index 51b54987..4f40cc6c 100644
--- a/inc/models/class-checkout-form.php
+++ b/inc/models/class-checkout-form.php
@@ -22,6 +22,16 @@
*/
class Checkout_Form extends Base_Model {
+ /**
+ * Meta key for thank you page ID.
+ */
+ const META_THANK_YOU_PAGE_ID = 'wu_thank_you_page_id';
+
+ /**
+ * Meta key for conversion snippets.
+ */
+ const META_CONVERSION_SNIPPETS = 'wu_conversion_snippets';
+
/**
* @var array|array
*/
@@ -1086,7 +1096,7 @@ public function has_thank_you_page() {
public function get_thank_you_page_id() {
if (null === $this->thank_you_page_id) {
- $this->thank_you_page_id = $this->get_meta('wu_thank_you_page_id', '');
+ $this->thank_you_page_id = $this->get_meta(self::META_THANK_YOU_PAGE_ID, '');
}
return $this->thank_you_page_id;
@@ -1101,7 +1111,7 @@ public function get_thank_you_page_id() {
*/
public function set_thank_you_page_id($thank_you_page_id): void {
- $this->meta['wu_thank_you_page_id'] = $thank_you_page_id;
+ $this->meta[ self::META_THANK_YOU_PAGE_ID ] = $thank_you_page_id;
$this->thank_you_page_id = $thank_you_page_id;
}
@@ -1115,7 +1125,7 @@ public function set_thank_you_page_id($thank_you_page_id): void {
public function get_conversion_snippets() {
if (null === $this->conversion_snippets) {
- $this->conversion_snippets = $this->get_meta('wu_conversion_snippets', '');
+ $this->conversion_snippets = $this->get_meta(self::META_CONVERSION_SNIPPETS, '');
}
return $this->conversion_snippets;
@@ -1130,7 +1140,7 @@ public function get_conversion_snippets() {
*/
public function set_conversion_snippets($conversion_snippets): void {
- $this->meta['wu_conversion_snippets'] = $conversion_snippets;
+ $this->meta[ self::META_CONVERSION_SNIPPETS ] = $conversion_snippets;
$this->conversion_snippets = $conversion_snippets;
}
diff --git a/inc/models/class-customer.php b/inc/models/class-customer.php
index 67b618a3..5d053eda 100644
--- a/inc/models/class-customer.php
+++ b/inc/models/class-customer.php
@@ -30,6 +30,26 @@ class Customer extends Base_Model implements Billable, Notable {
use Traits\Billable;
use Traits\Notable;
+ /**
+ * Meta key for IP country.
+ */
+ const META_IP_COUNTRY = 'ip_country';
+
+ /**
+ * Meta key for has trialed status.
+ */
+ const META_HAS_TRIALED = 'wu_has_trialed';
+
+ /**
+ * Meta key for customer extra information.
+ */
+ const META_EXTRA_INFORMATION = 'wu_customer_extra_information';
+
+ /**
+ * Meta key for verification key.
+ */
+ const META_VERIFICATION_KEY = 'wu_verification_key';
+
/**
* User ID of the associated user.
*
@@ -234,7 +254,7 @@ public function get_default_billing_address() {
[
'company_name' => $this->get_display_name(),
'billing_email' => $this->get_email_address(),
- 'billing_country' => $this->get_meta('ip_country'),
+ 'billing_country' => $this->get_meta(self::META_IP_COUNTRY),
]
);
}
@@ -252,7 +272,7 @@ public function get_country() {
$country = $billing_address->billing_country;
if ( ! $country) {
- return $this->get_meta('ip_country');
+ return $this->get_meta(self::META_IP_COUNTRY);
}
return $country;
@@ -375,7 +395,7 @@ public function has_trialed() {
return true;
}
- $this->has_trialed = $this->get_meta('wu_has_trialed');
+ $this->has_trialed = $this->get_meta(self::META_HAS_TRIALED);
if ( ! $this->has_trialed) {
$trial = wu_get_memberships(
@@ -388,7 +408,7 @@ public function has_trialed() {
);
if ( ! empty($trial)) {
- $this->update_meta('wu_has_trialed', true);
+ $this->update_meta(self::META_HAS_TRIALED, true);
$this->has_trialed = true;
}
@@ -406,7 +426,7 @@ public function has_trialed() {
*/
public function set_has_trialed($has_trialed): void {
- $this->meta['wu_has_trialed'] = $has_trialed;
+ $this->meta[ self::META_HAS_TRIALED ] = $has_trialed;
$this->has_trialed = $has_trialed;
}
@@ -534,7 +554,7 @@ public function update_last_login($update_ip = true, $update_country_and_state =
}
if ($update_country_and_state) {
- $this->update_meta('ip_country', $geolocation['country']);
+ $this->update_meta(self::META_IP_COUNTRY, $geolocation['country']);
$this->update_meta('ip_state', $geolocation['state']);
}
@@ -550,7 +570,7 @@ public function update_last_login($update_ip = true, $update_country_and_state =
public function get_extra_information() {
if (null === $this->extra_information) {
- $extra_information = (array) $this->get_meta('wu_customer_extra_information');
+ $extra_information = (array) $this->get_meta(self::META_EXTRA_INFORMATION);
$this->extra_information = array_filter($extra_information);
}
@@ -569,8 +589,8 @@ public function set_extra_information($extra_information): void {
$extra_information = array_filter((array) $extra_information);
- $this->extra_information = $extra_information;
- $this->meta['wu_customer_extra_information'] = $extra_information;
+ $this->extra_information = $extra_information;
+ $this->meta[ self::META_EXTRA_INFORMATION ] = $extra_information;
}
/**
@@ -796,7 +816,7 @@ public function generate_verification_key() {
$hash = \WP_Ultimo\Helpers\Hash::encode($seed, 'verification-key');
- return $this->update_meta('wu_verification_key', $hash);
+ return $this->update_meta(self::META_VERIFICATION_KEY, $hash);
}
/**
@@ -807,7 +827,7 @@ public function generate_verification_key() {
*/
public function get_verification_key() {
- return $this->get_meta('wu_verification_key', false);
+ return $this->get_meta(self::META_VERIFICATION_KEY, false);
}
/**
@@ -818,7 +838,7 @@ public function get_verification_key() {
*/
public function disable_verification_key() {
- return $this->update_meta('wu_verification_key', false);
+ return $this->update_meta(self::META_VERIFICATION_KEY, false);
}
/**
diff --git a/inc/models/class-discount-code.php b/inc/models/class-discount-code.php
index 637739f3..c86927ed 100644
--- a/inc/models/class-discount-code.php
+++ b/inc/models/class-discount-code.php
@@ -23,6 +23,16 @@ class Discount_Code extends Base_Model {
use \WP_Ultimo\Traits\WP_Ultimo_Coupon_Deprecated;
+ /**
+ * Meta key for allowed products.
+ */
+ const META_ALLOWED_PRODUCTS = 'wu_allowed_products';
+
+ /**
+ * Meta key for limit products.
+ */
+ const META_LIMIT_PRODUCTS = 'wu_limit_products';
+
/**
* Name of the discount code.
*
@@ -708,7 +718,7 @@ public function save() {
public function get_allowed_products() {
if (null === $this->allowed_products) {
- $this->allowed_products = $this->get_meta('wu_allowed_products', []);
+ $this->allowed_products = $this->get_meta(self::META_ALLOWED_PRODUCTS, []);
}
return (array) $this->allowed_products;
@@ -723,9 +733,9 @@ public function get_allowed_products() {
*/
public function set_allowed_products($allowed_products): void {
- $this->meta['wu_allowed_products'] = (array) $allowed_products;
+ $this->meta[ self::META_ALLOWED_PRODUCTS ] = (array) $allowed_products;
- $this->allowed_products = $this->meta['wu_allowed_products'];
+ $this->allowed_products = $this->meta[ self::META_ALLOWED_PRODUCTS ];
}
/**
@@ -737,7 +747,7 @@ public function set_allowed_products($allowed_products): void {
public function get_limit_products() {
if (null === $this->limit_products) {
- $this->limit_products = $this->get_meta('wu_limit_products', false);
+ $this->limit_products = $this->get_meta(self::META_LIMIT_PRODUCTS, false);
}
return (bool) $this->limit_products;
@@ -752,8 +762,8 @@ public function get_limit_products() {
*/
public function set_limit_products($limit_products): void {
- $this->meta['wu_limit_products'] = (bool) $limit_products;
+ $this->meta[ self::META_LIMIT_PRODUCTS ] = (bool) $limit_products;
- $this->limit_products = $this->meta['wu_limit_products'];
+ $this->limit_products = $this->meta[ self::META_LIMIT_PRODUCTS ];
}
}
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/models/class-email.php b/inc/models/class-email.php
index 3e2017d9..a965aa6f 100644
--- a/inc/models/class-email.php
+++ b/inc/models/class-email.php
@@ -21,6 +21,76 @@
*/
class Email extends Post_Base_Model {
+ /**
+ * Meta key for system email event.
+ */
+ const META_EVENT = 'wu_system_email_event';
+
+ /**
+ * Meta key for style.
+ */
+ const META_STYLE = 'wu_style';
+
+ /**
+ * Meta key for schedule.
+ */
+ const META_SCHEDULE = 'wu_schedule';
+
+ /**
+ * Meta key for schedule type.
+ */
+ const META_SCHEDULE_TYPE = 'system_email_schedule_type';
+
+ /**
+ * Meta key for send days.
+ */
+ const META_SEND_DAYS = 'system_email_send_days';
+
+ /**
+ * Meta key for send hours.
+ */
+ const META_SEND_HOURS = 'system_email_send_hours';
+
+ /**
+ * Meta key for custom sender.
+ */
+ const META_CUSTOM_SENDER = 'system_email_custom_sender';
+
+ /**
+ * Meta key for custom sender name.
+ */
+ const META_CUSTOM_SENDER_NAME = 'system_email_custom_sender_name';
+
+ /**
+ * Meta key for custom sender email.
+ */
+ const META_CUSTOM_SENDER_EMAIL = 'system_email_custom_sender_email';
+
+ /**
+ * Meta key for email schedule config.
+ */
+ const META_EMAIL_SCHEDULE = 'system_email_schedule';
+
+ /**
+ * Meta key for target.
+ */
+ const META_TARGET = 'wu_target';
+
+ /**
+ * Meta key for send copy to admin.
+ */
+ const META_SEND_COPY_TO_ADMIN = 'wu_send_copy_to_admin';
+
+ /**
+ * Meta key for active status.
+ */
+ const META_ACTIVE = 'wu_active';
+
+ /**
+ * Meta key for legacy status.
+ */
+ const META_LEGACY = 'wu_legacy';
+
/**
* Post model.
*
@@ -171,7 +241,7 @@ public function validation_rules() {
public function get_event() {
if (null === $this->event) {
- $this->event = $this->get_meta('wu_system_email_event');
+ $this->event = $this->get_meta(self::META_EVENT);
}
return $this->event;
@@ -207,7 +277,7 @@ public function get_name() {
*/
public function get_style() {
- $this->style = $this->get_meta('wu_style', 'html');
+ $this->style = $this->get_meta(self::META_STYLE, 'html');
if ('use_default' === $this->style) {
$this->style = wu_get_setting('email_template_type', 'html');
@@ -238,7 +308,7 @@ public function set_style($style): void {
$this->style = $style;
- $this->meta['wu_style'] = $this->style;
+ $this->meta[ self::META_STYLE ] = $this->style;
}
/**
@@ -250,7 +320,7 @@ public function set_style($style): void {
public function has_schedule() {
if (null === $this->schedule) {
- $this->schedule = $this->get_meta('wu_schedule', false);
+ $this->schedule = $this->get_meta(self::META_SCHEDULE, false);
}
return $this->schedule;
@@ -267,7 +337,7 @@ public function set_schedule($schedule): void {
$this->schedule = $schedule;
- $this->meta['wu_schedule'] = $schedule;
+ $this->meta[ self::META_SCHEDULE ] = $schedule;
}
/**
@@ -278,7 +348,7 @@ public function set_schedule($schedule): void {
*/
public function get_schedule_type() {
- return $this->get_meta('system_email_schedule_type', 'days');
+ return $this->get_meta(self::META_SCHEDULE_TYPE, 'days');
}
/**
@@ -289,7 +359,7 @@ public function get_schedule_type() {
*/
public function get_send_days() {
- return $this->get_meta('system_email_send_days', 0);
+ return $this->get_meta(self::META_SEND_DAYS, 0);
}
/**
@@ -300,7 +370,7 @@ public function get_send_days() {
*/
public function get_send_hours() {
- return $this->get_meta('system_email_send_hours', '12:00');
+ return $this->get_meta(self::META_SEND_HOURS, '12:00');
}
/**
@@ -349,7 +419,7 @@ public function get_slug() {
*/
public function get_custom_sender() {
- return $this->get_meta('system_email_custom_sender');
+ return $this->get_meta(self::META_CUSTOM_SENDER);
}
/**
@@ -360,7 +430,7 @@ public function get_custom_sender() {
*/
public function get_custom_sender_name() {
- return $this->get_meta('system_email_custom_sender_name');
+ return $this->get_meta(self::META_CUSTOM_SENDER_NAME);
}
/**
@@ -371,7 +441,7 @@ public function get_custom_sender_name() {
*/
public function get_custom_sender_email() {
- return $this->get_meta('system_email_custom_sender_email');
+ return $this->get_meta(self::META_CUSTOM_SENDER_EMAIL);
}
/**
@@ -403,7 +473,7 @@ public function set_event($event): void {
$this->event = $event;
- $this->meta['wu_system_email_event'] = $event;
+ $this->meta[ self::META_EVENT ] = $event;
}
/**
@@ -416,7 +486,7 @@ public function set_event($event): void {
*/
public function set_email_schedule($email_schedule): void {
- $this->meta['system_email_schedule'] = $email_schedule;
+ $this->meta[ self::META_EMAIL_SCHEDULE ] = $email_schedule;
}
/**
@@ -429,7 +499,7 @@ public function set_email_schedule($email_schedule): void {
*/
public function set_send_hours($send_hours): void {
- $this->meta['system_email_send_hours'] = $send_hours;
+ $this->meta[ self::META_SEND_HOURS ] = $send_hours;
}
/**
@@ -442,7 +512,7 @@ public function set_send_hours($send_hours): void {
*/
public function set_send_days($send_days): void {
- $this->meta['system_email_send_days'] = $send_days;
+ $this->meta[ self::META_SEND_DAYS ] = $send_days;
}
/**
@@ -456,7 +526,7 @@ public function set_send_days($send_days): void {
*/
public function set_schedule_type($schedule_type): void {
- $this->meta['system_email_schedule_type'] = $schedule_type;
+ $this->meta[ self::META_SCHEDULE_TYPE ] = $schedule_type;
}
/**
@@ -495,7 +565,7 @@ public function set_slug($slug): void {
*/
public function set_custom_sender($custom_sender): void {
- $this->meta['system_email_custom_sender'] = $custom_sender;
+ $this->meta[ self::META_CUSTOM_SENDER ] = $custom_sender;
}
/**
@@ -508,7 +578,7 @@ public function set_custom_sender($custom_sender): void {
*/
public function set_custom_sender_name($custom_sender_name): void {
- $this->meta['system_email_custom_sender_name'] = $custom_sender_name;
+ $this->meta[ self::META_CUSTOM_SENDER_NAME ] = $custom_sender_name;
}
/**
@@ -521,7 +591,7 @@ public function set_custom_sender_name($custom_sender_name): void {
*/
public function set_custom_sender_email($custom_sender_email): void {
- $this->meta['system_email_custom_sender_email'] = $custom_sender_email;
+ $this->meta[ self::META_CUSTOM_SENDER_EMAIL ] = $custom_sender_email;
}
/**
@@ -533,7 +603,7 @@ public function set_custom_sender_email($custom_sender_email): void {
public function get_target() {
if (null === $this->target) {
- $this->target = $this->get_meta('wu_target', 'admin');
+ $this->target = $this->get_meta(self::META_TARGET, 'admin');
}
return $this->target;
@@ -551,7 +621,7 @@ public function set_target($target): void {
$this->target = $target;
- $this->meta['wu_target'] = $target;
+ $this->meta[ self::META_TARGET ] = $target;
}
/**
@@ -671,7 +741,7 @@ public static function get_super_admin_targets() {
public function get_send_copy_to_admin() {
if (null === $this->send_copy_to_admin) {
- $this->send_copy_to_admin = $this->get_meta('wu_send_copy_to_admin', false);
+ $this->send_copy_to_admin = $this->get_meta(self::META_SEND_COPY_TO_ADMIN, false);
}
return $this->send_copy_to_admin;
@@ -688,7 +758,7 @@ public function set_send_copy_to_admin($send_copy_to_admin): void {
$this->send_copy_to_admin = $send_copy_to_admin;
- $this->meta['wu_send_copy_to_admin'] = $send_copy_to_admin;
+ $this->meta[ self::META_SEND_COPY_TO_ADMIN ] = $send_copy_to_admin;
}
/**
@@ -700,7 +770,7 @@ public function set_send_copy_to_admin($send_copy_to_admin): void {
public function is_active() {
if (null === $this->active) {
- $this->active = $this->get_meta('wu_active', true);
+ $this->active = $this->get_meta(self::META_ACTIVE, true);
}
return $this->active;
@@ -717,7 +787,7 @@ public function set_active($active): void {
$this->active = $active;
- $this->meta['wu_active'] = $active;
+ $this->meta[ self::META_ACTIVE ] = $active;
}
/**
@@ -729,7 +799,7 @@ public function set_active($active): void {
public function is_legacy() {
if (null === $this->legacy) {
- $this->legacy = $this->get_meta('wu_legacy', false);
+ $this->legacy = $this->get_meta(self::META_LEGACY, false);
}
return $this->legacy;
@@ -746,6 +816,6 @@ public function set_legacy($legacy): void {
$this->legacy = $legacy;
- $this->meta['wu_legacy'] = $legacy;
+ $this->meta[ self::META_LEGACY ] = $legacy;
}
}
diff --git a/inc/models/class-membership.php b/inc/models/class-membership.php
index 4272774b..b220122e 100644
--- a/inc/models/class-membership.php
+++ b/inc/models/class-membership.php
@@ -30,6 +30,36 @@ class Membership extends Base_Model implements Limitable, Billable, Notable {
use Traits\Notable;
use \WP_Ultimo\Traits\WP_Ultimo_Subscription_Deprecated;
+ /**
+ * Meta key for swap order.
+ */
+ const META_SWAP_ORDER = 'wu_swap_order';
+
+ /**
+ * Meta key for swap scheduled date.
+ */
+ const META_SWAP_SCHEDULED_DATE = 'wu_swap_scheduled_date';
+
+ /**
+ * Meta key for cancellation reason.
+ */
+ const META_CANCELLATION_REASON = 'cancellation_reason';
+
+ /**
+ * Meta key for discount code.
+ */
+ const META_DISCOUNT_CODE = 'discount_code';
+
+ /**
+ * Meta key for verified payment discount.
+ */
+ const META_VERIFIED_PAYMENT_DISCOUNT = 'verified_payment_discount';
+
+ /**
+ * Meta key for pending site.
+ */
+ const META_PENDING_SITE = 'pending_site';
+
/**
* ID of the customer attached to this membership.
*
@@ -800,12 +830,12 @@ public function schedule_swap($order, $schedule_date = false) {
*/
public function get_scheduled_swap() {
- $order = $this->get_meta('wu_swap_order');
- $scheduled_date = $this->get_meta('wu_swap_scheduled_date');
+ $order = $this->get_meta(self::META_SWAP_ORDER);
+ $scheduled_date = $this->get_meta(self::META_SWAP_SCHEDULED_DATE);
if ( ! $scheduled_date || ! $order) {
- $this->delete_meta('wu_swap_order');
- $this->delete_meta('wu_swap_scheduled_date');
+ $this->delete_meta(self::META_SWAP_ORDER);
+ $this->delete_meta(self::META_SWAP_SCHEDULED_DATE);
return false;
}
@@ -824,9 +854,9 @@ public function get_scheduled_swap() {
*/
public function delete_scheduled_swap(): void {
- $this->delete_meta('wu_swap_order');
+ $this->delete_meta(self::META_SWAP_ORDER);
- $this->delete_meta('wu_swap_scheduled_date');
+ $this->delete_meta(self::META_SWAP_SCHEDULED_DATE);
do_action('wu_membership_delete_scheduled_swap', $this);
}
@@ -1167,7 +1197,7 @@ public function get_cancellation_reason() {
}
if (null === $this->cancellation_reason) {
- $this->cancellation_reason = $this->get_meta('cancellation_reason');
+ $this->cancellation_reason = $this->get_meta(self::META_CANCELLATION_REASON);
}
return (string) $this->cancellation_reason;
@@ -1182,8 +1212,8 @@ public function get_cancellation_reason() {
*/
public function set_cancellation_reason($reason): void {
- $this->meta['cancellation_reason'] = $reason;
- $this->cancellation_reason = $reason;
+ $this->meta[ self::META_CANCELLATION_REASON ] = $reason;
+ $this->cancellation_reason = $reason;
}
/**
@@ -1349,11 +1379,11 @@ public function set_auto_renew($auto_renew): void {
public function get_discount_code() {
if (null === $this->discount_code) {
- $this->discount_code = $this->get_meta('discount_code');
+ $this->discount_code = $this->get_meta(self::META_DISCOUNT_CODE);
}
// Get discount code from original payment for compatibility
- if (empty($this->discount_code) && ! $this->get_meta('verified_payment_discount')) {
+ if (empty($this->discount_code) && ! $this->get_meta(self::META_VERIFIED_PAYMENT_DISCOUNT)) {
$original_payment = wu_get_payments(
[
'number' => 1,
@@ -1364,16 +1394,16 @@ public function get_discount_code() {
);
if (isset($original_payment[0])) {
- $original_cart = $original_payment[0]->get_meta('wu_original_cart');
+ $original_cart = $original_payment[0]->get_meta(Payment::META_ORIGINAL_CART);
$this->discount_code = $original_cart ? $original_cart->get_discount_code() : false;
if ($this->discount_code) {
- $this->update_meta('discount_code', $this->discount_code);
+ $this->update_meta(self::META_DISCOUNT_CODE, $this->discount_code);
}
}
- $this->update_meta('verified_payment_discount', true);
+ $this->update_meta(self::META_VERIFIED_PAYMENT_DISCOUNT, true);
}
return $this->discount_code;
@@ -1389,8 +1419,8 @@ public function get_discount_code() {
public function set_discount_code($discount_code): void {
if (is_a($discount_code, '\WP_Ultimo\Models\Discount_Code')) {
- $this->meta['discount_code'] = $discount_code;
- $this->discount_code = $discount_code;
+ $this->meta[ self::META_DISCOUNT_CODE ] = $discount_code;
+ $this->discount_code = $discount_code;
return;
}
@@ -1401,8 +1431,8 @@ public function set_discount_code($discount_code): void {
return;
}
- $this->meta['discount_code'] = $discount_code;
- $this->discount_code = $discount_code;
+ $this->meta[ self::META_DISCOUNT_CODE ] = $discount_code;
+ $this->discount_code = $discount_code;
}
/**
@@ -1879,7 +1909,7 @@ public function create_pending_site($site_info): Site {
*/
public function update_pending_site($site) {
- return $this->update_meta('pending_site', $site);
+ return $this->update_meta(self::META_PENDING_SITE, $site);
}
/**
@@ -1890,7 +1920,7 @@ public function update_pending_site($site) {
*/
public function get_pending_site() {
- return $this->get_meta('pending_site');
+ return $this->get_meta(self::META_PENDING_SITE);
}
/**
@@ -1934,7 +1964,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;
}
diff --git a/inc/models/class-payment.php b/inc/models/class-payment.php
index 77a696a2..a1eb8c72 100644
--- a/inc/models/class-payment.php
+++ b/inc/models/class-payment.php
@@ -29,6 +29,26 @@ class Payment extends Base_Model implements Notable {
use Traits\Notable;
+ /**
+ * Meta key for line items.
+ */
+ const META_LINE_ITEMS = 'wu_line_items';
+
+ /**
+ * Meta key for invoice number.
+ */
+ const META_INVOICE_NUMBER = 'wu_invoice_number';
+
+ /**
+ * Meta key for cancel membership on refund.
+ */
+ const META_CANCEL_MEMBERSHIP_ON_REFUND = 'wu_cancel_membership_on_refund';
+
+ /**
+ * Meta key for original cart.
+ */
+ const META_ORIGINAL_CART = 'wu_original_cart';
+
/**
* ID of the product of this payment.
*
@@ -599,7 +619,7 @@ public function has_line_items(): bool {
public function get_line_items(): array {
if (null === $this->line_items) {
- $line_items = (array) $this->get_meta('wu_line_items');
+ $line_items = (array) $this->get_meta(self::META_LINE_ITEMS);
$this->line_items = array_filter($line_items);
}
@@ -621,7 +641,7 @@ public function set_line_items(array $line_items): void {
$line_items = array_map(fn($item) => is_array($item) ? new \WP_Ultimo\Checkout\Line_Item($item) : $item, $line_items);
- $this->meta['wu_line_items'] = $line_items;
+ $this->meta[ self::META_LINE_ITEMS ] = $line_items;
$this->line_items = $line_items;
}
@@ -886,7 +906,7 @@ public function set_discount_total($discount_total): void {
public function get_saved_invoice_number() {
if (null === $this->invoice_number) {
- $this->invoice_number = $this->get_meta('wu_invoice_number', '');
+ $this->invoice_number = $this->get_meta(self::META_INVOICE_NUMBER, '');
}
return $this->invoice_number;
@@ -907,7 +927,7 @@ public function get_invoice_number() {
$provisional = false;
if (null === $this->invoice_number) {
- $this->invoice_number = $this->get_meta('wu_invoice_number');
+ $this->invoice_number = $this->get_meta(self::META_INVOICE_NUMBER);
}
if (false === $this->invoice_number) {
@@ -950,7 +970,7 @@ public function get_invoice_number() {
*/
public function set_invoice_number($invoice_number): void {
- $this->meta['wu_invoice_number'] = $invoice_number;
+ $this->meta[ self::META_INVOICE_NUMBER ] = $invoice_number;
$this->invoice_number = $invoice_number;
}
@@ -990,7 +1010,7 @@ public function remove_non_recurring_items() {
public function should_cancel_membership_on_refund() {
if (null === $this->cancel_membership_on_refund) {
- $this->cancel_membership_on_refund = $this->get_meta('wu_cancel_membership_on_refund', false);
+ $this->cancel_membership_on_refund = $this->get_meta(self::META_CANCEL_MEMBERSHIP_ON_REFUND, false);
}
return $this->cancel_membership_on_refund;
@@ -1005,7 +1025,7 @@ public function should_cancel_membership_on_refund() {
*/
public function set_cancel_membership_on_refund($cancel_membership_on_refund): void {
- $this->meta['wu_cancel_membership_on_refund'] = $cancel_membership_on_refund;
+ $this->meta[ self::META_CANCEL_MEMBERSHIP_ON_REFUND ] = $cancel_membership_on_refund;
$this->cancel_membership_on_refund = $cancel_membership_on_refund;
}
diff --git a/inc/models/class-product.php b/inc/models/class-product.php
index ff8696c2..90eec5b1 100644
--- a/inc/models/class-product.php
+++ b/inc/models/class-product.php
@@ -25,6 +25,56 @@ class Product extends Base_Model implements Limitable {
use Traits\Limitable;
use \WP_Ultimo\Traits\WP_Ultimo_Plan_Deprecated;
+ /**
+ * Meta key for featured image ID.
+ */
+ const META_FEATURED_IMAGE_ID = 'wu_featured_image_id';
+
+ /**
+ * Meta key for taxable status.
+ */
+ const META_TAXABLE = 'taxable';
+
+ /**
+ * Meta key for tax category.
+ */
+ const META_TAX_CATEGORY = 'tax_category';
+
+ /**
+ * Meta key for contact us label.
+ */
+ const META_CONTACT_US_LABEL = 'wu_contact_us_label';
+
+ /**
+ * Meta key for contact us link.
+ */
+ const META_CONTACT_US_LINK = 'wu_contact_us_link';
+
+ /**
+ * Meta key for feature list.
+ */
+ const META_FEATURE_LIST = 'feature_list';
+
+ /**
+ * Meta key for price variations.
+ */
+ const META_PRICE_VARIATIONS = 'price_variations';
+
+ /**
+ * Meta key for limitations.
+ */
+ const META_LIMITATIONS = 'wu_limitations';
+
+ /**
+ * Meta key for available addons.
+ */
+ const META_AVAILABLE_ADDONS = 'wu_available_addons';
+
+ /**
+ * Meta key for legacy options.
+ */
+ const META_LEGACY_OPTIONS = 'legacy_options';
+
/**
* The product name.
*
@@ -357,7 +407,7 @@ public function load_attributes_from_post() {
public function get_featured_image_id() {
if (null === $this->featured_image_id) {
- $this->featured_image_id = $this->get_meta('wu_featured_image_id');
+ $this->featured_image_id = $this->get_meta(self::META_FEATURED_IMAGE_ID);
}
return $this->featured_image_id;
@@ -390,7 +440,7 @@ public function get_featured_image($size = 'medium') {
*/
public function set_featured_image_id($image_id): void {
- $this->meta['wu_featured_image_id'] = $image_id;
+ $this->meta[ self::META_FEATURED_IMAGE_ID ] = $image_id;
$this->featured_image_id = $image_id;
}
@@ -1049,7 +1099,7 @@ public function to_search_results() {
*/
public function is_taxable() {
- $is_taxable = (bool) $this->get_meta('taxable', true);
+ $is_taxable = (bool) $this->get_meta(self::META_TAXABLE, true);
return apply_filters('wu_product_is_taxable', $is_taxable, $this);
}
@@ -1064,9 +1114,9 @@ public function is_taxable() {
*/
public function set_taxable($is_taxable): void {
- $this->meta['taxable'] = (bool) $is_taxable;
+ $this->meta[ self::META_TAXABLE ] = (bool) $is_taxable;
- $this->taxable = $this->meta['taxable'];
+ $this->taxable = $this->meta[ self::META_TAXABLE ];
}
/**
@@ -1078,7 +1128,7 @@ public function set_taxable($is_taxable): void {
public function get_tax_category() {
if (null === $this->tax_category) {
- $this->tax_category = $this->get_meta('tax_category', 'default');
+ $this->tax_category = $this->get_meta(self::META_TAX_CATEGORY, 'default');
}
return apply_filters('wu_product_tax_category', $this->tax_category, $this);
@@ -1094,9 +1144,9 @@ public function get_tax_category() {
*/
public function set_tax_category($tax_category): void {
- $this->meta['tax_category'] = $tax_category;
+ $this->meta[ self::META_TAX_CATEGORY ] = $tax_category;
- $this->tax_category = $this->meta['tax_category'];
+ $this->tax_category = $this->meta[ self::META_TAX_CATEGORY ];
}
/**
@@ -1108,7 +1158,7 @@ public function set_tax_category($tax_category): void {
public function get_contact_us_label() {
if (null === $this->contact_us_label) {
- $this->contact_us_label = $this->get_meta('wu_contact_us_label', '');
+ $this->contact_us_label = $this->get_meta(self::META_CONTACT_US_LABEL, '');
}
return $this->contact_us_label;
@@ -1123,9 +1173,9 @@ public function get_contact_us_label() {
*/
public function set_contact_us_label($contact_us_label): void {
- $this->meta['wu_contact_us_label'] = $contact_us_label;
+ $this->meta[ self::META_CONTACT_US_LABEL ] = $contact_us_label;
- $this->contact_us_label = $this->meta['wu_contact_us_label'];
+ $this->contact_us_label = $this->meta[ self::META_CONTACT_US_LABEL ];
}
/**
@@ -1137,7 +1187,7 @@ public function set_contact_us_label($contact_us_label): void {
public function get_contact_us_link() {
if (null === $this->contact_us_link) {
- $this->contact_us_link = $this->get_meta('wu_contact_us_link', '');
+ $this->contact_us_link = $this->get_meta(self::META_CONTACT_US_LINK, '');
}
return $this->contact_us_link;
@@ -1152,9 +1202,9 @@ public function get_contact_us_link() {
*/
public function set_contact_us_link($contact_us_link): void {
- $this->meta['wu_contact_us_link'] = $contact_us_link;
+ $this->meta[ self::META_CONTACT_US_LINK ] = $contact_us_link;
- $this->contact_us_link = $this->meta['wu_contact_us_link'];
+ $this->contact_us_link = $this->meta[ self::META_CONTACT_US_LINK ];
}
/**
@@ -1166,7 +1216,7 @@ public function set_contact_us_link($contact_us_link): void {
public function get_feature_list() {
if (null === $this->feature_list) {
- $this->feature_list = $this->get_meta('feature_list');
+ $this->feature_list = $this->get_meta(self::META_FEATURE_LIST);
}
return $this->feature_list;
@@ -1181,9 +1231,9 @@ public function get_feature_list() {
*/
public function set_feature_list($feature_list): void {
- $this->meta['feature_list'] = $feature_list;
+ $this->meta[ self::META_FEATURE_LIST ] = $feature_list;
- $this->feature_list = $this->meta['feature_list'];
+ $this->feature_list = $this->meta[ self::META_FEATURE_LIST ];
}
/**
@@ -1271,7 +1321,7 @@ function ($price_variation) {
return $price_variation;
},
- $this->get_meta('price_variations', [])
+ $this->get_meta(self::META_PRICE_VARIATIONS, [])
);
}
@@ -1298,9 +1348,9 @@ function ($price_variation) {
$price_variations ?? []
);
- $this->meta['price_variations'] = $price_variations;
+ $this->meta[ self::META_PRICE_VARIATIONS ] = $price_variations;
- $this->price_variations = $this->meta['price_variations'];
+ $this->price_variations = $this->meta[ self::META_PRICE_VARIATIONS ];
}
/**
@@ -1379,7 +1429,7 @@ public function save() {
*/
public function duplicate() {
- $this->meta['wu_limitations'] = $this->get_limitations(false);
+ $this->meta[ self::META_LIMITATIONS ] = $this->get_limitations(false);
$new_product = parent::duplicate();
@@ -1395,11 +1445,7 @@ public function duplicate() {
public function get_available_addons() {
if (null === $this->available_addons) {
- $this->available_addons = $this->get_meta(
- 'wu_
- available_addons',
- []
- );
+ $this->available_addons = $this->get_meta(self::META_AVAILABLE_ADDONS, []);
if (is_string($this->available_addons)) {
$this->available_addons = explode(',', $this->available_addons);
@@ -1418,9 +1464,9 @@ public function get_available_addons() {
*/
public function set_available_addons($available_addons): void {
- $this->meta['wu_available_addons'] = $available_addons;
+ $this->meta[ self::META_AVAILABLE_ADDONS ] = $available_addons;
- $this->available_addons = $this->meta['wu_available_addons'];
+ $this->available_addons = $this->meta[ self::META_AVAILABLE_ADDONS ];
}
/**
@@ -1467,7 +1513,7 @@ public function set_group($group): void {
public function get_legacy_options() {
if (null === $this->legacy_options) {
- $this->legacy_options = $this->get_meta('legacy_options', false);
+ $this->legacy_options = $this->get_meta(self::META_LEGACY_OPTIONS, false);
}
return $this->legacy_options;
@@ -1482,9 +1528,9 @@ public function get_legacy_options() {
*/
public function set_legacy_options($legacy_options): void {
- $this->meta['legacy_options'] = $legacy_options;
+ $this->meta[ self::META_LEGACY_OPTIONS ] = $legacy_options;
- $this->legacy_options = $this->meta['legacy_options'];
+ $this->legacy_options = $this->meta[ self::META_LEGACY_OPTIONS ];
}
/**
diff --git a/inc/models/class-site.php b/inc/models/class-site.php
index 670efb31..070976a3 100644
--- a/inc/models/class-site.php
+++ b/inc/models/class-site.php
@@ -29,6 +29,46 @@ class Site extends Base_Model implements Limitable, Notable {
use \WP_Ultimo\Traits\WP_Ultimo_Site_Deprecated;
use Traits\Notable;
+ /**
+ * Meta key for categories.
+ */
+ const META_CATEGORIES = 'wu_categories';
+
+ /**
+ * Meta key for featured image ID.
+ */
+ const META_FEATURED_IMAGE_ID = 'wu_featured_image_id';
+
+ /**
+ * Meta key for active status.
+ */
+ const META_ACTIVE = 'wu_active';
+
+ /**
+ * Meta key for customer ID.
+ */
+ const META_CUSTOMER_ID = 'wu_customer_id';
+
+ /**
+ * Meta key for membership ID.
+ */
+ const META_MEMBERSHIP_ID = 'wu_membership_id';
+
+ /**
+ * Meta key for template ID.
+ */
+ const META_TEMPLATE_ID = 'wu_template_id';
+
+ /**
+ * Meta key for site type.
+ */
+ const META_TYPE = 'wu_type';
+
+ /**
+ * Meta key for transient status.
+ */
+ const META_TRANSIENT = 'wu_transient';
+
/** DEFAULT WP_SITE COLUMNS */
/**
@@ -330,7 +370,7 @@ public function get_visits_count() {
*/
public function set_categories($categories): void {
- $this->meta['wu_categories'] = $categories;
+ $this->meta[ self::META_CATEGORIES ] = $categories;
$this->categories = $categories;
}
@@ -344,7 +384,7 @@ public function set_categories($categories): void {
public function get_categories() {
if (null === $this->categories) {
- $this->categories = $this->get_meta('wu_categories', []);
+ $this->categories = $this->get_meta(self::META_CATEGORIES, []);
}
if ( ! is_array($this->categories)) {
@@ -363,7 +403,7 @@ public function get_categories() {
public function get_featured_image_id() {
if (null === $this->featured_image_id) {
- return $this->get_meta('wu_featured_image_id');
+ return $this->get_meta(self::META_FEATURED_IMAGE_ID);
}
return $this->featured_image_id;
@@ -406,7 +446,7 @@ public function get_featured_image($size = 'wu-thumb-medium') {
*/
public function set_featured_image_id($image_id): void {
- $this->meta['wu_featured_image_id'] = $image_id;
+ $this->meta[ self::META_FEATURED_IMAGE_ID ] = $image_id;
$this->featured_image_id = $image_id;
}
@@ -714,7 +754,7 @@ public function set_publishing($publishing): void {
public function is_active() {
if (null === $this->active) {
- $this->active = $this->get_meta('wu_active', true);
+ $this->active = $this->get_meta(self::META_ACTIVE, true);
}
return $this->active;
@@ -729,7 +769,7 @@ public function is_active() {
*/
public function set_active($active): void {
- $this->meta['wu_active'] = $active;
+ $this->meta[ self::META_ACTIVE ] = $active;
$this->active = $active;
}
@@ -881,7 +921,7 @@ public function set_lang_id($lang_id): void {
public function get_customer_id() {
if (null === $this->customer_id) {
- $this->customer_id = $this->get_meta('wu_customer_id');
+ $this->customer_id = $this->get_meta(self::META_CUSTOMER_ID);
}
return (int) $this->customer_id;
@@ -896,7 +936,7 @@ public function get_customer_id() {
*/
public function set_customer_id($customer_id): void {
- $this->meta['wu_customer_id'] = $customer_id;
+ $this->meta[ self::META_CUSTOMER_ID ] = $customer_id;
$this->customer_id = $customer_id;
}
@@ -946,7 +986,7 @@ public function is_customer_allowed($customer_id = false) {
public function get_membership_id() {
if (null === $this->membership_id) {
- $this->membership_id = $this->get_meta('wu_membership_id');
+ $this->membership_id = $this->get_meta(self::META_MEMBERSHIP_ID);
}
return $this->membership_id;
@@ -961,7 +1001,7 @@ public function get_membership_id() {
*/
public function set_membership_id($membership_id): void {
- $this->meta['wu_membership_id'] = $membership_id;
+ $this->meta[ self::META_MEMBERSHIP_ID ] = $membership_id;
$this->membership_id = $membership_id;
}
@@ -1053,7 +1093,7 @@ public function get_plan() {
public function get_template_id() {
if (null === $this->template_id) {
- $this->template_id = $this->get_meta('wu_template_id');
+ $this->template_id = $this->get_meta(self::META_TEMPLATE_ID);
}
return $this->template_id;
@@ -1068,7 +1108,7 @@ public function get_template_id() {
*/
public function set_template_id($template_id): void {
- $this->meta['wu_template_id'] = absint($template_id);
+ $this->meta[ self::META_TEMPLATE_ID ] = absint($template_id);
$this->template_id = $template_id;
}
@@ -1148,7 +1188,7 @@ public function get_type() {
}
if (null === $this->type) {
- $type = $this->get_meta('wu_type');
+ $type = $this->get_meta(self::META_TYPE);
$this->type = $type ?: 'default';
}
@@ -1168,7 +1208,7 @@ public function set_type($type): void {
$this->meta = (array) $this->meta;
- $this->meta['wu_type'] = $type;
+ $this->meta[ self::META_TYPE ] = $type;
$this->type = $type;
}
@@ -1285,7 +1325,7 @@ public function __construct($object_model = null) {
public function get_transient() {
if (null === $this->transient) {
- $this->transient = $this->get_meta('wu_transient');
+ $this->transient = $this->get_meta(self::META_TRANSIENT);
}
return $this->transient;
@@ -1300,7 +1340,7 @@ public function get_transient() {
*/
public function set_transient($transient): void {
- $this->meta['wu_transient'] = $transient;
+ $this->meta[ self::META_TRANSIENT ] = $transient;
$this->transient = $transient;
}
diff --git a/inc/sso/class-magic-link.php b/inc/sso/class-magic-link.php
index 6638fcd3..66bfbf93 100644
--- a/inc/sso/class-magic-link.php
+++ b/inc/sso/class-magic-link.php
@@ -176,8 +176,14 @@ protected function verify_user_site_access($user_id, $site_id) {
return false;
}
- // Check if user is a member of the site.
- return is_user_member_of_blog($user_id, $site_id);
+ if (is_user_member_of_blog($user_id, $site_id)) {
+ return true;
+ }
+ // Check if the site is the dashboard site in WP Frontend Admin which the user would not be a member of.
+ if (function_exists('WPFA_Global_Dashboard_Obj') && (int) \WPFA_Global_Dashboard_Obj()->get_dashboard_site_id() === (int) $site_id) {
+ return true;
+ }
+ return false;
}
/**
diff --git a/inc/sso/class-nav-menu-subsite-links.php b/inc/sso/class-nav-menu-subsite-links.php
new file mode 100644
index 00000000..feabb1d8
--- /dev/null
+++ b/inc/sso/class-nav-menu-subsite-links.php
@@ -0,0 +1,306 @@
+ 100,
+ 'order' => 'ASC',
+ ]
+ );
+
+ ?>
+
+ 0) {
+ update_post_meta($menu_item_db_id, self::META_KEY_SUBSITE_ID, $subsite_id);
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Filter menu item URLs on the frontend to use magic links when needed.
+ *
+ * @since 2.5.0
+ *
+ * @param array $items The menu items.
+ * @param \stdClass $args The menu arguments.
+ * @return array The filtered menu items.
+ */
+ public function filter_menu_item_urls(array $items, \stdClass $args): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+
+ foreach ($items as $item) {
+ // Check if this is a subsite menu item by looking for our meta.
+ $subsite_id = get_post_meta($item->ID, self::META_KEY_SUBSITE_ID, true);
+
+ if (empty($subsite_id)) {
+ continue;
+ }
+
+ $subsite_id = absint($subsite_id);
+
+ // Get the home URL with magic link if needed.
+ $url = $this->get_subsite_url_with_magic_link($subsite_id);
+
+ if ($url) {
+ $item->url = $url;
+ }
+ }
+
+ return $items;
+ }
+
+ /**
+ * Get the subsite URL, adding a magic link if the site has a mapped domain.
+ *
+ * This method handles both WP_Ultimo managed sites and regular WordPress sites
+ * that have domain mappings.
+ *
+ * @since 2.5.0
+ *
+ * @param int $blog_id The blog ID.
+ * @return string The URL, with magic link token if needed.
+ */
+ protected function get_subsite_url_with_magic_link(int $blog_id): string {
+
+ // Get current user - magic links only work for logged-in users.
+ $current_user_id = get_current_user_id();
+
+ if ( ! $current_user_id) {
+ return get_home_url($blog_id);
+ }
+
+ // Check if magic links are enabled.
+ if ( ! wu_get_setting('enable_magic_links', true)) {
+ return get_home_url($blog_id);
+ }
+
+ // Get the magic link instance.
+ $magic_link = Magic_Link::get_instance();
+
+ // Get the site's active URL (may include mapped domain).
+ $site = wu_get_site($blog_id);
+ $home_url = $site ? $site->get_active_site_url() : get_home_url($blog_id);
+
+ // Parse the URL to get the domain.
+ $site_domain = wp_parse_url($home_url, PHP_URL_HOST);
+
+ if ( ! $site_domain) {
+ return $home_url;
+ }
+
+ // Need a magic link - generate one.
+ $magic_link_url = $magic_link->generate_magic_link($current_user_id, $blog_id, $home_url);
+
+ return $magic_link_url ?: $home_url;
+ }
+
+ /**
+ * Setup the nav menu item for display in the admin.
+ *
+ * @since 2.5.0
+ *
+ * @param object $menu_item The menu item object.
+ * @return object The modified menu item object.
+ */
+ public function setup_nav_menu_item(object $menu_item): object {
+
+ // Check if this is a subsite menu item by looking for our meta.
+ $subsite_id = get_post_meta($menu_item->ID, self::META_KEY_SUBSITE_ID, true);
+
+ if (empty($subsite_id)) {
+ return $menu_item;
+ }
+
+ $subsite_id = absint($subsite_id);
+ $site = wu_get_site($subsite_id);
+
+ if ( ! $site) {
+ $menu_item->type_label = __('Subsite (Deleted)', 'ultimate-multisite');
+ $menu_item->_invalid = true;
+ return $menu_item;
+ }
+
+ $menu_item->type_label = __('Subsite', 'ultimate-multisite');
+ $menu_item->url = $site->get_active_site_url();
+
+ // Set the title if not already set or if it's empty.
+ if (empty($menu_item->title)) {
+ $menu_item->title = $site->get_title();
+ }
+
+ return $menu_item;
+ }
+}
diff --git a/inc/sso/class-sso.php b/inc/sso/class-sso.php
index c4ad08b3..81ba2c90 100644
--- a/inc/sso/class-sso.php
+++ b/inc/sso/class-sso.php
@@ -518,6 +518,9 @@ public function handle_broker($response_type = 'redirect'): void {
$verify_code = $this->input('sso_verify');
if ($verify_code) {
+ if ('invalid' === $verify_code) {
+ return;
+ }
$broker->verify($verify_code);
$url = $this->input('return_url', $this->get_current_url());
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=',
);
diff --git a/inc/tax/class-tax.php b/inc/tax/class-tax.php
index 90eaf4ab..f786c69a 100644
--- a/inc/tax/class-tax.php
+++ b/inc/tax/class-tax.php
@@ -32,13 +32,12 @@ public function init(): void {
add_action('wu_page_wp-ultimo-settings_load', [$this, 'add_sidebar_widget']);
- if ($this->is_enabled()) {
- add_action('wp_ultimo_admin_pages', [$this, 'add_admin_page']);
-
- add_action('wp_ajax_wu_get_tax_rates', [$this, 'serve_taxes_rates_via_ajax']);
-
- add_action('wp_ajax_wu_save_tax_rates', [$this, 'save_taxes_rates']);
+ // Always register the Tax Rates admin page so users can manage rates even when taxes are disabled.
+ add_action('wp_ultimo_admin_pages', [$this, 'add_admin_page']);
+ add_action('wp_ajax_wu_get_tax_rates', [$this, 'serve_taxes_rates_via_ajax']);
+ add_action('wp_ajax_wu_save_tax_rates', [$this, 'save_taxes_rates']);
+ if ($this->is_enabled()) {
add_action(
'wu_before_search_models',
function () {
diff --git a/inc/ui/class-account-summary-element.php b/inc/ui/class-account-summary-element.php
index ccaf8d5a..072d792d 100644
--- a/inc/ui/class-account-summary-element.php
+++ b/inc/ui/class-account-summary-element.php
@@ -121,7 +121,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds a account summary block to the page.', 'ultimate-multisite');
+ return __('Displays a summary of the customer\'s account including membership and site overview.', 'ultimate-multisite');
}
/**
diff --git a/inc/ui/class-billing-info-element.php b/inc/ui/class-billing-info-element.php
index b9449767..04fbee26 100644
--- a/inc/ui/class-billing-info-element.php
+++ b/inc/ui/class-billing-info-element.php
@@ -135,7 +135,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds a checkout form block to the page.', 'ultimate-multisite');
+ return __('Displays the customer\'s billing address and contact information.', 'ultimate-multisite');
}
/**
@@ -313,14 +313,14 @@ protected function apply_placeholders($fields) {
* Renders the update billing address form.
*
* @since 2.0.0
- * @return string
+ * @return void
*/
public function render_update_billing_address() {
$membership = wu_get_membership_by_hash(wu_request('membership'));
if ( ! $membership) {
- return '';
+ return;
}
$billing_address = $membership->get_billing_address();
diff --git a/inc/ui/class-current-membership-element.php b/inc/ui/class-current-membership-element.php
index 822f7d38..cc47a9f5 100644
--- a/inc/ui/class-current-membership-element.php
+++ b/inc/ui/class-current-membership-element.php
@@ -143,7 +143,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds a checkout form block to the page.', 'ultimate-multisite');
+ return __('Displays the current membership details including plan, status, and billing cycle.', 'ultimate-multisite');
}
/**
diff --git a/inc/ui/class-current-site-element.php b/inc/ui/class-current-site-element.php
index 097a9398..0ffbcf75 100644
--- a/inc/ui/class-current-site-element.php
+++ b/inc/ui/class-current-site-element.php
@@ -124,7 +124,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds a block to display the current site being managed.', 'ultimate-multisite');
+ return __('Displays details about the currently selected site including title, URL, and quick actions.', 'ultimate-multisite');
}
/**
@@ -405,14 +405,14 @@ public function output($atts, $content = null): void {
* Renders the edit site modal.
*
* @since 2.0.0
- * @return string
+ * @return void
*/
public function render_edit_site() {
$site = wu_get_site_by_hash(wu_request('site'));
if ( ! $site) {
- return '';
+ return;
}
$fields = [
diff --git a/inc/ui/class-domain-mapping-element.php b/inc/ui/class-domain-mapping-element.php
index 2c4834dd..fea0c0f6 100644
--- a/inc/ui/class-domain-mapping-element.php
+++ b/inc/ui/class-domain-mapping-element.php
@@ -109,7 +109,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds the site\'s domains block.', 'ultimate-multisite');
+ return __('Allows customers to manage custom domains mapped to their site.', 'ultimate-multisite');
}
/**
diff --git a/inc/ui/class-invoices-element.php b/inc/ui/class-invoices-element.php
index a5224fd4..f89e60b4 100644
--- a/inc/ui/class-invoices-element.php
+++ b/inc/ui/class-invoices-element.php
@@ -97,7 +97,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds a checkout form block to the page.', 'ultimate-multisite');
+ return __('Displays a list of the customer\'s invoices and payment history.', 'ultimate-multisite');
}
/**
diff --git a/inc/ui/class-limits-element.php b/inc/ui/class-limits-element.php
index fe9d4d50..89d6f20d 100644
--- a/inc/ui/class-limits-element.php
+++ b/inc/ui/class-limits-element.php
@@ -96,7 +96,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds a checkout form block to the page.', 'ultimate-multisite');
+ return __('Displays the site\'s usage limits and quotas such as disk space, posts, and users.', 'ultimate-multisite');
}
/**
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/inc/ui/class-magic-link-url-element.php b/inc/ui/class-magic-link-url-element.php
new file mode 100644
index 00000000..a7c9c33a
--- /dev/null
+++ b/inc/ui/class-magic-link-url-element.php
@@ -0,0 +1,428 @@
+>
+ */
+ public function fields(): array {
+
+ $fields = [];
+
+ $fields['header'] = [
+ 'title' => __('General', 'ultimate-multisite'),
+ 'desc' => __('General', 'ultimate-multisite'),
+ 'type' => 'header',
+ ];
+
+ $fields['site_id'] = [
+ 'type' => 'text',
+ 'title' => __('Site ID', 'ultimate-multisite'),
+ 'placeholder' => __('E.g. 2', 'ultimate-multisite'),
+ 'desc' => __('The ID of the site to generate the magic link for.', 'ultimate-multisite'),
+ 'tooltip' => __('You can find the site ID in the Sites list in the network admin.', 'ultimate-multisite'),
+ ];
+
+ $fields['display_header'] = [
+ 'title' => __('Display Options', 'ultimate-multisite'),
+ 'desc' => __('Display Options', 'ultimate-multisite'),
+ 'type' => 'header',
+ ];
+
+ $fields['display_as'] = [
+ 'type' => 'select',
+ 'title' => __('Display As', 'ultimate-multisite'),
+ 'desc' => __('Choose how to display the magic link.', 'ultimate-multisite'),
+ 'options' => [
+ 'anchor' => __('Clickable Link', 'ultimate-multisite'),
+ 'button' => __('Button', 'ultimate-multisite'),
+ 'url' => __('Plain URL Text', 'ultimate-multisite'),
+ ],
+ 'value' => 'anchor',
+ ];
+
+ $fields['link_text'] = [
+ 'type' => 'text',
+ 'title' => __('Link Text', 'ultimate-multisite'),
+ 'placeholder' => __('E.g. Visit Site', 'ultimate-multisite'),
+ 'desc' => __('The text to display for the link or button.', 'ultimate-multisite'),
+ 'value' => __('Visit Site', 'ultimate-multisite'),
+ 'required' => [
+ 'display_as' => ['anchor', 'button'],
+ ],
+ ];
+
+ $fields['open_in_new_tab'] = [
+ 'type' => 'toggle',
+ 'title' => __('Open in New Tab?', 'ultimate-multisite'),
+ 'desc' => __('Toggle to open the link in a new browser tab.', 'ultimate-multisite'),
+ 'value' => 0,
+ 'required' => [
+ 'display_as' => ['anchor', 'button'],
+ ],
+ ];
+
+ return $fields;
+ }
+
+ /**
+ * Registers scripts and styles necessary to render this.
+ *
+ * @since 2.0.0
+ * @return void
+ */
+ public function register_scripts(): void {
+
+ wp_enqueue_style('wu-admin');
+ }
+
+ /**
+ * The list of keywords for this element.
+ *
+ * Return an array of strings with keywords describing this
+ * element. Gutenberg uses this to help customers find blocks.
+ *
+ * e.g.:
+ * return array(
+ * 'Ultimate Multisite',
+ * 'Magic Link',
+ * 'URL',
+ * );
+ *
+ * @since 2.0.0
+ * @return array
+ */
+ public function keywords(): array {
+
+ return [
+ 'WP Ultimo',
+ 'Ultimate Multisite',
+ 'Magic Link',
+ 'URL',
+ 'SSO',
+ 'Authentication',
+ 'Login',
+ 'Site Access',
+ ];
+ }
+
+ /**
+ * List of default parameters for the element.
+ *
+ * If you are planning to add controls using the fields,
+ * it might be a good idea to use this method to set defaults
+ * for the parameters you are expecting.
+ *
+ * These defaults will be used inside a 'wp_parse_args' call
+ * before passing the parameters down to the block render
+ * function and the shortcode render function.
+ *
+ * @since 2.0.0
+ * @return array
+ */
+ public function defaults(): array {
+
+ return [
+ 'site_id' => '',
+ 'redirect_to' => '',
+ 'display_as' => 'anchor',
+ 'link_text' => __('Visit Site', 'ultimate-multisite'),
+ 'open_in_new_tab' => 0,
+ ];
+ }
+
+ /**
+ * Runs early on the request lifecycle as soon as we detect the shortcode is present.
+ *
+ * @since 2.0.0
+ * @return void
+ */
+ public function setup(): void {
+ // No setup restrictions - we render for both logged-in and anonymous users.
+ // Anonymous users get a regular link without the magic token.
+ }
+
+ /**
+ * Allows the setup in the context of previews.
+ *
+ * @since 2.0.0
+ * @return void
+ */
+ public function setup_preview(): void {
+
+ $this->site = wu_mock_site();
+ $this->magic_link_url = home_url('?wu_magic_token=preview_token_example');
+ }
+
+ /**
+ * Get the site ID from attributes or current context.
+ *
+ * @since 2.0.0
+ *
+ * @param array $atts Block/shortcode attributes.
+ * @return int|null The site ID or null if not found.
+ */
+ protected function get_site_id_from_atts(array $atts): ?int {
+
+ // If site_id is explicitly provided, use it
+ if ( ! empty($atts['site_id'])) {
+ $site_id = $atts['site_id'];
+
+ if (is_numeric($site_id)) {
+ return absint($site_id);
+ }
+ }
+
+ // Try to get from URL query parameter
+ $site_hash = wu_request('site');
+
+ if ($site_hash && is_string($site_hash)) {
+ $site = wu_get_site_by_hash($site_hash);
+
+ if ($site) {
+ return $site->get_id();
+ }
+ }
+
+ // Try to get current site from WP_Ultimo context
+ $current_site = WP_Ultimo()->currents->get_site();
+
+ if ($current_site) {
+ return $current_site->get_id();
+ }
+
+ return null;
+ }
+
+ /**
+ * Generate the URL for the site.
+ *
+ * For logged-in users with site access, generates a magic link with authentication token.
+ * For anonymous users or users without access, returns a regular site URL.
+ *
+ * @since 2.0.0
+ *
+ * @param int $site_id The site ID.
+ * @param string $redirect_to Optional redirect URL.
+ * @return string|null The URL or null on failure.
+ */
+ protected function generate_url(int $site_id, string $redirect_to = ''): ?string {
+
+ $site = wu_get_site($site_id);
+
+ if ( ! $site) {
+ return null;
+ }
+
+ $user_id = get_current_user_id();
+ $url = null;
+
+ // Try to generate magic link for logged-in users with site access
+ if ($user_id && is_user_member_of_blog($user_id, $site_id) && wu_get_setting('enable_magic_links', true)) {
+ $magic_link = Magic_Link::get_instance();
+ $url = $magic_link->generate_magic_link($user_id, $site_id, $redirect_to);
+ }
+
+ // Fall back to regular site URL for anonymous users or if magic link fails
+ if ( ! $url) {
+ $url = $site->get_active_site_url();
+
+ if ($redirect_to) {
+ $url = trailingslashit($url) . ltrim($redirect_to, '/');
+ }
+ }
+
+ return $url;
+ }
+
+ /**
+ * The content to be output on the screen.
+ *
+ * Should return HTML markup to be used to display the block.
+ * This method is shared between the block render method and
+ * the shortcode implementation.
+ *
+ * @since 2.0.0
+ *
+ * @param array $atts Parameters of the block/shortcode.
+ * @param string|null $content The content inside the shortcode.
+ * @return void
+ */
+ public function output($atts, $content = null): void {
+
+ $this->ensure_setup();
+
+ // Get the site ID
+ $site_id = $this->get_site_id_from_atts($atts);
+
+ if ( ! $site_id) {
+ // No site ID available, show error message in admin context
+ if (current_user_can('manage_network')) {
+ echo '' . esc_html__('Magic Link URL: No site ID specified or found.', 'ultimate-multisite') . '
';
+ }
+
+ return;
+ }
+
+ // Get the site
+ $site = wu_get_site($site_id);
+
+ if ( ! $site) {
+ if (current_user_can('manage_network')) {
+ echo '' . esc_html__('Magic Link URL: Site not found.', 'ultimate-multisite') . '
';
+ }
+
+ return;
+ }
+
+ // Build redirect URL
+ $redirect_to = '';
+
+ if ( ! empty($atts['redirect_to']) && is_string($atts['redirect_to'])) {
+ $redirect_to = $atts['redirect_to'];
+
+ // If it's a relative path, make it absolute
+ if (strpos($redirect_to, 'http') !== 0) {
+ $redirect_to = trailingslashit($site->get_active_site_url()) . ltrim($redirect_to, '/');
+ }
+ }
+
+ // Generate the magic link URL
+ $magic_link_url = $this->generate_url($site_id, $redirect_to);
+
+ if ( ! $magic_link_url) {
+ return;
+ }
+
+ // Prepare template variables
+ $atts['magic_link_url'] = $magic_link_url;
+ $atts['site'] = $site;
+
+ wu_get_template('dashboard-widgets/magic-link-url', $atts);
+ }
+}
diff --git a/inc/ui/class-my-sites-element.php b/inc/ui/class-my-sites-element.php
index ddb15310..edbbf4de 100644
--- a/inc/ui/class-my-sites-element.php
+++ b/inc/ui/class-my-sites-element.php
@@ -107,7 +107,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds a block to display the sites owned by the current customer.', 'ultimate-multisite');
+ return __('Displays a list of all sites owned by the current customer with quick access links.', 'ultimate-multisite');
}
/**
diff --git a/inc/ui/class-payment-methods-element.php b/inc/ui/class-payment-methods-element.php
index f0fe469e..50e99d0e 100644
--- a/inc/ui/class-payment-methods-element.php
+++ b/inc/ui/class-payment-methods-element.php
@@ -81,7 +81,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds a checkout form block to the page.', 'ultimate-multisite');
+ return __('Displays and manages the customer\'s saved payment methods.', 'ultimate-multisite');
}
/**
diff --git a/inc/ui/class-site-actions-element.php b/inc/ui/class-site-actions-element.php
index b1c5cfe4..de2d1655 100644
--- a/inc/ui/class-site-actions-element.php
+++ b/inc/ui/class-site-actions-element.php
@@ -120,7 +120,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds a checkout form block to the page.', 'ultimate-multisite');
+ return __('Displays action buttons for site management such as preview, publish, and delete.', 'ultimate-multisite');
}
/**
@@ -371,7 +371,7 @@ public function get_actions($atts) {
$is_template_switching_enabled = wu_get_setting('allow_template_switching', true);
if ($is_template_switching_enabled &&
- $this->site->has_limitations() &&
+ $this->site && $this->site->has_limitations() &&
Limit_Site_Templates::MODE_ASSIGN_TEMPLATE === $this->site->get_limitations()->site_templates->get_mode()) {
$is_template_switching_enabled = false;
}
@@ -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/inc/ui/class-site-maintenance-element.php b/inc/ui/class-site-maintenance-element.php
index 165c0f63..e572f5a2 100644
--- a/inc/ui/class-site-maintenance-element.php
+++ b/inc/ui/class-site-maintenance-element.php
@@ -104,7 +104,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds the toggle control to turn maintenance mode on.', 'ultimate-multisite');
+ return __('Provides a toggle control for customers to enable or disable site maintenance mode.', 'ultimate-multisite');
}
/**
diff --git a/inc/ui/class-template-switching-element.php b/inc/ui/class-template-switching-element.php
index be235061..fba02104 100644
--- a/inc/ui/class-template-switching-element.php
+++ b/inc/ui/class-template-switching-element.php
@@ -92,7 +92,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds the template switching form to this page.', 'ultimate-multisite');
+ return __('Allows customers to switch their site to a different template design.', 'ultimate-multisite');
}
/**
@@ -273,8 +273,10 @@ public function switch_template() {
$template_id = (int) wu_request('template_id', '');
- if (! in_array($template_id, $this->site->get_limitations()->site_templates->get_available_site_templates(), true)) {
- wp_send_json_error(new \WP_Error('not_authorized', __('You are not allow to use this template.', 'ultimate-multisite')));
+ $available_templates = array_map('intval', $this->site->get_limitations()->site_templates->get_available_site_templates());
+
+ if (! in_array($template_id, $available_templates, true)) {
+ wp_send_json_error(new \WP_Error('not_authorized', __('You are not allowed to use this template.', 'ultimate-multisite')));
}
if ( ! $template_id) {
diff --git a/inc/ui/class-thank-you-element.php b/inc/ui/class-thank-you-element.php
index 8954d4d8..31076bab 100644
--- a/inc/ui/class-thank-you-element.php
+++ b/inc/ui/class-thank-you-element.php
@@ -172,7 +172,7 @@ public function get_title() {
*/
public function get_description() {
- return __('Adds a checkout form block to the page.', 'ultimate-multisite');
+ return __('Displays a confirmation message after successful checkout or registration.', 'ultimate-multisite');
}
/**
diff --git a/lang/ultimate-multisite.pot b/lang/ultimate-multisite.pot
index 20b4d05a..b565de1d 100644
--- a/lang/ultimate-multisite.pot
+++ b/lang/ultimate-multisite.pot
@@ -1,15 +1,15 @@
-# Copyright (C) 2025 Ultimate Multisite Community
+# Copyright (C) 2026 Ultimate Multisite Community
# This file is distributed under the GPL2.
msgid ""
msgstr ""
-"Project-Id-Version: Ultimate Multisite 2.4.9\n"
+"Project-Id-Version: Ultimate Multisite 2.4.10\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/ultimate-multisite\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"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-23T00:10:37+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"
@@ -19,7 +19,7 @@ msgstr ""
#: inc/admin-pages/class-dashboard-admin-page.php:551
#: inc/admin-pages/class-migration-alert-admin-page.php:103
#: inc/admin-pages/class-setup-wizard-admin-page.php:310
-#: inc/admin-pages/class-setup-wizard-admin-page.php:676
+#: inc/admin-pages/class-setup-wizard-admin-page.php:675
#: inc/admin-pages/class-top-admin-nav-menu.php:55
#: inc/class-credits.php:156
#: inc/class-credits.php:200
@@ -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 ""
@@ -261,8 +261,8 @@ msgstr ""
#: inc/list-tables/class-broadcast-list-table.php:440
#: inc/list-tables/class-email-list-table.php:39
#: inc/list-tables/class-email-list-table.php:298
-#: inc/models/class-checkout-form.php:550
-#: inc/models/class-checkout-form.php:707
+#: inc/models/class-checkout-form.php:560
+#: inc/models/class-checkout-form.php:717
#: views/dashboard-widgets/thank-you.php:73
#: views/dashboard-widgets/thank-you.php:181
msgid "Email"
@@ -407,7 +407,7 @@ msgstr ""
#: inc/admin-pages/class-domain-edit-admin-page.php:497
#: inc/admin-pages/class-edit-admin-page.php:277
#: inc/admin-pages/class-email-edit-admin-page.php:452
-#: inc/admin-pages/class-email-template-customize-admin-page.php:556
+#: inc/admin-pages/class-email-template-customize-admin-page.php:498
#: inc/admin-pages/class-event-view-admin-page.php:253
#: inc/admin-pages/class-membership-edit-admin-page.php:997
#: inc/admin-pages/class-payment-edit-admin-page.php:1225
@@ -494,7 +494,7 @@ msgstr ""
#: inc/admin-pages/class-broadcast-list-admin-page.php:378
#: inc/admin-pages/class-checkout-form-edit-admin-page.php:858
-#: inc/admin-pages/class-email-template-customize-admin-page.php:247
+#: inc/admin-pages/class-email-template-customize-admin-page.php:244
#: inc/admin-pages/class-placeholders-admin-page.php:112
#: inc/checkout/signup-fields/class-base-signup-field.php:510
#: inc/list-tables/class-broadcast-list-table.php:418
@@ -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 ""
@@ -661,7 +661,7 @@ msgstr ""
#: inc/admin-pages/class-checkout-form-edit-admin-page.php:724
#: inc/compat/class-legacy-shortcodes.php:352
-#: inc/models/class-checkout-form.php:831
+#: inc/models/class-checkout-form.php:841
#: views/legacy/signup/pricing-table/frequency-selector.php:31
msgid "Monthly"
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:1752
msgid "Advanced Options"
msgstr ""
@@ -939,7 +939,7 @@ msgid "Enter Checkout Form Name"
msgstr ""
#: inc/admin-pages/class-checkout-form-edit-admin-page.php:1455
-#: inc/admin-pages/class-email-template-customize-admin-page.php:552
+#: inc/admin-pages/class-email-template-customize-admin-page.php:494
#: inc/admin-pages/class-invoice-template-customize-admin-page.php:365
#: inc/admin-pages/class-template-previewer-customize-admin-page.php:289
msgid "This name is used for internal reference only."
@@ -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
@@ -1120,7 +1120,7 @@ msgstr ""
#: views/base/checkout-forms/steps.php:144
#: views/base/checkout-forms/steps.php:154
#: views/dashboard-widgets/domain-mapping.php:85
-#: views/taxes/list.php:66
+#: views/taxes/list.php:88
msgid "Delete"
msgstr ""
@@ -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
@@ -1286,6 +1286,8 @@ msgstr ""
#: inc/ui/class-limits-element.php:125
#: inc/ui/class-login-form-element.php:140
#: inc/ui/class-login-form-element.php:141
+#: inc/ui/class-magic-link-url-element.php:138
+#: inc/ui/class-magic-link-url-element.php:139
#: inc/ui/class-my-sites-element.php:135
#: inc/ui/class-my-sites-element.php:136
#: inc/ui/class-payment-methods-element.php:109
@@ -1339,8 +1341,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 +1355,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
@@ -1395,8 +1397,8 @@ msgstr ""
#: inc/admin-pages/class-customer-edit-admin-page.php:881
#: inc/list-tables/class-product-list-table.php:132
#: inc/list-tables/class-product-list-table.php:178
-#: inc/models/class-payment.php:552
-#: inc/models/class-payment.php:558
+#: inc/models/class-payment.php:572
+#: inc/models/class-payment.php:578
#: views/admin-pages/fields/field-text-display.php:47
#: views/admin-pages/fields/field-text-edit.php:50
#: views/base/customers/grid-item.php:129
@@ -1511,8 +1513,8 @@ msgstr ""
#: inc/admin-pages/class-customer-list-admin-page.php:209
#: inc/checkout/class-legacy-checkout.php:487
#: inc/checkout/signup-fields/class-signup-field-username.php:72
-#: inc/models/class-checkout-form.php:559
-#: inc/models/class-checkout-form.php:716
+#: inc/models/class-checkout-form.php:569
+#: inc/models/class-checkout-form.php:726
msgid "Username"
msgstr ""
@@ -1542,8 +1544,8 @@ msgstr ""
#: inc/admin-pages/class-customer-list-admin-page.php:236
#: inc/checkout/class-legacy-checkout.php:509
#: inc/checkout/signup-fields/class-signup-field-password.php:72
-#: inc/models/class-checkout-form.php:569
-#: inc/models/class-checkout-form.php:726
+#: inc/models/class-checkout-form.php:579
+#: inc/models/class-checkout-form.php:736
#: inc/ui/class-login-form-element.php:230
#: views/checkout/partials/inline-login-prompt.php:31
msgid "Password"
@@ -1809,8 +1811,8 @@ msgstr ""
#: inc/admin-pages/class-discount-code-edit-admin-page.php:268
#: inc/checkout/signup-fields/class-signup-field-discount-code.php:58
-#: inc/models/class-checkout-form.php:1009
-#: inc/models/class-checkout-form.php:1010
+#: inc/models/class-checkout-form.php:1019
+#: inc/models/class-checkout-form.php:1020
msgid "Coupon Code"
msgstr ""
@@ -2545,7 +2547,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 ""
@@ -2557,215 +2559,176 @@ msgstr ""
msgid "Sample Subject"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:221
+#: inc/admin-pages/class-email-template-customize-admin-page.php:218
msgid "System emails and broadcasts will be sent using this template."
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:246
+#: inc/admin-pages/class-email-template-customize-admin-page.php:243
msgid "Header"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:248
+#: inc/admin-pages/class-email-template-customize-admin-page.php:245
msgid "Footer"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:253
+#: inc/admin-pages/class-email-template-customize-admin-page.php:250
+msgid "Hide Logo"
+msgstr ""
+
+#: inc/admin-pages/class-email-template-customize-admin-page.php:251
+msgid "Toggle to hide the logo in the email header."
+msgstr ""
+
+#: inc/admin-pages/class-email-template-customize-admin-page.php:262
#: inc/admin-pages/class-invoice-template-customize-admin-page.php:258
#: inc/admin-pages/class-template-previewer-customize-admin-page.php:192
msgid "Use Custom Logo"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:254
+#: inc/admin-pages/class-email-template-customize-admin-page.php:263
msgid "You can set a different logo to be used on the system emails."
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:266
+#: inc/admin-pages/class-email-template-customize-admin-page.php:275
#: inc/admin-pages/class-invoice-template-customize-admin-page.php:270
#: inc/admin-pages/class-template-previewer-customize-admin-page.php:205
msgid "Custom Logo"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:267
+#: inc/admin-pages/class-email-template-customize-admin-page.php:276
msgid "The custom logo is used in the email header, if HTML emails are used."
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:280
+#: inc/admin-pages/class-email-template-customize-admin-page.php:289
#: inc/admin-pages/class-template-previewer-customize-admin-page.php:166
msgid "Background Color"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:281
+#: inc/admin-pages/class-email-template-customize-admin-page.php:290
msgid "The cover background color of the email."
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:293
-msgid "Title Color"
-msgstr ""
-
-#: inc/admin-pages/class-email-template-customize-admin-page.php:305
-msgid "Title Size"
-msgstr ""
-
-#: inc/admin-pages/class-email-template-customize-admin-page.php:308
-msgid "h1"
-msgstr ""
-
-#: inc/admin-pages/class-email-template-customize-admin-page.php:309
-msgid "h2"
-msgstr ""
-
-#: inc/admin-pages/class-email-template-customize-admin-page.php:310
-msgid "h3"
-msgstr ""
-
-#: inc/admin-pages/class-email-template-customize-admin-page.php:311
-msgid "h4"
-msgstr ""
-
-#: inc/admin-pages/class-email-template-customize-admin-page.php:312
-msgid "h5"
+#: inc/admin-pages/class-email-template-customize-admin-page.php:302
+msgid "Content Color"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:324
-msgid "Title Align"
+#: inc/admin-pages/class-email-template-customize-admin-page.php:314
+msgid "Content Alignment"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:325
-msgid "Aligment of the font in the title."
+#: inc/admin-pages/class-email-template-customize-admin-page.php:315
+msgid "Alignment of the font in the main email content."
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:328
-#: inc/admin-pages/class-email-template-customize-admin-page.php:376
-#: inc/admin-pages/class-email-template-customize-admin-page.php:467
+#: inc/admin-pages/class-email-template-customize-admin-page.php:318
+#: inc/admin-pages/class-email-template-customize-admin-page.php:409
#: inc/ui/class-current-site-element.php:221
msgid "Left"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:329
-#: inc/admin-pages/class-email-template-customize-admin-page.php:377
-#: inc/admin-pages/class-email-template-customize-admin-page.php:468
+#: inc/admin-pages/class-email-template-customize-admin-page.php:319
+#: inc/admin-pages/class-email-template-customize-admin-page.php:410
msgid "Center"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:330
-#: inc/admin-pages/class-email-template-customize-admin-page.php:378
-#: inc/admin-pages/class-email-template-customize-admin-page.php:469
+#: inc/admin-pages/class-email-template-customize-admin-page.php:320
+#: inc/admin-pages/class-email-template-customize-admin-page.php:411
#: inc/ui/class-current-site-element.php:220
msgid "Right"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:342
-msgid "Title Font-Family"
+#: inc/admin-pages/class-email-template-customize-admin-page.php:332
+msgid "Content Font-Family"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:345
-#: inc/admin-pages/class-email-template-customize-admin-page.php:393
-#: inc/admin-pages/class-email-template-customize-admin-page.php:436
+#: inc/admin-pages/class-email-template-customize-admin-page.php:335
+#: inc/admin-pages/class-email-template-customize-admin-page.php:378
msgid "Helvetica"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:346
-#: inc/admin-pages/class-email-template-customize-admin-page.php:394
-#: inc/admin-pages/class-email-template-customize-admin-page.php:437
+#: inc/admin-pages/class-email-template-customize-admin-page.php:336
+#: inc/admin-pages/class-email-template-customize-admin-page.php:379
msgid "Arial"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:347
-#: inc/admin-pages/class-email-template-customize-admin-page.php:395
-#: inc/admin-pages/class-email-template-customize-admin-page.php:438
+#: inc/admin-pages/class-email-template-customize-admin-page.php:337
+#: inc/admin-pages/class-email-template-customize-admin-page.php:380
msgid "Times New Roman"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:348
-#: inc/admin-pages/class-email-template-customize-admin-page.php:396
-#: inc/admin-pages/class-email-template-customize-admin-page.php:439
+#: inc/admin-pages/class-email-template-customize-admin-page.php:338
+#: inc/admin-pages/class-email-template-customize-admin-page.php:381
msgid "Lucida"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:360
-msgid "Content Color"
-msgstr ""
-
-#: inc/admin-pages/class-email-template-customize-admin-page.php:372
-msgid "Content Alignment"
-msgstr ""
-
-#: inc/admin-pages/class-email-template-customize-admin-page.php:373
-msgid "Alignment of the font in the main email content."
-msgstr ""
-
-#: inc/admin-pages/class-email-template-customize-admin-page.php:390
-msgid "Content Font-Family"
-msgstr ""
-
-#: inc/admin-pages/class-email-template-customize-admin-page.php:408
+#: inc/admin-pages/class-email-template-customize-admin-page.php:350
msgid "Display Company Address"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:409
+#: inc/admin-pages/class-email-template-customize-admin-page.php:351
msgid "Toggle to show/hide your company address."
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:420
+#: inc/admin-pages/class-email-template-customize-admin-page.php:362
#: inc/admin-pages/class-invoice-template-customize-admin-page.php:231
msgid "Footer Content"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:421
+#: inc/admin-pages/class-email-template-customize-admin-page.php:363
msgid "e.g. Extra info in the email footer."
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:433
+#: inc/admin-pages/class-email-template-customize-admin-page.php:375
msgid "Footer Font-Family"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:451
+#: inc/admin-pages/class-email-template-customize-admin-page.php:393
msgid "Footer Color"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:463
+#: inc/admin-pages/class-email-template-customize-admin-page.php:405
msgid "Footer Alignment"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:464
+#: inc/admin-pages/class-email-template-customize-admin-page.php:406
msgid "Alignment of the font in the main email footer."
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:492
+#: inc/admin-pages/class-email-template-customize-admin-page.php:434
#: inc/admin-pages/class-invoice-template-customize-admin-page.php:305
#: inc/admin-pages/class-template-previewer-customize-admin-page.php:229
msgid "Customizer"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:513
+#: inc/admin-pages/class-email-template-customize-admin-page.php:455
msgid "Customize Email Template:"
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-email-template-customize-admin-page.php:466
+#: inc/admin-pages/class-email-template-customize-admin-page.php:489
+#: inc/admin-pages/class-email-template-customize-admin-page.php:490
+#: inc/admin-pages/class-settings-admin-page.php:417
+#: inc/admin-pages/class-settings-admin-page.php:421
msgid "Customize Email Template"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:549
+#: inc/admin-pages/class-email-template-customize-admin-page.php:491
msgid "Edit Email Template"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:550
+#: inc/admin-pages/class-email-template-customize-admin-page.php:492
msgid "Email Template updated with success!"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:551
+#: inc/admin-pages/class-email-template-customize-admin-page.php:493
msgid "Enter Email Template Name"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:553
+#: inc/admin-pages/class-email-template-customize-admin-page.php:495
msgid "Save Template"
msgstr ""
-#: inc/admin-pages/class-email-template-customize-admin-page.php:555
+#: inc/admin-pages/class-email-template-customize-admin-page.php:497
msgid "Delete Email Template"
msgstr ""
@@ -2829,6 +2792,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 ""
@@ -3483,9 +3447,9 @@ msgstr ""
#: inc/admin-pages/class-payment-edit-admin-page.php:319
#: inc/admin-pages/class-payment-edit-admin-page.php:320
-#: inc/models/class-checkout-form.php:659
-#: inc/models/class-checkout-form.php:693
-#: inc/models/class-checkout-form.php:737
+#: inc/models/class-checkout-form.php:669
+#: inc/models/class-checkout-form.php:703
+#: inc/models/class-checkout-form.php:747
msgid "Next Step"
msgstr ""
@@ -3632,7 +3596,7 @@ msgid "Tax description. This is shown on invoices to end customers."
msgstr ""
#: inc/admin-pages/class-payment-edit-admin-page.php:802
-#: inc/tax/class-tax.php:204
+#: inc/tax/class-tax.php:205
msgid "Tax Rate"
msgstr ""
@@ -3913,7 +3877,7 @@ msgid "Products can be free, paid, or require further contact for pricing."
msgstr ""
#: inc/admin-pages/class-product-edit-admin-page.php:300
-#: inc/models/class-product.php:649
+#: inc/models/class-product.php:699
msgid "Contact Us"
msgstr ""
@@ -4127,8 +4091,8 @@ msgstr ""
#: inc/list-tables/class-line-item-list-table.php:216
#: inc/tax/class-dashboard-taxes-tab.php:63
#: inc/tax/class-dashboard-taxes-tab.php:151
-#: inc/tax/class-tax.php:104
#: inc/tax/class-tax.php:105
+#: inc/tax/class-tax.php:106
#: views/checkout/templates/order-summary/simple.php:188
msgid "Taxes"
msgstr ""
@@ -4240,88 +4204,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 +4293,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 ""
@@ -4464,66 +4462,66 @@ msgstr ""
msgid "Optionally install helpful plugins. We will install them one by one and report progress."
msgstr ""
-#: inc/admin-pages/class-setup-wizard-admin-page.php:609
+#: inc/admin-pages/class-setup-wizard-admin-page.php:608
msgid "A server error happened while processing this item."
msgstr ""
-#: inc/admin-pages/class-setup-wizard-admin-page.php:649
+#: inc/admin-pages/class-setup-wizard-admin-page.php:648
msgid "PHP"
msgstr ""
-#: inc/admin-pages/class-setup-wizard-admin-page.php:658
+#: inc/admin-pages/class-setup-wizard-admin-page.php:657
msgid "WordPress"
msgstr ""
-#: inc/admin-pages/class-setup-wizard-admin-page.php:670
+#: inc/admin-pages/class-setup-wizard-admin-page.php:669
msgid "WordPress Multisite"
msgstr ""
-#: inc/admin-pages/class-setup-wizard-admin-page.php:672
+#: inc/admin-pages/class-setup-wizard-admin-page.php:671
msgid "Installed & Activated"
msgstr ""
-#: inc/admin-pages/class-setup-wizard-admin-page.php:678
+#: inc/admin-pages/class-setup-wizard-admin-page.php:677
msgid "Bypassed via filter"
msgstr ""
#. Translators: The plugin is network wide active, the string is for each plugin possible.
-#: inc/admin-pages/class-setup-wizard-admin-page.php:678
+#: inc/admin-pages/class-setup-wizard-admin-page.php:677
#: inc/admin/class-network-usage-columns.php:127
msgid "Network Activated"
msgstr ""
-#: inc/admin-pages/class-setup-wizard-admin-page.php:682
+#: inc/admin-pages/class-setup-wizard-admin-page.php:681
msgid "WordPress Cron"
msgstr ""
-#: inc/admin-pages/class-setup-wizard-admin-page.php:684
+#: inc/admin-pages/class-setup-wizard-admin-page.php:683
#: inc/integrations/host-providers/class-base-host-provider.php:200
msgid "Activated"
msgstr ""
#. translators: %s code snippet.
-#: inc/admin-pages/class-setup-wizard-admin-page.php:773
+#: inc/admin-pages/class-setup-wizard-admin-page.php:772
#, php-format
msgid "The SUNRISE constant is missing. Domain mapping and plugin/theme limits will not function until `%s` is added to wp-config.php. Please complete the setup to attempt to do this automatically."
msgstr ""
-#: inc/admin-pages/class-setup-wizard-admin-page.php:775
+#: inc/admin-pages/class-setup-wizard-admin-page.php:774
msgid "Ultimate Multisite installation is incomplete. The sunrise.php file is missing. Please complete the setup to ensure proper functionality."
msgstr ""
-#: inc/admin-pages/class-setup-wizard-admin-page.php:780
+#: inc/admin-pages/class-setup-wizard-admin-page.php:779
msgid "Complete Setup"
msgstr ""
-#: inc/admin-pages/class-setup-wizard-admin-page.php:896
-#: inc/class-scripts.php:196
+#: inc/admin-pages/class-setup-wizard-admin-page.php:895
+#: 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/admin-pages/class-setup-wizard-admin-page.php:896
+#: inc/class-scripts.php:228
msgid "Use this image"
msgstr ""
@@ -4539,6 +4537,7 @@ msgid "integer"
msgstr ""
#: inc/admin-pages/class-shortcodes-admin-page.php:160
+#: inc/class-scripts.php:177
msgid "number"
msgstr ""
@@ -4568,7 +4567,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 +4580,8 @@ msgid "Site Type"
msgstr ""
#: inc/admin-pages/class-site-edit-admin-page.php:328
+#: inc/ui/class-magic-link-url-element.php:145
+#: views/wizards/host-integrations/rocket-instructions.php:12
msgid "Site ID"
msgstr ""
@@ -4594,7 +4595,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 ""
@@ -4726,6 +4727,8 @@ msgstr ""
#: inc/list-tables/class-customers-site-list-table.php:79
#: inc/list-tables/class-memberships-site-list-table.php:65
#: inc/ui/class-current-site-element.php:364
+#: inc/ui/class-magic-link-url-element.php:182
+#: inc/ui/class-magic-link-url-element.php:262
#: views/dashboard-widgets/my-sites.php:111
msgid "Visit Site"
msgstr ""
@@ -4789,9 +4792,9 @@ msgstr ""
#: inc/admin-pages/class-site-list-admin-page.php:352
#: inc/checkout/class-legacy-checkout.php:445
#: inc/checkout/signup-fields/class-signup-field-site-title.php:69
-#: inc/models/class-checkout-form.php:580
-#: inc/models/class-checkout-form.php:672
-#: inc/models/class-checkout-form.php:1520
+#: inc/models/class-checkout-form.php:590
+#: inc/models/class-checkout-form.php:682
+#: inc/models/class-checkout-form.php:1530
#: inc/ui/class-current-site-element.php:421
msgid "Site Title"
msgstr ""
@@ -4888,7 +4891,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 ""
@@ -4938,8 +4941,8 @@ msgstr ""
#: inc/admin-pages/class-tax-rates-admin-page.php:75
#: inc/admin-pages/class-tax-rates-admin-page.php:86
#: inc/admin-pages/class-tax-rates-admin-page.php:97
-#: inc/tax/class-tax.php:148
-#: views/taxes/list.php:12
+#: inc/tax/class-tax.php:149
+#: views/taxes/list.php:14
msgid "Tax Rates"
msgstr ""
@@ -5081,6 +5084,7 @@ msgstr ""
#: inc/admin-pages/class-view-logs-admin-page.php:103
#: inc/admin-pages/class-webhook-edit-admin-page.php:103
#: inc/admin-pages/class-webhook-list-admin-page.php:77
+#: inc/class-tracker.php:782
msgid "Copied!"
msgstr ""
@@ -5341,13 +5345,13 @@ msgstr ""
#: inc/admin-pages/customer-panel/class-checkout-admin-page.php:101
#: inc/compat/class-legacy-shortcodes.php:397
#: inc/compat/class-legacy-shortcodes.php:537
-#: inc/models/class-checkout-form.php:536
-#: inc/models/class-checkout-form.php:623
-#: inc/models/class-checkout-form.php:645
-#: inc/models/class-checkout-form.php:772
-#: inc/models/class-checkout-form.php:991
-#: inc/models/class-checkout-form.php:1235
-#: inc/models/class-checkout-form.php:1431
+#: inc/models/class-checkout-form.php:546
+#: inc/models/class-checkout-form.php:633
+#: inc/models/class-checkout-form.php:655
+#: inc/models/class-checkout-form.php:782
+#: inc/models/class-checkout-form.php:1001
+#: inc/models/class-checkout-form.php:1245
+#: inc/models/class-checkout-form.php:1441
#: inc/ui/class-checkout-element.php:112
msgid "Checkout"
msgstr ""
@@ -5577,8 +5581,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 +5675,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 +5756,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 +5899,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,8 +6715,8 @@ 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-site.php:1435
+#: inc/models/class-base-model.php:639
+#: inc/models/class-site.php:1475
msgid "Item not found."
msgstr ""
@@ -6879,38 +6883,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 ""
@@ -7035,9 +7039,9 @@ msgid "OFF"
msgstr ""
#: inc/checkout/class-legacy-checkout.php:252
-#: inc/models/class-membership.php:911
-#: inc/models/class-product.php:645
-#: inc/models/class-product.php:726
+#: inc/models/class-membership.php:941
+#: inc/models/class-product.php:695
+#: inc/models/class-product.php:776
#: views/checkout/templates/pricing-table/legacy.php:163
#: views/legacy/signup/pricing-table/plan.php:29
#: views/legacy/signup/pricing-table/plan.php:48
@@ -7064,9 +7068,9 @@ msgstr ""
#: inc/checkout/class-legacy-checkout.php:423
#: inc/checkout/signup-fields/class-signup-field-template-selection.php:142
-#: inc/models/class-checkout-form.php:892
-#: inc/models/class-checkout-form.php:1467
-#: inc/models/class-checkout-form.php:1484
+#: inc/models/class-checkout-form.php:902
+#: inc/models/class-checkout-form.php:1477
+#: inc/models/class-checkout-form.php:1494
msgid "Template Selection"
msgstr ""
@@ -7114,15 +7118,15 @@ msgstr ""
#: inc/checkout/class-legacy-checkout.php:520
#: inc/checkout/signup-fields/class-signup-field-password.php:127
-#: inc/models/class-checkout-form.php:939
+#: inc/models/class-checkout-form.php:949
msgid "Confirm Password"
msgstr ""
#: inc/checkout/class-legacy-checkout.php:534
#: inc/checkout/signup-fields/class-signup-field-site-url.php:66
-#: inc/models/class-checkout-form.php:590
-#: inc/models/class-checkout-form.php:682
-#: inc/models/class-checkout-form.php:1531
+#: inc/models/class-checkout-form.php:600
+#: inc/models/class-checkout-form.php:692
+#: inc/models/class-checkout-form.php:1541
#: views/emails/admin/domain-created.php:79
#: views/emails/admin/site-published.php:30
msgid "Site URL"
@@ -7150,8 +7154,8 @@ msgstr ""
#. translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc)
#: inc/checkout/class-line-item.php:1103
-#: inc/models/class-checkout-form.php:1311
-#: inc/models/class-checkout-form.php:1342
+#: inc/models/class-checkout-form.php:1321
+#: inc/models/class-checkout-form.php:1352
#, php-format
msgid "%2$s"
msgid_plural "every %1$s %2$s"
@@ -7452,12 +7456,12 @@ msgstr ""
#: inc/checkout/signup-fields/class-signup-field-pricing-table.php:286
#: inc/checkout/signup-fields/class-signup-field-steps.php:216
#: inc/checkout/signup-fields/class-signup-field-template-selection.php:365
-#: inc/ui/class-template-switching-element.php:361
+#: inc/ui/class-template-switching-element.php:363
msgid "Template does not exist."
msgstr ""
#: inc/checkout/signup-fields/class-signup-field-order-summary.php:57
-#: inc/models/class-checkout-form.php:994
+#: inc/models/class-checkout-form.php:1004
msgid "Order Summary"
msgstr ""
@@ -7520,7 +7524,7 @@ msgstr ""
#: inc/list-tables/class-payment-list-table-widget.php:41
#: inc/list-tables/class-payment-list-table.php:41
#: inc/managers/class-payment-manager.php:74
-#: inc/models/class-checkout-form.php:745
+#: inc/models/class-checkout-form.php:755
#: views/emails/admin/payment-received.php:15
#: views/emails/customer/payment-received.php:16
#: views/emails/customer/renewal-payment-created.php:16
@@ -7637,7 +7641,7 @@ msgstr ""
#: views/checkout/templates/template-selection/clean.php:142
#: views/checkout/templates/template-selection/legacy.php:237
#: views/sites/edit-placeholders.php:105
-#: views/taxes/list.php:170
+#: views/taxes/list.php:192
msgid "Select"
msgstr ""
@@ -7959,24 +7963,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 ""
@@ -8178,10 +8182,10 @@ msgid "Finish the Setup Wizard"
msgstr ""
#: inc/class-dashboard-widgets.php:192
-#: inc/models/class-checkout-form.php:609
-#: inc/models/class-checkout-form.php:758
-#: inc/models/class-checkout-form.php:1219
-#: inc/models/class-checkout-form.php:1413
+#: inc/models/class-checkout-form.php:619
+#: inc/models/class-checkout-form.php:768
+#: inc/models/class-checkout-form.php:1229
+#: inc/models/class-checkout-form.php:1423
#: views/dashboard-widgets/billing-info.php:105
#: views/invoice/template.php:177
msgid "Payment Method"
@@ -8313,11 +8317,11 @@ msgstr ""
msgid "This is the WP-CLI command to run the MCP server via STDIO transport."
msgstr ""
-#: inc/class-newsletter.php:45
+#: inc/class-newsletter.php:48
msgid "Signup for Ultimate Multisite Newsletter"
msgstr ""
-#: inc/class-newsletter.php:46
+#: inc/class-newsletter.php:49
msgid "Be informed of new releases and all things related to running a WaaS Network."
msgstr ""
@@ -8359,6 +8363,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 +8465,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 +8518,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 +8647,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 +8745,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,476 +8797,599 @@ 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
-msgid "Send Error Data to Ultimate Multisite Developers"
+#: inc/class-settings.php:1737
+#: views/settings/widget-settings-body.php:278
+#: views/settings/widget-settings-body.php:283
+msgid "Help Improve Ultimate Multisite"
msgstr ""
-#: inc/class-settings.php:1602
-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)."
+#. translators: %s is a link to the privacy policy
+#: inc/class-settings.php:1740
+#, php-format
+msgid "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 ."
msgstr ""
-#: inc/class-settings.php:1613
+#: inc/class-settings.php:1753
msgid "Change the plugin and wordpress behavior."
msgstr ""
-#: inc/class-settings.php:1628
+#: inc/class-settings.php:1768
msgid "Run Migration Again"
msgstr ""
-#: inc/class-settings.php:1630
+#: inc/class-settings.php:1770
msgid "Rerun the Migration Wizard if you experience data-loss after migrate."
msgstr ""
-#: inc/class-settings.php:1633
+#: inc/class-settings.php:1773
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:1776
msgid "Migrate"
msgstr ""
-#: inc/class-settings.php:1659
+#: inc/class-settings.php:1799
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:1801
#, 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:1812
msgid "Remove Data on Uninstall"
msgstr ""
-#: inc/class-settings.php:1673
+#: inc/class-settings.php:1813
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 ""
+#: inc/class-tracker.php:478
+msgid "There has been a critical error on this site."
+msgstr ""
+
+#: inc/class-tracker.php:481
+msgid "Please contact your network administrator for assistance."
+msgstr ""
+
+#. translators: %s is the admin email address
+#: inc/class-tracker.php:490
+#, php-format
+msgid "You can reach them at %s."
+msgstr ""
+
+#: inc/class-tracker.php:536
+msgid "Unknown error"
+msgstr ""
+
+#: inc/class-tracker.php:537
+msgid "Unknown file"
+msgstr ""
+
+#: inc/class-tracker.php:559
+#: inc/class-tracker.php:591
+#: inc/class-tracker.php:787
+msgid "Unknown"
+msgstr ""
+
+#. translators: %1$s is the type of error message, %2$s is the source plugin
+#: inc/class-tracker.php:750
+#, php-format
+msgid "[%1$s] in %2$s"
+msgstr ""
+
+#: inc/class-tracker.php:760
+msgid "Please describe what you were doing when this error occurred:"
+msgstr ""
+
+#: inc/class-tracker.php:780
+msgid "Show Technical Details"
+msgstr ""
+
+#: inc/class-tracker.php:781
+msgid "Copy to Clipboard"
+msgstr ""
+
+#: inc/class-tracker.php:783
+msgid "Get Support from Ultimate Multisite"
+msgstr ""
+
+#: inc/class-tracker.php:784
+msgid "Source:"
+msgstr ""
+
+#: inc/class-tracker.php:844
+msgid "Return to the main site"
+msgstr ""
+
#: inc/class-user-switching.php:77
msgid "This feature requires the plugin User Switching to be installed and active."
msgstr ""
@@ -9343,21 +9503,21 @@ msgid "You need to pass a valid plan ID."
msgstr ""
#: inc/compat/class-legacy-shortcodes.php:357
-#: inc/models/class-checkout-form.php:836
+#: inc/models/class-checkout-form.php:846
#: views/legacy/signup/pricing-table/frequency-selector.php:32
msgid "Quarterly"
msgstr ""
#: inc/compat/class-legacy-shortcodes.php:362
-#: inc/models/class-checkout-form.php:841
+#: inc/models/class-checkout-form.php:851
#: views/legacy/signup/pricing-table/frequency-selector.php:33
msgid "Yearly"
msgstr ""
#: inc/compat/class-legacy-shortcodes.php:372
#: inc/list-tables/class-product-list-table.php:313
-#: inc/models/class-checkout-form.php:541
-#: inc/models/class-checkout-form.php:1370
+#: inc/models/class-checkout-form.php:551
+#: inc/models/class-checkout-form.php:1380
msgid "Plans"
msgstr ""
@@ -13104,23 +13264,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 +14959,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"
@@ -15469,7 +15629,7 @@ msgstr ""
msgid "An attempt to create a new site failed."
msgstr ""
-#: inc/helpers/class-site-duplicator.php:306
+#: inc/helpers/class-site-duplicator.php:300
msgid "We were not able to create a new admin user for the site being duplicated."
msgstr ""
@@ -15522,10 +15682,11 @@ msgid "Oops! Your %1$s and %2$s don’t match."
msgstr ""
#: inc/helpers/class-validator.php:97
-#: inc/models/class-discount-code.php:660
+#: inc/models/class-discount-code.php:670
#: 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 +15885,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 +16124,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 +16302,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: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:130
+msgid "Website ID"
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: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:246
-msgid "Server ID is not configured"
+#: 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:260
+#: 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 +16499,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 +16592,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 ""
@@ -16443,8 +16691,8 @@ msgstr ""
#: inc/list-tables/class-base-list-table.php:175
#: views/sites/edit-placeholders.php:49
#: views/sites/edit-placeholders.php:177
-#: views/taxes/list.php:102
-#: views/taxes/list.php:309
+#: views/taxes/list.php:124
+#: views/taxes/list.php:331
msgid "Select All"
msgstr ""
@@ -17095,8 +17343,8 @@ msgstr ""
#: inc/list-tables/class-membership-list-table-widget.php:176
#: inc/list-tables/class-membership-list-table.php:139
#: inc/list-tables/class-product-list-table.php:146
-#: inc/models/class-membership.php:844
-#: inc/models/class-product.php:788
+#: inc/models/class-membership.php:874
+#: inc/models/class-product.php:838
#, php-format
msgid "every %2$s"
msgid_plural "every %1$s %2$s"
@@ -17107,8 +17355,8 @@ msgstr[1] ""
#: inc/list-tables/class-membership-list-table-widget.php:184
#: inc/list-tables/class-membership-list-table.php:147
#: inc/list-tables/class-product-list-table.php:154
-#: inc/models/class-membership.php:896
-#: inc/models/class-product.php:745
+#: inc/models/class-membership.php:926
+#: inc/models/class-product.php:795
#, php-format
msgid "for %s cycle"
msgid_plural "for %s cycles"
@@ -17340,252 +17588,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,17 +18314,17 @@ 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:140
+#: inc/managers/class-membership-manager.php:163
+#: inc/managers/class-membership-manager.php:197
+#: inc/managers/class-membership-manager.php:204
+#: inc/managers/class-membership-manager.php:316
+#: inc/managers/class-membership-manager.php:387
#: inc/managers/class-payment-manager.php:336
#: inc/managers/class-payment-manager.php:381
#: inc/ui/class-site-actions-element.php:594
-#: inc/ui/class-site-actions-element.php:936
-#: inc/ui/class-site-actions-element.php:1124
+#: inc/ui/class-site-actions-element.php:940
+#: inc/ui/class-site-actions-element.php:1132
msgid "An unexpected error happened."
msgstr ""
@@ -18161,6 +18409,16 @@ msgstr ""
msgid "This invoice does not exist."
msgstr ""
+#. translators: %1$s opening strong tag, %2$s closing strong tag, %3$s review link opening tag, %4$s link closing tag
+#: inc/managers/class-rating-notice-manager.php:102
+#, php-format
+msgid "Hello! 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"
+msgstr ""
+
+#: inc/managers/class-rating-notice-manager.php:111
+msgid "Leave a Review"
+msgstr ""
+
#: inc/managers/class-site-manager.php:121
msgid "Site names can only contain lowercase letters (a-z) and numbers."
msgstr ""
@@ -18229,20 +18487,20 @@ 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 ""
-#: inc/models/class-checkout-form.php:601
-#: inc/models/class-checkout-form.php:750
-#: inc/models/class-checkout-form.php:1211
-#: inc/models/class-checkout-form.php:1405
+#: inc/models/class-checkout-form.php:611
+#: inc/models/class-checkout-form.php:760
+#: inc/models/class-checkout-form.php:1221
+#: inc/models/class-checkout-form.php:1415
msgid "Your Order"
msgstr ""
-#: inc/models/class-checkout-form.php:615
-#: inc/models/class-checkout-form.php:764
-#: inc/models/class-checkout-form.php:1001
+#: inc/models/class-checkout-form.php:625
+#: inc/models/class-checkout-form.php:774
+#: inc/models/class-checkout-form.php:1011
#: inc/ui/class-billing-info-element.php:171
#: inc/ui/class-billing-info-element.php:224
#: views/dashboard-widgets/thank-you.php:455
@@ -18251,77 +18509,77 @@ msgstr ""
msgid "Billing Address"
msgstr ""
-#: inc/models/class-checkout-form.php:667
+#: inc/models/class-checkout-form.php:677
msgid "Site Info"
msgstr ""
-#: inc/models/class-checkout-form.php:701
+#: inc/models/class-checkout-form.php:711
msgid "User Info"
msgstr ""
-#: inc/models/class-checkout-form.php:866
+#: inc/models/class-checkout-form.php:876
msgid "Pricing Tables"
msgstr ""
-#: inc/models/class-checkout-form.php:970
+#: inc/models/class-checkout-form.php:980
msgid "Continue to the Next Step"
msgstr ""
-#: inc/models/class-checkout-form.php:1014
+#: inc/models/class-checkout-form.php:1024
#: inc/ui/class-payment-methods-element.php:68
msgid "Payment Methods"
msgstr ""
-#: inc/models/class-checkout-form.php:1021
+#: inc/models/class-checkout-form.php:1031
msgid "Pay & Create Account"
msgstr ""
-#: inc/models/class-checkout-form.php:1225
+#: inc/models/class-checkout-form.php:1235
msgid "Finish Payment"
msgstr ""
-#: inc/models/class-checkout-form.php:1419
+#: inc/models/class-checkout-form.php:1429
msgid "Complete Checkout"
msgstr ""
-#: inc/models/class-checkout-form.php:1542
-#: inc/models/class-checkout-form.php:1550
+#: inc/models/class-checkout-form.php:1552
+#: inc/models/class-checkout-form.php:1560
msgid "Create Site"
msgstr ""
-#: inc/models/class-customer.php:216
+#: inc/models/class-customer.php:236
msgid "User Deleted"
msgstr ""
-#: inc/models/class-customer.php:272
-#: inc/models/class-customer.php:289
+#: inc/models/class-customer.php:292
+#: inc/models/class-customer.php:309
msgid "none"
msgstr ""
-#: inc/models/class-discount-code.php:468
-#: inc/models/class-discount-code.php:490
-#: inc/models/class-discount-code.php:498
-#: inc/models/class-discount-code.php:516
+#: inc/models/class-discount-code.php:478
+#: inc/models/class-discount-code.php:500
+#: inc/models/class-discount-code.php:508
+#: inc/models/class-discount-code.php:526
msgid "This coupon code is not valid."
msgstr ""
-#: inc/models/class-discount-code.php:475
+#: inc/models/class-discount-code.php:485
msgid "This discount code was already redeemed the maximum amount of times allowed."
msgstr ""
#. translators: placeholder is the value off. Can be wither $X.XX or X%
-#: inc/models/class-discount-code.php:641
+#: inc/models/class-discount-code.php:651
#, php-format
msgid "%1$s OFF on Subscriptions"
msgstr ""
#. translators: placeholder is the value off. Can be wither $X.XX or X%
-#: inc/models/class-discount-code.php:655
+#: inc/models/class-discount-code.php:665
#, php-format
msgid "%1$s OFF on Setup Fees"
msgstr ""
-#: inc/models/class-domain.php:581
+#: inc/models/class-domain.php:597
msgid "Domain deleted and logs cleared..."
msgstr ""
@@ -18341,30 +18599,30 @@ msgstr ""
msgid "No Message"
msgstr ""
-#: inc/models/class-membership.php:683
-#: inc/models/class-membership.php:760
+#: inc/models/class-membership.php:713
+#: inc/models/class-membership.php:790
msgid "Swap Cart is invalid."
msgstr ""
-#: inc/models/class-membership.php:756
+#: inc/models/class-membership.php:786
msgid "Schedule date is invalid."
msgstr ""
#. translators: times billed / subscription duration in cycles. e.g. 1/12 cycles
-#: inc/models/class-membership.php:860
+#: inc/models/class-membership.php:890
#, php-format
msgid "%1$s / %2$s cycles"
msgstr ""
#. translators: the place holder is the number of times the membership was billed.
-#: inc/models/class-membership.php:865
+#: inc/models/class-membership.php:895
#, php-format
msgid "%1$s / until cancelled"
msgstr ""
#. translators: %1$s is the formatted price, %2$s the duration, and %3$s the duration unit (day, week, month, etc)
-#: inc/models/class-membership.php:885
-#: inc/models/class-product.php:734
+#: inc/models/class-membership.php:915
+#: inc/models/class-product.php:784
#, php-format
msgid "%1$s every %3$s"
msgid_plural "%1$s every %2$s %3$s"
@@ -18372,41 +18630,41 @@ msgstr[0] ""
msgstr[1] ""
#. translators: %1$s is the formatted price of the product
-#: inc/models/class-membership.php:905
-#: inc/models/class-product.php:754
+#: inc/models/class-membership.php:935
+#: inc/models/class-product.php:804
#, php-format
msgid "%1$s one time payment"
msgstr ""
-#: inc/models/class-payment.php:941
+#: inc/models/class-payment.php:961
msgid "(provisional)"
msgstr ""
-#: inc/models/class-payment.php:1061
+#: inc/models/class-payment.php:1081
msgid "Full Refund"
msgstr ""
-#: inc/models/class-payment.php:1065
+#: inc/models/class-payment.php:1085
msgid "Partial Refund"
msgstr ""
#. translators: %s is the date of processing.
-#: inc/models/class-payment.php:1075
+#: inc/models/class-payment.php:1095
#, php-format
msgid "Processed on %s"
msgstr ""
-#: inc/models/class-product.php:722
+#: inc/models/class-product.php:772
msgid "Contact us"
msgstr ""
#. translators: %1$s is the formatted price of the setup fee
-#: inc/models/class-product.php:762
+#: inc/models/class-product.php:812
#, php-format
msgid "Setup Fee of %1$s"
msgstr ""
-#: inc/models/class-product.php:783
+#: inc/models/class-product.php:833
msgid "one-time payment"
msgstr ""
@@ -18482,6 +18740,30 @@ msgstr ""
msgid "Placeholders successfully updated!"
msgstr ""
+#: inc/sso/class-nav-menu-subsite-links.php:77
+msgid "Subsites"
+msgstr ""
+
+#: inc/sso/class-nav-menu-subsite-links.php:133
+msgid "No subsites found."
+msgstr ""
+
+#: inc/sso/class-nav-menu-subsite-links.php:144
+msgid "Select all"
+msgstr ""
+
+#: inc/sso/class-nav-menu-subsite-links.php:148
+msgid "Add to menu"
+msgstr ""
+
+#: inc/sso/class-nav-menu-subsite-links.php:289
+msgid "Subsite (Deleted)"
+msgstr ""
+
+#: inc/sso/class-nav-menu-subsite-links.php:294
+msgid "Subsite"
+msgstr ""
+
#: inc/sso/class-sso.php:962
msgid "SSO secret creation failed."
msgstr ""
@@ -18516,60 +18798,60 @@ msgstr ""
msgid "Taxes Collected"
msgstr ""
-#: inc/tax/class-tax.php:115
+#: inc/tax/class-tax.php:116
msgid "Enable Taxes"
msgstr ""
-#: inc/tax/class-tax.php:116
+#: inc/tax/class-tax.php:117
msgid "Enable this option to be able to collect sales taxes on your network payments."
msgstr ""
-#: inc/tax/class-tax.php:126
+#: inc/tax/class-tax.php:127
msgid "Inclusive Tax"
msgstr ""
-#: inc/tax/class-tax.php:127
+#: inc/tax/class-tax.php:128
msgid "Enable this option if your prices include taxes. In that case, Ultimate Multisite will calculate the included tax instead of adding taxes to the price."
msgstr ""
-#: inc/tax/class-tax.php:189
+#: inc/tax/class-tax.php:190
msgid "Regular"
msgstr ""
-#: inc/tax/class-tax.php:232
+#: inc/tax/class-tax.php:233
#: inc/ui/class-site-actions-element.php:185
#: views/limitations/plugin-selector.php:80
msgid "Default"
msgstr ""
-#: inc/tax/class-tax.php:295
+#: inc/tax/class-tax.php:296
msgid "You don't have permission to alter tax rates"
msgstr ""
-#: inc/tax/class-tax.php:314
+#: inc/tax/class-tax.php:315
msgid "No tax rates present in the request"
msgstr ""
-#: inc/tax/class-tax.php:346
+#: inc/tax/class-tax.php:347
msgid "Tax Rates successfully updated!"
msgstr ""
-#: inc/tax/class-tax.php:386
-#: inc/tax/class-tax.php:390
+#: inc/tax/class-tax.php:387
+#: inc/tax/class-tax.php:391
msgid "Manage Tax Rates"
msgstr ""
-#: inc/tax/class-tax.php:394
+#: inc/tax/class-tax.php:395
msgid "Add different tax rates depending on the country of your customers."
msgstr ""
-#: inc/tax/class-tax.php:400
+#: inc/tax/class-tax.php:401
msgid "You need to activate tax support first."
msgstr ""
-#: inc/tax/class-tax.php:408
-#: inc/tax/class-tax.php:414
-#: inc/tax/class-tax.php:418
+#: inc/tax/class-tax.php:409
+#: inc/tax/class-tax.php:415
+#: inc/tax/class-tax.php:419
msgid "Manage Tax Rates →"
msgstr ""
@@ -18632,7 +18914,7 @@ msgstr ""
#: inc/traits/trait-wp-ultimo-settings-deprecated.php:79
#: inc/traits/trait-wp-ultimo-settings-deprecated.php:80
-#: inc/ui/class-site-actions-element.php:1046
+#: inc/ui/class-site-actions-element.php:1050
#: views/dashboard-statistics/widget-countries.php:95
msgid "Other"
msgstr ""
@@ -18646,7 +18928,7 @@ msgid "Account Summary"
msgstr ""
#: inc/ui/class-account-summary-element.php:124
-msgid "Adds a account summary block to the page."
+msgid "Displays a summary of the customer's account including membership and site overview."
msgstr ""
#: inc/ui/class-account-summary-element.php:157
@@ -18709,14 +18991,7 @@ msgid "Billing Information"
msgstr ""
#: inc/ui/class-billing-info-element.php:138
-#: inc/ui/class-checkout-element.php:128
-#: inc/ui/class-current-membership-element.php:146
-#: inc/ui/class-invoices-element.php:100
-#: inc/ui/class-limits-element.php:99
-#: inc/ui/class-payment-methods-element.php:84
-#: inc/ui/class-site-actions-element.php:123
-#: inc/ui/class-thank-you-element.php:175
-msgid "Adds a checkout form block to the page."
+msgid "Displays the customer's billing address and contact information."
msgstr ""
#: inc/ui/class-billing-info-element.php:333
@@ -18727,6 +19002,10 @@ msgstr ""
msgid "Enter your billing address here. This info will be used on your invoices."
msgstr ""
+#: inc/ui/class-checkout-element.php:128
+msgid "Adds a checkout form block to the page."
+msgstr ""
+
#: inc/ui/class-checkout-element.php:160
msgid "The checkout form slug."
msgstr ""
@@ -18828,6 +19107,10 @@ msgstr ""
msgid "Registration is closed for your location."
msgstr ""
+#: inc/ui/class-current-membership-element.php:146
+msgid "Displays the current membership details including plan, status, and billing cycle."
+msgstr ""
+
#: inc/ui/class-current-membership-element.php:179
#: inc/ui/class-current-membership-element.php:250
msgid "Your Membership"
@@ -18855,7 +19138,7 @@ msgstr ""
#: inc/ui/class-current-membership-element.php:368
#: inc/ui/class-site-actions-element.php:842
-#: inc/ui/class-site-actions-element.php:989
+#: inc/ui/class-site-actions-element.php:993
msgid "Membership not selected."
msgstr ""
@@ -18868,9 +19151,9 @@ msgstr ""
#: inc/ui/class-site-actions-element.php:503
#: inc/ui/class-site-actions-element.php:600
#: inc/ui/class-site-actions-element.php:848
-#: inc/ui/class-site-actions-element.php:946
-#: inc/ui/class-site-actions-element.php:995
-#: inc/ui/class-site-actions-element.php:1134
+#: inc/ui/class-site-actions-element.php:950
+#: inc/ui/class-site-actions-element.php:999
+#: inc/ui/class-site-actions-element.php:1142
msgid "You are not allowed to do this."
msgstr ""
@@ -18898,7 +19181,7 @@ msgid "remove %1$s %2$s from membership"
msgstr ""
#: inc/ui/class-current-site-element.php:127
-msgid "Adds a block to display the current site being managed."
+msgid "Displays details about the currently selected site including title, URL, and quick actions."
msgstr ""
#: inc/ui/class-current-site-element.php:159
@@ -18984,7 +19267,7 @@ msgid "Site title can not be empty."
msgstr ""
#: inc/ui/class-domain-mapping-element.php:112
-msgid "Adds the site's domains block."
+msgid "Allows customers to manage custom domains mapped to their site."
msgstr ""
#: inc/ui/class-domain-mapping-element.php:294
@@ -19026,6 +19309,10 @@ msgstr ""
msgid "The field type \"%1$s\" is no longer supported, use \"%2$s\" instead."
msgstr ""
+#: inc/ui/class-invoices-element.php:100
+msgid "Displays a list of the customer's invoices and payment history."
+msgstr ""
+
#: inc/ui/class-invoices-element.php:140
msgid "Limit"
msgstr ""
@@ -19137,6 +19424,10 @@ msgstr ""
msgid "Limits & Quotas"
msgstr ""
+#: inc/ui/class-limits-element.php:99
+msgid "Displays the site's usage limits and quotas such as disk space, posts, and users."
+msgstr ""
+
#: inc/ui/class-limits-element.php:132
#: inc/ui/class-limits-element.php:196
msgid "Site Limits"
@@ -19262,7 +19553,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 ""
@@ -19275,8 +19566,97 @@ msgstr ""
msgid "Log In"
msgstr ""
+#: inc/ui/class-magic-link-url-element.php:97
+msgid "Magic Link URL"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:113
+msgid "Generates a magic link URL for quick site access with automatic authentication."
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:146
+msgid "E.g. 2 or leave empty for current site"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:147
+msgid "The ID of the site to generate the magic link for. Leave empty to use the current site from the URL."
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:148
+msgid "You can find the site ID in the Sites list in the network admin."
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:153
+msgid "Redirect URL"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:154
+msgid "E.g. /wp-admin/"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:155
+msgid "Optional path to redirect to after authentication. Leave empty to go to the site home."
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:156
+msgid "This can be a relative path (e.g., /wp-admin/) or full URL on the target site."
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:160
+#: inc/ui/class-magic-link-url-element.php:161
+msgid "Display Options"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:167
+msgid "Display As"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:168
+msgid "Choose how to display the magic link."
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:170
+msgid "Clickable Link"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:171
+msgid "Button"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:172
+msgid "Plain URL Text"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:179
+msgid "Link Text"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:180
+msgid "E.g. Visit Site"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:181
+msgid "The text to display for the link or button."
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:190
+msgid "Open in New Tab?"
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:191
+msgid "Toggle to open the link in a new browser tab."
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:394
+msgid "Magic Link URL: No site ID specified or found."
+msgstr ""
+
+#: inc/ui/class-magic-link-url-element.php:405
+msgid "Magic Link URL: Site not found."
+msgstr ""
+
#: inc/ui/class-my-sites-element.php:110
-msgid "Adds a block to display the sites owned by the current customer."
+msgid "Displays a list of all sites owned by the current customer with quick access links."
msgstr ""
#: inc/ui/class-my-sites-element.php:142
@@ -19319,6 +19699,10 @@ msgstr ""
msgid "The page to redirect user after select a site."
msgstr ""
+#: inc/ui/class-payment-methods-element.php:84
+msgid "Displays and manages the customer's saved payment methods."
+msgstr ""
+
#: inc/ui/class-payment-methods-element.php:116
msgid "Password Strength Meter"
msgstr ""
@@ -19352,6 +19736,10 @@ msgstr ""
msgid "Actions"
msgstr ""
+#: inc/ui/class-site-actions-element.php:123
+msgid "Displays action buttons for site management such as preview, publish, and delete."
+msgstr ""
+
#: inc/ui/class-site-actions-element.php:155
msgid "Show Change Password"
msgstr ""
@@ -19402,8 +19790,8 @@ msgid "Cancel Current Payment Method"
msgstr ""
#: inc/ui/class-site-actions-element.php:465
-#: inc/ui/class-site-actions-element.php:1081
-#: inc/ui/class-site-actions-element.php:1082
+#: inc/ui/class-site-actions-element.php:1089
+#: inc/ui/class-site-actions-element.php:1090
msgid "Cancel Membership"
msgstr ""
@@ -19465,68 +19853,68 @@ msgstr ""
msgid "Confirm Payment Method Cancellation"
msgstr ""
-#: inc/ui/class-site-actions-element.php:893
-#: inc/ui/class-site-actions-element.php:894
+#: inc/ui/class-site-actions-element.php:897
+#: inc/ui/class-site-actions-element.php:898
msgid "Cancel Payment Method"
msgstr ""
-#: inc/ui/class-site-actions-element.php:1032
+#: inc/ui/class-site-actions-element.php:1036
msgid "Please tell us why you are cancelling."
msgstr ""
-#: inc/ui/class-site-actions-element.php:1033
+#: inc/ui/class-site-actions-element.php:1037
msgid "We would love your feedback."
msgstr ""
-#: inc/ui/class-site-actions-element.php:1039
+#: inc/ui/class-site-actions-element.php:1043
msgid "Select a reason"
msgstr ""
-#: inc/ui/class-site-actions-element.php:1040
-#: inc/ui/class-site-actions-element.php:1142
+#: inc/ui/class-site-actions-element.php:1044
+#: inc/ui/class-site-actions-element.php:1150
msgid "I no longer need it"
msgstr ""
-#: inc/ui/class-site-actions-element.php:1041
-#: inc/ui/class-site-actions-element.php:1143
+#: inc/ui/class-site-actions-element.php:1045
+#: inc/ui/class-site-actions-element.php:1151
msgid "It's too expensive"
msgstr ""
-#: inc/ui/class-site-actions-element.php:1042
-#: inc/ui/class-site-actions-element.php:1144
+#: inc/ui/class-site-actions-element.php:1046
+#: inc/ui/class-site-actions-element.php:1152
msgid "I need more features"
msgstr ""
-#: inc/ui/class-site-actions-element.php:1043
-#: inc/ui/class-site-actions-element.php:1145
+#: inc/ui/class-site-actions-element.php:1047
+#: inc/ui/class-site-actions-element.php:1153
msgid "Switched to another service"
msgstr ""
-#: inc/ui/class-site-actions-element.php:1044
-#: inc/ui/class-site-actions-element.php:1146
+#: inc/ui/class-site-actions-element.php:1048
+#: inc/ui/class-site-actions-element.php:1154
msgid "Customer support is less than expected"
msgstr ""
-#: inc/ui/class-site-actions-element.php:1045
-#: inc/ui/class-site-actions-element.php:1147
+#: inc/ui/class-site-actions-element.php:1049
+#: inc/ui/class-site-actions-element.php:1155
msgid "Too complex"
msgstr ""
-#: inc/ui/class-site-actions-element.php:1051
+#: inc/ui/class-site-actions-element.php:1055
msgid "Please provide additional details."
msgstr ""
-#: inc/ui/class-site-actions-element.php:1058
+#: inc/ui/class-site-actions-element.php:1066
msgid "Type CANCEL to confirm this membership cancellation."
msgstr ""
#. translators: %s: Next charge date.
-#: inc/ui/class-site-actions-element.php:1075
+#: inc/ui/class-site-actions-element.php:1083
#, php-format
msgid "Your sites will stay working until %s."
msgstr ""
-#: inc/ui/class-site-actions-element.php:1087
+#: inc/ui/class-site-actions-element.php:1095
msgid "CANCEL"
msgstr ""
@@ -19535,7 +19923,7 @@ msgid "Site Maintenance"
msgstr ""
#: inc/ui/class-site-maintenance-element.php:107
-msgid "Adds the toggle control to turn maintenance mode on."
+msgid "Provides a toggle control for customers to enable or disable site maintenance mode."
msgstr ""
#: inc/ui/class-site-maintenance-element.php:140
@@ -19561,7 +19949,7 @@ msgid "Template Switching"
msgstr ""
#: inc/ui/class-template-switching-element.php:95
-msgid "Adds the template switching form to this page."
+msgid "Allows customers to switch their site to a different template design."
msgstr ""
#: inc/ui/class-template-switching-element.php:155
@@ -19572,30 +19960,34 @@ msgstr ""
msgid "Want to add customized template selection templates?See how you can do that here ."
msgstr ""
-#: inc/ui/class-template-switching-element.php:277
-msgid "You are not allow to use this template."
+#: inc/ui/class-template-switching-element.php:279
+msgid "You are not allowed to use this template."
msgstr ""
-#: inc/ui/class-template-switching-element.php:281
+#: inc/ui/class-template-switching-element.php:283
msgid "You need to provide a valid template to duplicate."
msgstr ""
-#: inc/ui/class-template-switching-element.php:385
+#: inc/ui/class-template-switching-element.php:387
msgid "← Back to Template Selection"
msgstr ""
-#: inc/ui/class-template-switching-element.php:395
+#: inc/ui/class-template-switching-element.php:397
msgid "Confirm template switch?"
msgstr ""
-#: inc/ui/class-template-switching-element.php:396
+#: inc/ui/class-template-switching-element.php:398
msgid "Switching your current template completely overwrites the content of your site with the contents of the newly chosen template. All customizations will be lost. This action cannot be undone."
msgstr ""
-#: inc/ui/class-template-switching-element.php:410
+#: inc/ui/class-template-switching-element.php:412
msgid "Process Switch"
msgstr ""
+#: inc/ui/class-thank-you-element.php:175
+msgid "Displays a confirmation message after successful checkout or registration."
+msgstr ""
+
#: inc/ui/class-thank-you-element.php:215
msgid "Thank You Message"
msgstr ""
@@ -19676,6 +20068,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 +20487,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 +20517,7 @@ msgid "PayPal Status:"
msgstr ""
#: views/checkout/paypal/confirm.php:85
+#: views/wizards/host-integrations/rocket-instructions.php:39
msgid "Email:"
msgstr ""
@@ -20953,6 +21348,14 @@ msgstr ""
msgid "Check / Uncheck All"
msgstr ""
+#: views/settings/widget-settings-body.php:287
+msgid "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, aggregate counts, active gateways, and error logs. We never collect personal data, customer information, or domain names."
+msgstr ""
+
+#: views/settings/widget-settings-body.php:288
+msgid "Learn more"
+msgstr ""
+
#: views/shortcodes/shortcodes.php:47
msgid "Parameter"
msgstr ""
@@ -20966,7 +21369,7 @@ msgid "Template Placeholders"
msgstr ""
#: views/sites/edit-placeholders.php:31
-#: views/taxes/list.php:83
+#: views/taxes/list.php:105
msgid "item(s)"
msgstr ""
@@ -20975,27 +21378,27 @@ msgid "Loading Template Placeholders..."
msgstr ""
#: views/sites/edit-placeholders.php:91
-#: views/taxes/list.php:144
+#: views/taxes/list.php:166
msgid "No items to display"
msgstr ""
#: views/sites/edit-placeholders.php:209
-#: views/taxes/list.php:342
+#: views/taxes/list.php:364
msgid "Add new Row"
msgstr ""
#: views/sites/edit-placeholders.php:215
-#: views/taxes/list.php:348
+#: views/taxes/list.php:370
msgid "Delete Selected Rows"
msgstr ""
#: views/sites/edit-placeholders.php:241
-#: views/taxes/list.php:374
+#: views/taxes/list.php:396
msgid "Save your changes!"
msgstr ""
#: views/sites/edit-placeholders.php:245
-#: views/taxes/list.php:378
+#: views/taxes/list.php:400
msgid "Saving..."
msgstr ""
@@ -21015,41 +21418,51 @@ msgstr ""
msgid "Download File"
msgstr ""
-#: views/taxes/list.php:16
+#: views/taxes/list.php:18
msgid "Go to the Tax Settings Page"
msgstr ""
-#: views/taxes/list.php:31
+#. translators: %s is a link to the tax settings page
+#: views/taxes/list.php:32
+#, php-format
+msgid "Taxes are currently disabled. The tax rates below are not being applied to any transactions. To enable taxes, go to the %s."
+msgstr ""
+
+#: views/taxes/list.php:36
+msgid "Tax Settings page"
+msgstr ""
+
+#: views/taxes/list.php:53
msgid "Tax Category Name"
msgstr ""
-#: views/taxes/list.php:34
+#: views/taxes/list.php:56
msgid "Create"
msgstr ""
-#: views/taxes/list.php:38
-#: views/taxes/list.php:46
+#: views/taxes/list.php:60
+#: views/taxes/list.php:68
msgid "← Back"
msgstr ""
-#: views/taxes/list.php:62
+#: views/taxes/list.php:84
msgid "Switch"
msgstr ""
-#: views/taxes/list.php:72
+#: views/taxes/list.php:94
msgid "Add new Tax Category"
msgstr ""
-#: views/taxes/list.php:130
+#: views/taxes/list.php:152
msgid "Loading Tax Rates..."
msgstr ""
-#: views/taxes/list.php:244
-#: views/taxes/list.php:259
+#: views/taxes/list.php:266
+#: views/taxes/list.php:281
msgid "Leave blank to apply to all"
msgstr ""
-#: views/taxes/list.php:388
+#: views/taxes/list.php:410
msgid "Save Tax Rates"
msgstr ""
@@ -21329,6 +21742,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 +21916,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 +22160,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..da84077f 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.
@@ -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**
+- 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: 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)
+- Service URL: https://ultimatemultisite.com/wp-json/wu-telemetry/v1/track
+- Privacy Policy: https://ultimatemultisite.com/privacy-policy/
+
== Screenshots ==
1. One of many settings pages.
@@ -228,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
diff --git a/uninstall.php b/uninstall.php
index 910fa071..18daa2dc 100644
--- a/uninstall.php
+++ b/uninstall.php
@@ -8,7 +8,7 @@
if ( ! defined('WP_UNINSTALL_PLUGIN')) {
exit;
-} // end if;
+}
global $wpdb;
@@ -56,15 +56,15 @@
$wpdb->query("DROP TABLE IF EXISTS $wu_table_name"); // phpcs:ignore
delete_network_option(null, $wu_table_version);
- } // end foreach;
+ }
/*
* Remove states saved
*/
delete_network_option(null, "wp-ultimo_{$wu_settings_key}");
delete_network_option(null, 'wp-ultimo_debug_faker');
- delete_network_option(null, 'wu_setup_finished');
+ delete_network_option(null, \WP_Ultimo::NETWORK_OPTION_SETUP_FINISHED);
delete_network_option(null, 'wu_default_email_template');
delete_network_option(null, 'wu_default_system_emails_created');
delete_network_option(null, 'wu_default_invoice_template');
-} // end if;
+}
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/broadcast/emails/base.php b/views/broadcast/emails/base.php
index c3c600fe..78d21ef6 100644
--- a/views/broadcast/emails/base.php
+++ b/views/broadcast/emails/base.php
@@ -41,6 +41,7 @@
+
@@ -56,6 +57,7 @@
+
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(); ?>>
@@ -42,8 +40,8 @@ class="wu-pwd-toggle hide-if-no-js wu-absolute wu-bg-transparent wu-border-0 wu-
meter) : ?>
-
-
+
+
diff --git a/views/dashboard-widgets/magic-link-url.php b/views/dashboard-widgets/magic-link-url.php
new file mode 100644
index 00000000..dc6b6067
--- /dev/null
+++ b/views/dashboard-widgets/magic-link-url.php
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
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..e69185e1 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
- Send Error Data to Ultimate Multisite Developers
+
- Send Error Data to Ultimate Multisite Developers
+
+
- 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).
+
+ .
@@ -332,4 +330,4 @@ class="wu-bg-gray-100 wu--mt-1 wu--mx-3 wu-p-4 wu-border-solid wu-border-b wu-bo
value="/wp-admin/network/admin.php?page=wp-ultimo">
-
\ No newline at end of file
+
diff --git a/views/taxes/list.php b/views/taxes/list.php
index 93267768..d7213bb2 100644
--- a/views/taxes/list.php
+++ b/views/taxes/list.php
@@ -5,6 +5,8 @@
* @since 2.0.0
*/
defined('ABSPATH') || exit;
+
+$taxes_enabled = wu_get_setting('enable_taxes', false);
?>
@@ -20,6 +22,26 @@
+
+
+
+
+ %s',
+ esc_url(network_admin_url('admin.php?page=wp-ultimo-settings&tab=taxes')),
+ esc_html__('Tax Settings page', 'ultimate-multisite')
+ )
+ );
+ ?>
+
+
+
+
+