Atomic Edge analysis of CVE-2026-7761: This is a critical account takeover vulnerability in the Ultimate Member plugin for WordPress (versions <= 2.11.4). The vulnerability allows an authenticated attacker with Contributor-level access to leak active password reset URLs for all users, including administrators, leading to complete account compromise. The attack exploits three chained logic flaws within the member directory, meta key parsing, and field validation functions.

CVE-2026-7761: Ultimate Member <= 2.11.4 Authenticated (Contributor+) Account Takeover via Password Reset Link Disclosure PoC, Patch Analysis & Rule
CVE-2026-7761
ultimate-member
2.11.4
2.12.0
Analysis Overview
Differential between vulnerable and patched code
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/ultimate-member/includes/admin/class-site-health.php
+++ b/ultimate-member/includes/admin/class-site-health.php
@@ -1595,10 +1595,6 @@
'label' => __( 'Allowed Choice Callbacks', 'ultimate-member' ),
'value' => UM()->options()->get( 'allowed_choice_callbacks' ),
),
- 'rest_api_version' => array(
- 'label' => __( 'REST API version', 'ultimate-member' ),
- 'value' => UM()->options()->get( 'rest_api_version' ),
- ),
'allow_url_redirect_confirm' => array(
'label' => __( 'Allow external link redirect confirm (enable JS.confirm for external links)', 'ultimate-member' ),
'value' => UM()->options()->get( 'allow_url_redirect_confirm' ) ? $labels['yes'] : $labels['no'],
--- a/ultimate-member/includes/admin/core/class-admin-settings.php
+++ b/ultimate-member/includes/admin/core/class-admin-settings.php
@@ -1020,9 +1020,6 @@
'enable_as_email_sending' => array(
'sanitize' => 'bool',
),
- 'rest_api_version' => array(
- 'sanitize' => 'text',
- ),
'disable_restriction_pre_queries' => array(
'sanitize' => 'bool',
),
@@ -2229,16 +2226,6 @@
'label' => __( 'Allowed Choice Callbacks (Enter one PHP function per line)', 'ultimate-member' ),
'description' => __( 'This option lets you specify the choice callback functions to prevent anyone from using 3rd-party functions that may put your site at risk.', 'ultimate-member' ),
),
- array(
- 'id' => 'rest_api_version',
- 'type' => 'select',
- 'label' => __( 'REST API Version', 'ultimate-member' ),
- 'description' => __( 'This controls the REST API version, we recommend to use the last version.', 'ultimate-member' ),
- 'options' => array(
- '1.0' => __( '1.0 version', 'ultimate-member' ),
- '2.0' => __( '2.0 version', 'ultimate-member' ),
- ),
- ),
),
),
'redirect' => array(
--- a/ultimate-member/includes/class-config.php
+++ b/ultimate-member/includes/class-config.php
@@ -731,7 +731,6 @@
'cover_photo_max_size' => 999999999,
'custom_roles_increment' => 1,
'um_profile_object_cache_stop' => false,
- 'rest_api_version' => '2.0',
'member_directory_own_table' => false,
'profile_show_bio' => false,
'profile_show_html_bio' => false,
--- a/ultimate-member/includes/class-init.php
+++ b/ultimate-member/includes/class-init.php
@@ -427,8 +427,6 @@
if ( ! get_option( 'show_avatars' ) ) {
update_option( 'show_avatars', 1 );
}
- } else {
- UM()->options()->update( 'rest_api_version', '1.0' );
}
if ( $version != UM_VERSION ) {
@@ -442,7 +440,6 @@
$this->cron()->schedule_events();
}
-
/**
*
*/
@@ -450,7 +447,6 @@
do_action( 'um_core_loaded' );
}
-
/**
* Include required core files used in admin and on the frontend.
*
@@ -523,14 +519,8 @@
if ( is_multisite() ) {
$this->multisite();
}
-
- // Call only when REST_API request
- if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
- $this->rest_api();
- }
}
-
/**
* @since 2.1.0
*
@@ -938,27 +928,6 @@
/**
* @since 2.0
*
- * @return umcorerestAPI_v1|umcorerestAPI_v2
- */
- public function rest_api() {
- $api_version = $this->options()->get( 'rest_api_version' );
-
- if ( empty( $this->classes['rest_api'] ) ) {
- if ( '1.0' === $api_version ) {
- $this->classes['rest_api'] = new umcorerestAPI_v1();
- } elseif ( '2.0' === $api_version ) {
- $this->classes['rest_api'] = new umcorerestAPI_v2();
- } else {
- $this->classes['rest_api'] = new umcorerestAPI_v1();
- }
- }
-
- return $this->classes['rest_api'];
- }
-
- /**
- * @since 2.0
- *
* @return umcoreRewrite
*/
function rewrite() {
@@ -969,7 +938,6 @@
return $this->classes['rewrite'];
}
-
/**
* @since 2.0
*
--- a/ultimate-member/includes/core/class-builtin.php
+++ b/ultimate-member/includes/core/class-builtin.php
@@ -1540,7 +1540,7 @@
*
* @return array
*/
- function all_user_fields( $exclude_types = null, $show_all = false ) {
+ public function all_user_fields( $exclude_types = null, $show_all = false ) {
$fields_without_metakey = $this->get_fields_without_metakey();
$fields_without_metakey = apply_filters( 'um_all_user_fields_without_metakey', $fields_without_metakey );
--- a/ultimate-member/includes/core/class-fields.php
+++ b/ultimate-member/includes/core/class-fields.php
@@ -834,7 +834,6 @@
return $classes;
}
-
/**
* Gets field value
*
@@ -844,26 +843,26 @@
*
* @return mixed
*/
- function field_value( $key, $default = false, $data = null ) {
- // preview in backend
+ public function field_value( $key, $default = false, $data = null ) {
+ // Preview in backend
if ( isset( UM()->user()->preview ) && UM()->user()->preview ) {
- if ( $this->set_mode == 'login' || $this->set_mode == 'register' ) {
+ if ( 'login' === $this->set_mode || 'register' === $this->set_mode ) {
return '';
- } else {
- $val = um_user( $key );
- if ( ! empty( $val ) ) {
- return $val;
- } else {
- return '';
- }
}
+
+ $val = um_user( $key );
+ if ( ! empty( $val ) ) {
+ return $val;
+ }
+
+ return '';
}
- if ( isset( $_SESSION ) && isset( $_SESSION['um_social_profile'][ $key ] ) && isset( $this->set_mode ) && $this->set_mode == 'register' ) {
+ if ( isset( $_SESSION ) && isset( $_SESSION['um_social_profile'][ $key ] ) && isset( $this->set_mode ) && 'register' === $this->set_mode ) {
return $_SESSION['um_social_profile'][ $key ];
}
- $type = ( isset( $data['type'] ) ) ? $data['type'] : '';
+ $type = isset( $data['type'] ) ? $data['type'] : '';
// normal state
if ( isset( UM()->form()->post_form[ $key ] ) ) {
@@ -4384,7 +4383,7 @@
/**
- * Gets a field in `view mode`
+ * Gets a field in `view mode`. Works in the User Profile form > View mode only.
*
* @param string $key
* @param array $data
@@ -4436,7 +4435,8 @@
$default = array_key_exists( 'default', $data ) ? $data['default'] : false;
- // Hide if empty.
+ // Hide if the value of the field with metakey is empty.
+ $_field_value = null;
$fields_without_metakey = UM()->builtin()->get_fields_without_metakey();
if ( ! in_array( $type, $fields_without_metakey, true ) ) {
$_field_value = $this->field_value( $key, $default, $data );
@@ -4467,8 +4467,6 @@
switch ( $type ) {
/* Default */
default:
- $_field_value = $this->field_value( $key, $default, $data );
-
if ( ( isset( $_field_value ) && '' !== $_field_value ) || in_array( $type, $fields_without_metakey, true ) ) {
$output .= '<div ' . $this->get_atts( $key, $classes, $conditional, $data ) . '>';
@@ -4508,16 +4506,30 @@
if ( $show_bio ) {
if ( true === $bio_html && ! empty( $data['html'] ) ) {
- $res = wp_kses_post( make_clickable( wpautop( $res ) ) );
+ $res = wp_kses( make_clickable( $res ), 'user_description' );
} else {
- $res = esc_html( $res );
+ $res = wp_kses(
+ make_clickable( $res ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ 'rel' => array(),
+ ),
+ )
+ );
}
+ } elseif ( ! empty( $data['html'] ) ) {
+ $res = wp_kses( make_clickable( $res ), 'user_description' );
} else {
- if ( ! empty( $data['html'] ) ) {
- $res = wp_kses_post( make_clickable( wpautop( $res ) ) );
- } else {
- $res = esc_html( $res );
- }
+ $res = wp_kses(
+ make_clickable( $res ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ 'rel' => array(),
+ ),
+ )
+ );
}
$res = nl2br( $res );
--- a/ultimate-member/includes/core/class-form.php
+++ b/ultimate-member/includes/core/class-form.php
@@ -841,9 +841,10 @@
$form[ $k ] = '' !== $form[ $k ] ? (int) $form[ $k ] : '';
break;
case 'textarea':
- if ( ! empty( $field['html'] ) || ( UM()->profile()->get_show_bio_key( $form ) === $k && UM()->options()->get( 'profile_show_html_bio' ) ) ) {
+ $description_key = UM()->profile()->get_show_bio_key( $form );
+ if ( ! empty( $field['html'] ) || ( $description_key === $k && UM()->options()->get( 'profile_show_html_bio' ) ) ) {
$allowed_html = UM()->get_allowed_html( 'templates' );
- if ( UM()->profile()->get_show_bio_key( $form ) === $k ) {
+ if ( $description_key === $k ) {
$allowed_html = 'user_description';
}
@@ -851,6 +852,7 @@
$form[ $k ] = self::maybe_apply_tidy( $form[ $k ], $field );
$form[ $k ] = wp_kses( strip_shortcodes( $form[ $k ] ), $allowed_html );
} else {
+ // When save textarea value without HTML supported property, strip shortcodes, sanitize like a standard textarea.
$form[ $k ] = sanitize_textarea_field( strip_shortcodes( $form[ $k ] ) );
}
break;
@@ -961,8 +963,10 @@
}
$description_key = UM()->profile()->get_show_bio_key( $this->form_data );
+ // Case when user_description is added to the User Profile header, and it's editable and not empty.
if ( $show_bio && ! empty( $form[ $description_key ] ) ) {
$field_exists = false;
+ // Check the case when user_description is also added to the User Profile form, and it's editable and not empty.
if ( ! empty( $this->form_data['custom_fields'] ) ) {
$custom_fields = maybe_unserialize( $this->form_data['custom_fields'] );
if ( array_key_exists( $description_key, $custom_fields ) ) {
@@ -972,6 +976,7 @@
$form[ $description_key ] = self::maybe_apply_tidy( $form[ $description_key ], $custom_fields[ $description_key ] );
$form[ $description_key ] = wp_kses( strip_shortcodes( $form[ $description_key ] ), 'user_description' );
} else {
+ // When save user_description value without HTML supported property, strip shortcodes, sanitize like a standard textarea.
$form[ $description_key ] = sanitize_textarea_field( strip_shortcodes( $form[ $description_key ] ) );
}
}
@@ -983,6 +988,7 @@
$form[ $description_key ] = self::maybe_apply_tidy( $form[ $description_key ], array() );
$form[ $description_key ] = wp_kses( strip_shortcodes( $form[ $description_key ] ), 'user_description' );
} else {
+ // When save user_description value without HTML supported property, strip shortcodes, sanitize like a standard textarea.
$form[ $description_key ] = sanitize_textarea_field( strip_shortcodes( $form[ $description_key ] ) );
}
}
--- a/ultimate-member/includes/core/class-member-directory-meta.php
+++ b/ultimate-member/includes/core/class-member-directory-meta.php
@@ -671,7 +671,7 @@
$cover_photo_where = " AND umm_general.um_value LIKE '%s:11:"cover_photo";b:1;%'";
}
- if ( ! UM()->roles()->um_user_can( 'can_edit_everyone' ) ) {
+ if ( ! $this->can_edit_users() ) {
if ( ! $this->general_meta_joined ) {
$this->joins[] = "LEFT JOIN {$wpdb->prefix}um_metadata umm_general ON umm_general.user_id = u.ID";
--- a/ultimate-member/includes/core/class-member-directory.php
+++ b/ultimate-member/includes/core/class-member-directory.php
@@ -262,9 +262,8 @@
return $default;
}
-
/**
- * Getting member directory post ID via hash
+ * Getting member directory post ID via hash.
* Hash is unique attr, which we use visible at frontend
*
* @param string $hash
@@ -277,9 +276,12 @@
$directory_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT post_id
- FROM {$wpdb->postmeta}
- WHERE meta_key = '_um_directory_token' AND
- meta_value = %s
+ FROM {$wpdb->postmeta} pm
+ LEFT JOIN {$wpdb->posts} p ON pm.post_id = p.ID
+ WHERE p.post_type = 'um_directory' AND
+ p.post_status = 'publish' AND
+ pm.meta_key = '_um_directory_token' AND
+ pm.meta_value = %s
LIMIT 1",
$hash
)
@@ -290,7 +292,9 @@
$wpdb->prepare(
"SELECT ID
FROM {$wpdb->posts}
- WHERE SUBSTRING( MD5( ID ), 11, 5 ) = %s",
+ WHERE SUBSTRING( MD5( ID ), 11, 5 ) = %s AND
+ post_type='um_directory' AND
+ post_status = 'publish'",
$hash
)
);
@@ -1183,43 +1187,46 @@
}
/**
- * @param $filter
+ * Get the placeholders array for the slider-type filter.
+ * Format `array( 'singular-placeholder', 'plural-placeholder' )`.
*
- * @return mixed
+ * @param string $filter
+ * @param array $attrs
+ *
+ * @return array
*/
- function slider_range_placeholder( $filter, $attrs ) {
+ public function slider_range_placeholder( $filter, $attrs ) {
switch ( $filter ) {
- default: {
- $label = ! empty( $attrs['label'] ) ? $attrs['label'] : $filter;
- $label = ucwords( str_replace( array( 'um_', '_' ), array( '', ' ' ), $label ) );
+ default:
+ $label = ! empty( $attrs['label'] ) ? $attrs['label'] : $filter;
+ $label = ucwords( str_replace( array( 'um_', '_' ), array( '', ' ' ), $label ) );
$placeholders = apply_filters( 'um_member_directory_filter_slider_range_placeholder', false, $filter );
if ( ! $placeholders ) {
switch ( $attrs['type'] ) {
default:
$placeholders = array(
- "<strong>$label:</strong> {value}",
- "<strong>$label:</strong> {min_range} - {max_range}",
+ "$label: {value}",
+ "$label: {min_range} - {max_range}",
);
break;
case 'rating':
$placeholders = array(
- "<strong>$label:</strong> {value}" . __( ' stars', 'ultimate-member' ),
- "<strong>$label:</strong> {min_range} - {max_range}" . __( ' stars', 'ultimate-member' )
+ "$label: {value}" . __( ' stars', 'ultimate-member' ),
+ "$label: {min_range} - {max_range}" . __( ' stars', 'ultimate-member' ),
);
break;
}
}
-
break;
- }
- case 'birth_date': {
+
+ case 'birth_date':
$placeholders = array(
- __( '<strong>Age:</strong> {value} years old', 'ultimate-member' ),
- __( '<strong>Age:</strong> {min_range} - {max_range} years old', 'ultimate-member' )
+ __( 'Age: {value} years old', 'ultimate-member' ),
+ __( 'Age: {min_range} - {max_range} years old', 'ultimate-member' ),
);
break;
- }
+
}
return $placeholders;
@@ -1344,12 +1351,30 @@
do_action( 'um_member_directory_restrictions_handle_extend' );
}
+ /**
+ * Determine if the current user can edit other users
+ *
+ * @return bool Returns true if the current user can edit users, false otherwise
+ */
+ protected function can_edit_users() {
+ if ( is_user_logged_in() ) {
+ $rolename = UM()->roles()->get_priority_user_role( get_current_user_id() );
+ $role = get_role( $rolename );
+
+ // Don't specify only approved users when the current user has 'edit_users' capability.
+ if ( current_user_can( 'edit_users' ) || ( ! is_null( $role ) && $role->has_cap( 'edit_users' ) ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
/**
*
*/
- function hide_not_approved() {
- if ( UM()->roles()->um_user_can( 'can_edit_everyone' ) ) {
+ public function hide_not_approved() {
+ if ( $this->can_edit_users() ) {
return;
}
@@ -1360,7 +1385,6 @@
) ) );
}
-
/**
*
*/
@@ -1393,7 +1417,7 @@
return;
}
- if ( UM()->roles()->um_user_can( 'can_edit_everyone' ) ) {
+ if ( $this->can_edit_users() ) {
return;
}
@@ -2671,6 +2695,8 @@
* @return array
*/
public function build_user_card_data( $user_id, $directory_data ) {
+ $user_fields = UM()->builtin()->all_user_fields();
+
um_fetch_user( $user_id );
$dropdown_actions = $this->build_user_actions_list( $user_id );
@@ -2690,9 +2716,9 @@
$data_array = array(
'card_anchor' => esc_html( $this->get_user_hash( $user_id ) ),
- 'role' => is_user_logged_in() ? esc_html( um_user( 'role' ) ) : 'undefined', // make the role hidden for the nopriv requests.
- 'account_status' => is_user_logged_in() ? esc_html( UM()->common()->users()->get_status( $user_id ) ) : 'undefined', // make the status hidden for the nopriv requests.
- 'account_status_name' => is_user_logged_in() ? esc_html( UM()->common()->users()->get_status( $user_id, 'formatted' ) ) : __( 'Undefined', 'ultimate-member' ), // make the status hidden for the nopriv requests.
+ 'role' => 'undefined', // make the role hidden here.
+ 'account_status' => 'undefined', // make the status hidden here.
+ 'account_status_name' => esc_html__( 'Undefined', 'ultimate-member' ), // make the status hidden here.
'cover_photo' => wp_kses( um_user( 'cover_photo', $this->cover_size ), UM()->get_allowed_html( 'templates' ) ),
'display_name' => esc_html( um_user( 'display_name' ) ),
'profile_url' => esc_url( um_user_profile_url() ),
@@ -2705,45 +2731,53 @@
'hook_after_user_name' => wp_kses( preg_replace( '/^s+/im', '', $hook_after_user_name ), UM()->get_allowed_html( 'templates' ) ),
);
- if ( ! empty( $directory_data['show_tagline'] ) ) {
-
- if ( ! empty( $directory_data['tagline_fields'] ) ) {
- $directory_data['tagline_fields'] = maybe_unserialize( $directory_data['tagline_fields'] );
+ // Make the role and status visible for the user who can edit these users in the request.
+ if ( is_user_logged_in() && UM()->common()->users()->can_current_user_edit_user( $user_id ) ) {
+ $data_array['role'] = esc_html( um_user( 'role' ) );
+ $data_array['account_status'] = esc_html( UM()->common()->users()->get_status( $user_id ) );
+ $data_array['account_status_name'] = esc_html( UM()->common()->users()->get_status( $user_id, 'formatted' ) );
+ }
- if ( is_array( $directory_data['tagline_fields'] ) ) {
- foreach ( $directory_data['tagline_fields'] as $key ) {
- if ( ! $key ) {
- continue;
- }
+ $description_key = UM()->profile()->get_show_bio_key( UM()->fields()->global_args );
- if ( '_um_last_login' === $key ) {
- $show_last_login = get_user_meta( $user_id, 'um_show_last_login', true );
- if ( ! empty( $show_last_login ) && 'no' === $show_last_login[0] ) {
- continue;
- }
- }
+ if ( ! empty( $directory_data['show_tagline'] ) && ! empty( $directory_data['tagline_fields'] ) ) {
+ $directory_data['tagline_fields'] = maybe_unserialize( $directory_data['tagline_fields'] );
- $value = um_filtered_value( $key );
+ if ( is_array( $directory_data['tagline_fields'] ) ) {
+ foreach ( $directory_data['tagline_fields'] as $key ) {
+ if ( ! $key || ! array_key_exists( $key, $user_fields ) ) {
+ continue;
+ }
- if ( ! $value ) {
+ if ( '_um_last_login' === $key ) {
+ $show_last_login = get_user_meta( $user_id, 'um_show_last_login', true );
+ if ( ! empty( $show_last_login ) && 'no' === $show_last_login[0] ) {
continue;
}
+ }
+
+ $value = um_filtered_value( $key );
+ if ( ! $value ) {
+ continue;
+ }
- $data_array[ $key ] = wp_kses( $value, UM()->get_allowed_html( 'templates' ) );
+ if ( $description_key === $key ) {
+ $value = nl2br( wp_kses( make_clickable( $value ), 'user_description' ) );
}
+
+ $data_array[ $key ] = wp_kses( $value, UM()->get_allowed_html( 'templates' ) );
}
}
}
if ( ! empty( $directory_data['show_userinfo'] ) ) {
-
if ( ! empty( $directory_data['reveal_fields'] ) ) {
$directory_data['reveal_fields'] = maybe_unserialize( $directory_data['reveal_fields'] );
if ( is_array( $directory_data['reveal_fields'] ) ) {
foreach ( $directory_data['reveal_fields'] as $key ) {
- if ( ! $key ) {
+ if ( ! $key || ! array_key_exists( $key, $user_fields ) ) {
continue;
}
@@ -2759,16 +2793,23 @@
continue;
}
+ if ( $description_key === $key ) {
+ $value = nl2br( wp_kses( make_clickable( $value ), 'user_description' ) );
+ }
+
$label = UM()->fields()->get_label( $key );
- if ( $key == 'role_select' || $key == 'role_radio' ) {
- $label = strtr( $label, array(
- ' (Dropdown)' => '',
- ' (Radio)' => ''
- ) );
+ if ( 'role_select' === $key || 'role_radio' === $key ) {
+ $label = strtr(
+ $label,
+ array(
+ ' (Dropdown)' => '',
+ ' (Radio)' => '',
+ )
+ );
}
- $data_array[ "label_{$key}" ] = esc_html__( $label, 'ultimate-member' );
- $data_array[ $key ] = wp_kses( $value, UM()->get_allowed_html( 'templates' ) );
+ $data_array[ "label_{$key}" ] = esc_html( $label );
+ $data_array[ $key ] = wp_kses( $value, UM()->get_allowed_html( 'templates' ) );
}
}
}
--- a/ultimate-member/includes/core/class-profile.php
+++ b/ultimate-member/includes/core/class-profile.php
@@ -40,23 +40,20 @@
/**
* Profile constructor.
*/
- function __construct() {
+ public function __construct() {
add_action( 'template_redirect', array( &$this, 'active_tab' ), 10002 );
add_action( 'template_redirect', array( &$this, 'active_subnav' ), 10002 );
}
-
/**
* @param array $args
*
* @return string
*/
- function get_show_bio_key( $args ) {
- $key = apply_filters( 'um_profile_bio_key', 'description', $args );
- return $key;
+ public function get_show_bio_key( $args ) {
+ return apply_filters( 'um_profile_bio_key', 'description', $args );
}
-
/**
* Delete profile avatar AJAX handler
*/
@@ -448,22 +445,25 @@
if ( $description_key === $key ) {
$global_setting = UM()->options()->get( 'profile_show_html_bio' );
$bio_html = ! empty( $global_setting );
-
- if ( ! empty( $args['custom_fields'][ $description_key ] ) ) {
- if ( empty( $args['custom_fields'][ $description_key ]['html'] ) ) {
- $bio_html = false;
- }
+ if ( ! empty( $args['custom_fields'][ $description_key ] ) && empty( $args['custom_fields'][ $description_key ]['html'] ) ) {
+ $bio_html = false;
}
if ( $bio_html ) {
- $data['html'] = true;
- $value = um_filtered_value( $key, $data );
- $res = wp_kses_post( make_clickable( wpautop( $value ) ) );
+ $value = wp_kses( make_clickable( $value ), 'user_description' );
} else {
- $res = esc_html( $value );
+ $value = wp_kses(
+ make_clickable( $value ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ 'rel' => array(),
+ ),
+ )
+ );
}
- $value = nl2br( $res );
+ $value = nl2br( $value );
}
if ( ! $value && ( ! array_key_exists( 'type', $data ) || ! in_array( $data['type'], $fields_without_metakey, true ) ) ) {
--- a/ultimate-member/includes/core/class-query.php
+++ b/ultimate-member/includes/core/class-query.php
@@ -426,29 +426,38 @@
* @return array
*/
public function post_data( $post_id ) {
- $array['form_id'] = $post_id;
- $mode = $this->get_attr( 'mode', $post_id );
- $meta = get_post_custom( $post_id );
+ $mode = $this->get_attr( 'mode', $post_id );
+
+ $array = array(
+ 'form_id' => $post_id,
+ 'mode' => $mode,
+ );
+
+ $meta = get_post_custom( $post_id );
foreach ( $meta as $k => $v ) {
- if ( strstr( $k, '_um_' . $mode . '_' ) ) {
+ if ( '_um_mode' === $k ) {
+ continue;
+ }
+
+ if ( 0 === strpos( $k, '_um_' . $mode . '_' ) ) {
$k = str_replace( '_um_' . $mode . '_', '', $k );
$array[ $k ] = $v[0];
- } elseif ( '_um_mode' === $k ) {
- $k = str_replace( '_um_', '', $k );
- $array[ $k ] = $v[0];
- } elseif ( strstr( $k, '_um_' ) ) {
+ } elseif ( 0 === strpos( $k, '_um_' ) ) {
$k = str_replace( '_um_', '', $k );
$array[ $k ] = $v[0];
}
}
- foreach ( $array as $k => $v ) {
- if ( strstr( $k, 'login_' ) || strstr( $k, 'register_' ) || strstr( $k, 'profile_' ) ) {
- if ( 'directory' !== $mode ) {
+ // Remove another form mode meta if exists. E.g. login meta for the profile-mode form.
+ if ( 'directory' !== $mode ) {
+ // There isn't such meta in the member directory mode, so skip this.
+ foreach ( $array as $k => $v ) {
+ if ( 0 === strpos( $k, 'login_' ) || 0 === strpos( $k, 'register_' ) || 0 === strpos( $k, 'profile_' ) ) {
unset( $array[ $k ] );
}
}
}
+
return $array;
}
--- a/ultimate-member/includes/core/class-user.php
+++ b/ultimate-member/includes/core/class-user.php
@@ -110,6 +110,8 @@
'user_activation_key',
'level_',
$wpdb->get_blog_prefix() . 'user_level',
+ 'um_user_public_key', // UM REST API public. Legacy, not used for now!
+ 'um_user_secret_key', // UM REST API secret. Legacy, not used for now!
);
// a list of keys that should never be in wp_usermeta
@@ -1180,13 +1182,11 @@
return '';
}
-
/**
* @param $user_id
* @param $profile
*/
- function setup_cache( $user_id, $profile ) {
-
+ public function setup_cache( $user_id, $profile ) {
$disallow_cache = UM()->options()->get( 'um_profile_object_cache_stop' );
if ( $disallow_cache ) {
return;
@@ -1195,11 +1195,10 @@
update_option( "um_cache_userdata_{$user_id}", $profile, false );
}
-
/**
* @param $user_id
*/
- function remove_cache( $user_id ) {
+ public function remove_cache( $user_id ) {
delete_option( "um_cache_userdata_{$user_id}" );
}
@@ -1319,13 +1318,10 @@
// Setup cache
$this->setup_cache( $this->id, $this->profile );
-
}
}
-
}
-
/**
* Reset user data
*
--- a/ultimate-member/includes/core/rest/class-api-v1.php
+++ b/ultimate-member/includes/core/rest/class-api-v1.php
@@ -1,445 +0,0 @@
-<?php
-namespace umcorerest;
-
-if ( ! defined( 'ABSPATH' ) ) {
- exit;
-}
-
-if ( ! class_exists( 'umcorerestAPI_v1' ) ) {
-
-
- /**
- * Class REST_API
- * @package umcore
- */
- class API_v1 extends API {
-
- /**
- *
- */
- const VERSION = '1.0';
-
- /**
- * REST_API constructor.
- */
- public function __construct() {
- parent::__construct();
-
- add_filter( 'query_vars', array( $this, 'query_vars' ) );
- }
-
- /**
- * Registers query vars for API access
- *
- * @param $vars
- *
- * @return array
- */
- public function query_vars( $vars ) {
- $vars[] = 'key';
- $vars[] = 'token';
- $vars[] = 'format';
- $vars[] = 'query';
- $vars[] = 'type';
- $vars[] = 'data';
- $vars[] = 'fields';
- $vars[] = 'value';
- $vars[] = 'number';
- $vars[] = 'id';
- $vars[] = 'email';
- $vars[] = 'orderby';
- $vars[] = 'order';
- $vars[] = 'include';
- $vars[] = 'exclude';
-
- $this->vars = $vars;
-
- return $vars;
- }
-
- /**
- * Validate the API request
- */
- protected function validate_request() {
- global $wp_query;
-
- $this->override = false;
-
- // Make sure we have both user and api key
- if ( ! empty( $wp_query->query_vars['um-api'] ) ) {
-
- if ( empty( $wp_query->query_vars['token'] ) || empty( $wp_query->query_vars['key'] ) ) {
- $this->missing_auth();
- }
-
- // Retrieve the user by public API key and ensure they exist
- if ( ! ( $user = $this->get_user( $wp_query->query_vars['key'] ) ) ) {
- $this->invalid_key();
- } else {
- $token = urldecode( $wp_query->query_vars['token'] );
- $secret = get_user_meta( $user, 'um_user_secret_key', true );
- $public = urldecode( $wp_query->query_vars['key'] );
-
- if ( hash_equals( md5( $secret . $public ), $token ) ) {
- $this->is_valid_request = true;
- } else {
- $this->invalid_auth();
- }
- }
- }
- }
-
- /**
- * Retrieve the user ID based on the public key provided
- *
- * @param string $key
- *
- * @return bool|mixed|null|string
- */
- public function get_user( $key = '' ) {
- global $wpdb, $wp_query;
-
- if ( empty( $key ) ) {
- $key = urldecode( $wp_query->query_vars['key'] );
- }
-
- if ( empty( $key ) ) {
- return false;
- }
-
- $user = get_transient( md5( 'um_api_user_' . $key ) );
-
- if ( false === $user ) {
- $user = $wpdb->get_var(
- $wpdb->prepare(
- "SELECT user_id
- FROM $wpdb->usermeta
- WHERE meta_key = 'um_user_public_key' AND
- meta_value = %s
- LIMIT 1",
- $key
- )
- );
- set_transient( md5( 'um_api_user_' . $key ) , $user, DAY_IN_SECONDS );
- }
-
- if ( $user != null ) {
- $this->user_id = $user;
- return $user;
- }
-
- return false;
- }
-
- /**
- * Process Get users API Request.
- *
- * @param array $args
- *
- * @return array
- */
- public function get_users( $args ) {
- $response = array();
-
- $number = array_key_exists( 'number', $args ) && is_numeric( $args['number'] ) ? absint( $args['number'] ) : 10;
- $orderby = array_key_exists( 'orderby', $args ) ? sanitize_key( $args['orderby'] ) : 'user_registered';
- $order = array_key_exists( 'order', $args ) ? sanitize_key( $args['order'] ) : 'desc';
-
- $loop_a = array(
- 'number' => $number,
- 'orderby' => $orderby,
- 'order' => $order,
- );
-
- if ( array_key_exists( 'include', $args ) ) {
- $include = explode( ',', sanitize_text_field( $args['include'] ) );
- $loop_a['include'] = $include;
- }
-
- if ( array_key_exists( 'exclude', $args ) ) {
- $exclude = explode( ',', sanitize_text_field( $args['exclude'] ) );
- $loop_a['exclude'] = $exclude;
- }
-
- $loop = get_users( $loop_a );
-
- foreach ( $loop as $user ) {
- unset( $user->data->user_status, $user->data->user_activation_key, $user->data->user_pass );
-
- um_fetch_user( $user->ID );
-
- foreach ( $user as $key => $val ) {
- if ( 'data' !== $key ) {
- continue;
- }
-
- $val->roles = $user->roles;
- $val->first_name = um_user( 'first_name' );
- $val->last_name = um_user( 'last_name' );
- $val->account_status = UM()->common()->users()->get_status( $user->ID );
- $val->profile_pic_original = um_get_user_avatar_url( '', 'original' );
- $val->profile_pic_normal = um_get_user_avatar_url( '', 200 );
- $val->profile_pic_small = um_get_user_avatar_url( '', 40 );
- $val->cover_photo = $this->getsrc( um_user( 'cover_photo', 1000 ) );
-
- /**
- * Filters the output data for Rest API userdata call.
- *
- * @param {mixed} $val User data value.
- * @param {int} $user_id User ID.
- *
- * @return {mixed} User data value.
- *
- * @since 2.0
- * @hook um_rest_userdata
- *
- * @example <caption>Force change the output data for Rest API userdata call.</caption>
- * function my_custom_um_rest_userdata( $value, $user_id ) {
- * // your code here
- * return $response;
- * }
- * add_filter( 'um_rest_userdata', 'my_custom_um_rest_userdata', 10, 2 );
- */
- $response[ $user->ID ] = apply_filters( 'um_rest_userdata', $val, $user->ID );
- }
- }
-
- return $response;
- }
-
- /**
- * Update user API query
- *
- * @param $args
- *
- * @return array
- */
- public function update_user( $args ) {
- $response = array();
- $error = array();
-
- if ( empty( $args['id'] ) ) {
- $error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
- return $error;
- }
-
- if ( empty( $args['data'] ) ) {
- $error['error'] = __( 'You need to provide data to update', 'ultimate-member' );
- return $error;
- }
-
- if ( ! array_key_exists( 'value', $args ) ) {
- $error['error'] = __( 'You need to provide value to update', 'ultimate-member' );
- return $error;
- }
-
- $id = absint( $args['id'] );
- $data = sanitize_text_field( $args['data'] );
- $value = sanitize_text_field( $args['value'] );
-
- um_fetch_user( $id );
-
- switch ( $data ) {
- case 'status':
- // Force update of the user status without email notifications.
- UM()->common()->users()->set_status( $id, $value );
- $response['success'] = __( 'User status has been changed.', 'ultimate-member' );
- break;
- case 'role':
- $wp_user_object = new WP_User( $id );
- $old_roles = $wp_user_object->roles;
- $wp_user_object->set_role( $value );
-
- /** This action is documented in includes/core/class-user.php */
- do_action( 'um_after_member_role_upgrade', array( $value ), $old_roles, $id );
-
- $response['success'] = __( 'User role has been changed.', 'ultimate-member' );
- break;
- default:
- update_user_meta( $id, $data, $value );
- $response['success'] = __( 'User meta has been changed.', 'ultimate-member' );
- break;
- }
-
- return $response;
- }
-
- /**
- * Process delete user via API
- *
- * @param $args
- *
- * @return array
- */
- public function delete_user( $args ) {
- $response = array();
- $error = array();
-
- if ( empty( $args['id'] ) ) {
- $error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
- return $error;
- }
-
- $id = absint( $args['id'] );
-
- $user = get_userdata( $id );
- if ( ! $user ) {
- $error['error'] = __( 'Invalid user specified', 'ultimate-member' );
- return $error;
- }
-
- um_fetch_user( $id );
- UM()->user()->delete();
-
- $response['success'] = __( 'User has been successfully deleted.', 'ultimate-member' );
-
- return $response;
- }
-
- /**
- * Process Get user API Request
- *
- * @param $args
- *
- * @return array|mixed
- */
- public function get_auser( $args ) {
- $response = array();
- $error = array();
-
- if ( empty( $args['id'] ) ) {
- $error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
- return $error;
- }
-
- $id = absint( $args['id'] );
- $user = get_userdata( $id );
- if ( ! $user ) {
- $error['error'] = __( 'Invalid user specified', 'ultimate-member' );
- return $error;
- }
-
- unset( $user->data->user_status, $user->data->user_activation_key, $user->data->user_pass );
-
- um_fetch_user( $user->ID );
-
- if ( array_key_exists( 'fields', $args ) ) {
- $fields = explode( ',', sanitize_text_field( $args['fields'] ) );
- $response['ID'] = $user->ID;
- $response['username'] = $user->user_login;
- foreach ( $fields as $field ) {
-
- switch ( $field ) {
- default:
- $profile_data = um_profile( $field );
- $response[ $field ] = $profile_data ? $profile_data : '';
-
- /**
- * Filters the output data for Rest API user authentication call.
- *
- * @param {array} $response REST API response.
- * @param {string} $field Field Options.
- * @param {int} $user_id User ID.
- *
- * @return {array} REST API response.
- *
- * @since 2.0
- * @hook um_rest_get_auser
- *
- * @example <caption>Force change the output data for Rest API user authentication call.</caption>
- * function my_custom_um_rest_get_auser( $response, $field, $user_id ) {
- * // your code here
- * return $response;
- * }
- * add_filter( 'um_rest_get_auser', 'my_custom_um_rest_get_auser', 10, 3 );
- */
- $response = apply_filters( 'um_rest_get_auser', $response, $field, $user->ID );
- break;
- case 'cover_photo':
- $response['cover_photo'] = $this->getsrc( um_user( 'cover_photo', 1000 ) );
- break;
- case 'profile_pic':
- $response['profile_pic_original'] = um_get_user_avatar_url( '', 'original' );
- $response['profile_pic_normal'] = um_get_user_avatar_url( '', 200 );
- $response['profile_pic_small'] = um_get_user_avatar_url( '', 40 );
- break;
- case 'status':
- $response['status'] = UM()->common()->users()->get_status( $user->ID );
- break;
- case 'role':
- //get priority role here
- $response['role'] = um_user( 'role' );
- break;
- case 'email':
- case 'user_email':
- $response['email'] = um_user( 'user_email' );
- break;
- }
- }
- } else {
- foreach ( $user as $key => $val ) {
- if ( 'data' !== $key ) {
- continue;
- }
-
- $val->roles = $user->roles;
- $val->first_name = um_user( 'first_name' );
- $val->last_name = um_user( 'last_name' );
- $val->account_status = UM()->common()->users()->get_status( $user->ID );
- $val->profile_pic_original = um_get_user_avatar_url( '', 'original' );
- $val->profile_pic_normal = um_get_user_avatar_url( '', 200 );
- $val->profile_pic_small = um_get_user_avatar_url( '', 40 );
- $val->cover_photo = $this->getsrc( um_user( 'cover_photo', 1000 ) );
-
- /** This filter is documented in includes/core/rest/class-api-v1.php */
- $response = apply_filters( 'um_rest_userdata', $val, $user->ID );
- }
- }
-
- return $response;
- }
-
- /**
- * Get source
- *
- * @param $image
- *
- * @return string
- */
- public function getsrc( $image ) {
- if ( preg_match( '/<img.+?src(?: )*=(?: )*['"](.*?)['"]/si', $image, $arr_result ) ) {
- return $arr_result[1];
- }
- return '';
- }
-
- /**
- * Retrieve the output format
- */
- public function get_output_format() {
- global $wp_query;
-
- $format = isset( $wp_query->query_vars['format'] ) ? $wp_query->query_vars['format'] : 'json';
-
- /**
- * Filters the REST API output format. JSON by default.
- *
- * @param {string} $format REST API output format.
- *
- * @return {string} REST API output format.
- *
- * @since 1.3.x
- * @hook um_api_output_format
- *
- * @example <caption>Changing the REST API output format.</caption>
- * function my_custom_um_api_output_format( $format ) {
- * // your code here
- * $format = 'xml';
- * return $format;
- * }
- * add_filter( 'um_api_output_format', 'my_custom_um_api_output_format' );
- */
- return apply_filters( 'um_api_output_format', $format );
- }
- }
-}
--- a/ultimate-member/includes/core/rest/class-api-v2.php
+++ b/ultimate-member/includes/core/rest/class-api-v2.php
@@ -1,392 +0,0 @@
-<?php
-namespace umcorerest;
-
-if ( ! defined( 'ABSPATH' ) ) {
- exit;
-}
-
-if ( ! class_exists( 'umcorerestAPI_v2' ) ) {
-
- /**
- * Class API_v2
- * @package umcorerest
- */
- class API_v2 extends API {
-
- /**
- *
- */
- const VERSION = '2.0';
-
- /**
- * REST_API constructor.
- */
- public function __construct() {
- parent::__construct();
-
- add_filter( 'query_vars', array( $this, 'query_vars' ) );
- }
-
- /**
- * Registers query vars for API access
- *
- * @param $vars
- *
- * @return array
- */
- public function query_vars( $vars ) {
- $vars[] = 'um_key';
- $vars[] = 'um_token';
- $vars[] = 'um_format';
- $vars[] = 'um_query';
- $vars[] = 'um_type';
- $vars[] = 'um_data';
- $vars[] = 'um_fields';
- $vars[] = 'um_value';
- $vars[] = 'um_number';
- $vars[] = 'um_id';
- $vars[] = 'um_email';
- $vars[] = 'um_orderby';
- $vars[] = 'um_order';
- $vars[] = 'um_include';
- $vars[] = 'um_exclude';
-
- $this->vars = $vars;
-
- return $vars;
- }
-
- /**
- * Validate the API request
- */
- protected function validate_request() {
- global $wp_query;
-
- $this->override = false;
-
- // Make sure we have both user and api key
- if ( ! empty( $wp_query->query_vars['um-api'] ) ) {
-
- if ( empty( $wp_query->query_vars['um_token'] ) || empty( $wp_query->query_vars['um_key'] ) ) {
- $this->missing_auth();
- }
-
- // Retrieve the user by public API key and ensure they exist
- if ( ! ( $user = $this->get_user( $wp_query->query_vars['um_key'] ) ) ) {
- $this->invalid_key();
- } else {
- $token = urldecode( $wp_query->query_vars['um_token'] );
- $secret = get_user_meta( $user, 'um_user_secret_key', true );
- $public = urldecode( $wp_query->query_vars['um_key'] );
-
- if ( hash_equals( md5( $secret . $public ), $token ) ) {
- $this->is_valid_request = true;
- } else {
- $this->invalid_auth();
- }
- }
- }
- }
-
- /**
- * Retrieve the user ID based on the public key provided
- *
- * @param string $key
- *
- * @return bool|mixed|null|string
- */
- public function get_user( $key = '' ) {
- global $wpdb, $wp_query;
-
- if ( empty( $key ) ) {
- $key = urldecode( $wp_query->query_vars['um_key'] );
- }
-
- if ( empty( $key ) ) {
- return false;
- }
-
- $user = get_transient( md5( 'um_api_user_' . $key ) );
-
- if ( false === $user ) {
- $user = $wpdb->get_var(
- $wpdb->prepare(
- "SELECT user_id
- FROM $wpdb->usermeta
- WHERE meta_key = 'um_user_public_key' AND
- meta_value = %s
- LIMIT 1",
- $key
- )
- );
- set_transient( md5( 'um_api_user_' . $key ) , $user, DAY_IN_SECONDS );
- }
-
- if ( $user != NULL ) {
- $this->user_id = $user;
- return $user;
- }
-
- return false;
- }
-
- /**
- * Process Get users API Request
- *
- * @param array $args
- *
- * @return array
- */
- public function get_users( $args ) {
- $response = array();
-
- $number = array_key_exists( 'um_number', $args ) && is_numeric( $args['um_number'] ) ? absint( $args['um_number'] ) : 10;
- $orderby = array_key_exists( 'um_orderby', $args ) ? sanitize_key( $args['um_orderby'] ) : 'user_registered';
- $order = array_key_exists( 'um_order', $args ) ? sanitize_key( $args['um_order'] ) : 'desc';
-
- $loop_a = array(
- 'number' => $number,
- 'orderby' => $orderby,
- 'order' => $order,
- );
-
- if ( array_key_exists( 'um_include', $args ) ) {
- $include = explode( ',', sanitize_text_field( $args['um_include'] ) );
- $loop_a['include'] = $include;
- }
-
- if ( array_key_exists( 'um_exclude', $args ) ) {
- $exclude = explode( ',', sanitize_text_field( $args['um_exclude'] ) );
- $loop_a['exclude'] = $exclude;
- }
-
- $loop = get_users( $loop_a );
-
- foreach ( $loop as $user ) {
- unset( $user->data->user_status, $user->data->user_activation_key, $user->data->user_pass );
-
- um_fetch_user( $user->ID );
-
- foreach ( $user as $key => $val ) {
- if ( 'data' !== $key ) {
- continue;
- }
-
- $val->roles = $user->roles;
- $val->first_name = um_user( 'first_name' );
- $val->last_name = um_user( 'last_name' );
- $val->account_status = UM()->common()->users()->get_status( $user->ID );
- $val->profile_pic_original = um_get_user_avatar_url( '', 'original' );
- $val->profile_pic_normal = um_get_user_avatar_url( '', 200 );
- $val->profile_pic_small = um_get_user_avatar_url( '', 40 );
- $val->cover_photo = $this->getsrc( um_user( 'cover_photo', 1000 ) );
-
- /** This filter is documented in includes/core/rest/class-api-v1.php */
- $response[ $user->ID ] = apply_filters( 'um_rest_userdata', $val, $user->ID );
- }
- }
-
- return $response;
- }
-
- /**
- * Update user API query
- *
- * @param $args
- *
- * @return array
- */
- public function update_user( $args ) {
- $response = array();
- $error = array();
-
- if ( empty( $args['um_id'] ) ) {
- $error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
- return $error;
- }
-
- if ( empty( $args['um_data'] ) ) {
- $error['error'] = __( 'You need to provide data to update', 'ultimate-member' );
- return $error;
- }
-
- if ( ! array_key_exists( 'um_value', $args ) ) {
- $error['error'] = __( 'You need to provide value to update', 'ultimate-member' );
- return $error;
- }
-
- $id = absint( $args['um_id'] );
- $data = sanitize_text_field( $args['um_data'] );
- $value = sanitize_text_field( $args['um_value'] );
-
- um_fetch_user( $id );
-
- switch ( $data ) {
- case 'status':
- // Force update of the user status without email notifications.
- UM()->common()->users()->set_status( $id, $value );
- $response['success'] = __( 'User status has been changed.', 'ultimate-member' );
- break;
- case 'role':
- $wp_user_object = new WP_User( $id );
- $old_roles = $wp_user_object->roles;
- $wp_user_object->set_role( $value );
-
- /** This action is documented in includes/core/class-user.php */
- do_action( 'um_after_member_role_upgrade', array( $value ), $old_roles, $id );
-
- $response['success'] = __( 'User role has been changed.', 'ultimate-member' );
- break;
- default:
- update_user_meta( $id, $data, $value );
- $response['success'] = __( 'User meta has been changed.', 'ultimate-member' );
- break;
- }
-
- return $response;
- }
-
- /**
- * Process delete user via API.
- *
- * @param array $args
- *
- * @return array
- */
- public function delete_user( $args ) {
- $response = array();
- $error = array();
-
- if ( empty( $args['um_id'] ) ) {
- $error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
- return $error;
- }
-
- $id = absint( $args['um_id'] );
-
- $user = get_userdata( $id );
- if ( ! $user ) {
- $error['error'] = __( 'Invalid user specified', 'ultimate-member' );
- return $error;
- }
-
- um_fetch_user( $id );
- UM()->user()->delete();
-
- $response['success'] = __( 'User has been successfully deleted.', 'ultimate-member' );
-
- return $response;
- }
-
- /**
- * Process Get user API Request
- *
- * @param $args
- *
- * @return array
- */
- public function get_auser( $args ) {
- $response = array();
- $error = array();
-
- if ( empty( $args['um_id'] ) ) {
- $error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
- return $error;
- }
-
- $id = absint( $args['um_id'] );
- $user = get_userdata( $id );
- if ( ! $user ) {
- $error['error'] = __( 'Invalid user specified', 'ultimate-member' );
- return $error;
- }
-
- unset( $user->data->user_status, $user->data->user_activation_key, $user->data->user_pass );
-
- um_fetch_user( $user->ID );
-
- if ( array_key_exists( 'um_fields', $args ) ) {
- $fields = explode( ',', sanitize_text_field( $args['um_fields'] ) );
- $response['ID'] = $user->ID;
- $response['username'] = $user->user_login;
- foreach ( $fields as $field ) {
-
- switch ( $field ) {
- default:
- $profile_data = um_profile( $field );
- $response[ $field ] = $profile_data ? $profile_data : '';
-
- /** This filter is documented in includes/core/rest/class-api-v1.php */
- $response = apply_filters( 'um_rest_get_auser', $response, $field, $user->ID );
- break;
- case 'cover_photo':
- $response['cover_photo'] = $this->getsrc( um_user( 'cover_photo', 1000 ) );
- break;
- case 'profile_pic':
- $response['profile_pic_original'] = um_get_user_avatar_url( '', 'original' );
- $response['profile_pic_normal'] = um_get_user_avatar_url( '', 200 );
- $response['profile_pic_small'] = um_get_user_avatar_url( '', 40 );
- break;
- case 'status':
- $response['status'] = UM()->common()->users()->get_status( $user->ID );
- break;
- case 'role':
- //get priority role here
- $response['role'] = um_user( 'role' );
- break;
- case 'email':
- case 'user_email':
- $response['email'] = um_user( 'user_email' );
- break;
- }
- }
- } else {
- foreach ( $user as $key => $val ) {
- if ( 'data' !== $key ) {
- continue;
- }
-
- $val->roles = $user->roles;
- $val->first_name = um_user( 'first_name' );
- $val->last_name = um_user( 'last_name' );
- $val->account_status = UM()->common()->users()->get_status( $user->ID );
- $val->profile_pic_original = um_get_user_avatar_url( '', 'original' );
- $val->profile_pic_normal = um_get_user_avatar_url( '', 200 );
- $val->profile_pic_small = um_get_user_avatar_url( '', 40 );
- $val->cover_photo = $this->getsrc( um_user( 'cover_photo', 1000 ) );
-
- /** This filter is documented in includes/core/rest/class-api-v1.php */
- $response = apply_filters( 'um_rest_userdata', $val, $user->ID );
- }
- }
-
- return $response;
- }
-
- /**
- * Get source
- *
- * @param $image
- *
- * @return string
- */
- public function getsrc( $image ) {
- if ( preg_match( '/<img.+?src(?: )*=(?: )*['"](.*?)['"]/si', $image, $arr_result ) ) {
- return $arr_result[1];
- }
- return '';
- }
-
- /**
- * Retrieve the output format
- */
- public function get_output_format() {
- global $wp_query;
-
- $format = isset( $wp_query->query_vars['um_format'] ) ? $wp_query->query_vars['um_format'] : 'json';
-
- /** This filter is documented in includes/core/rest/class-api-v1.php */
- return apply_filters( 'um_api_output_format', $format );
- }
- }
-}
--- a/ultimate-member/includes/core/rest/class-api.php
+++ b/ultimate-member/includes/core/rest/class-api.php
@@ -1,615 +0,0 @@
-<?php
-namespace umcorerest;
-
-
-if ( ! defined( 'ABSPATH' ) ) exit;
-
-
-if ( ! class_exists( 'umcorerestAPI' ) ) {
-
-
- /**
- * Class API
- *
- * @package umcorerest
- */
- class API {
-
-
- /**
- * @var bool|int|null
- */
- protected $pretty_print = false;
-
-
- /**
- * @var bool|mixed|void
- */
- public $log_requests = true;
-
-
- /**
- * @var bool
- */
- protected $is_valid_request = false;
-
-
- /**
- * @var int
- */
- protected $user_id = 0;
-
-
- /**
- * @var
- */
- protected $stats;
-
-
- /**
- * @var array
- */
- protected $data = array();
-
-
- /**
- * @var bool
- */
- protected $override = true;
-
-
- /**
- * @var array
- */
- protected $vars = array();
-
-
- /**
- * REST_API constructor.
- */
- public function __construct() {
-
- add_action( 'init', array( $this, 'add_endpoint' ) );
- add_action( 'template_redirect', array( $this, 'process_query' ), -1 );
-
- // Determine if JSON_PRETTY_PRINT is available
- $this->pretty_print = defined( 'JSON_PRETTY_PRINT' ) ? JSON_PRETTY_PRINT : null;
-
- /**
- * UM hook
- *
- * @type filter
- * @title um_api_log_requests
- * @description Allow API request logging to be turned off
- * @input_vars
- * [{"var":"$allow_log","type":"bool","desc":"Enable api logs"}]
- * @change_log
- * ["Since: 2.0"]
- * @usage
- * <?php add_filter( 'um_api_log_requests', 'function_name', 10, 1 ); ?>
- * @example
- * <?php
- * add_filter( 'um_api_log_requests', 'my_api_log_requests', 10, 1 );
- * function my_api_log_requests( $allow_log ) {
- * // your code here
- * return $allow_log;
- * }
- * ?>
- */
- $this->log_requests = apply_filters( 'um_api_log_requests', $this->log_requests );
- }
-
-
- /**
- * Registers a new rewrite endpoint for accessing the API
- *
- * @param $rewrite_rules
- */
- public function add_endpoint( $rewrite_rules ) {
- add_rewrite_endpoint( 'um-api', EP_ALL );
- }
-
-
- /**
- * Listens for the API and then processes the API requests
- */
- public function process_query() {
- global $wp_query;
-
- // Check for um-api var. Get out if not present
- if ( ! isset( $wp_query->query_vars['um-api'] ) ) {
- return;
- }
-
- // Check for a valid user and set errors if necessary
- $this->validate_request();
-
- // Only proceed if no errors have been noted
- if ( ! $this->is_valid_request ) {
- return;
- }
-
- if ( ! defined( 'UM_DOING_API' ) ) {
- define( 'UM_DOING_API', true );
- }
-
- // Determine the kind of query
- $args = array();
- $query_mode = $this->get_query_mode();
- foreach ( $this->vars as $k ) {
- $args[ $k ] = isset( $wp_query->query_vars[ $k ] ) ? $wp_query->query_vars[ $k ] : null;
- }
-
- $data = array();
-
- switch ( $query_mode ) {
- case 'get.stats':
- $data = $this->get_stats( $args );
- break;
-
- case 'get.users':
- $data = $this->get_users( $args );
- break;
-
- case 'get.user':
- $data = $this->get_auser( $args );
- break;
-
- case 'update.user':
- $data = $this->update_user( $args );
- break;
-
- case 'delete.user':
- $data = $this->delete_user( $args );
- break;
-
- default:
- /**
- * UM hook
- *
- * @type filter
- * @title um_rest_query_mode
- * @description Change query attributes
- * @input_vars
- * [{"var":"$data","type":"array","desc":"Query Data"},
- * {"var":"$query_mode","type":"string","desc":"Query Mode"},
- * {"var":"$args","type":"array","desc":"Query Arguments"}]
- * @change_log
- * ["Since: 2.0"]
- * @usage
- * <?php add_filter( 'um_rest_query_mode', 'function_name', 10, 3 ); ?>
- * @example
- * <?php
- * add_filter( 'um_rest_query_mode', 'my_rest_query_mode', 10, 3 );
- * function um_rest_query_mode( $data, $query_mode, $args ) {
- * // your code here
- * return $data;
- * }
- * ?>
- */
- $data = apply_filters( 'um_rest_query_mode', $data, $query_mode, $args );
- break;
- }
-
- /**
- * UM hook
- *
- * @type filter
- * @title um_api_output_data
- * @description Change output data for Rest API call
- * @input_vars
- * [{"var":"$data","type":"array","desc":"Output Data"},
- * {"var":"$query_mode","type":"string","desc":"Query Mode"},
- * {"var":"$api_class","type":"REST_API","desc":"REST_API instance"}]
- * @change_log
- * ["Since: 2.0"]
- * @usage
- * <?php add_filter( 'um_api_output_data', 'function_name', 10, 3 ); ?>
- * @example
- * <?php
- * add_filter( 'um_api_output_data', 'my_api_output_data', 10, 3 );
- * function my_api_output_data( $data, $query_mode, $api_class ) {
- * // your code here
- * return $data;
- * }
- * ?>
- */
- $this->data = apply_filters( 'um_api_output_data', $data, $query_mode, $this );
-
- // Log this API request, if enabled. We log it here because we have access to errors.
- $this->log_request( $this->data );
-
- // Send out data to the output function
- $this->output();
- }
-
-
- /**
- * Validate the API request
- */
- protected function validate_request() {
-
- }
-
-
- /**
- * Retrieve the user ID based on the public key provided
- *
- * @param string $key
- *
- * @return bool
- */
- public function get_user( $key = '' ) {
- return false;
- }
-
-
- /**
- * Displays a missing authentication error if all the parameters aren't
- * provided
- */
- protected function missing_auth() {
- $error = array();
- $error['error'] = __( 'You must specify both a token and API key!', 'ultimate-member' );
-
- $this->data = $error;
- $this->output( 401 );
- }
-
-
- /**
- * Displays an authentication failed error if the user failed to provide valid credentials
- */
- protected function invalid_auth() {
- $error = array();
- $error['error'] = __( 'Your request could not be authenticated', 'ultimate-member' );
-
- $this->data = $error;
- $this->output( 401 );
- }
-
-
- /**
- * Displays an invalid API key error if the API key provided couldn't be validated
- */
- protected function invalid_key() {
- $error = array();
- $error['error'] = __( 'Invalid API key', 'ultimate-member' );
-
- $this->data = $error;
- $this->output( 401 );
- }
-
-
- /**
- * Get some stats
- *
- * @param $args
- *
- * @return array
- */
- public function get_stats( $args ) {
- global $wpdb;
-
- $response = array();
-
- $count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}users" ) );
- $response['stats']['total_users'] = $count;
-
- $pending = UM()->query()->get_pending_users_count();
- $response['stats']['pending_users'] = absint( $pending );
-
- /**
- * UM hook
- *
- * @type filter
- * @title um_rest_api_get_stats
- * @description Change output data for Rest API get stats call
- * @input_vars
- * [{"var":"$response","type":"array","desc":"Output Data"}]
- * @change_log
- * ["Since: 2.0"]
- * @usage
- * <?php add_filter( 'um_rest_api_get_stats', 'function_name', 10, 1 ); ?>
- * @example
- * <?php
- * add_filter( 'um_rest_api_get_stats', 'my_rest_api_get_stats', 10, 1 );
- * function my_rest_api_get_stats( $response ) {
- * // your code here
- * return $response;
- * }
- * ?>
- */
- $response = apply_filters( 'um_rest_api_get_stats', $response );
- return $response;
- }
-
-
- /**
- * Process Get users API Request
- *
- * @param $args
- *
- * @return array
- */
- public function get_users( $args ) {
- return array();
- }
-
-
- /**
- * Update user API query
- *
- * @param $args
- *
- * @return array
- */
- public function update_user( $args ) {
- return array();
- }
-
-
- /**
- * Process delete user via API
- *
- * @param $args
- *
- * @return array
- */
- public function delete_user( $args ) {
- return array();
- }
-
-
- /**
- * Process Get user API Request
- *
- * @param $args
- *
- * @return array
- */
- public function get_auser( $args ) {
- return array();
- }
-
-
- /**
- * Get source
- *
- * @param $image
- *
- * @return string
- */
- protected function getsrc( $image ) {
- if ( preg_match( '/<img.+?src(?: )*=(?: )*['"](.*?)['"]/si', $image, $arrResult ) ) {
- return $arrResult[1];
- }
- return '';
- }
-
-
- /**
- * Determines the kind of query requested and also ensure it is a valid query
- *
- * @return null
- */
- public function get_query_mode() {
- global $wp_query;
-
- /**
- * UM hook
- *
- * @type filter
- * @title um_api_valid_query_modes
- * @description Whitelist UM query options
- * @input_vars
- * [{"var":"$list","type":"array","desc":"Whitelist"}]
- * @change_log
- * ["Since: 2.0"]
- * @usage
- * <?php add_filter( 'um_api_valid_query_modes', 'function_name', 10, 1 ); ?>
- * @example
- * <?php
- * add_filter( 'um_api_valid_query_modes', 'my_api_valid_query_modes', 10, 1 );
- * function my_api_valid_query_modes( $list ) {
- * // your code here
- * return $list;
- * }
- * ?>
- */
- $accepted = apply_filters( 'um_api_valid_query_modes', array(
- 'get.users',
- 'get.user',
- 'update.user',
- 'delete.user',
- 'get.following',
- 'get.followers',
- 'get.stats',
- ) );
-
- $query = isset( $wp_query->query_vars['um-api'] ) ? $wp_query->query_vars['um-api'] : null;
- $error = array();
- // Make sure our query is valid
- if (
Frequently Asked Questions
What is CVE-2026-7761?
Vulnerability OverviewCVE-2026-7761 is a high-severity vulnerability in the Ultimate Member plugin for WordPress (versions up to and including 2.11.4) that allows an authenticated attacker with Contributor-level access to leak active password reset URLs for all users, including administrators, leading to account takeover.
How does the account takeover work?
Attack MechanismThe attack chains three logic bugs: an MD5 hash fallback in get_directory_by_hash() that allows any post to be used as a member directory, a strstr() parsing flaw in post_data() that bypasses protected meta key restrictions, and missing field name validation in build_user_card_data() that allows arbitrary field names including ‘password_reset_link’ to be passed to um_filtered_value(). This leaks live password reset URLs.
Who is affected by this vulnerability?
Affected UsersAny WordPress site running Ultimate Member version 2.11.4 or earlier is affected. The attack requires an authenticated user with at least Contributor-level access, so sites with untrusted contributors or compromised accounts are at higher risk.
How can I check if my site is vulnerable?
DetectionCheck the Ultimate Member plugin version in the WordPress admin dashboard under Plugins. If the version is 2.11.4 or lower, the site is vulnerable. You can also verify by looking for the presence of the ‘rest_api_version’ setting in the database or plugin options.
What is the CVSS score and severity?
Risk LevelThe vulnerability has a CVSS score of 8.8, which is classified as High severity. This indicates a significant risk of complete account compromise with relatively low complexity and no user interaction required beyond the initial authentication.
How do I fix this vulnerability?
Patch and MitigationUpdate the Ultimate Member plugin to version 2.12.0 or later, which contains the fix. As an immediate mitigation, you can disable the Ultimate Member plugin or restrict Contributor-level access until the update is applied.
What does the proof of concept demonstrate?
PoC ExplanationThe proof of concept shows how an attacker with Contributor access can create a malicious post via XMLRPC with crafted meta fields, use the MD5 fallback to point the member directory AJAX handler to their post, inject ‘password_reset_link’ into tagline_fields, and then fetch the member directory to obtain live password reset URLs for all users.
Why is the password reset link disclosure dangerous?
Impact of LeakA live password reset URL allows anyone with the link to reset the user’s password without knowing the current password. If an administrator’s reset link is leaked, the attacker can take over the entire site.
What are the three logic bugs involved?
Technical DetailsBug 1: get_directory_by_hash() uses an MD5 hash fallback that allows any post to be used as a member directory. Bug 2: post_data() uses strstr() to check for ‘_um_’ anywhere in the meta key, bypassing WordPress’s protected meta key restrictions. Bug 3: build_user_card_data() lacks field name validation, allowing arbitrary field names like ‘password_reset_link’ to be passed to um_filtered_value().
Can this vulnerability be exploited remotely?
ExploitabilityYes, the attack can be carried out remotely via XMLRPC or the WordPress REST API. The attacker only needs authenticated Contributor-level access, which can be obtained through compromised accounts or registration.
Does the patch remove any features?
Patch ChangesThe patch removes the ‘rest_api_version’ setting and related code, which was unused and potentially insecure. No core features are removed; the fix only addresses the security flaws.
What should I do if I suspect my site has been compromised?
Incident ResponseImmediately update the plugin to version 2.12.0, reset all user passwords, and review user accounts for unauthorized changes. Check logs for suspicious activity related to member directory requests or XMLRPC calls.
How Atomic Edge Works
Simple Setup. Powerful Security.
Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.
Trusted by Developers & Organizations






