Merge pull request #1661 from ultimatemember/development/2.9.x

Version 2.10.2
This commit is contained in:
Mykyta Synelnikov
2025-04-02 11:40:28 +03:00
committed by GitHub
9 changed files with 233 additions and 194 deletions
+1 -1
View File
@@ -15,7 +15,7 @@
"step": "installPlugin",
"pluginZipFile": {
"resource": "url",
"url": "https:\/\/downloads.wordpress.org\/plugin\/ultimate-member.2.10.1.zip"
"url": "https:\/\/downloads.wordpress.org\/plugin\/ultimate-member.2.10.2.zip"
},
"options": {
"activate": true
+1 -1
View File
@@ -44,7 +44,7 @@ GNU Version 2 or Any Later Version
### IMPORTANT: PLEASE UPDATE THE PLUGIN TO AT LEAST VERSION 2.6.7 IMMEDIATELY. VERSION 2.6.7 PATCHES SECURITY PRIVILEGE ESCALATION VULNERABILITY. PLEASE SEE [THIS ARTICLE](https://docs.ultimatemember.com/article/1866-security-incident-update-and-recommended-actions) FOR MORE INFORMATION
[Official Release Version: 2.10.1](https://github.com/ultimatemember/ultimatemember/releases/tag/2.10.1).
[Official Release Version: 2.10.2](https://github.com/ultimatemember/ultimatemember/releases/tag/2.10.2).
## Changelog
+11
View File
@@ -1,5 +1,16 @@
== Changelog ==
= 2.10.2 April 02, 2025 =
* Enhancements:
- Added: `UM()->common()-filesystem()::maybe_init_wp_filesystem();` method.
- Added: `UM()->common()-filesystem()::remove_dir();` method.
* Bugfixes:
- Fixed: Security issue CVE ID: CVE-2025-1702. Reviewed general search scripts and suggested another solution that uses only `$wpdb->prepare()`.
= 2.10.1 March 03, 2025 =
* Bugfixes:
+1 -1
View File
@@ -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();
+62 -12
View File
@@ -59,6 +59,13 @@ class Filesystem {
$this->temp_upload_url = $this->get_upload_url( 'ultimatemember/temp' );
}
/**
* Files Age in the temp folder. By default, it's 24 hours.
*
* @return int Temp file age in seconds.
* @since 2.8.7
*
*/
public function files_age() {
/**
* Filters the maximum file age in the temp folder. By default, it's 24 hours.
@@ -73,6 +80,17 @@ class Filesystem {
return apply_filters( 'um_filesystem_max_file_age', 24 * HOUR_IN_SECONDS ); // Temp file age in seconds
}
/**
* Image MIME Types
*
* Retrieves a list of image MIME types based on the context.
*
* @param string $context The context in which the MIME types are needed ('list' or 'allowed').
*
* @return array List of image MIME types based on the context.
*
* @since 2.8.7
*/
public static function image_mimes( $context = 'list' ) {
$mimes = array();
@@ -148,12 +166,7 @@ class Filesystem {
public function clear_temp_dir() {
global $wp_filesystem;
if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
$credentials = request_filesystem_credentials( site_url() );
WP_Filesystem( $credentials );
}
self::maybe_init_wp_filesystem();
if ( ! $wp_filesystem->is_dir( $this->temp_upload_dir ) ) {
return;
@@ -197,12 +210,7 @@ class Filesystem {
// Please add define('FS_METHOD', 'direct'); to avoid question about FTP.
global $wp_filesystem;
if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
$credentials = request_filesystem_credentials( site_url() );
WP_Filesystem( $credentials );
}
self::maybe_init_wp_filesystem();
if ( ! $blog_id ) {
$blog_id = get_current_blog_id();
@@ -290,4 +298,46 @@ class Filesystem {
return '';
}
/**
* Probably define global $wp_filesystem.
*
* @since 2.10.2
*
* @return void
*/
public static function maybe_init_wp_filesystem() {
global $wp_filesystem;
// If you need to fix this issue on the localhost
// https://stackoverflow.com/questions/30688431/wordpress-needs-the-ftp-credentials-to-update-plugins
// Please add define('FS_METHOD', 'direct'); to avoid question about FTP.
if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
$credentials = request_filesystem_credentials( site_url() );
WP_Filesystem( $credentials );
}
}
/**
* Remove a directory using WP Filesystem.
*
* @param string $dir The directory path to be removed. Should end with DIRECTORY_SEPARATOR.
*
* @return bool True on success, false on failure or if directory doesn't exist.
*
* @since 2.10.2
*/
public static function remove_dir( $dir ) {
global $wp_filesystem;
self::maybe_init_wp_filesystem();
if ( ! $wp_filesystem->is_dir( $dir ) ) {
return false;
}
return $wp_filesystem->delete( $dir, true );
}
}
+2
View File
@@ -216,6 +216,8 @@ class Users {
/**
* Reset User cache
*
* @since 2.8.7
*
* @param int $user_id User ID.
*/
public function remove_cache( $user_id ) {
+139 -177
View File
@@ -1,6 +1,7 @@
<?php
namespace um\core;
use Exception;
use WP_User_Query;
if ( ! defined( 'ABSPATH' ) ) {
@@ -70,7 +71,7 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) {
*
* @var string[]
*/
var $core_search_fields = array(
public static $core_search_fields = array(
'user_login',
'user_url',
'display_name',
@@ -127,9 +128,35 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) {
/**
* 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.
* @param string|null $search Search line value.
* @return array
*/
protected function get_core_search_fields() {
protected function get_core_search_fields( $qv = null, $search = null ) {
$search_columns = array();
if ( ! is_null( $qv ) && ! empty( $search ) ) {
// WordPress native code from wp-includes/class-wp-user-query.php is below in this condition.
if ( $qv['search_columns'] ) {
$search_columns = array_intersect( $qv['search_columns'], array( 'ID', 'user_login', 'user_email', 'user_url', 'user_nicename', 'display_name' ) );
}
if ( ! $search_columns ) {
if ( str_contains( $search, '@' ) ) {
$search_columns = array( 'user_email' );
} elseif ( is_numeric( $search ) ) {
$search_columns = array( 'user_login', 'ID' );
} elseif ( preg_match( '|^https?://|', $search ) && ! ( is_multisite() && wp_is_large_network( 'users' ) ) ) {
$search_columns = array( 'user_url' );
} else {
$search_columns = array( 'user_login', 'user_url', 'user_email', 'user_nicename', 'display_name' );
}
}
/** This filter is documented in wp-includes/class-wp-user-query.php */
$search_columns = apply_filters( 'user_search_columns', $search_columns, $search, $this );
} else {
$search_columns = self::$core_search_fields;
}
/**
* Filters the WordPress core searching fields in wp_users query for UM Member directory query.
*
@@ -149,7 +176,7 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) {
* }
* add_filter( 'um_member_directory_core_search_fields', 'my_um_member_directory_core_search_fields' );
*/
return apply_filters( 'um_member_directory_core_search_fields', $this->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,27 +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\(\d+\)/im", // avoid any sleep 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;
}
/**
@@ -1737,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
*/
@@ -2778,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;
@@ -2807,6 +2676,102 @@ 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'] ) ) { // 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 ) ) {
$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 ) {
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 );
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 );
}
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
@@ -2939,16 +2904,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.
*
+15 -1
View File
@@ -6,7 +6,7 @@ Tags: community, member, membership, user-profile, user-registration
Requires PHP: 7.0
Requires at least: 6.2
Tested up to: 6.7
Stable tag: 2.10.1
Stable tag: 2.10.2
License: GPLv3
License URI: http://www.gnu.org/licenses/gpl-3.0.txt
@@ -167,6 +167,17 @@ No specific extensions are needed. But we highly recommended keep active these P
IMPORTANT: PLEASE UPDATE THE PLUGIN TO AT LEAST VERSION 2.6.7 IMMEDIATELY. VERSION 2.6.7 PATCHES SECURITY PRIVILEGE ESCALATION VULNERABILITY. PLEASE SEE [THIS ARTICLE](https://docs.ultimatemember.com/article/1866-security-incident-update-and-recommended-actions) FOR MORE INFORMATION
= 2.10.2 2025-04-02 =
**Enhancements**
* Added: `UM()->common()-filesystem()::maybe_init_wp_filesystem();` method.
* Added: `UM()->common()-filesystem()::remove_dir();` method.
**Bugfixes**
* Fixed: Security issue CVE ID: CVE-2025-1702. Reviewed general search scripts and suggested another solution that uses only `$wpdb->prepare()`.
= 2.10.1 2025-03-03 =
**Bugfixes**
@@ -278,6 +289,9 @@ IMPORTANT: PLEASE UPDATE THE PLUGIN TO AT LEAST VERSION 2.6.7 IMMEDIATELY. VERSI
== Upgrade Notice ==
= 2.10.2 =
This version fixes a security related bug. Upgrade immediately.
= 2.10.1 =
This version fixes a security related bug. Upgrade immediately.
+1 -1
View File
@@ -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