Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/user-registration/chunks/dashboard.asset.php
+++ b/user-registration/chunks/dashboard.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '8bf239503745017ae83b');
+<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => 'a90781b6227decba5f58');
--- a/user-registration/includes/admin/class-ur-admin-menus.php
+++ b/user-registration/includes/admin/class-ur-admin-menus.php
@@ -583,12 +583,22 @@
$content_rules->add_urcr_menus();
}
+ // Instantiate regardless of the gate: the constructor wires the invoice
+ // download handler and gateway options, which must keep working even when
+ // the Payments submenu itself is hidden.
$orders_obj = new Orders();
- $orders_obj->add_orders_menu();
+
+ if ( ur_should_show_payments_menu() ) {
+ $orders_obj->add_orders_menu();
+ }
if ( ur_check_module_activation( 'membership' ) ) {
+ // Instantiated for its admin_init delete handler, see above.
$subscription_obj = new Subscriptions();
- $subscription_obj->add_menu();
+
+ if ( ur_should_show_subscriptions_menu() ) {
+ $subscription_obj->add_menu();
+ }
}
if ( UR_PRO_ACTIVE && ur_check_module_activation( 'coupon' ) && class_exists( 'WPEverestURMembershipCouponsCoupons' ) ) {
--- a/user-registration/includes/admin/class-ur-admin.php
+++ b/user-registration/includes/admin/class-ur-admin.php
@@ -67,12 +67,12 @@
*/
public function run_membership_migration_script() {
- $membership_service = new MembershipService();
- $logger = ur_get_logger();
-
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
+
+ $logger = ur_get_logger();
+
update_option( 'user_registration_content_restriction_enable', true );
if ( ur_check_module_activation( 'payments' ) && ! get_option( 'global_paypal_setting_migration', false ) ) {
$get_all_forms = ur_get_all_user_registration_form();
@@ -89,6 +89,18 @@
if ( UR_PRO_ACTIVE && UR_VERSION <= '5.0' && is_plugin_active( 'user-registration-membership/user-registration-membership.php' ) && ! get_option( 'membership_migration_finished', false ) ) {
+ if ( ! class_exists( MembershipService::class ) ) {
+ $logger->error(
+ 'MembershipService class not found. Skipping membership migration.',
+ array(
+ 'source' => 'migration-logger',
+ )
+ );
+ return;
+ }
+
+ $membership_service = new MembershipService();
+
deactivate_plugins( 'user-registration-membership/user-registration-membership.php' );
$logger->notice( '---------- Begin Membership Migration. ----------', array( 'source' => 'migration-logger' ) );
@@ -384,7 +396,7 @@
if ( $user_id > 0 ) {
$user_meta = get_userdata( $user_id );
$user_roles = $user_meta->roles;
- $option_roles = get_option( 'user_registration_general_setting_disabled_user_roles', array() );
+ $option_roles = get_option( 'user_registration_general_setting_disabled_user_roles', array( 'subscriber' ) );
if ( ! is_array( $option_roles ) ) {
$option_roles = array();
}
--- a/user-registration/includes/admin/notifications/class-ur-admin-notices.php
+++ b/user-registration/includes/admin/notifications/class-ur-admin-notices.php
@@ -60,6 +60,7 @@
add_action('admin_init', array(__CLASS__, 'user_registration_install_pages_notice'));
add_action('admin_notices', array(__CLASS__, 'php_deprecation_notice'));
add_action('admin_init', array(__CLASS__, 'same_membership_in_group'));
+ add_action('admin_notices', array(__CLASS__, 'registration_disabled_notice'));
/**
* Render Notice with Logo and Buttons.
@@ -773,6 +774,11 @@
foreach ($wp_filter[$wp_notice]->callbacks as $priority => $hooks) {
foreach ($hooks as $name => $arr) {
+ // Always keep the registration disabled notice, it blocks every registration form.
+ if (is_string($name) && false !== strpos($name, 'registration_disabled_notice')) {
+ continue;
+ }
+
// Remove all notices if the page is form builder page.
if ('add-new-registration' === $_REQUEST['page'] || 'user-registration-dashboard' === $_REQUEST['page']) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
unset($wp_filter[$wp_notice]->callbacks[$priority][$name]);
@@ -900,6 +906,27 @@
}
/**
+ * Notify admins when the WordPress "Anyone can register" option blocks registration.
+ *
+ * @since 5.2.7
+ */
+ public static function registration_disabled_notice()
+ {
+ if (ur_users_can_register() || ! current_user_can('manage_options')) {
+ return;
+ }
+
+ $message = '<strong>' . esc_html__('User Registration & Membership: ', 'user-registration') . '</strong>';
+ $message .= sprintf(
+ /* translators: %1$s - WordPress general settings link. */
+ __('Your registration forms are showing "Registration is currently disabled." because the WordPress <strong>Anyone can register</strong> option is turned off. Enable it in <a href="%1$s">General Settings</a> to let visitors register.', 'user-registration'),
+ esc_url(admin_url('options-general.php#users_can_register'))
+ );
+
+ echo '<div class="notice notice-warning is-dismissible ur-registration-disabled-notice"><p>' . wp_kses_post($message) . '</p></div>';
+ }
+
+ /**
* If user have same membership in multiple groupds display the notice.
*/
public static function same_membership_in_group()
--- a/user-registration/includes/admin/settings/class-ur-settings-membership.php
+++ b/user-registration/includes/admin/settings/class-ur-settings-membership.php
@@ -76,39 +76,7 @@
*
* @param array Options to be enlisted.
*/
- $settings = apply_filters(
- 'user_registration_membership_settings',
- array(
- 'title' => '',
- 'sections' => array(
- 'membership_settings' => array(
- 'title' => __( 'General', 'user-registration' ),
- 'type' => 'card',
- 'desc' => sprintf(
- /* translators: %s - Admin URL for membership page settings */
- __( '<strong>Membership page setting has moved.</strong> Configure your membership page <a href="%s">here</a>.', 'user-registration' ),
- admin_url( 'admin.php?page=user-registration-settings&tab=general§ion=pages' )
- ),
- 'settings' => array(
- array(
- 'title' => __( 'Renewal Behaviour', 'user-registration' ),
- 'desc' => __( 'Choose how membership subscriptions are renewed, automatically through the payment provider or manually by the user', 'user-registration' ),
- 'id' => 'user_registration_renewal_behaviour',
- 'type' => 'select',
- 'default' => 'automatic',
- 'class' => 'ur-enhanced-select',
- 'css' => '',
- 'options' => array(
- 'automatic' => __( 'Renew Automatically', 'user-registration' ),
- 'manual' => __( 'Renew Manually', 'user-registration' ),
- ),
- 'desc_tip' => true,
- ),
- ),
- ),
- ),
- )
- );
+ $settings = apply_filters( 'user_registration_membership_settings', $this->get_general_membership_settings() );
} elseif ( 'content-rules' === $current_section ) {
$settings = $this->urcr_settings();
}
@@ -116,6 +84,106 @@
}
/**
+ * General membership settings (Renewal Behaviour), or an empty-state
+ * notice in place of it when no active membership plan exists yet.
+ *
+ * @return array
+ */
+ private function get_general_membership_settings() {
+ $card = array(
+ 'title' => __( 'General', 'user-registration' ),
+ 'type' => 'card',
+ );
+
+ $plan_state = $this->get_membership_plan_state();
+
+ if ( 'active' !== $plan_state ) {
+ if ( 'inactive' === $plan_state ) {
+ $card['desc'] = sprintf(
+ /* translators: %s - Admin URL to the membership plan list */
+ __( '<strong>No active membership plan.</strong> Activate a plan to configure the Renewal Behaviour. <a href="%s">Manage memberships →</a>', 'user-registration' ),
+ admin_url( 'admin.php?page=user-registration-membership' )
+ );
+ } else {
+ $card['desc'] = sprintf(
+ /* translators: %s - Admin URL to create a new membership plan */
+ __( '<strong>No membership plans yet.</strong> Set up a plan to configure the Renewal Behaviour. <a href="%s">Create a membership →</a>', 'user-registration' ),
+ admin_url( 'admin.php?page=user-registration-membership&action=add_new_membership' )
+ );
+ }
+
+ $card['settings'] = array();
+ } else {
+ $is_new_installation = ur_string_to_bool( get_option( 'urm_is_new_installation', '' ) );
+ if ( ! $is_new_installation ) {
+ $card['desc'] = sprintf(
+ /* translators: %s - Admin URL for membership page settings */
+ __( '<strong>Membership page setting has moved.</strong> Configure your membership page <a href="%s">here</a>.', 'user-registration' ),
+ admin_url( 'admin.php?page=user-registration-settings&tab=general§ion=pages' )
+ );
+ }
+ $card['settings'] = array(
+ array(
+ 'title' => __( 'Renewal Behaviour', 'user-registration' ),
+ 'desc' => __( 'Choose how membership subscriptions are renewed, automatically through the payment provider or manually by the user', 'user-registration' ),
+ 'id' => 'user_registration_renewal_behaviour',
+ 'type' => 'select',
+ 'default' => 'automatic',
+ 'class' => 'ur-enhanced-select',
+ 'css' => '',
+ 'options' => array(
+ 'automatic' => __( 'Renew Automatically', 'user-registration' ),
+ 'manual' => __( 'Renew Manually', 'user-registration' ),
+ ),
+ 'desc_tip' => true,
+ ),
+ );
+ }
+
+ return array(
+ 'title' => '',
+ 'sections' => array(
+ 'membership_settings' => $card,
+ ),
+ );
+ }
+
+ /**
+ * State of the published membership plans.
+ *
+ * Deactivated plans do not count as usable, since there is nothing to
+ * renew while every plan is switched off.
+ *
+ * @return string One of 'active' (at least one active plan exists),
+ * 'inactive' (plans exist but all are deactivated) or
+ * 'none' (no plan has been created yet).
+ */
+ private function get_membership_plan_state() {
+ if ( ! class_exists( 'WPEverestURMembershipAdminRepositoriesMembershipRepository' ) ) {
+ return 'active';
+ }
+
+ $membership_repository = new WPEverestURMembershipAdminRepositoriesMembershipRepository();
+ $memberships = $membership_repository->get_all_memberships_without_status_filter();
+ $has_plan = false;
+
+ foreach ( $memberships as $membership ) {
+ if ( ! isset( $membership['post_status'] ) || 'publish' !== $membership['post_status'] ) {
+ continue;
+ }
+
+ $has_plan = true;
+ $status = isset( $membership['post_content']['status'] ) ? $membership['post_content']['status'] : false;
+
+ if ( ur_string_to_bool( $status ) ) {
+ return 'active';
+ }
+ }
+
+ return $has_plan ? 'inactive' : 'none';
+ }
+
+ /**
* Content restriction settings.
*
* @return array
@@ -137,10 +205,50 @@
if ( ! empty( $global_rule_id ) ) {
$content_rule_url .= '&id=' . $global_rule_id;
}
+
+ $has_membership_plans = false;
+ if ( class_exists( 'WPEverestURMembershipAdminRepositoriesMembershipRepository' ) ) {
+ $membership_repository = new WPEverestURMembershipAdminRepositoriesMembershipRepository();
+ $has_membership_plans = ! empty( $membership_repository->get_all_memberships_without_status_filter() );
+ }
+
+ /*
+ * Rules that prove content restriction is actually in use:
+ * - 'custom' rules, which can only be created in Pro.
+ * - the auto-migrated "Legacy: Global Site Rule" (flagged with urcr_is_global), present on
+ * older installs that used the old global restriction setting, in both free and Pro.
+ * Membership-generated rules are deliberately excluded, they are already covered by
+ * $has_membership_plans.
+ */
+ $has_content_rules = false;
+ if ( post_type_exists( 'urcr_access_rule' ) ) {
+ $has_content_rules = (bool) get_posts(
+ array(
+ 'post_type' => 'urcr_access_rule',
+ 'post_status' => 'any',
+ 'posts_per_page' => 1,
+ 'fields' => 'ids',
+ 'meta_query' => array(
+ 'relation' => 'OR',
+ array(
+ 'key' => 'urcr_rule_type',
+ 'value' => 'custom',
+ ),
+ array(
+ 'key' => 'urcr_is_global',
+ 'compare' => 'EXISTS',
+ ),
+ ),
+ )
+ );
+ }
+
+ $has_restriction_in_use = $has_membership_plans || $has_content_rules;
+
$sections['user_registration_content_restriction_settings'] = array(
'title' => __( 'Content Restriction', 'user-registration' ),
'type' => 'card',
- 'settings' => array(
+ 'settings' => $has_restriction_in_use ? array(
array(
'title' => __( 'Global Restriction Message', 'user-registration' ),
'desc' => __( ' Default message for all restricted content.', 'user-registration' ),
@@ -153,15 +261,35 @@
'show-reset-content-button' => false,
'desc_tip' => true,
),
- ),
+ ) : array(),
);
- $is_new_installation = ur_string_to_bool( get_option( 'urm_is_new_installation', '' ) );
- if ( ! $is_new_installation ) {
- $sections['user_registration_content_restriction_settings']['desc'] = sprintf(
- /* translators: %s - Content rule URL */
- __( '<strong>The Global Restriction setting has moved.</strong> You can now manage it <a href="%1$s" target="_blank" style="text-decoration: underline;" >here.</a>', 'user-registration' ),
- esc_url_raw( $content_rule_url )
- );
+
+ if ( ! $has_restriction_in_use ) {
+ $create_membership_url = admin_url( 'admin.php?page=user-registration-membership&action=add_new_membership' );
+
+ if ( defined( 'UR_PRO_ACTIVE' ) && UR_PRO_ACTIVE && ur_check_module_activation( 'content-restriction' ) ) {
+ $sections['user_registration_content_restriction_settings']['desc'] = sprintf(
+ /* translators: 1: Add new membership URL, 2: Content rule URL */
+ __( '<strong>No membership plans yet.</strong> Restrict content by creating a membership plan or setting up Content Rules. <a href="%1$s">Create a membership →</a> <a href="%2$s">Set up Content Rules →</a>', 'user-registration' ),
+ esc_url( $create_membership_url ),
+ esc_url( $content_rule_url )
+ );
+ } else {
+ $sections['user_registration_content_restriction_settings']['desc'] = sprintf(
+ /* translators: %s - Add new membership URL */
+ __( '<strong>No membership plans yet.</strong> Create a membership plan to start restricting content and customize this message. <a href="%s">Create a membership →</a>', 'user-registration' ),
+ esc_url( $create_membership_url )
+ );
+ }
+ } else {
+ $is_new_installation = ur_string_to_bool( get_option( 'urm_is_new_installation', '' ) );
+ if ( ! $is_new_installation ) {
+ $sections['user_registration_content_restriction_settings']['desc'] = sprintf(
+ /* translators: %s - Content rule URL */
+ __( '<strong>The Global Restriction setting has moved.</strong> You can now manage it <a href="%1$s" target="_blank" style="text-decoration: underline;" >here.</a>', 'user-registration' ),
+ esc_url_raw( $content_rule_url )
+ );
+ }
}
return apply_filters(
--- a/user-registration/includes/blocks/block-types/class-ur-block-membership-buy-now.php
+++ b/user-registration/includes/blocks/block-types/class-ur-block-membership-buy-now.php
@@ -55,7 +55,7 @@
$block_id = isset( $this->attributes['clientId'] ) ? $this->attributes['clientId'] : '';
$attr = $this->attributes;
- $page_id = get_option( 'user_registration_member_registration_page_id' );
+ $page_id = ur_get_translated_page_id( get_option( 'user_registration_member_registration_page_id' ) );
$page_url = get_permalink( absint( $page_id ) );
@@ -302,7 +302,7 @@
$membership_details = $membership_repository->get_single_membership_by_ID( $membership_id );
$intended_action = $membership_service->fetch_intended_action( $action_to_take, $membership_details, $user_membership_ids );
- $thank_you_page_id = get_option( 'user_registration_thank_you_page_id', false );
+ $thank_you_page_id = ur_get_translated_page_id( get_option( 'user_registration_thank_you_page_id', false ) );
$redirect_link_builder = array(
'action' => $intended_action,
--- a/user-registration/includes/class-ur-ajax.php
+++ b/user-registration/includes/class-ur-ajax.php
@@ -94,6 +94,7 @@
'create_default_form' => false,
'generate_required_pages' => false,
'handle_default_wordpress_login' => false,
+ 'enable_emails' => false,
'skip_site_assistant_section' => false,
'login_settings_page_validation' => false,
'activate_dependent_module' => false,
@@ -273,8 +274,23 @@
}
}
- $profile = user_registration_form_data( $user_id, $form_id );
- $is_admin_user = $_POST['is_admin_user'] ?? false;
+ $profile = user_registration_form_data( $user_id, $form_id );
+
+ // No resolvable UR form (e.g. wp-admin/WooCommerce created user) - fall back to basic fields.
+ if ( empty( $profile ) ) {
+ $profile = ur_get_non_urm_user_profile_fields( $user_id );
+ }
+
+ if ( empty( $profile ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => __( 'Unable to update profile. No editable profile fields were found for this account.', 'user-registration' ),
+ )
+ );
+ }
+
+ // Server-derived, not client-supplied - avoids a spoofable $_POST flag.
+ $is_admin_user = $user_id !== get_current_user_id();
list( $profile, $single_field ) = urm_process_profile_fields( $profile, $single_field, $form_data, $form_id, $user_id, $is_admin_user );
$user = get_userdata( $user_id );
@@ -301,7 +317,7 @@
'email' => ! empty( $single_field['user_registration_user_email'] ) ? $single_field['user_registration_user_email'] : '',
);
- if ( $email_updated && ! is_admin() ) {
+ if ( $email_updated && ! $is_admin_user ) {
UR_Form_Handler::send_confirmation_email( $user, $pending_email, $form_id );
$response['oldUserEmail'] = $user->user_email;
/* translators: %s : user email */
@@ -329,7 +345,7 @@
);
}
- if ( is_admin() && ! empty( $pending_email ) ) {
+ if ( $is_admin_user && ! empty( $pending_email ) ) {
wp_update_user(
array(
'ID' => $user_id,
@@ -2614,6 +2630,25 @@
}
/**
+ * Turn the master "Disable emails" setting back off.
+ */
+ public static function enable_emails() {
+ check_ajax_referer( 'wp_rest', 'security' );
+
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( array( 'message' => __( 'You do not have permission to modify email settings.', 'user-registration' ) ) );
+ }
+
+ update_option( 'user_registration_email_setting_disable_email', 'no' );
+
+ wp_send_json_success(
+ array(
+ 'message' => __( 'Emails have been enabled successfully.', 'user-registration' ),
+ )
+ );
+ }
+
+ /**
* Skip site assistant section.
*
* @since 4.0
--- a/user-registration/includes/class-ur-emailer.php
+++ b/user-registration/includes/class-ur-emailer.php
@@ -97,11 +97,15 @@
* @return void
*/
public static function ur_after_register_mail( $valid_form_data, $form_id, $user_id ) {
+ if ( ur_option_checked( 'user_registration_email_setting_disable_email' ) ) {
+ return;
+ }
+
$valid_form_data = ur_array_clone( $valid_form_data );
$login_option = ur_get_user_login_option( $user_id );
- if ( ( 'email_confirmation' !== $login_option || 'admin_approval_after_email_confirmation' !== $login_option ) && ur_option_checked( 'user_registration_email_setting_disable_email' ) ) {
+ if ( ( 'email_confirmation' !== $login_option && 'admin_approval_after_email_confirmation' !== $login_option ) && ur_option_checked( 'user_registration_email_setting_disable_email' ) ) {
return;
}
/**
@@ -134,7 +138,7 @@
self::send_mail_to_user( $email, $username, $user_id, $data_html, $name_value, $attachments, $template_id );
- if ( 'admin_approval' === $login_option || 'admin_approval_after_email_confirmation' === $login_option ) {
+ if ( 'admin_approval' === $login_option ) {
self::send_approve_link_in_email( $email, $username, $user_id, $data_html, $name_value, $attachments, $template_id );
}
self::send_mail_to_admin( $email, $username, $user_id, $data_html, $name_value, $attachments, $template_id );
@@ -294,6 +298,11 @@
public static function user_registration_process_and_send_email( $email, $subject, $message, $header, $attachment, $template_id ) {
$logger = ur_get_logger();
+
+ if ( ur_option_checked( 'user_registration_email_setting_disable_email' ) ) {
+ $logger->notice( 'Email not sent: emails are disabled from email settings.', array( 'source' => 'ur_mail_logs' ) );
+ return true;
+ }
$logger->notice( '=============== Email Sending Start ================', array( 'source' => 'ur_mail_logs' ) );
$logger->debug(
'Email details:' . "n" . wp_json_encode(
@@ -564,9 +573,6 @@
*/
public static function ur_profile_details_changed_mail( $user_id, $form_id ) {
- if ( ur_option_checked( 'user_registration_email_setting_disable_email' ) ) {
- return;
- }
$profile = user_registration_form_data( $user_id, $form_id );
$name_value = array();
$data_html = '<table class="user-registration-email__entries" cellpadding="0" cellspacing="0"><tbody>';
--- a/user-registration/includes/class-ur-shortcodes.php
+++ b/user-registration/includes/class-ur-shortcodes.php
@@ -265,18 +265,9 @@
* @param mixed $atts Extra attributes.
*/
public static function form( $atts ) {
- /**
- * Applies a filter to override the 'users_can_register' setting.
- *
- * The 'ur_register_setting_override' filter allows developers to customize
- * the 'users_can_register' setting by providing an alternative value.
- *
- * @param bool $default_value Default value retrieved from the 'users_can_register' setting.
- */
- $users_can_register = apply_filters( 'ur_register_setting_override', get_option( 'users_can_register' ) );
- $check_user_state = isset( $atts['userState'] ) && 'logged_in' === $atts['userState'];
+ $check_user_state = isset( $atts['userState'] ) && 'logged_in' === $atts['userState'];
- if ( ! is_user_logged_in() && ! $check_user_state && ! $users_can_register ) {
+ if ( ! is_user_logged_in() && ! $check_user_state && ! ur_users_can_register() ) {
return apply_filters( 'ur_register_pre_form_message', '<p class="alert" id="ur_register_pre_form_message">' . __( 'Registration is currently disabled.', 'user-registration' ) . '</p>' );
}
--- a/user-registration/includes/class-ur-user-approval.php
+++ b/user-registration/includes/class-ur-user-approval.php
@@ -315,8 +315,9 @@
$last_order = $members_order_repository->get_member_orders( $user->ID );
}
- $payment_status = get_user_meta( $user->ID, 'ur_payment_status', true );
- $is_member = $is_membership_active && ! empty( $membership ) && ! empty( $last_order );
+ $payment_status = get_user_meta( $user->ID, 'ur_payment_status', true );
+ $requires_payment = 'yes' === get_user_meta( $user->ID, 'ur_requires_payment', true );
+ $is_member = $is_membership_active && ! empty( $membership ) && ! empty( $last_order );
if ( $is_member ) {
$payment_status = $last_order['status'];
$membership_payment_method = $last_order['payment_method'];
@@ -331,7 +332,7 @@
*/
do_action( 'ur_user_before_check_payment_status_on_login', $payment_status, $user );
- if ( ! empty( $payment_status ) && 'completed' !== $payment_status ) {
+ if ( 'completed' !== $payment_status && ( $requires_payment || ! empty( $payment_status ) ) ) {
$message = '<strong>' . __( 'ERROR:', 'user-registration' ) . '</strong> ' . __( 'Your account is still pending payment.', 'user-registration' );
$payment_method = $is_member ? $membership_payment_method : get_user_meta( $user->ID, 'ur_payment_method', true );
--- a/user-registration/includes/frontend/class-ur-frontend-form-handler.php
+++ b/user-registration/includes/frontend/class-ur-frontend-form-handler.php
@@ -60,6 +60,10 @@
)
);
+ // Reset per-submission static state so a prior request can't leak stale data into this one (e.g. on persistent PHP workers).
+ self::$response_array = array();
+ self::$valid_form_data = array();
+
self::$form_id = $form_id;
$post_content_array = ( $form_id ) ? UR()->form->get_form( $form_id, array( 'content_only' => true ) ) : array();
@@ -506,6 +510,19 @@
$login_option = ur_get_user_login_option( $user_id );
update_user_meta( $user_id, 'ur_login_option', $login_option );
+ // Server-side payment-gate flag so login fails closed even if the client omits membership signals. UR-4811.
+ if ( 'payment' === $login_option && function_exists( 'ur_check_module_activation' ) && ur_check_module_activation( 'membership' ) ) {
+ $hidden_fields = isset( $_POST['urcl_hide_fields'] ) ? (array) json_decode( wp_unslash( $_POST['urcl_hide_fields'] ), true ) : array();
+ foreach ( $valid_form_data as $field ) {
+ if ( isset( $field->extra_params['field_key'] ) && 'membership' === $field->extra_params['field_key'] ) {
+ if ( ! in_array( $field->field_name, $hidden_fields, true ) ) {
+ update_user_meta( $user_id, 'ur_requires_payment', 'yes' );
+ }
+ break;
+ }
+ }
+ }
+
$current_language = ur_get_current_language();
$current_language = isset( $_POST['registration_language'] ) ? ur_clean( $_POST['registration_language'] ) : $current_language; //phpcs:ignore.
update_user_meta( $user_id, 'ur_registered_language', $current_language );
--- a/user-registration/includes/frontend/class-ur-frontend.php
+++ b/user-registration/includes/frontend/class-ur-frontend.php
@@ -93,10 +93,11 @@
$form_data = json_decode( wp_unslash( $_POST['form_data'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
foreach ( $form_data as $data ) {
- if ( isset( $data->field_name ) && 'user_registration_profile_pic_url' === $data->field_name ) {
+ // Unrendered fields send field_name only - require value too, or this wipes the picture.
+ if ( isset( $data->field_name, $data->value ) && 'user_registration_profile_pic_url' === $data->field_name ) {
if ( ! is_array( $data->value ) && ! ur_is_valid_url( $data->value ) ) {
$valid_form_data['profile_pic_url'] = new stdClass();
- $valid_form_data['profile_pic_url']->value = isset( $data->value ) ? $data->value : '';
+ $valid_form_data['profile_pic_url']->value = $data->value;
}
}
}
@@ -361,6 +362,10 @@
* Check if can add payment tabs.
*/
public function ur_register_payment_tab_if_eligible() {
+ // Rewrite rules are global, so the endpoints are always registered regardless of the current user.
+ $this->ur_add_payments_tab_endpoint();
+ $this->ur_add_membership_tab_endpoint();
+
$user_id = get_current_user_id();
$payment_method = get_user_meta( $user_id, 'ur_payment_method', true );
@@ -374,7 +379,6 @@
$is_admin = in_array( 'administrator', (array) $user->roles, true );
if ( 'membership' === $user_source || $payment_method || $is_admin ) {
- add_action( 'wp_loaded', array( $this, 'ur_add_payments_tab_endpoint' ) );
add_filter( 'user_registration_account_menu_items', array( $this, 'urm_payment_history_tab' ), 10, 1 );
add_action(
'user_registration_account_urm-payments_endpoint',
@@ -386,7 +390,6 @@
}
if ( 'membership' === $user_source || ( ! empty( $payment_method ) && ( ! empty( $ur_payment_subscription ) || 'paypal_standard' === $payment_method ) ) ) {
- add_action( 'wp_loaded', array( $this, 'ur_add_membership_tab_endpoint' ) );
add_filter( 'user_registration_account_menu_items', array( $this, 'ur_membership_tab' ), 10, 1 );
add_action(
'user_registration_account_ur-membership_endpoint',
@@ -408,9 +411,6 @@
$new_items['urm-payments'] = __( 'Payments', 'user-registration' );
$items = array_merge( $items, $new_items );
- $mask = Ur()->query->get_endpoints_mask();
- add_rewrite_endpoint( 'ur-membership', $mask );
-
return $this->insert_after_helper( $items, $new_items, 'edit-profile' );
}
@@ -450,7 +450,7 @@
$is_admin = in_array( 'administrator', (array) $user->roles, true );
if ( $is_admin ) {
- echo esc_html_e( 'You do not have any payment records', 'user-registration' );
+ esc_html_e( 'You do not have any payment records', 'user-registration' );
return;
}
@@ -564,7 +564,7 @@
$mask = Ur()->query->get_endpoints_mask();
add_rewrite_endpoint( 'urm-payments', $mask );
- flush_rewrite_rules();
+ ur_maybe_flush_rewrite_rules( 'urm-payments' );
}
/**
@@ -574,7 +574,7 @@
$mask = Ur()->query->get_endpoints_mask();
add_rewrite_endpoint( 'ur-membership', $mask );
- flush_rewrite_rules();
+ ur_maybe_flush_rewrite_rules( 'ur-membership' );
}
/**
--- a/user-registration/includes/functions-ur-core.php
+++ b/user-registration/includes/functions-ur-core.php
@@ -3737,7 +3737,8 @@
'requires_membership' => true,
),
'user_registration_thank_you_page_id' => array(
- 'name' => 'membership-thankyou',
+ // Same slug the setup wizard uses, so the thank-you URL does not depend on which flow created the page.
+ 'name' => 'thankyou',
'title' => __( 'Membership Thank You', 'user-registration' ),
'content' => '[user_registration_membership_thank_you]',
'requires_membership' => true,
@@ -5652,13 +5653,7 @@
}
}
- /**
- * Filter to override the register settings.
- * Default value is the get_option('users_can_register')
- */
- $users_can_register = apply_filters( 'ur_register_setting_override', get_option( 'users_can_register' ) );
-
- if ( ! is_user_logged_in() && ! $users_can_register ) {
+ if ( ! is_user_logged_in() && ! ur_users_can_register() ) {
$logger->warning(
sprintf( '[Form #%d] Registration is disabled by the site administrator.', $form_id ) . "n",
array(
@@ -8402,6 +8397,23 @@
}
add_filter( 'ur_register_setting_override', 'ur_rsssl_anyone_can_register_conflict_resolver', 10, 1 );
+if ( ! function_exists( 'ur_users_can_register' ) ) {
+ /**
+ * Whether registration is allowed, honoring the WordPress "Anyone can register" option.
+ *
+ * @since 5.2.7
+ *
+ * @return bool
+ */
+ function ur_users_can_register() {
+ /**
+ * Filter to override the register settings.
+ * Default value is the get_option('users_can_register')
+ */
+ return (bool) apply_filters( 'ur_register_setting_override', get_option( 'users_can_register' ) );
+ }
+}
+
add_filter( 'user_registration_settings_prevent_default_login', 'ur_prevent_default_login' );
if ( ! function_exists( 'ur_prevent_default_login' ) ) {
/**
@@ -10662,6 +10674,70 @@
}
}
+if ( ! function_exists( 'ur_has_membership_plans' ) ) {
+ /**
+ * Check whether at least one active membership plan exists.
+ *
+ * A deactivated plan stays published and only has its status flag turned off,
+ * so the post status alone is not enough. This mirrors the active check in
+ * MembershipService::prepare_membership_data().
+ *
+ * @return bool
+ */
+ function ur_has_membership_plans() {
+ if ( ! post_type_exists( 'ur_membership' ) ) {
+ return false;
+ }
+
+ $memberships = get_posts(
+ array(
+ 'post_type' => 'ur_membership',
+ 'post_status' => 'publish',
+ 'posts_per_page' => -1,
+ 'no_found_rows' => true,
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ )
+ );
+
+ foreach ( $memberships as $membership ) {
+ $membership_content = json_decode( wp_unslash( $membership->post_content ), true );
+
+ if ( ! empty( $membership_content['status'] ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
+
+if ( ! function_exists( 'ur_is_membership_registration_type' ) ) {
+ /**
+ * Check whether the setup wizard's registration type is a membership type
+ * (as opposed to "Advanced Registration", stored as 'normal').
+ *
+ * @return bool
+ */
+ function ur_is_membership_registration_type() {
+ $membership_type = get_option( 'urm_onboarding_membership_type', 'normal' );
+
+ return in_array( $membership_type, array( 'free_membership', 'paid_membership' ), true );
+ }
+}
+
+if ( ! function_exists( 'ur_should_show_membership_requirements' ) ) {
+ /**
+ * Membership-registration-type users always need these; Advanced Registration
+ * users only need them once a membership plan is created.
+ *
+ * @return bool
+ */
+ function ur_should_show_membership_requirements() {
+ return ur_is_membership_registration_type() || ur_has_membership_plans();
+ }
+}
+
if ( ! function_exists( 'ur_get_site_assistant_data' ) ) {
/**
* Get site assistant data with all options status.
@@ -10679,8 +10755,10 @@
'user_registration_membership_pricing_page_id' => 'Membership Pricing Page',
);
- // Check if membership module is activated.
- $is_membership_activated = ur_check_module_activation( 'membership' );
+ $has_membership_plans = ur_has_membership_plans();
+
+ // Advanced Registration users only need membership pages once a plan exists.
+ $show_membership_requirements = ur_is_membership_registration_type() || $has_membership_plans;
$missing_pages_data = array();
@@ -10697,7 +10775,7 @@
}
if ( $is_page_missing ) {
- // Only include membership pages if membership module is activated.
+ // Only include membership pages if membership requirements should be shown.
$is_membership_page = in_array(
$option_name,
array(
@@ -10708,7 +10786,7 @@
true
);
- if ( ! $is_membership_page || $is_membership_activated ) {
+ if ( ! $is_membership_page || $show_membership_requirements ) {
$missing_pages_data[] = array(
'name' => $page_name,
'option' => $option_name,
@@ -10739,22 +10817,11 @@
$membership_field_handled = ( ! $membership_enabled ) || $default_form_has_membership || $membership_field_skipped;
- $has_membership_plans = false;
-
- if ( post_type_exists( 'ur_membership' ) ) {
- $has_membership_plans = (bool) get_posts(
- array(
- 'post_type' => 'ur_membership',
- 'post_status' => 'publish',
- 'posts_per_page' => 1,
- 'fields' => 'ids',
- )
- );
- }
-
$site_assistant_data = array(
+ 'users_can_register' => ur_users_can_register(),
'has_default_form' => ! empty( $default_form_post ),
'missing_pages' => $missing_pages_data,
+ 'disabled_emails_handled' => ! ur_option_checked( 'user_registration_email_setting_disable_email' ),
'test_email_sent' => get_option( 'user_registration_successful_test_mail', false ),
'spam_protection_handled' => ur_string_to_bool( get_option( 'user_registration_captcha_setting_v2_connection_status', false ) ) || ur_string_to_bool( get_option( 'user_registration_spam_protection_skipped', false ) ),
'payment_setup_handled' => $payment_setup_handled,
@@ -10833,7 +10900,7 @@
$connections = array();
// Check Stripe connection (available in free version).
- if ( ur_check_module_activation( 'stripe' ) || ur_check_module_activation( 'membership' ) ) {
+ if ( ur_check_module_activation( 'stripe' ) || ur_should_show_membership_requirements() ) {
$connections['stripe'] = array(
'name' => 'Stripe',
'is_connected' => ur_string_to_bool( get_option( 'urm_stripe_connection_status', false ) ),
@@ -10842,7 +10909,7 @@
}
// Check PayPal connection (available in free version).
- if ( ur_check_module_activation( 'payments' ) || ur_check_module_activation( 'membership' ) ) {
+ if ( ur_check_module_activation( 'payments' ) || ur_should_show_membership_requirements() ) {
$connections['paypal'] = array(
'name' => 'PayPal',
'is_connected' => ur_string_to_bool( get_option( 'urm_paypal_connection_status', false ) ),
@@ -10851,7 +10918,7 @@
}
// Check Bank connection (membership only).
- if ( ur_check_module_activation( 'membership' ) ) {
+ if ( ur_should_show_membership_requirements() ) {
$connections['bank'] = array(
'name' => 'Bank Payment',
'is_connected' => ur_string_to_bool( get_option( 'urm_bank_connection_status', false ) ),
@@ -10991,8 +11058,10 @@
$site_assistant_data = ur_get_site_assistant_data();
return (
- ! $site_assistant_data['has_default_form']
+ ! $site_assistant_data['users_can_register']
+ || ! $site_assistant_data['has_default_form']
|| ! empty( $site_assistant_data['missing_pages'] )
+ || ! $site_assistant_data['disabled_emails_handled']
|| ! $site_assistant_data['test_email_sent']
|| ! $site_assistant_data['spam_protection_handled']
|| ! $site_assistant_data['payment_setup_handled']
@@ -11012,8 +11081,10 @@
$site_assistant_data = ur_get_site_assistant_data();
$checks = array(
+ ! $site_assistant_data['users_can_register'],
! $site_assistant_data['has_default_form'],
! empty( $site_assistant_data['missing_pages'] ),
+ ! $site_assistant_data['disabled_emails_handled'],
! $site_assistant_data['test_email_sent'],
! $site_assistant_data['spam_protection_handled'],
! $site_assistant_data['payment_setup_handled'],
@@ -11134,6 +11205,51 @@
}
}
+if ( ! function_exists( 'ur_get_non_urm_user_profile_fields' ) ) {
+ /**
+ * Fallback profile fields for users with no resolvable UR registration form.
+ *
+ * @param int $user_id User ID.
+ *
+ * @return array
+ */
+ function ur_get_non_urm_user_profile_fields( $user_id ) {
+ if ( ! get_userdata( $user_id ) ) {
+ return array();
+ }
+
+ return apply_filters(
+ 'user_registration_non_urm_user_profile_fields',
+ array(
+ 'user_registration_user_login' => array(
+ 'label' => __( 'Username', 'user-registration' ),
+ 'type' => 'text',
+ 'field_key' => 'user_login',
+ 'required' => true,
+ ),
+ 'user_registration_first_name' => array(
+ 'label' => __( 'First Name', 'user-registration' ),
+ 'type' => 'text',
+ 'field_key' => 'first_name',
+ 'required' => false,
+ ),
+ 'user_registration_user_email' => array(
+ 'label' => __( 'User Email', 'user-registration' ),
+ 'type' => 'email',
+ 'field_key' => 'user_email',
+ 'required' => true,
+ ),
+ 'user_registration_last_name' => array(
+ 'label' => __( 'Last Name', 'user-registration' ),
+ 'type' => 'text',
+ 'field_key' => 'last_name',
+ 'required' => false,
+ ),
+ ),
+ $user_id
+ );
+ }
+}
if ( ! function_exists( 'urm_update_user_profile_data' ) ) {
/**
@@ -11383,6 +11499,314 @@
}
}
+if ( ! function_exists( 'ur_membership_table_exists' ) ) {
+ /**
+ * Check whether a membership module table exists.
+ *
+ * Mirrors the guard the membership repositories apply before querying, e.g.
+ * OrdersRepository::get_all() and SubscriptionRepository::query().
+ *
+ * @param string $table Fully qualified table name.
+ *
+ * @return bool
+ * @since x.x.x
+ */
+ function ur_membership_table_exists( $table ) {
+ global $wpdb;
+
+ static $checked = array();
+
+ if ( empty( $table ) ) {
+ return false;
+ }
+
+ if ( ! isset( $checked[ $table ] ) ) {
+ $checked[ $table ] = $table === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
+ }
+
+ return $checked[ $table ];
+ }
+}
+
+if ( ! function_exists( 'ur_has_membership_plans' ) ) {
+ /**
+ * Check whether at least one published membership plan exists.
+ *
+ * The `ur_membership` post type is only registered when the membership module
+ * is loaded, so fall back to a direct query when it is not.
+ *
+ * @return bool
+ * @since x.x.x
+ */
+ function ur_has_membership_plans() {
+ static $has_plans = null;
+
+ if ( null !== $has_plans ) {
+ return $has_plans;
+ }
+
+ if ( post_type_exists( 'ur_membership' ) ) {
+ $has_plans = (bool) get_posts(
+ array(
+ 'post_type' => 'ur_membership',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 1,
+ 'fields' => 'ids',
+ )
+ );
+
+ return $has_plans;
+ }
+
+ global $wpdb;
+
+ $has_plans = (bool) $wpdb->get_var(
+ $wpdb->prepare(
+ "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s LIMIT 1",
+ 'ur_membership',
+ 'publish'
+ )
+ );
+
+ return $has_plans;
+ }
+}
+
+if ( ! function_exists( 'ur_has_payment_entries' ) ) {
+ /**
+ * Check whether the Payments page has at least one record to display.
+ *
+ * Mirrors the two sources merged by OrdersListTable::prepare_items():
+ * membership orders (only when the membership module is active, and only rows
+ * whose plan post and user still exist, matching the INNER JOINs in
+ * OrdersRepository::get_all()) and normal registration payments, which are
+ * stored as the `ur_payment_status` user meta.
+ *
+ * @return bool
+ * @since x.x.x
+ */
+ function ur_has_payment_entries() {
+ global $wpdb;
+
+ static $has_entries = null;
+
+ if ( null !== $has_entries ) {
+ return $has_entries;
+ }
+
+ $has_entries = false;
+
+ // Membership orders, gated exactly as OrdersListTable gates its repository.
+ if ( function_exists( 'ur_check_module_activation' ) && ur_check_module_activation( 'membership' ) ) {
+ $orders_table = class_exists( 'WPEverestURMembershipTableList' )
+ ? WPEverestURMembershipTableList::orders_table()
+ : $wpdb->prefix . 'ur_membership_orders';
+
+ if ( ur_membership_table_exists( $orders_table ) ) {
+ $orders_query = "SELECT o.ID
+ FROM {$orders_table} o
+ INNER JOIN {$wpdb->posts} p ON o.item_id = p.ID
+ INNER JOIN {$wpdb->users} u ON o.user_id = u.ID
+ LIMIT 1";
+
+ $has_entries = (bool) $wpdb->get_var( $orders_query ); // phpcs:ignore
+ }
+ }
+
+ // Payments made through a registration form.
+ if ( ! $has_entries ) {
+ $has_entries = (bool) $wpdb->get_var(
+ $wpdb->prepare(
+ "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s LIMIT 1",
+ 'ur_payment_status'
+ )
+ );
+ }
+
+ return $has_entries;
+ }
+}
+
+if ( ! function_exists( 'ur_has_subscription_entries' ) ) {
+ /**
+ * Check whether at least one subscription record exists.
+ *
+ * Mirrors SubscriptionRepository::query(), which counts the subscriptions
+ * table directly without joining anything.
+ *
+ * @return bool
+ * @since x.x.x
+ */
+ function ur_has_subscription_entries() {
+ global $wpdb;
+
+ static $has_entries = null;
+
+ if ( null !== $has_entries ) {
+ return $has_entries;
+ }
+
+ $subscriptions_table = class_exists( 'WPEverestURMembershipTableList' )
+ ? WPEverestURMembershipTableList::subscriptions_table()
+ : $wpdb->prefix . 'ur_membership_subscriptions';
+
+ $has_entries = ur_membership_table_exists( $subscriptions_table )
+ && (bool) $wpdb->get_var( "SELECT ID FROM {$subscriptions_table} LIMIT 1" ); // phpcs:ignore
+
+ return $has_entries;
+ }
+}
+
+if ( ! function_exists( 'ur_has_payment_enabled_form' ) ) {
+ /**
+ * Check whether any published registration form collects a payment.
+ *
+ * There is no per-form "payments enabled" flag; payment fields live inside the
+ * form's post_content JSON, so this matches on the `"field_key":"..."` markers
+ * the same way MembershipRepository::get_membership_forms() does.
+ *
+ * @return bool
+ * @since x.x.x
+ */
+ function ur_has_payment_enabled_form() {
+ global $wpdb;
+
+ static $has_form = null;
+
+ if ( null !== $has_form ) {
+ return $has_form;
+ }
+
+ /**
+ * Field keys that on their own make a registration form charge the user.
+ *
+ * Deliberately narrower than user_registration_payment_fields(): `total_field`
+ * and `quantity_field` are payment fields but never trigger a gateway by
+ * themselves. This list matches the gateway check in
+ * UR_Pro_Payments_Frontend::payment_process_after_registration().
+ *
+ * @param array $field_keys Charging field keys.
+ *
+ * @since x.x.x
+ */
+ $field_keys = apply_filters(
+ 'user_registration_payments_menu_field_keys',
+ array( 'single_item', 'multiple_choice', 'subscription_plan' )
+ );
+
+ $conditions = array();
+
+ foreach ( (array) $field_keys as $field_key ) {
+ $pattern = '%' . $wpdb->esc_like( '"field_key":"' . $field_key . '"' ) . '%';
+ $conditions[] = 'post_content LIKE ' . $wpdb->prepare( '%s', $pattern );
+ }
+
+ if ( ! empty( $conditions ) ) {
+ $where_clause = implode( ' OR ', $conditions );
+
+ $fields_query = "SELECT ID FROM {$wpdb->posts}
+ WHERE post_type = 'user_registration'
+ AND post_status = 'publish'
+ AND ({$where_clause})
+ LIMIT 1";
+
+ $has_form = (bool) $wpdb->get_var( $fields_query ); // phpcs:ignore
+
+ if ( $has_form ) {
+ return $has_form;
+ }
+ }
+
+ $has_form = false;
+
+ // A `range` field only charges when its payment slider is enabled. The stored
+ // value is boolean-ish, so candidate forms have to be confirmed in PHP.
+ if ( ! function_exists( 'ur_get_form_fields' ) ) {
+ return $has_form;
+ }
+
+ $candidates = $wpdb->get_col(
+ $wpdb->prepare(
+ "SELECT ID FROM {$wpdb->posts}
+ WHERE post_type = 'user_registration'
+ AND post_status = 'publish'
+ AND post_content LIKE %s",
+ '%' . $wpdb->esc_like( 'enable_payment_slider' ) . '%'
+ )
+ );
+
+ foreach ( $candidates as $form_id ) {
+ foreach ( (array) ur_get_form_fields( $form_id ) as $field ) {
+ if ( isset( $field->field_key, $field->advance_setting->enable_payment_slider )
+ && 'range' === $field->field_key
+ && ur_string_to_bool( $field->advance_setting->enable_payment_slider ) ) {
+ $has_form = true;
+
+ return $has_form;
+ }
+ }
+ }
+
+ return $has_form;
+ }
+}
+
+if ( ! function_exists( 'ur_should_show_payments_menu' ) ) {
+ /**
+ * Whether the Payments submenu should be registered.
+ *
+ * Shown when payments are actually in use: the Payments feature is enabled, the
+ * page already has records, a membership plan exists, or a registration form
+ * collects a payment. Onboarding's "Advanced Registration" path matches none of
+ * these, which is what keeps the menu hidden there.
+ *
+ * @return bool
+ * @since x.x.x
+ */
+ function ur_should_show_payments_menu() {
+ $show = ( function_exists( 'ur_check_module_activation' ) && ur_check_module_activation( 'payments' ) )
+ || ur_has_payment_entries()
+ || ur_has_membership_plans()
+ || ur_has_payment_enabled_form();
+
+ /**
+ * Filter whether the Payments submenu is registered.
+ *
+ * @param bool $show Whether to show the menu.
+ *
+ * @since x.x.x
+ */
+ return (bool) apply_filters( 'user_registration_show_payments_menu', $show );
+ }
+}
+
+if ( ! function_exists( 'ur_should_show_subscriptions_menu' ) ) {
+ /**
+ * Whether the Subscriptions submenu should be registered.
+ *
+ * The membership module alone is not enough: onboarding enables it before the
+ * user picks a registration type, so also require something to manage — a
+ * membership plan or an existing subscription record.
+ *
+ * @return bool
+ * @since x.x.x
+ */
+ function ur_should_show_subscriptions_menu() {
+ $show = function_exists( 'ur_check_module_activation' )
+ && ur_check_module_activation( 'membership' )
+ && ( ur_has_membership_plans() || ur_has_subscription_entries() );
+
+ /**
+ * Filter whether the Subscriptions submenu is registered.
+ *
+ * @param bool $show Whether to show the menu.
+ *
+ * @since x.x.x
+ */
+ return (bool) apply_filters( 'user_registration_show_subscriptions_menu', $show );
+ }
+}
+
/**
* Get the count of membership rules.
* This function checks for content access rules with rule_type = 'membership'.
@@ -12298,3 +12722,21 @@
}
}
}
+
+if ( ! function_exists( 'ur_maybe_flush_rewrite_rules' ) ) {
+ // Flush only when one of the given account endpoints is missing from the saved rules.
+ function ur_maybe_flush_rewrite_rules( $endpoints ) {
+ if ( ! get_option( 'permalink_structure' ) ) {
+ return;
+ }
+
+ $rules = get_option( 'rewrite_rules', array() );
+
+ foreach ( (array) $endpoints as $endpoint ) {
+ if ( ! isset( $rules[ '(.?.+?)/' . $endpoint . '(/(.*))?/?$' ] ) ) {
+ flush_rewrite_rules();
+ return;
+ }
+ }
+ }
+}
--- a/user-registration/includes/functions-ur-page.php
+++ b/user-registration/includes/functions-ur-page.php
@@ -236,8 +236,8 @@
$permalink = 0 < $my_account_page_id ? get_permalink( $my_account_page_id ) : '';
if ( $permalink ) {
- if ( '/' !== substr( $permalink, -1 ) ) {
- $permalink = $permalink . '/';
+ if ( false === strpos( $permalink, '?' ) ) {
+ $permalink = trailingslashit( $permalink );
}
return $permalink;
}
--- a/user-registration/includes/validation/class-ur-form-validation.php
+++ b/user-registration/includes/validation/class-ur-form-validation.php
@@ -68,6 +68,10 @@
* @return array
*/
public function reorganize_form_data( $valid_form_data, $form_field_data, $form_id ) {
+ if ( ! is_array( $valid_form_data ) ) {
+ $valid_form_data = array();
+ }
+
if ( empty( $form_field_data ) ) {
return $valid_form_data;
}
--- a/user-registration/modules/functions-ur-modules.php
+++ b/user-registration/modules/functions-ur-modules.php
@@ -462,7 +462,8 @@
'option' => '',
);
$pages['membership_thankyou'] = array(
- 'name' => _x( 'membership-thankyou', 'Page slug', 'user-registration' ),
+ // Same slug the setup wizard uses, so the thank-you URL does not depend on which flow created the page.
+ 'name' => _x( 'thankyou', 'Page slug', 'user-registration' ),
'title' => _x( 'Membership ThankYou', 'Page title', 'user-registration' ),
'content' => '[user_registration_membership_thank_you]',
'option' => 'user_registration_thank_you_page_id',
--- a/user-registration/modules/masteriyo/includes/Frontend.php
+++ b/user-registration/modules/masteriyo/includes/Frontend.php
@@ -311,7 +311,7 @@
add_rewrite_endpoint( 'urm-courses', $mask );
add_rewrite_endpoint( 'urm-course-portal', $mask );
- flush_rewrite_rules();
+ ur_maybe_flush_rewrite_rules( array( 'urm-courses', 'urm-course-portal' ) );
}
public function get_current_user_course() {
--- a/user-registration/modules/membership/includes/AJAX.php
+++ b/user-registration/modules/membership/includes/AJAX.php
@@ -945,6 +945,9 @@
if ( $update_stripe_order['status'] ) {
+ // UR-4710: payment confirmed — apply any deferred membership role for this in-session member.
+ ( new MembersService() )->maybe_grant_pending_role( $member_id );
+
if ( $is_upgrading ) {
$next_subscription = json_decode( get_user_meta( $member_id, 'urm_next_subscription_data', true ), true );
$previous_subscription = get_user_meta( $member_id, 'urm_previous_subscription_data', true );
@@ -1902,7 +1905,7 @@
}
if ( ! empty( $_POST['tax_rate'] ) ) {
- $data['tax_rate'] = sanitize_text_field( $_POST['tax_rate'] );
+ $data['tax_rate'] = max( 0, floatval( $_POST['tax_rate'] ) );
$data['tax_calculation_method'] = ! empty( $_POST['tax_calculation_method'] ) ? sanitize_text_field( $_POST['tax_calculation_method'] ) : '1';
}
@@ -2179,7 +2182,7 @@
}
if ( ! empty( $_POST['tax_rate'] ) ) {
- $data['tax_rate'] = sanitize_text_field( $_POST['tax_rate'] );
+ $data['tax_rate'] = max( 0, floatval( $_POST['tax_rate'] ) );
$data['tax_calculation_method'] = ! empty( $_POST['tax_calculation_method'] ) ? sanitize_text_field( $_POST['tax_calculation_method'] ) : '1';
}
--- a/user-registration/modules/membership/includes/Admin.php
+++ b/user-registration/modules/membership/includes/Admin.php
@@ -319,6 +319,10 @@
}
public function add_memberships_in_urcr_settings( $settings ) {
+ if ( empty( $settings['sections']['user_registration_content_restriction_settings']['settings'] ) ) {
+ return $settings;
+ }
+
$options = get_active_membership_id_name();
$additional_settings = array(
array(
@@ -347,12 +351,22 @@
* is not sent during form submission — it should only fire after payment is confirmed.
*/
public function set_payment_process_for_membership( $success_params, $valid_form_data, $form_id, $user_id ) {
- if ( empty( $_POST['is_membership_active'] ) && empty( $_POST['membership_type'] ) ) {
+ $selected_membership = 0;
+ foreach ( (array) $valid_form_data as $field_data ) {
+ if ( isset( $field_data->extra_params['field_key'] ) && 'membership' === $field_data->extra_params['field_key'] ) {
+ $selected_membership = isset( $field_data->value ) ? absint( $field_data->value ) : 0;
+ break;
+ }
+ }
+ if ( empty( $selected_membership ) ) {
return $success_params;
}
- $data = isset( $_POST['members_data'] ) ? (array) json_decode( wp_unslash( $_POST['members_data'] ), true ) : array();
- if ( empty( $data['payment_method'] ) || 'free' === $data['payment_method'] ) {
+ // Determine paid vs free from the server-side membership record, not client members_data.
+ $membership_repository = new MembershipRepository();
+ $membership_data = $membership_repository->get_single_membership_by_ID( $selected_membership );
+ $membership_meta = ! empty( $membership_data['meta_value'] ) ? json_decode( wp_unslash( $membership_data['meta_value'] ), true ) : array();
+ if ( empty( $membership_meta['type'] ) || 'free' === $membership_meta['type'] ) {
return $success_params;
}
@@ -396,37 +410,46 @@
if ( ! ur_check_module_activation( 'membership' ) ) {
return $success_params;
}
- // membership POST signals present
- if ( empty( $_POST['is_membership_active'] ) && empty( $_POST['membership_type'] ) ) {
- return $success_params;
- }
- // Guard 3: form has a membership field
- $has_membership_field = false;
+ // Membership processing requirement is derived from the server-side selected value of the
+ // membership field, never from client POST signals (is_membership_active / membership_type)
+ // or members_data — those are trivially omitted to skip enrollment and the payment gate.
$membership_field_data = null;
foreach ( $valid_form_data as $field_data ) {
if ( isset( $field_data->extra_params['field_key'] ) && 'membership' === $field_data->extra_params['field_key'] ) {
- $has_membership_field = true;
$membership_field_data = $field_data;
break;
}
}
- if ( ! $has_membership_field ) {
+ if ( ! $membership_field_data ) {
return $success_params;
}
- $data = apply_filters(
- 'user_registration_membership_before_register_member',
- isset( $_POST['members_data'] ) ? (array) json_decode( wp_unslash( $_POST['members_data'] ), true ) : array()
- );
- if ( empty( $data ) || empty( $data['payment_method'] ) || empty( $data['membership'] ) ) {
+ $selected_membership = isset( $membership_field_data->value ) ? absint( $membership_field_data->value ) : 0;
+ if ( empty( $selected_membership ) ) {
+ // No plan selected server-side: no enrollment intended. Leave a plain account.
return $success_params;
}
- // Inject user identity from the just-created user
$member = get_userdata( $user_id );
$member_id = $user_id;
if ( ! $member ) {
- return $success_params;
+ wp_delete_user( absint( $user_id ) );
+ wp_send_json_error( array( 'message' => esc_html__( 'Invalid membership selection.', 'user-registration' ) ) );
+ }
+
+ $data = apply_filters(
+ 'user_registration_membership_before_register_member',
+ isset( $_POST['members_data'] ) ? (array) json_decode( wp_unslash( $_POST['members_data'] ), true ) : array()
+ );
+ if ( ! is_array( $data ) ) {
+ $data = array();
+ }
+ // Authoritative membership id from the server-side field value, not client members_data.
+ $data['membership'] = $selected_membership;
+
+ // Client-submitted tax_rate must never be negative (would reduce the charge below base).
+ if ( isset( $data['tax_rate'] ) ) {
+ $data['tax_rate'] = max( 0, floatval( $data['tax_rate'] ) );
}
// Validate that the submitted membership ID is one the form is actually configured to offer.
@@ -511,12 +534,25 @@
}
}
- // Get membership type for logging
+ // Resolve membership type from the server-side membership record.
$membership_repository = new MembershipRepository();
$membership_data = $membership_repository->get_single_membership_by_ID( $data['membership'] );
- $membership_meta = json_decode( wp_unslash( $membership_data['meta_value'] ), true );
- $membership_type = $membership_meta['type'] ?? 'unknown';
- $payment_gateway = $data['payment_method'] ?? 'unknown';
+ if ( empty( $membership_data ) || empty( $membership_data['meta_value'] ) ) {
+ wp_delete_user( absint( $member_id ) );
+ wp_send_json_error( array( 'message' => esc_html__( 'Invalid membership selection.', 'user-registration' ) ) );
+ }
+ $membership_meta = json_decode( wp_unslash( $membership_data['meta_value'] ), true );
+ $membership_type = $membership_meta['type'] ?? 'unknown';
+
+ // A paid plan needs a payment method to produce a payable order. Missing method (forged or
+ // omitted members_data) must fail closed, not create an unpaid payment-gated account.
+ if ( 'free' === $membership_type ) {
+ $data['payment_method'] = 'free';
+ } elseif ( empty( $data['payment_method'] ) ) {
+ wp_delete_user( absint( $member_id ) );
+ wp_send_json_error( array( 'message' => esc_html__( 'Invalid membership selection.', 'user-registration' ) ) );
+ }
+ $payment_gateway = $data['payment_method'] ?? 'unknown';
// Reject attacker-supplied payment_method values that don't match the membership.
// A paid/subscription membership must use one of its configured gateways; 'free'
@@ -701,6 +737,7 @@
} else {
$message = isset( $response['message'] ) ? $response['message'] : esc_html__( 'Sorry! There was an unexpected error while registering the user.', 'user-registration' );
+ wp_delete_user( absint( $member_id ) );
wp_send_json_error( array( 'message' => $message ) );
}
}
--- a/user-registration/modules/membership/includes/Admin/Forms/Views/admin-membership.php
+++ b/user-registration/modules/membership/includes/Admin/Forms/Views/admin-membership.php
@@ -107,6 +107,10 @@
// Get field label.
$field_label = esc_html( $this->get_general_setting_data( 'label' ) );
+// A single membership is auto-selected and rendered without a selectable radio,
+// both here in the builder preview and on the live frontend.
+$memberships_count = count( $memberships );
+
?>
<div class="ur-input-type-select ur-admin-template">
<div class="ur-label">
@@ -165,7 +169,7 @@
}
}
?>
- <div class="urmg-plan-card <?php echo