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) + ' "\u{FF1C}script", + ' "\u{FF1C}iframe", + ' "\u{FF1C}img", + + // Path traversal already handled by / replacement, but be explicit + '..∕' => "\u{FF0E}\u{FF0E}\u{2215}", // ..∕ + ]; + + // Apply replacements (order matters - do multi-char patterns first) + $text = str_replace(array_keys($replacements), array_values($replacements), $text); + + return $text; + } + + /** + * Build the support URL with pre-filled error information. + * + * @since 2.5.0 + * @param array $error_details Error details array with 'type', 'source_plugin', and 'full' keys. + * @param string $admin_email The admin email address. + * @return string + */ + protected function build_support_url(array $error_details, string $admin_email): string { + + // translators: %1$s is the type of error message, %2$s is the source plugin + $subject = sprintf(__('[%1$s] in %2$s', 'ultimate-multisite'), $error_details['type'], $error_details['source_plugin']); + + // Sanitize error details to avoid WAF triggers using Unicode lookalikes + $safe_error = $this->sanitize_error_for_url($error_details['full']); + + return add_query_arg( + [ + 'wpf17_3' => rawurlencode($admin_email), + 'wpf17_6' => rawurlencode($subject), + 'wpf17_5' => rawurlencode( + __('Please describe what you were doing when this error occurred:', 'ultimate-multisite') . + "\n\n--- ---\n" . + $safe_error + ), + ], + 'https://ultimatemultisite.com/support/' + ); + } + + /** + * Build the error message HTML for super admins with technical details. + * + * @since 2.5.0 + * @param string $custom_message The main error message. + * @param array $error_details Contains 'summary', 'full', and 'source_plugin' error details. + * @param string $support_url The support URL with pre-filled params. + * @return string + */ + protected function build_admin_error_message(string $custom_message, array $error_details, string $support_url): string { + + $show_details_text = esc_html__('Show Technical Details', 'ultimate-multisite'); + $copy_text = esc_html__('Copy to Clipboard', 'ultimate-multisite'); + $copied_text = esc_html__('Copied!', 'ultimate-multisite'); + $get_support_text = esc_html__('Get Support from Ultimate Multisite', 'ultimate-multisite'); + $source_label = esc_html__('Source:', 'ultimate-multisite'); + $escaped_details = esc_html($error_details['full']); + $escaped_support = esc_attr($support_url); + $source_plugin = esc_html($error_details['source_plugin'] ?? __('Unknown', 'ultimate-multisite')); + + return << +.wu-error-container { max-width: 600px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } +.wu-error-source { background: #fcf0f1; border-left: 4px solid #d63638; padding: 12px 16px; margin: 1em 0; } +.wu-error-source strong { color: #d63638; } +.wu-error-actions { display: flex; gap: 10px; margin: 1.5em 0; } +.wu-error-btn { display: inline-block; padding: 10px 16px; border-radius: 4px; text-decoration: none; font-size: 14px; cursor: pointer; } +.wu-error-btn-primary { background: #2271b1; color: #fff; border: none; } +.wu-error-btn-primary:hover { background: #135e96; color: #fff; } +.wu-error-btn-secondary { background: #f0f0f1; color: #2c3338; border: 1px solid #ccc; } +.wu-error-btn-success { background: #00a32a; color: #fff; border: 1px solid #00a32a; } +.wu-error-details { margin-top: 1em; } +.wu-error-details summary { cursor: pointer; font-weight: 500; padding: 8px 0; } +.wu-error-details pre { background: #f6f7f7; border: 1px solid #dcdcde; border-radius: 4px; padding: 1em; font-size: 13px; white-space: pre-wrap; word-break: break-word; margin: 0.5em 0; } + +
+

{$custom_message}

+
+ {$source_label} {$source_plugin} +
+ +
+ {$show_details_text} +
{$escaped_details}
+ +
+
+ +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', + ] + ); + + ?> +
+
+
    + get_id(); + + // Skip the main site. + if (is_main_site($blog_id)) { + continue; + } + + $title = $site->get_title(); + $url = $site->get_active_site_url(); + ?> +
  • + + + + + +
  • + +
  • + +
  • + +
+
+

+ + + + + + + +

+
+ 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 @@
+ + - + @@ -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') + ) + ); + ?> +

+
+ + +
@@ -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(); ?>>
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
+ +

- 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). + + .