From 77f6a1373093c267b5b6462da160b62dcf3f132f Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Tue, 25 Mar 2025 23:31:13 +0200 Subject: [PATCH 1/3] Refactor and enhance member directory search functionality. Replaced deprecated and complex search query methods with a more streamlined and flexible implementation. Introduced improved handling of core and custom search fields, optimized search logic, and added better filter support. Incremented plugin version to 2.10.2 to reflect these changes. --- includes/admin/core/class-admin-settings.php | 2 +- includes/core/class-member-directory.php | 303 ++++++++----------- ultimate-member.php | 2 +- 3 files changed, 127 insertions(+), 180 deletions(-) diff --git a/includes/admin/core/class-admin-settings.php b/includes/admin/core/class-admin-settings.php index 122310c7..deb4f727 100644 --- a/includes/admin/core/class-admin-settings.php +++ b/includes/admin/core/class-admin-settings.php @@ -151,7 +151,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Settings' ) ) { } $skip_fields = UM()->builtin()->get_fields_without_metakey(); - $skip_fields = array_merge( $skip_fields, UM()->member_directory()->core_search_fields ); + $skip_fields = array_merge( $skip_fields, UM()->member_directory()::$core_search_fields ); $real_usermeta = $wpdb->get_col( "SELECT DISTINCT meta_key FROM {$wpdb->usermeta}" ); $real_usermeta = ! empty( $real_usermeta ) ? $real_usermeta : array(); diff --git a/includes/core/class-member-directory.php b/includes/core/class-member-directory.php index 0135a85f..b0bb59b2 100644 --- a/includes/core/class-member-directory.php +++ b/includes/core/class-member-directory.php @@ -1,6 +1,7 @@ core_search_fields ); + return apply_filters( 'um_member_directory_core_search_fields', $search_columns ); } /** @@ -456,7 +483,7 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { if ( ! empty( UM()->builtin()->all_user_fields() ) ) { foreach ( UM()->builtin()->all_user_fields() as $key => $data ) { if ( in_array( $key, $core_search_keys, true ) ) { - if ( isset( $data['title'] ) && array_search( $data['title'], $this->searching_fields, true ) !== false ) { + if ( isset( $data['title'] ) && in_array( $data['title'], $this->searching_fields, true ) ) { $data['title'] = $data['title'] . ' (' . $key . ')'; } @@ -1705,28 +1732,7 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { if ( empty( $search ) ) { return ''; } - - // Make the search line empty if it contains the mySQL query statements. - $regexp_map = array( - '/select(.*?)from/im', - '/select(.*?)sleep/im', - "/sleep\([^)]+\)/im", // avoid any sleep injections - "/benchmark\([^)]+\)/im", // avoid any benchmark injections - '/select(.*?)database/im', - '/select(.*?)where/im', - '/update(.*?)set/im', - '/delete(.*?)from/im', - ); - - foreach ( $regexp_map as $regexp ) { - preg_match( $regexp, $search, $matches ); - if ( ! empty( $matches ) ) { - $search = ''; - break; - } - } - // Early escape of the search line. The same as `$wpdb->prepare()`. - return esc_sql( $search ); + return $search; } /** @@ -1738,155 +1744,18 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { // complex using with change_meta_sql function $search = $this->prepare_search( $_POST['search'] ); if ( ! empty( $search ) ) { - $meta_query = array( - 'relation' => 'OR', - array( - 'value' => $search, - 'compare' => '=', - ), - array( - 'value' => $search, - 'compare' => 'LIKE', - ), - array( - 'value' => serialize( (string) $search ), - 'compare' => 'LIKE', - ), - ); - + $meta_query = array(); $meta_query = apply_filters( 'um_member_directory_general_search_meta_query', $meta_query, $search ); - $this->query_args['meta_query'][] = $meta_query; + if ( ! empty( $meta_query ) ) { + $this->query_args['meta_query'][] = $meta_query; + } $this->is_search = true; } } } - /** - * Change mySQL meta query join attribute - * for search only by UM user meta fields and WP core fields in WP Users table - * - * @param array $sql Array containing the query's JOIN and WHERE clauses. - * @param $queries - * @param $type - * @param $primary_table - * @param $primary_id_column - * @param WP_User_Query $context - * - * @return array - */ - public function change_meta_sql( $sql, $queries, $type, $primary_table, $primary_id_column, $context ) { - if ( ! empty( $_POST['search'] ) ) { - $search = $this->prepare_search( $_POST['search'] ); - if ( ! empty( $search ) ) { - global $wpdb; - - $meta_value = '%' . $wpdb->esc_like( $search ) . '%'; - $search_meta = $wpdb->prepare( '%s', $meta_value ); - - preg_match( '~(?<=\{)(.*?)(?=\})~', $search_meta, $matches, PREG_OFFSET_CAPTURE, 0 ); - - // workaround for standard mySQL hashes which are used by $wpdb->prepare instead of the %symbol - // sometimes it breaks error for strings like that wp_postmeta.meta_value LIKE '{12f209b48a89eeab33424902879d05d503f251ca8812dde03b59484a2991dc74}AMS{12f209b48a89eeab33424902879d05d503f251ca8812dde03b59484a2991dc74}' - // {12f209b48a89eeab33424902879d05d503f251ca8812dde03b59484a2991dc74} isn't applied by the `preg_replace()` below - if ( $matches[0][0] ) { - $search_meta = str_replace( '{' . $matches[0][0] . '}', '#%&', $search_meta ); - $sql['where'] = str_replace( '{' . $matches[0][0] . '}', '#%&', $sql['where'] ); - } - - // str_replace( '/', '\/', wp_slash( $search_meta ) ) means that we add backslashes to special symbols + add backslash to slash(/) symbol for proper regular pattern. - preg_match( - '/^(.*).meta_value LIKE ' . str_replace( '/', '\/', wp_slash( $search_meta ) ) . '[^\)]/im', - $sql['where'], - $join_matches - ); - - $sql['where'] = str_replace( '#%&', '{' . $matches[0][0] . '}', $sql['where'] ); - - $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); - $exclude_fields = get_post_meta( $directory_id, '_um_search_exclude_fields', true ); - $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); - - if ( isset( $join_matches[1] ) ) { - $meta_join_for_search = trim( $join_matches[1] ); - - // skip private invisible fields - $custom_fields = array(); - if ( empty( $include_fields ) ) { - foreach ( array_keys( UM()->builtin()->all_user_fields ) as $field_key ) { - if ( empty( $field_key ) ) { - continue; - } - - $data = UM()->fields()->get_field( $field_key ); - if ( ! um_can_view_field( $data ) ) { - continue; - } - - $custom_fields[] = $field_key; - } - } else { - foreach ( $include_fields as $field_key ) { - if ( empty( $field_key ) ) { - continue; - } - - $data = UM()->fields()->get_field( $field_key ); - if ( ! um_can_view_field( $data ) ) { - continue; - } - - $custom_fields[] = $field_key; - } - } - - $custom_fields = apply_filters( 'um_general_search_custom_fields', $custom_fields ); - - if ( ! empty( $custom_fields ) ) { - if ( ! empty( $exclude_fields ) ) { - $custom_fields = array_diff( $custom_fields, $exclude_fields ); - } - - $sql['join'] = preg_replace( - '/(' . $meta_join_for_search . ' ON \( ' . $wpdb->users . '\.ID = ' . $meta_join_for_search . '\.user_id )(\))/im', - "$1 AND " . $meta_join_for_search . ".meta_key IN( '" . implode( "','", $custom_fields ) . "' ) $2", - $sql['join'] - ); - } - } - - $core_search = $this->get_core_search_fields(); - if ( ! empty( $include_fields ) ) { - $core_search = array_intersect( $core_search, $include_fields ); - } - if ( ! empty( $exclude_fields ) ) { - $core_search = array_diff( $core_search, $exclude_fields ); - } - - if ( ! empty( $core_search ) ) { - // Add OR instead AND to search in WP core fields user_email, user_login, user_display_name - $search_where = $context->get_search_sql( $search, $core_search, 'both' ); - - $search_where = preg_replace( '/ AND \((.*?)\)/im', "$1 OR", $search_where ); - - // str_replace( '/', '\/', wp_slash( $search ) ) means that we add backslashes to special symbols + add backslash to slash(/) symbol for proper regular pattern. - $pattern = $wpdb->prepare( $meta_join_for_search . '.meta_value = %s', $search ); - $pattern = '/(' . str_replace( '/', '\/', wp_slash( $pattern ) ) . ')/im'; - - $sql['where'] = preg_replace( - $pattern, - trim( $search_where ) . " $1", - $sql['where'], - 1 - ); - } - } - } - - return $sql; - } - /** * Handle filters request */ @@ -2779,16 +2648,15 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { return $data_array; } - /** * Update limit query * - * @param $user_query + * @param WP_User_Query $user_query */ - function pagination_changes( $user_query ) { + public function pagination_changes( $user_query ) { global $wpdb; - $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); + $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); $directory_data = UM()->query()->post_data( $directory_id ); $qv = $user_query->query_vars; @@ -2808,6 +2676,88 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { } } + /** + * Update search query + * + * @param WP_User_Query $user_query + * + * @throws Exception + */ + public function search_changes( $user_query ) { + global $wpdb; + + if ( ! empty( $_POST['search'] ) ) { + $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); + $qv = $user_query->query_vars; + + $search = $this->prepare_search( $_POST['search'] ); + if ( empty( $search ) ) { + return; + } + + $exclude_fields = get_post_meta( $directory_id, '_um_search_exclude_fields', true ); + $exclude_fields = ! empty( $exclude_fields ) && is_array( $exclude_fields ) ? $exclude_fields : array(); + $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); + $include_fields = ! empty( $include_fields ) && is_array( $include_fields ) ? $include_fields : array_keys( UM()->builtin()->all_user_fields ); + + $custom_fields = array(); + foreach ( $include_fields as $field_key ) { + if ( empty( $field_key ) ) { + continue; + } + + $data = UM()->fields()->get_field( $field_key ); + if ( isset( $data['type'] ) && 'password' === $data['type'] ) { + continue; + } + if ( isset( $data['metakey'] ) && in_array( $data['metakey'], array( 'role_select', 'role_radio' ), true ) ) { + continue; + } + if ( ! empty( $data['account_only'] ) ) { + continue; + } + + if ( ! um_can_view_field( $data ) ) { + continue; + } + + $custom_fields[] = $field_key; + } + + if ( ! empty( $custom_fields ) && ! empty( $exclude_fields ) ) { + $custom_fields = array_diff( $custom_fields, $exclude_fields ); + } + + $custom_fields = apply_filters( 'um_general_search_custom_fields', $custom_fields ); + $custom_fields = array_values( array_unique( $custom_fields ) ); + + $search_columns = $this->get_core_search_fields( $qv, $search ); + $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); + if ( ! empty( $include_fields ) ) { + $search_columns = array_intersect( $search_columns, $include_fields ); + } + if ( ! empty( $exclude_fields ) ) { + $search_columns = array_diff( $search_columns, $exclude_fields ); + } + + if ( ! empty( $custom_fields ) ) { + $search_columns[] = 'um_search.meta_value'; + $user_query->query_from .= " INNER JOIN {$wpdb->usermeta} AS um_search ON ( {$wpdb->users}.ID = um_search.user_id AND um_search.meta_key IN('" . implode( "','", $custom_fields ) . "'))"; + } + + if ( ! empty( $search_columns ) ) { + $search_where = $user_query->get_search_sql( $search, $search_columns, 'both' ); + $search_where = apply_filters( 'um_general_search_custom_search_where', $search_where, $user_query, $search ); + $user_query->query_where .= $search_where; + } + + // Required `DISTINCT` if not exists, because we add `OR` condition manually. + if ( ( ! empty( $custom_fields ) || ! empty( $search_columns ) ) && false === strpos( $user_query->query_fields, 'DISTINCT' ) ) { + $user_query->query_fields = 'DISTINCT ' . $user_query->query_fields; + } + } + } + function predefined_no_caps( $directory_data ) { //predefined result for user without capabilities to see other members @@ -2940,16 +2890,13 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { */ do_action( 'um_user_before_query', $this->query_args, $this ); - add_filter( 'get_meta_sql', array( &$this, 'change_meta_sql' ), 10, 6 ); - - add_filter( 'pre_user_query', array( &$this, 'pagination_changes' ), 10, 1 ); + add_action( 'pre_user_query', array( &$this, 'search_changes' ) ); + add_action( 'pre_user_query', array( &$this, 'pagination_changes' ) ); $user_query = new WP_User_Query( $this->query_args ); - remove_filter( 'pre_user_query', array( &$this, 'pagination_changes' ), 10 ); - - remove_filter( 'get_meta_sql', array( &$this, 'change_meta_sql' ), 10 ); - + remove_action( 'pre_user_query', array( &$this, 'search_changes' ) ); + remove_action( 'pre_user_query', array( &$this, 'pagination_changes' ) ); /** * Fires just after the users query for getting users in member directory. * diff --git a/ultimate-member.php b/ultimate-member.php index 54da7c77..960538ca 100644 --- a/ultimate-member.php +++ b/ultimate-member.php @@ -3,7 +3,7 @@ * Plugin Name: Ultimate Member * Plugin URI: http://ultimatemember.com/ * Description: The easiest way to create powerful online communities and beautiful user profiles with WordPress - * Version: 2.10.1 + * Version: 2.10.2 * Author: Ultimate Member * Author URI: http://ultimatemember.com/ * Text Domain: ultimate-member From 6987660e6200c0fcbb26f6c1f0f7504b9a3f5ef5 Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Wed, 26 Mar 2025 00:18:52 +0200 Subject: [PATCH 2/3] Refactor member directory search to handle missing directory_id. Ensure proper validation for `directory_id` when processing search queries. Default include and exclude fields are applied if `directory_id` is absent, preserving functionality and preventing errors. --- includes/core/class-member-directory.php | 30 +++++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/includes/core/class-member-directory.php b/includes/core/class-member-directory.php index b0bb59b2..9eb772da 100644 --- a/includes/core/class-member-directory.php +++ b/includes/core/class-member-directory.php @@ -2687,18 +2687,27 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { global $wpdb; if ( ! empty( $_POST['search'] ) ) { - $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); - $qv = $user_query->query_vars; + $directory_id = null; + if ( ! empty( $_POST['directory_id'] ) ) { + $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); + } + + $qv = $user_query->query_vars; $search = $this->prepare_search( $_POST['search'] ); if ( empty( $search ) ) { return; } - $exclude_fields = get_post_meta( $directory_id, '_um_search_exclude_fields', true ); - $exclude_fields = ! empty( $exclude_fields ) && is_array( $exclude_fields ) ? $exclude_fields : array(); - $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); - $include_fields = ! empty( $include_fields ) && is_array( $include_fields ) ? $include_fields : array_keys( UM()->builtin()->all_user_fields ); + $exclude_fields = array(); + $include_fields = array_keys( UM()->builtin()->all_user_fields ); + if ( ! empty( $directory_id ) ) { + $exclude_fields = get_post_meta( $directory_id, '_um_search_exclude_fields', true ); + $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); + + $exclude_fields = ! empty( $exclude_fields ) && is_array( $exclude_fields ) ? $exclude_fields : array(); + $include_fields = ! empty( $include_fields ) && is_array( $include_fields ) ? $include_fields : array_keys( UM()->builtin()->all_user_fields ); + } $custom_fields = array(); foreach ( $include_fields as $field_key ) { @@ -2732,9 +2741,12 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { $custom_fields = array_values( array_unique( $custom_fields ) ); $search_columns = $this->get_core_search_fields( $qv, $search ); - $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); - if ( ! empty( $include_fields ) ) { - $search_columns = array_intersect( $search_columns, $include_fields ); + + if ( ! empty( $directory_id ) ) { + $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); + if ( ! empty( $include_fields ) ) { + $search_columns = array_intersect( $search_columns, $include_fields ); + } } if ( ! empty( $exclude_fields ) ) { $search_columns = array_diff( $search_columns, $exclude_fields ); From 8c7d7cadecf0b6ab8ea44db93dab0cb27c485fff Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Wed, 26 Mar 2025 00:20:45 +0200 Subject: [PATCH 3/3] Refactor nonce verification handling in search processing Improved code readability by consolidating and consistently ignoring nonce verification warnings where it has already been verified. This simplifies the logic and ensures clearer handling of `$_POST` variables in the member directory search functionality. --- includes/core/class-member-directory.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/includes/core/class-member-directory.php b/includes/core/class-member-directory.php index 9eb772da..d1ca1d31 100644 --- a/includes/core/class-member-directory.php +++ b/includes/core/class-member-directory.php @@ -2686,19 +2686,19 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { public function search_changes( $user_query ) { global $wpdb; - if ( ! empty( $_POST['search'] ) ) { - $directory_id = null; - if ( ! empty( $_POST['directory_id'] ) ) { - $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); - } - - $qv = $user_query->query_vars; - - $search = $this->prepare_search( $_POST['search'] ); + if ( ! empty( $_POST['search'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification -- already verified here + $search = $this->prepare_search( $_POST['search'] ); // phpcs:ignore WordPress.Security.NonceVerification -- already verified here if ( empty( $search ) ) { return; } + $directory_id = null; + if ( ! empty( $_POST['directory_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification -- already verified here + $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification -- already verified here + } + + $qv = $user_query->query_vars; + $exclude_fields = array(); $include_fields = array_keys( UM()->builtin()->all_user_fields ); if ( ! empty( $directory_id ) ) { @@ -2754,12 +2754,14 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { if ( ! empty( $custom_fields ) ) { $search_columns[] = 'um_search.meta_value'; + $user_query->query_from .= " INNER JOIN {$wpdb->usermeta} AS um_search ON ( {$wpdb->users}.ID = um_search.user_id AND um_search.meta_key IN('" . implode( "','", $custom_fields ) . "'))"; } if ( ! empty( $search_columns ) ) { $search_where = $user_query->get_search_sql( $search, $search_columns, 'both' ); $search_where = apply_filters( 'um_general_search_custom_search_where', $search_where, $user_query, $search ); + $user_query->query_where .= $search_where; }