Implement directory privacy settings and add rate limiting

Adds configurable privacy options for member directories, allowing restrictions on visibility based on roles or login status. Introduces rate limiting for unauthenticated AJAX requests to prevent brute-force attacks or abuse.
This commit is contained in:
Mykyta Synelnikov
2025-12-11 17:36:42 +02:00
parent b75a2145dd
commit fc2c5456e4
18 changed files with 285 additions and 78 deletions
+6
View File
@@ -284,6 +284,12 @@ if ( ! class_exists( 'um\admin\Admin' ) ) {
'_um_mode' => array(
'sanitize' => 'key',
),
'_um_privacy' => array(
'sanitize' => 'absint',
),
'_um_privacy_roles' => array(
'sanitize' => array( $this, 'sanitize_existed_role' ),
),
'_um_view_types' => array(
'sanitize' => array( $this, 'sanitize_md_view_types' ),
),
+7
View File
@@ -209,6 +209,13 @@ if ( ! class_exists( 'um\admin\Secure' ) ) {
$scanner_content .= '</span>';
$secure_fields = array(
array(
'id' => 'ajax_nopriv_rate_limit',
'type' => 'checkbox',
'label' => __( 'Rate Limit', 'ultimate-member' ),
'checkbox_label' => __( 'Enable Rate Limiting', 'ultimate-member' ),
'description' => __( 'This prevents brute-force enumeration attempts in guest AJAX requests.', 'ultimate-member' ),
),
array(
'id' => 'banned_capabilities',
'type' => 'multi_checkbox',
+2 -1
View File
@@ -728,7 +728,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Metabox' ) ) {
* @param $object
* @param $box
*/
function load_metabox_directory( $object, $box ) {
public function load_metabox_directory( $object, $box ) {
$box['id'] = str_replace( 'um-admin-form-', '', $box['id'] );
preg_match('#\{.*?\}#s', $box['id'], $matches );
@@ -857,6 +857,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Metabox' ) ) {
public function add_metabox_directory() {
add_meta_box( 'submitdiv', __( 'Publish', 'ultimate-member' ), array( &$this, 'custom_submitdiv' ), 'um_directory', 'side', 'high' );
add_meta_box( 'um-admin-form-general', __( 'General Options', 'ultimate-member' ), array( &$this, 'load_metabox_directory' ), 'um_directory', 'normal', 'default' );
add_meta_box( 'um-admin-form-privacy', __( 'Privacy Options', 'ultimate-member' ), array( &$this, 'load_metabox_directory' ), 'um_directory', 'normal', 'default' );
add_meta_box( 'um-admin-form-sorting', __( 'Sorting', 'ultimate-member' ), array( &$this, 'load_metabox_directory' ), 'um_directory', 'normal', 'default' );
add_meta_box( 'um-admin-form-profile', __( 'Profile Card', 'ultimate-member' ), array( &$this, 'load_metabox_directory' ), 'um_directory', 'normal', 'default' );
add_meta_box( 'um-admin-form-search', __( 'Search Options', 'ultimate-member' ), array( &$this, 'load_metabox_directory' ), 'um_directory', 'normal', 'default' );
@@ -1032,6 +1032,9 @@ if ( ! class_exists( 'um\admin\core\Admin_Settings' ) ) {
'display_login_form_notice' => array(
'sanitize' => 'bool',
),
'ajax_nopriv_rate_limit' => array(
'sanitize' => 'bool',
),
'banned_capabilities' => array(
'sanitize' => array( UM()->admin(), 'sanitize_wp_capabilities_assoc' ),
),
@@ -0,0 +1,48 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$fields = array(
array(
'id' => '_um_privacy',
'type' => 'select',
'label' => __( 'Who can see this member directory', 'ultimate-member' ),
'description' => __( 'Select which users can view this member directory. Minimum recommended visibility is `Members only`. Please pay attention that visible for guests (anyone) member directory can have sensitive information.', 'ultimate-member' ),
'options' => array(
0 => __( 'Anyone', 'ultimate-member' ),
1 => __( 'Guests only', 'ultimate-member' ),
2 => __( 'Members only', 'ultimate-member' ),
3 => __( 'Only specific roles', 'ultimate-member' ),
),
'value' => UM()->query()->get_meta_value( '_um_privacy', null, 2 ),
),
array(
'id' => '_um_privacy_roles',
'type' => 'select',
'multi' => true,
'label' => __( 'Allowed roles', 'ultimate-member' ),
'description' => __( 'Select the the user roles allowed to view this member directory.', 'ultimate-member' ),
'options' => UM()->roles()->get_roles(),
'placeholder' => __( 'Choose user roles...', 'ultimate-member' ),
'conditional' => array( '_um_privacy', '=', '3' ),
'value' => UM()->query()->get_meta_value( '_um_privacy_roles', null, 'na' ),
),
);
$fields = apply_filters( 'um_admin_extend_directory_options_privacy', $fields );
?>
<div class="um-admin-metabox">
<?php
UM()->admin_forms(
array(
'class' => 'um-member-directory-privacy um-half-column',
'prefix_id' => 'um_metadata',
'fields' => $fields,
)
)->render_form();
?>
<div class="clear"></div>
</div>
+3
View File
@@ -141,6 +141,8 @@ if ( ! class_exists( 'um\Config' ) ) {
'_um_core' => 'members',
'_um_template' => 'members',
'_um_mode' => 'directory',
'_um_privacy' => 2,
'_um_privacy_roles' => array(),
'_um_view_types' => array( 'grid' ),
'_um_default_view' => 'grid',
'_um_roles' => array(),
@@ -729,6 +731,7 @@ if ( ! class_exists( 'um\Config' ) ) {
'lock_register_forms' => false,
'display_login_form_notice' => false,
'secure_ban_admins_accounts' => false,
'ajax_nopriv_rate_limit' => true,
'banned_capabilities' => array( 'manage_options', 'promote_users', 'level_10' ),
'secure_notify_admins_banned_accounts' => false,
'secure_notify_admins_banned_accounts__interval' => 'instant',
+31
View File
@@ -47,6 +47,37 @@ if ( ! class_exists( 'UM_Functions' ) ) {
}
}
/**
* Check if the user rate limit has been reached based on the provided context
*
* @param string $context The context for which the rate limit check is being performed
* @param int|null $requests_num Number requests per minute. Default 10.
*
* @return bool True if the rate limit has been reached, false otherwise
*/
public function is_rate_limited( $context, $requests_num = 10 ) {
if ( is_user_logged_in() ) {
return false;
}
if ( ! UM()->options()->get( 'ajax_nopriv_rate_limit' ) ) {
return false;
}
$transient_name = 'um_ajax_nopriv_rate_limit_' . md5( $context . '_' . $_SERVER['REMOTE_ADDR'] );
$rate_limit = get_transient( $transient_name );
$requests_num = apply_filters( 'um_ajax_rate_limit_requests_num', $requests_num, $context ); // 10 requests per minute per IP
if ( false === $rate_limit ) {
set_transient( $transient_name, 1, MINUTE_IN_SECONDS );
} elseif ( $rate_limit < $requests_num ) {
set_transient( $transient_name, $rate_limit + 1, MINUTE_IN_SECONDS );
} else {
return true;
}
return false;
}
/**
* What type of request is this?
-1
View File
@@ -1375,7 +1375,6 @@ if ( ! class_exists( 'UM' ) ) {
require_once 'core/um-actions-form.php';
require_once 'core/um-actions-access.php';
require_once 'core/um-actions-wpadmin.php';
require_once 'core/um-actions-ajax.php';
require_once 'core/um-actions-login.php';
require_once 'core/um-actions-register.php';
require_once 'core/um-actions-profile.php';
+17
View File
@@ -266,6 +266,10 @@ if ( ! class_exists( 'um\core\Files' ) ) {
public function ajax_remove_file() {
UM()->check_ajax_nonce();
if ( UM()->is_rate_limited( 'remove_file' ) ) {
wp_send_json_error( __( 'Too many requests', 'ultimate-member' ) );
}
if ( empty( $_POST['src'] ) ) {
wp_send_json_error( __( 'Wrong path', 'ultimate-member' ) );
}
@@ -314,6 +318,11 @@ if ( ! class_exists( 'um\core\Files' ) ) {
*/
public function ajax_resize_image() {
UM()->check_ajax_nonce();
if ( UM()->is_rate_limited( 'resize_image' ) ) {
wp_send_json_error( __( 'Too many requests', 'ultimate-member' ) );
}
// phpcs:disable WordPress.Security.NonceVerification -- verified by the `check_ajax_nonce()`
if ( ! isset( $_REQUEST['src'], $_REQUEST['coord'], $_REQUEST['key'] ) ) {
wp_send_json_error( esc_js( __( 'Invalid parameters', 'ultimate-member' ) ) );
@@ -447,6 +456,10 @@ if ( ! class_exists( 'um\core\Files' ) ) {
* @throws Exception
*/
public function ajax_image_upload() {
if ( UM()->is_rate_limited( 'upload_image' ) ) {
wp_send_json_error( __( 'Too many requests', 'ultimate-member' ) );
}
$ret['error'] = null;
$ret = array();
@@ -645,6 +658,10 @@ if ( ! class_exists( 'um\core\Files' ) ) {
* @throws Exception
*/
public function ajax_file_upload() {
if ( UM()->is_rate_limited( 'upload_file' ) ) {
wp_send_json_error( __( 'Too many requests', 'ultimate-member' ) );
}
$ret['error'] = null;
$ret = array();
+4
View File
@@ -116,6 +116,10 @@ if ( ! class_exists( 'um\core\Form' ) ) {
public function ajax_select_options() {
UM()->check_ajax_nonce();
if ( UM()->is_rate_limited( 'select_options' ) ) {
wp_send_json_error( __( 'Too many requests', 'ultimate-member' ) );
}
// phpcs:disable WordPress.Security.NonceVerification
$arr_options = array();
@@ -595,6 +595,10 @@ if ( ! class_exists( 'um\core\Member_Directory_Meta' ) ) {
public function ajax_get_members() {
UM()->check_ajax_nonce();
if ( UM()->is_rate_limited( 'member_directory' ) ) {
wp_send_json_error( __( 'Too many requests', 'ultimate-member' ) );
}
global $wpdb;
$blog_id = get_current_blog_id();
@@ -609,6 +613,10 @@ if ( ! class_exists( 'um\core\Member_Directory_Meta' ) ) {
}
// phpcs:enable WordPress.Security.NonceVerification -- verified via `UM()->check_ajax_nonce();`.
if ( ! $this->can_view_directory( $directory_id ) ) {
wp_send_json_error( __( 'You cannot see this member directory', 'ultimate-member' ) );
}
$directory_data = UM()->query()->post_data( $directory_id );
// Predefined result for user without capabilities to see other members.
+131 -15
View File
@@ -2,6 +2,7 @@
namespace um\core;
use Exception;
use WP_Post;
use WP_User_Query;
if ( ! defined( 'ABSPATH' ) ) {
@@ -123,11 +124,79 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) {
public function __construct() {
add_filter( 'init', array( &$this, 'init_variables' ) );
add_action( 'wp_insert_post', array( &$this, 'set_token' ), 10, 3 );
add_action( 'template_redirect', array( &$this, 'access_members' ), 555 );
}
/**
* Set member directory token as soon as it's created.
*
* @param int $post_ID
* @param WP_Post $post
* @param bool $update
*
* @return void
*/
public function set_token( $post_ID, $post, $update ) {
if ( 'um_directory' === $post->post_type && ! $update ) {
$this->set_directory_hash( $post_ID );
}
}
/**
* Check if the user can view the member directory.
*
* @param int $directory_id
* @param int|null $user_id
*
* @return bool
*/
public function can_view_directory( $directory_id, $user_id = null ) {
if ( is_null( $user_id ) && is_user_logged_in() ) {
$user_id = get_current_user_id();
}
$can_view = false;
$privacy = get_post_meta( $directory_id, '_um_privacy', true );
if ( '' === $privacy ) {
$can_view = true;
} else {
$privacy = absint( $privacy );
switch ( $privacy ) {
case 0:
$can_view = true;
break;
case 1:
if ( ! is_user_logged_in() ) {
$can_view = true;
}
break;
case 2:
if ( is_user_logged_in() ) {
$can_view = true;
}
break;
case 3:
if ( is_user_logged_in() ) {
$privacy_roles = get_post_meta( $directory_id, '_um_privacy_roles', true );
$privacy_roles = ! empty( $privacy_roles ) && is_array( $privacy_roles ) ? $privacy_roles : array();
$current_user_roles = um_user( 'roles' );
if ( ! empty( $current_user_roles ) && count( array_intersect( $current_user_roles, $privacy_roles ) ) > 0 ) {
$can_view = true;
}
}
break;
}
}
return apply_filters( 'um_directory_user_can_view', $can_view, $directory_id, $user_id );
}
/**
* Get the WordPress core searching fields in wp_users query.
*
* @since 2.6.10
* @version 2.10.2
* @param array|null $qv WP_User_Query variables.
@@ -202,30 +271,71 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) {
*
* @return bool|int
*/
function get_directory_by_hash( $hash ) {
public function get_directory_by_hash( $hash ) {
global $wpdb;
$directory_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE SUBSTRING( MD5( ID ), 11, 5 ) = %s", $hash ) );
$directory_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT post_id
FROM {$wpdb->postmeta}
WHERE meta_key = '_um_directory_token' AND
meta_value = %s
LIMIT 1",
$hash
)
);
if ( empty( $directory_id ) ) {
return false;
// Fallback, use old value.
$directory_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT ID
FROM {$wpdb->posts}
WHERE SUBSTRING( MD5( ID ), 11, 5 ) = %s",
$hash
)
);
if ( empty( $directory_id ) ) {
return false;
}
}
return (int) $directory_id;
}
/**
* Generate a secure random token for each directory
*
* @param int $id
*
* @return false|string
*/
public function set_directory_hash( $id ) {
$unique_hash = wp_generate_password( 32, false );
$result = update_post_meta( $id, '_um_directory_token', $unique_hash );
if ( false === $result ) {
return false;
}
return $unique_hash;
}
/**
* @param $id
*
* @return bool|string
*/
function get_directory_hash( $id ) {
$hash = substr( md5( $id ), 10, 5 );
public function get_directory_hash( $id ) {
$hash = get_post_meta( $id, '_um_directory_token', true );
if ( '' === $hash ) {
// Set the hash if empty.
$hash = $this->set_directory_hash( $id );
}
if ( empty( $hash ) ) {
// Fallback, use old value.
$hash = substr( md5( $id ), 10, 5 );
}
return $hash;
}
/**
* Get view Type template
* @param string $type
@@ -673,7 +783,7 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) {
*/
$attrs = apply_filters( 'um_search_fields', $attrs, $field_key, $directory_data['form_id'] );
$unique_hash = substr( md5( $directory_data['form_id'] ), 10, 5 );
$unique_hash = $this->get_directory_hash( $directory_data['form_id'] );
ob_start();
@@ -2546,15 +2656,14 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) {
$data_array = array(
'card_anchor' => esc_html( substr( md5( $user_id ), 10, 5 ) ),
'id' => absint( $user_id ),
'role' => esc_html( um_user( 'role' ) ),
'account_status' => esc_html( UM()->common()->users()->get_status( $user_id ) ),
'account_status_name' => esc_html( UM()->common()->users()->get_status( $user_id, 'formatted' ) ),
'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.
'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() ),
'can_edit' => (bool) $can_edit,
'edit_profile_url' => esc_url( um_edit_profile_url() ),
'edit_profile_url' => $can_edit ? esc_url( um_edit_profile_url() ) : '',
'avatar' => wp_kses( get_avatar( $user_id, $this->avatar_size ), UM()->get_allowed_html( 'templates' ) ),
'display_name_html' => wp_kses( um_user( 'display_name', 'html' ), UM()->get_allowed_html( 'templates' ) ),
'dropdown_actions' => $dropdown_actions,
@@ -2788,13 +2897,16 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) {
}
}
/**
* Main Query function for getting members via AJAX
*/
function ajax_get_members() {
public function ajax_get_members() {
UM()->check_ajax_nonce();
if ( UM()->is_rate_limited( 'member_directory' ) ) {
wp_send_json_error( __( 'Too many requests', 'ultimate-member' ) );
}
global $wpdb;
if ( empty( $_POST['directory_id'] ) ) {
@@ -2807,6 +2919,10 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) {
wp_send_json_error( __( 'Wrong member directory data', 'ultimate-member' ) );
}
if ( ! $this->can_view_directory( $directory_id ) ) {
wp_send_json_error( __( 'You cannot see this member directory', 'ultimate-member' ) );
}
$directory_data = UM()->query()->post_data( $directory_id );
//predefined result for user without capabilities to see other members
+3
View File
@@ -794,6 +794,9 @@ if ( ! class_exists( 'um\core\Shortcodes' ) ) {
}
if ( 'directory' === $args['mode'] ) {
if ( ! UM()->member_directory()->can_view_directory( $this->form_id ) ) {
return ''; // Checking for privacy settings of the member directory
}
wp_enqueue_script( 'um_members' );
wp_enqueue_style( 'um_members' );
}
+8
View File
@@ -105,6 +105,10 @@ if ( ! class_exists( 'um\core\User_posts' ) ) {
function load_posts() {
UM()->check_ajax_nonce();
if ( UM()->is_rate_limited( 'paginate_posts' ) ) {
wp_send_json_error( __( 'Too many requests', 'ultimate-member' ) );
}
$author = ! empty( $_POST['author'] ) ? absint( $_POST['author'] ) : get_current_user_id();
$page = ! empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
@@ -153,6 +157,10 @@ if ( ! class_exists( 'um\core\User_posts' ) ) {
function load_comments() {
UM()->check_ajax_nonce();
if ( UM()->is_rate_limited( 'paginate_comments' ) ) {
wp_send_json_error( __( 'Too many requests', 'ultimate-member' ) );
}
$user_id = ! empty( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : get_current_user_id();
$page = ! empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
-49
View File
@@ -1,49 +0,0 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) exit;
/**
* Run check if username exists
* @uses action hooks: wp_ajax_nopriv_ultimatemember_check_username_exists, wp_ajax_ultimatemember_check_username_exists
* @return boolean
*/
function ultimatemember_check_username_exists() {
UM()->check_ajax_nonce();
$username = isset( $_REQUEST['username'] ) ? sanitize_user( $_REQUEST['username'] ) : '';
$exists = username_exists( $username );
/**
* UM hook
*
* @type filter
* @title um_validate_username_exists
* @description Change username exists validation
* @input_vars
* [{"var":"$exists","type":"bool","desc":"Exists?"},
* {"var":"$username","type":"string","desc":"Username"}]
* @change_log
* ["Since: 2.0"]
* @usage
* <?php add_filter( 'um_validate_username_exists', 'function_name', 10, 2 ); ?>
* @example
* <?php
* add_filter( 'um_validate_username_exists', 'my_validate_username_exists', 10, 2 );
* function my_account_pre_updating_profile( $exists, $username ) {
* // your code here
* return $exists;
* }
* ?>
*/
$exists = apply_filters( 'um_validate_username_exists', $exists, $username );
if ( $exists ) {
echo 1;
} else {
echo 0;
}
die();
}
add_action('wp_ajax_nopriv_ultimatemember_check_username_exists', 'ultimatemember_check_username_exists');
add_action('wp_ajax_ultimatemember_check_username_exists', 'ultimatemember_check_username_exists');
+3 -3
View File
@@ -6,7 +6,7 @@
*
* Page: "Members"
*
* @version 2.6.1
* @version 2.11.1
*
* @var array $args
* @var bool $cover_photos
@@ -23,8 +23,8 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$unique_hash = substr( md5( $args['form_id'] ), 10, 5 ); ?>
$unique_hash = UM()->member_directory()->get_directory_hash( $args['form_id'] );
?>
<script type="text/template" id="tmpl-um-member-grid-<?php echo esc_attr( $unique_hash ) ?>">
<div class="um-members um-members-grid">
<div class="um-gutter-sizer"></div>
+6 -4
View File
@@ -6,7 +6,7 @@
*
* Page: "Members"
*
* @version 2.6.1
* @version 2.11.1
*
* @var array $args
* @var bool $cover_photos
@@ -19,10 +19,12 @@
* @var array $reveal_fields
* @var string $no_users
*/
if ( ! defined( 'ABSPATH' ) ) exit;
$unique_hash = substr( md5( $args['form_id'] ), 10, 5 ); ?>
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$unique_hash = UM()->member_directory()->get_directory_hash( $args['form_id'] );
?>
<script type="text/template" id="tmpl-um-member-list-<?php echo esc_attr( $unique_hash ) ?>">
<div class="um-members um-members-list">
+5 -5
View File
@@ -6,7 +6,7 @@
*
* Page: "Members"
*
* @version 2.6.8
* @version 2.11.1
*
* @var array $args
*/
@@ -23,7 +23,7 @@ foreach ( UM()->config()->core_directory_meta['members'] as $k => $v ) {
$def_args[ $key ] = $v;
}
$unique_hash = substr( md5( $args['form_id'] ), 10, 5 );
$unique_hash = UM()->member_directory()->get_directory_hash( $args['form_id'] );
$args = array_merge( $def_args, $args );
@@ -237,8 +237,8 @@ if ( ( ( $search && $show_search ) || ( $filters && $show_filters && count( $sea
$postid = ! empty( $post->ID ) ? $post->ID : '';
?>
<div class="um <?php echo esc_attr( $this->get_class( $mode ) ); ?> um-<?php echo esc_attr( substr( md5( $form_id ), 10, 5 ) ); ?>"
data-hash="<?php echo esc_attr( substr( md5( $form_id ), 10, 5 ) ) ?>" data-base-post="<?php echo esc_attr( $postid ) ?>"
<div class="um <?php echo esc_attr( $this->get_class( $mode ) ); ?> um-<?php echo esc_attr( $unique_hash ); ?>"
data-hash="<?php echo esc_attr( $unique_hash ) ?>" data-base-post="<?php echo esc_attr( $postid ) ?>"
data-must-search="<?php echo esc_attr( $must_search ); ?>" data-searched="<?php echo $not_searched ? '0' : '1'; ?>"
data-view_type="<?php echo esc_attr( $current_view ) ?>" data-page="<?php echo esc_attr( $current_page ) ?>"
data-sorting="<?php echo esc_attr( $sort_from_url ) ?>">
@@ -313,7 +313,7 @@ $postid = ! empty( $post->ID ) ? $post->ID : '';
<?php $items = array();
foreach ( $sorting_options as $value => $title ) {
$items[] = '<a href="javascript:void(0);" data-directory-hash="' . esc_attr( substr( md5( $form_id ), 10, 5 ) ) . '" class="um-sortyng-by-' . esc_attr( $value ) . '" data-value="' . esc_attr( $value ) . '" data-selected="' . ( ( $sort_from_url == $value ) ? '1' : '0' ) . '" data-default="' . ( ( $default_sorting == $value ) ? '1' : '0' ) . '">' . $title . '</a>'; ?>
$items[] = '<a href="javascript:void(0);" data-directory-hash="' . esc_attr( $unique_hash ) . '" class="um-sortyng-by-' . esc_attr( $value ) . '" data-value="' . esc_attr( $value ) . '" data-selected="' . ( ( $sort_from_url == $value ) ? '1' : '0' ) . '" data-default="' . ( ( $default_sorting == $value ) ? '1' : '0' ) . '">' . $title . '</a>'; ?>
<?php }
UM()->member_directory()->dropdown_menu( '.um-member-directory-sorting-a', 'click', $items ); ?>