From 689123eea8b48d387abbb4a48b462f959b47643e Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Sat, 3 Sep 2022 01:57:41 +0300 Subject: [PATCH 01/18] - updated version; --- readme.txt | 4 ++++ ultimate-member.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index edca5c33..06ed0531 100644 --- a/readme.txt +++ b/readme.txt @@ -163,6 +163,10 @@ No, you do not need to use our plugin’s login or registration pages and can us * To learn more about version 2.1 please see this [docs](https://docs.ultimatemember.com/article/1512-upgrade-2-1-0) * UM2.1+ is a significant update to the Member Directories' code base from 2.0.x. Please make sure you take a full-site backup with restore point before updating the plugin += 2.5.1: September xx, 2022 = + + + = 2.5.0: August 17, 2022 = * Enhancements: diff --git a/ultimate-member.php b/ultimate-member.php index 609ffb7d..a0f8ba9d 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.5.0 +Version: 2.5.1-alpha1 Author: Ultimate Member Author URI: http://ultimatemember.com/ Text Domain: ultimate-member From 7bff69f52b10ac137fb5e53dc1c0569664431d86 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 26 Sep 2022 13:59:45 +0300 Subject: [PATCH 02/18] - fixed admin upgrade scripts and upgrades pack validation; --- includes/admin/core/class-admin-upgrade.php | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/includes/admin/core/class-admin-upgrade.php b/includes/admin/core/class-admin-upgrade.php index bff5b63b..e6d0c39c 100644 --- a/includes/admin/core/class-admin-upgrade.php +++ b/includes/admin/core/class-admin-upgrade.php @@ -356,10 +356,20 @@ if ( ! class_exists( 'um\admin\core\Admin_Upgrade' ) ) { if ( empty( $_POST['pack'] ) ) { exit(''); } else { - ob_start(); - include_once $this->packages_dir . sanitize_text_field( $_POST['pack'] ) . DIRECTORY_SEPARATOR . 'init.php'; - ob_get_flush(); - exit; + $pack = sanitize_text_field( $_POST['pack'] ); + if ( in_array( $pack, $this->necessary_packages, true ) ) { + $file = $this->packages_dir . $pack . DIRECTORY_SEPARATOR . 'init.php'; + if ( file_exists( $file ) ) { + ob_start(); + include_once $file; + ob_get_flush(); + exit; + } else { + exit(''); + } + } else { + exit(''); + } } } @@ -407,4 +417,4 @@ if ( ! class_exists( 'um\admin\core\Admin_Upgrade' ) ) { } } -} \ No newline at end of file +} From 14dc36b813b8e697c7f2d99f9e3bcd7b2202f05e Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Tue, 27 Sep 2022 13:58:01 +0300 Subject: [PATCH 03/18] - fixed directory traversal vulnerability; --- includes/core/class-shortcodes.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/includes/core/class-shortcodes.php b/includes/core/class-shortcodes.php index 1f122743..461e06d2 100644 --- a/includes/core/class-shortcodes.php +++ b/includes/core/class-shortcodes.php @@ -276,6 +276,9 @@ if ( ! class_exists( 'um\core\Shortcodes' ) ) { extract( $args ); } + // Avoid Directory Traversal vulnerability. + $tpl = trim( $tpl, "./\\" ); + $file = um_path . "templates/{$tpl}.php"; $theme_file = get_stylesheet_directory() . "/ultimate-member/templates/{$tpl}.php"; if ( file_exists( $theme_file ) ) { From e1bc94c1100f02a129721ba4be5fbc44c3d78ec4 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Tue, 27 Sep 2022 15:13:35 +0300 Subject: [PATCH 04/18] - fixed Directory Traversal vulnerability. Using realpath for that; --- includes/core/class-shortcodes.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/includes/core/class-shortcodes.php b/includes/core/class-shortcodes.php index 461e06d2..fdc5ec7a 100644 --- a/includes/core/class-shortcodes.php +++ b/includes/core/class-shortcodes.php @@ -276,9 +276,6 @@ if ( ! class_exists( 'um\core\Shortcodes' ) ) { extract( $args ); } - // Avoid Directory Traversal vulnerability. - $tpl = trim( $tpl, "./\\" ); - $file = um_path . "templates/{$tpl}.php"; $theme_file = get_stylesheet_directory() . "/ultimate-member/templates/{$tpl}.php"; if ( file_exists( $theme_file ) ) { @@ -286,7 +283,12 @@ if ( ! class_exists( 'um\core\Shortcodes' ) ) { } if ( file_exists( $file ) ) { - include $file; + // Avoid Directory Traversal vulnerability by the checking the realpath. + // Templates can be situated only in the get_stylesheet_directory() or plugindir templates. + $real_file = realpath( $file ); + if ( 0 === strpos( $real_file, um_path . "templates" . DIRECTORY_SEPARATOR ) || 0 === strpos( $real_file, get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'ultimate-member' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR ) ) { + include $file; + } } } From aa6a238c6198776c91abb1000116ea9a3728eb53 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Fri, 30 Sep 2022 12:31:40 +0300 Subject: [PATCH 05/18] - added callbacks blacklist. Added PHP command execution functions here to exclude the running them from the custom callback; --- includes/admin/core/class-admin-builder.php | 4 ++ includes/core/class-fields.php | 76 ++++++++++++++------- includes/core/class-form.php | 7 ++ includes/core/class-validation.php | 6 +- includes/core/um-actions-profile.php | 10 +-- includes/core/um-filters-fields.php | 3 + 6 files changed, 75 insertions(+), 31 deletions(-) diff --git a/includes/admin/core/class-admin-builder.php b/includes/admin/core/class-admin-builder.php index 0cd129ac..b655d341 100644 --- a/includes/admin/core/class-admin-builder.php +++ b/includes/admin/core/class-admin-builder.php @@ -1220,6 +1220,10 @@ if ( ! class_exists( 'um\admin\core\Admin_Builder' ) ) { $arr_options['function_exists'] = function_exists( $um_callback_func ); } + if ( in_array( $um_callback_func, UM()->fields()->dropdown_options_source_blacklist(), true ) ) { + wp_send_json_error( __( 'This is not possible for security reasons. Don\'t use internal PHP functions.', 'ultimate-member' ) ); + } + $arr_options['data'] = array(); if ( function_exists( $um_callback_func ) ) { $arr_options['data'] = call_user_func( $um_callback_func ); diff --git a/includes/core/class-fields.php b/includes/core/class-fields.php index f31a17f8..0ff3768f 100644 --- a/includes/core/class-fields.php +++ b/includes/core/class-fields.php @@ -146,17 +146,19 @@ if ( ! class_exists( 'um\core\Fields' ) ) { if ( array_key_exists( 'custom_dropdown_options_source', $args ) ) { if ( function_exists( wp_unslash( $args['custom_dropdown_options_source'] ) ) ) { - $allowed_callbacks = UM()->options()->get( 'allowed_choice_callbacks' ); - if ( ! empty( $allowed_callbacks ) ) { - $allowed_callbacks = array_map( 'rtrim', explode( "\n", $allowed_callbacks ) ); - $allowed_callbacks[] = $args['custom_dropdown_options_source']; - } else { - $allowed_callbacks = array( $args['custom_dropdown_options_source'] ); - } - $allowed_callbacks = array_unique( $allowed_callbacks ); - $allowed_callbacks = implode( "\r\n", $allowed_callbacks ); + if ( ! in_array( $args['custom_dropdown_options_source'], UM()->fields()->dropdown_options_source_blacklist(), true ) ) { + $allowed_callbacks = UM()->options()->get( 'allowed_choice_callbacks' ); + if ( ! empty( $allowed_callbacks ) ) { + $allowed_callbacks = array_map( 'rtrim', explode( "\n", $allowed_callbacks ) ); + $allowed_callbacks[] = $args['custom_dropdown_options_source']; + } else { + $allowed_callbacks = array( $args['custom_dropdown_options_source'] ); + } + $allowed_callbacks = array_unique( $allowed_callbacks ); + $allowed_callbacks = implode( "\r\n", $allowed_callbacks ); - UM()->options()->update( 'allowed_choice_callbacks', $allowed_callbacks ); + UM()->options()->update( 'allowed_choice_callbacks', $allowed_callbacks ); + } } } @@ -201,19 +203,21 @@ if ( ! class_exists( 'um\core\Fields' ) ) { if ( array_key_exists( 'custom_dropdown_options_source', $args ) ) { if ( function_exists( wp_unslash( $args['custom_dropdown_options_source'] ) ) ) { - $allowed_callbacks = UM()->options()->get( 'allowed_choice_callbacks' ); - if ( ! empty( $allowed_callbacks ) ) { - $allowed_callbacks = array_map( 'rtrim', explode( "\n", $allowed_callbacks ) ); - $allowed_callbacks[] = $args['custom_dropdown_options_source']; - } else { - $allowed_callbacks = array( $args['custom_dropdown_options_source'] ); + if ( ! in_array( $args['custom_dropdown_options_source'], UM()->fields()->dropdown_options_source_blacklist(), true ) ) { + $allowed_callbacks = UM()->options()->get( 'allowed_choice_callbacks' ); + if ( ! empty( $allowed_callbacks ) ) { + $allowed_callbacks = array_map( 'rtrim', explode( "\n", $allowed_callbacks ) ); + $allowed_callbacks[] = $args['custom_dropdown_options_source']; + } else { + $allowed_callbacks = array( $args['custom_dropdown_options_source'] ); + } + $allowed_callbacks = array_unique( $allowed_callbacks ); + $allowed_callbacks = implode( "\r\n", $allowed_callbacks ); + + UM()->options()->update( 'allowed_choice_callbacks', $allowed_callbacks ); + + $args['custom_dropdown_options_source'] = wp_unslash( $args['custom_dropdown_options_source'] ); } - $allowed_callbacks = array_unique( $allowed_callbacks ); - $allowed_callbacks = implode( "\r\n", $allowed_callbacks ); - - UM()->options()->update( 'allowed_choice_callbacks', $allowed_callbacks ); - - $args['custom_dropdown_options_source'] = wp_unslash( $args['custom_dropdown_options_source'] ); } } @@ -1291,6 +1295,17 @@ if ( ! class_exists( 'um\core\Fields' ) ) { return ''; } + public function dropdown_options_source_blacklist() { + $blacklist = array( + 'phpinfo', + 'exec', + 'passthru', + 'shell_exec', + 'system', + ); + $blacklist = apply_filters( 'um_dropdown_options_source_blacklist', $blacklist ); + return $blacklist; + } /** * Gets selected option value from a callback function @@ -1305,6 +1320,10 @@ if ( ! class_exists( 'um\core\Fields' ) ) { if ( in_array( $type, array( 'select', 'multiselect' ) ) && ! empty( $data['custom_dropdown_options_source'] ) ) { + if ( in_array( $data['custom_dropdown_options_source'], $this->dropdown_options_source_blacklist(), true ) ) { + return $value; + } + $has_custom_source = apply_filters( "um_has_dropdown_options_source__{$data['metakey']}", false ); if ( $has_custom_source ) { @@ -1372,6 +1391,10 @@ if ( ! class_exists( 'um\core\Fields' ) ) { if ( in_array( $type, array( 'select', 'multiselect' ) ) && ! empty( $data['custom_dropdown_options_source'] ) ) { + if ( in_array( $data['custom_dropdown_options_source'], $this->dropdown_options_source_blacklist(), true ) ) { + return $arr_options; + } + if ( function_exists( $data['custom_dropdown_options_source'] ) ) { if ( isset( $data['parent_dropdown_relationship'] ) ) { $arr_options = call_user_func( $data['custom_dropdown_options_source'], $data['parent_dropdown_relationship'] ); @@ -3037,7 +3060,9 @@ if ( ! class_exists( 'um\core\Fields' ) ) { if ( ! empty( $data['custom_dropdown_options_source'] ) && $has_parent_option && function_exists( $data['custom_dropdown_options_source'] ) && um_user( $data['parent_dropdown_relationship'] ) ) { - $options = call_user_func( $data['custom_dropdown_options_source'], $data['parent_dropdown_relationship'] ); + if ( ! in_array( $data['custom_dropdown_options_source'], $this->dropdown_options_source_blacklist(), true ) ) { + $options = call_user_func( $data['custom_dropdown_options_source'], $data['parent_dropdown_relationship'] ); + } $disabled_by_parent_option = ''; if ( um_user( $form_key ) ) { @@ -3053,10 +3078,11 @@ if ( ! class_exists( 'um\core\Fields' ) ) { // Child dropdown if ( $has_parent_option ) { - if ( ! empty( $data['custom_dropdown_options_source'] ) && $has_parent_option && function_exists( $data['custom_dropdown_options_source'] ) && isset( UM()->form()->post_form[ $form_key ] ) ) { - $options = call_user_func( $data['custom_dropdown_options_source'], $data['parent_dropdown_relationship'] ); + if ( ! in_array( $data['custom_dropdown_options_source'], $this->dropdown_options_source_blacklist(), true ) ) { + $options = call_user_func( $data['custom_dropdown_options_source'], $data['parent_dropdown_relationship'] ); + } } } diff --git a/includes/core/class-form.php b/includes/core/class-form.php index 3b919ede..6a529bbe 100644 --- a/includes/core/class-form.php +++ b/includes/core/class-form.php @@ -148,6 +148,13 @@ if ( ! class_exists( 'um\core\Form' ) ) { wp_send_json( $arr_options ); } + if ( in_array( $ajax_source_func, UM()->fields()->dropdown_options_source_blacklist(), true ) ) { + $arr_options['status'] = 'error'; + $arr_options['message'] = __( 'This is not possible for security reasons.', 'ultimate-member' ); + + wp_send_json( $arr_options ); + } + if ( isset( $_POST['form_id'] ) ) { UM()->fields()->set_id = absint( $_POST['form_id'] ); } diff --git a/includes/core/class-validation.php b/includes/core/class-validation.php index f318877b..6ef84de6 100644 --- a/includes/core/class-validation.php +++ b/includes/core/class-validation.php @@ -95,8 +95,10 @@ if ( ! class_exists( 'um\core\Validation' ) ) { isset( $fields[ $key ]['custom_dropdown_options_source'] ) && ! empty( $fields[ $key ]['custom_dropdown_options_source'] ) && function_exists( $fields[ $key ]['custom_dropdown_options_source'] ) ) { - $arr_options = call_user_func( $fields[ $key ]['custom_dropdown_options_source'] ); - $fields[ $key ]['options'] = array_keys( $arr_options ); + if ( ! in_array( $fields[ $key ]['custom_dropdown_options_source'], UM()->fields()->dropdown_options_source_blacklist(), true ) ) { + $arr_options = call_user_func( $fields[ $key ]['custom_dropdown_options_source'] ); + $fields[ $key ]['options'] = array_keys( $arr_options ); + } } // Unset changed value that doesn't match the option list diff --git a/includes/core/um-actions-profile.php b/includes/core/um-actions-profile.php index 12d06e13..49f01f7d 100644 --- a/includes/core/um-actions-profile.php +++ b/includes/core/um-actions-profile.php @@ -294,10 +294,12 @@ function um_user_edit_profile( $args ) { if ( isset( $array['options'] ) && in_array( $array['type'], array( 'select', 'multiselect' ) ) ) { $options = array(); - if ( ! empty( $array['custom_dropdown_options_source'] ) && function_exists( $array['custom_dropdown_options_source'] ) && ! $has_custom_source ) { - $callback_result = call_user_func( $array['custom_dropdown_options_source'], $array['options'] ); - if ( is_array( $callback_result ) ) { - $options = array_keys( $callback_result ); + if ( ! empty( $array['custom_dropdown_options_source'] ) && function_exists( $array['custom_dropdown_options_source'] ) && ! $has_custom_source ) { + if ( ! in_array( $array['custom_dropdown_options_source'], UM()->fields()->dropdown_options_source_blacklist(), true ) ) { + $callback_result = call_user_func( $array['custom_dropdown_options_source'], $array['options'] ); + if ( is_array( $callback_result ) ) { + $options = array_keys( $callback_result ); + } } } diff --git a/includes/core/um-filters-fields.php b/includes/core/um-filters-fields.php index 7bde5b92..d431a434 100644 --- a/includes/core/um-filters-fields.php +++ b/includes/core/um-filters-fields.php @@ -694,6 +694,9 @@ add_filter( 'um_field_non_utf8_value', 'um_field_non_utf8_value' ); */ function um_select_dropdown_dynamic_callback_options( $options, $data ) { if ( ! empty( $data['custom_dropdown_options_source'] ) && function_exists( $data['custom_dropdown_options_source'] ) ) { + if ( in_array( $data['custom_dropdown_options_source'], UM()->fields()->dropdown_options_source_blacklist(), true ) ) { + return $options; + } $options = call_user_func( $data['custom_dropdown_options_source'] ); } From 8cdbafe879466bee5450d96cd233d68cd9e55290 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Thu, 6 Oct 2022 15:27:51 +0300 Subject: [PATCH 06/18] - changed getting blacklist functions; --- includes/core/class-fields.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/includes/core/class-fields.php b/includes/core/class-fields.php index 0ff3768f..88a6cb35 100644 --- a/includes/core/class-fields.php +++ b/includes/core/class-fields.php @@ -1295,14 +1295,15 @@ if ( ! class_exists( 'um\core\Fields' ) ) { return ''; } + /** + * Getting the blacklist of the functions that cannot be used as callback. + * All internal PHP functions are insecure for using inside callback functions. + * + * @return array + */ public function dropdown_options_source_blacklist() { - $blacklist = array( - 'phpinfo', - 'exec', - 'passthru', - 'shell_exec', - 'system', - ); + $list = get_defined_functions(); + $blacklist = ! empty( $list['internal'] ) ? $list['internal'] : array(); $blacklist = apply_filters( 'um_dropdown_options_source_blacklist', $blacklist ); return $blacklist; } From 0c8e24a1948bd23ee70fe75c56ac70772f21f2c6 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 10 Oct 2022 14:10:32 +0300 Subject: [PATCH 07/18] - fixed #1081; --- includes/core/class-user.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/includes/core/class-user.php b/includes/core/class-user.php index 42830346..b2795d78 100644 --- a/includes/core/class-user.php +++ b/includes/core/class-user.php @@ -1641,6 +1641,11 @@ if ( ! class_exists( 'um\core\User' ) ) { function email_pending() { $this->assign_secretkey(); $this->set_status( 'awaiting_email_confirmation' ); + + //clear all sessions for email confirmation pending users + $user = \WP_Session_Tokens::get_instance( um_user( 'ID' ) ); + $user->destroy_all(); + UM()->mail()->send( um_user( 'user_email' ), 'checkmail_email' ); } @@ -1662,6 +1667,11 @@ if ( ! class_exists( 'um\core\User' ) ) { */ function pending() { $this->set_status( 'awaiting_admin_review' ); + + //clear all sessions for awaiting admin confirmation users + $user = \WP_Session_Tokens::get_instance( um_user( 'ID' ) ); + $user->destroy_all(); + UM()->mail()->send( um_user( 'user_email' ), 'pending_email' ); } @@ -1684,6 +1694,11 @@ if ( ! class_exists( 'um\core\User' ) ) { */ function reject() { $this->set_status( 'rejected' ); + + //clear all sessions for rejected users + $user = \WP_Session_Tokens::get_instance( um_user( 'ID' ) ); + $user->destroy_all(); + UM()->mail()->send( um_user( 'user_email' ), 'rejected_email' ); } @@ -1705,6 +1720,11 @@ if ( ! class_exists( 'um\core\User' ) ) { */ function deactivate() { $this->set_status( 'inactive' ); + + //clear all sessions for inactive users + $user = \WP_Session_Tokens::get_instance( um_user( 'ID' ) ); + $user->destroy_all(); + /** * UM hook * From 6746f0ce03f96fec8bac41f596e62b94d678bf89 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 10 Oct 2022 15:29:30 +0300 Subject: [PATCH 08/18] - fixed directory checking for localhosts; --- includes/core/class-shortcodes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/core/class-shortcodes.php b/includes/core/class-shortcodes.php index fdc5ec7a..cd7fa823 100644 --- a/includes/core/class-shortcodes.php +++ b/includes/core/class-shortcodes.php @@ -285,8 +285,8 @@ if ( ! class_exists( 'um\core\Shortcodes' ) ) { if ( file_exists( $file ) ) { // Avoid Directory Traversal vulnerability by the checking the realpath. // Templates can be situated only in the get_stylesheet_directory() or plugindir templates. - $real_file = realpath( $file ); - if ( 0 === strpos( $real_file, um_path . "templates" . DIRECTORY_SEPARATOR ) || 0 === strpos( $real_file, get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'ultimate-member' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR ) ) { + $real_file = wp_normalize_path( realpath( $file ) ); + if ( 0 === strpos( $real_file, wp_normalize_path( um_path . "templates" . DIRECTORY_SEPARATOR ) ) || 0 === strpos( $real_file, wp_normalize_path( get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'ultimate-member' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR ) ) ) { include $file; } } From a93970aa56ce20885c75175e8afad3fd5ecbf7a3 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 10 Oct 2022 17:11:21 +0300 Subject: [PATCH 09/18] - probably fixed #1078; --- includes/core/class-access.php | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/includes/core/class-access.php b/includes/core/class-access.php index c85eb2ee..a53c13ca 100644 --- a/includes/core/class-access.php +++ b/includes/core/class-access.php @@ -48,23 +48,10 @@ if ( ! class_exists( 'um\core\Access' ) ) { $this->allow_access = false; // NEW HOOKS - - // Change recent posts widget query - add_filter( 'widget_posts_args', array( &$this, 'exclude_restricted_posts_widget' ), 99, 1 ); - // Exclude pages displayed by wp_list_pages function - add_filter( 'wp_list_pages_excludes', array( &$this, 'exclude_restricted_pages' ), 10, 1 ); - // Archives list change where based on restricted posts - add_filter( 'getarchives_where', array( &$this, 'exclude_restricted_posts_archives_widget' ), 99, 2 ); - // Navigation line below the post content, change query to exclude restricted add_filter( 'get_next_post_where', array( &$this, 'exclude_navigation_posts' ), 99, 5 ); add_filter( 'get_previous_post_where', array( &$this, 'exclude_navigation_posts' ), 99, 5 ); - // callbacks for changing posts query - add_action( 'pre_get_posts', array( &$this, 'exclude_posts' ), 99, 1 ); - add_filter( 'posts_where', array( &$this, 'exclude_posts_where' ), 10, 2 ); - add_filter( 'wp_count_posts', array( &$this, 'custom_count_posts_handler' ), 99, 3 ); - // change the title of the post add_filter( 'the_title', array( &$this, 'filter_restricted_post_title' ), 10, 2 ); // change the content of the restricted post @@ -111,7 +98,22 @@ if ( ! class_exists( 'um\core\Access' ) ) { * Rollback function for old business logic to avoid security enhancements with 404 errors */ function disable_restriction_pre_queries() { - // callbacks for changing terms query + // Using inside plugins_loaded hook because of there can be earlier direct queries without hooks. + // Avoid using to not getting fatal error for not exists WordPress native functions. + + // Change recent posts widget query. + add_filter( 'widget_posts_args', array( &$this, 'exclude_restricted_posts_widget' ), 99, 1 ); + // Exclude pages displayed by wp_list_pages function. + add_filter( 'wp_list_pages_excludes', array( &$this, 'exclude_restricted_pages' ), 10, 1 ); + // Archives list change where based on restricted posts. + add_filter( 'getarchives_where', array( &$this, 'exclude_restricted_posts_archives_widget' ), 99, 2 ); + + // Callbacks for changing posts query. + add_action( 'pre_get_posts', array( &$this, 'exclude_posts' ), 99, 1 ); + add_filter( 'posts_where', array( &$this, 'exclude_posts_where' ), 10, 2 ); + add_filter( 'wp_count_posts', array( &$this, 'custom_count_posts_handler' ), 99, 3 ); + + // Callbacks for changing terms query. add_action( 'pre_get_terms', array( &$this, 'exclude_hidden_terms_query' ), 99, 1 ); if ( ! UM()->options()->get( 'disable_restriction_pre_queries' ) ) { From 5db8232202980781a64b3b2d1286c8120e3f259d Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 10 Oct 2022 17:42:52 +0300 Subject: [PATCH 10/18] - fixed restriction post displaying when 404 is enabled and old restiction logic isn't active; --- includes/core/class-access.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/core/class-access.php b/includes/core/class-access.php index a53c13ca..a056c1ed 100644 --- a/includes/core/class-access.php +++ b/includes/core/class-access.php @@ -1138,7 +1138,7 @@ if ( ! class_exists( 'um\core\Access' ) ) { continue; } else { $restriction_settings = $this->get_post_privacy_settings( $menu_item->object_id ); - if ( empty( $restriction_settings['_um_access_hide_from_queries'] ) || ! UM()->options()->get( 'disable_restriction_pre_queries' ) ) { + if ( empty( $restriction_settings['_um_access_hide_from_queries'] ) || UM()->options()->get( 'disable_restriction_pre_queries' ) ) { $filtered_items[] = $this->maybe_replace_nav_menu_title( $menu_item ); continue; } @@ -1416,7 +1416,7 @@ if ( ! class_exists( 'um\core\Access' ) ) { } } } else { - if ( empty( $restriction['_um_access_hide_from_queries'] ) || ! UM()->options()->get( 'disable_restriction_pre_queries' ) ) { + if ( empty( $restriction['_um_access_hide_from_queries'] ) || UM()->options()->get( 'disable_restriction_pre_queries' ) ) { $filtered_posts[] = $this->maybe_replace_title( $post ); continue; } From c8fac38bb96c34df18e34bbf2a28022e45f702c8 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 10 Oct 2022 17:57:44 +0300 Subject: [PATCH 11/18] - fixed #1077; --- includes/core/um-filters-navmenu.php | 35 +++++++++++++++------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/includes/core/um-filters-navmenu.php b/includes/core/um-filters-navmenu.php index 2390734f..a6ff193f 100644 --- a/includes/core/um-filters-navmenu.php +++ b/includes/core/um-filters-navmenu.php @@ -20,13 +20,13 @@ if ( ! is_admin() ) { } foreach ( $sorted_menu_items as &$menu_item ) { - if ( $menu_item->title ) { + if ( ! empty( $menu_item->title ) ) { $menu_item->title = UM()->shortcodes()->convert_user_tags( $menu_item->title ); } - if ( $menu_item->attr_title ) { + if ( ! empty( $menu_item->attr_title ) ) { $menu_item->attr_title = UM()->shortcodes()->convert_user_tags( $menu_item->attr_title ); } - if ( $menu_item->description ) { + if ( ! empty( $menu_item->description ) ) { $menu_item->description = UM()->shortcodes()->convert_user_tags( $menu_item->description ); } } @@ -56,19 +56,22 @@ if ( ! is_admin() ) { um_fetch_user( get_current_user_id() ); - $filtered_items = array(); + $filtered_items = array(); $hide_children_of = array(); //other filter foreach ( $menu_items as $item ) { + if ( empty( $item->ID ) ) { + continue; + } - $mode = get_post_meta( $item->ID, 'menu-item-um_nav_public', true ); + $mode = get_post_meta( $item->ID, 'menu-item-um_nav_public', true ); $roles = get_post_meta( $item->ID, 'menu-item-um_nav_roles', true ); $visible = true; // hide any item that is the child of a hidden item - if ( in_array( $item->menu_item_parent, $hide_children_of ) ) { + if ( isset( $item->menu_item_parent ) && in_array( $item->menu_item_parent, $hide_children_of ) ) { $visible = false; $hide_children_of[] = $item->ID; // for nested menus } @@ -79,16 +82,16 @@ if ( ! is_admin() ) { case 2: if ( is_user_logged_in() && ! empty( $roles ) ) { - if ( current_user_can( 'administrator' ) ) { - $visible = true; - } else { - $current_user_roles = um_user( 'roles' ); - if ( empty( $current_user_roles ) ) { - $visible = false; - } else { - $visible = ( count( array_intersect( $current_user_roles, (array)$roles ) ) > 0 ) ? true : false; - } - } + if ( current_user_can( 'administrator' ) ) { + $visible = true; + } else { + $current_user_roles = um_user( 'roles' ); + if ( empty( $current_user_roles ) ) { + $visible = false; + } else { + $visible = ( count( array_intersect( $current_user_roles, (array)$roles ) ) > 0 ) ? true : false; + } + } } else { $visible = is_user_logged_in() ? true : false; } From 26b54a48c4586d5be468bf519b165f478acf4cb1 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 10 Oct 2022 18:02:10 +0300 Subject: [PATCH 12/18] - probably fixed #1075; --- includes/core/class-access.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/includes/core/class-access.php b/includes/core/class-access.php index a056c1ed..921b981d 100644 --- a/includes/core/class-access.php +++ b/includes/core/class-access.php @@ -78,13 +78,6 @@ if ( ! class_exists( 'um\core\Access' ) ) { // Gutenberg blocks restrictions add_filter( 'render_block', array( $this, 'restrict_blocks' ), 10, 2 ); - // there is posts (Posts/Page/CPT) filtration if site is accessible - // there also will be redirects if they need - // protect posts types - add_filter( 'the_posts', array( &$this, 'filter_protected_posts' ), 99, 2 ); - // protect pages for wp_list_pages func - add_filter( 'get_pages', array( &$this, 'filter_protected_posts' ), 99, 2 ); - // check the site's accessible more priority have Individual Post/Term Restriction settings add_action( 'template_redirect', array( &$this, 'template_redirect' ), 1000 ); add_action( 'um_access_check_individual_term_settings', array( &$this, 'um_access_check_individual_term_settings' ) ); @@ -116,6 +109,13 @@ if ( ! class_exists( 'um\core\Access' ) ) { // Callbacks for changing terms query. add_action( 'pre_get_terms', array( &$this, 'exclude_hidden_terms_query' ), 99, 1 ); + // there is posts (Posts/Page/CPT) filtration if site is accessible + // there also will be redirects if they need + // protect posts types + add_filter( 'the_posts', array( &$this, 'filter_protected_posts' ), 99, 2 ); + // protect pages for wp_list_pages func + add_filter( 'get_pages', array( &$this, 'filter_protected_posts' ), 99, 2 ); + if ( ! UM()->options()->get( 'disable_restriction_pre_queries' ) ) { return; } From 98d416deaa8668f82508e21414855841736a1433 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 10 Oct 2022 18:48:16 +0300 Subject: [PATCH 13/18] - ignore auto_login if the user is already logged in. Fixed #1070; --- includes/core/um-actions-register.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/core/um-actions-register.php b/includes/core/um-actions-register.php index 604f3177..20859e01 100644 --- a/includes/core/um-actions-register.php +++ b/includes/core/um-actions-register.php @@ -178,8 +178,11 @@ function um_check_user_status( $user_id, $args ) { do_action( "track_{$status}_user_registration" ); if ( $status == 'approved' ) { - - UM()->user()->auto_login( $user_id ); + // Check if user is logged in because there can be the customized way when through 'um_registration_for_loggedin_users' hook the registration is enabled for the logged in users (e.g. Administrator). + if ( ! is_user_logged_in() ) { + // Custom way if 'um_registration_for_loggedin_users' hook after custom callbacks returns true. Then don't make auto-login because user is already logged-in. + UM()->user()->auto_login( $user_id ); + } UM()->user()->generate_profile_slug( $user_id ); /** From e6b4d66b447fd2158ccd329fa12b118aed9e49dd Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 10 Oct 2022 19:10:58 +0300 Subject: [PATCH 14/18] - fixed #1071 typo; --- includes/core/class-uploader.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/core/class-uploader.php b/includes/core/class-uploader.php index b8735dc3..49cea88c 100644 --- a/includes/core/class-uploader.php +++ b/includes/core/class-uploader.php @@ -786,14 +786,14 @@ if ( ! class_exists( 'um\core\Uploader' ) ) { if ( isset( $image_info['invalid_image'] ) && $image_info['invalid_image'] == true ) { $error = sprintf(__('Your image is invalid or too large!','ultimate-member') ); - } elseif ( isset($data['min_size']) && ( $image_info['size'] < $data['min_size'] ) ) { + } elseif ( isset( $data['min_size'] ) && ( $image_info['size'] < $data['min_size'] ) ) { $error = $data['min_size_error']; - } elseif ( isset($data['max_file_size']) && ( $image_info['size'] > $data['max_file_size'] ) ) { + } elseif ( isset( $data['max_file_size'] ) && ( $image_info['size'] > $data['max_file_size'] ) ) { $error = $data['max_file_size_error']; - } elseif ( isset($data['min_width']) && ( $image_info['width'] < $data['min_width'] ) ) { - $error = sprintf(__('Your photo is too small. It must be at least %spx wide.','ultimate-member'), $data['min_width']); - } elseif ( isset($data['min_height']) && ( $image_info['height'] < $data['min_height'] ) ) { - $error = sprintf(__('Your photo is too small. It must be at least %spx wide.','ultimate-member'), $data['min_height']); + } elseif ( isset( $data['min_width'] ) && ( $image_info['width'] < $data['min_width'] ) ) { + $error = sprintf( __( 'Your photo is too small. It must be at least %spx wide.', 'ultimate-member' ), $data['min_width'] ); + } elseif ( isset( $data['min_height'] ) && ( $image_info['height'] < $data['min_height'] ) ) { + $error = sprintf( __( 'Your photo is too small. It must be at least %spx high.', 'ultimate-member' ), $data['min_height'] ); } return $error; From b547d899bce24ef4152eed6d8478d3f3aaf53727 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 24 Oct 2022 14:29:04 +0300 Subject: [PATCH 15/18] - removed outdated setting using in code (force_display_name_capitlized). Moved the functionality to extended repo: https://github.com/ultimatemember/Extended/tree/main/um-capitalize-name#readme --- includes/admin/core/class-admin-settings.php | 1 - includes/um-short-functions.php | 17 +---------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/includes/admin/core/class-admin-settings.php b/includes/admin/core/class-admin-settings.php index 34d3d515..a37ddbee 100644 --- a/includes/admin/core/class-admin-settings.php +++ b/includes/admin/core/class-admin-settings.php @@ -3172,7 +3172,6 @@ do_action( "um_install_info_after_page_config" ); ?> Default New User Role: options()->get('register_role') . "\n"; ?> Profile Permalink Base: options()->get('permalink_base') . "\n"; ?> User Display Name: options()->get('display_name') . "\n"; ?> -Force Name to Uppercase: info_value( UM()->options()->get('force_display_name_capitlized'), 'yesno', true ); ?> Redirect author to profile: info_value( UM()->options()->get('author_redirect'), 'yesno', true ); ?> Enable Members Directory: info_value( UM()->options()->get('members_page'), 'yesno', true ); ?> Use Gravatars: info_value( UM()->options()->get('use_gravatars'), 'yesno', true ); ?> diff --git a/includes/um-short-functions.php b/includes/um-short-functions.php index 4388bae4..a35e69b1 100644 --- a/includes/um-short-functions.php +++ b/includes/um-short-functions.php @@ -2306,10 +2306,6 @@ function um_user( $data, $attrs = null ) { $name = um_profile( $data ); - if ( UM()->options()->get( 'force_display_name_capitlized' ) ) { - $name = implode( '-', array_map( 'ucfirst', explode( '-', $name ) ) ); - } - /** * UM hook * @@ -2366,14 +2362,7 @@ function um_user( $data, $attrs = null ) { $f_and_l_initial = um_profile( $data ); } - $f_and_l_initial = UM()->validation()->safe_name_in_url( $f_and_l_initial ); - - if ( UM()->options()->get( 'force_display_name_capitlized' ) ) { - $name = implode( '-', array_map( 'ucfirst', explode( '-', $f_and_l_initial ) ) ); - } else { - $name = $f_and_l_initial; - } - + $name = UM()->validation()->safe_name_in_url( $f_and_l_initial ); return $name; break; @@ -2459,10 +2448,6 @@ function um_user( $data, $attrs = null ) { } } - if ( UM()->options()->get( 'force_display_name_capitlized' ) ) { - $name = implode( '-', array_map( 'ucfirst', explode( '-', $name ) ) ); - } - /** * UM hook * From af13de140affb856ecc9b0664c5fed9ff16fc4cb Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 24 Oct 2022 16:22:04 +0300 Subject: [PATCH 16/18] - fixed using "'" symbols in emails. There is possible to register with it and login after that. Closed #1059; --- includes/core/class-form.php | 2 +- includes/core/class-user.php | 2 +- includes/core/um-actions-form.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/core/class-form.php b/includes/core/class-form.php index 6a529bbe..d6494d9b 100644 --- a/includes/core/class-form.php +++ b/includes/core/class-form.php @@ -445,7 +445,7 @@ if ( ! class_exists( 'um\core\Form' ) ) { * } * ?> */ - $this->post_form = apply_filters( 'um_submit_post_form', $_POST ); + $this->post_form = apply_filters( 'um_submit_post_form', wp_unslash( $_POST ) ); if ( isset( $this->post_form[ UM()->honeypot ] ) && '' !== $this->post_form[ UM()->honeypot ] ) { wp_die( esc_html__( 'Hello, spam bot!', 'ultimate-member' ) ); diff --git a/includes/core/class-user.php b/includes/core/class-user.php index b2795d78..fae8e09d 100644 --- a/includes/core/class-user.php +++ b/includes/core/class-user.php @@ -2107,7 +2107,7 @@ if ( ! class_exists( 'um\core\User' ) ) { update_user_meta( $this->id, $key, $value ); } } else { - $args[ $key ] = esc_attr( $changes[ $key ] ); + $args[ $key ] = $changes[ $key ]; } } diff --git a/includes/core/um-actions-form.php b/includes/core/um-actions-form.php index 1aefd617..61531d60 100644 --- a/includes/core/um-actions-form.php +++ b/includes/core/um-actions-form.php @@ -806,7 +806,7 @@ function um_submit_form_errors_hook_( $args ) { $args['user_id'] = um_get_requested_user(); } - $email_exists = email_exists( $args[ $key ] ); + $email_exists = email_exists( $args[ $key ] ); if ( $args[ $key ] == '' && in_array( $key, array( 'user_email' ) ) ) { UM()->form()->add_error( $key, __( 'You must provide your email', 'ultimate-member' ) ); From fac2f9fdc5187e84dd635de5456964d805c621cc Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 24 Oct 2022 17:34:54 +0300 Subject: [PATCH 17/18] - fixed #1085; --- includes/core/class-builtin.php | 31 +++++++++--------- includes/core/class-fields.php | 5 +-- includes/core/class-form.php | 52 ++++++++++++++++++++++++------- includes/core/um-actions-form.php | 2 +- 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/includes/core/class-builtin.php b/includes/core/class-builtin.php index 8a507fe0..a296437a 100644 --- a/includes/core/class-builtin.php +++ b/includes/core/class-builtin.php @@ -1058,21 +1058,24 @@ if ( ! class_exists( 'um\core\Builtin' ) ) { ), 'youtube' => array( - 'title' => __('YouTube','ultimate-member'), - 'metakey' => 'youtube', - 'type' => 'url', - 'label' => __('YouTube','ultimate-member'), - 'required' => 0, - 'public' => 1, - 'editable' => 1, + 'title' => __( 'YouTube', 'ultimate-member' ), + 'metakey' => 'youtube', + 'type' => 'url', + 'label' => __( 'YouTube', 'ultimate-member' ), + 'required' => 0, + 'public' => 1, + 'editable' => 1, 'url_target' => '_blank', - 'url_rel' => 'nofollow', - 'icon' => 'um-faicon-youtube', - 'validate' => 'youtube_url', - 'url_text' => 'YouTube', - 'advanced' => 'social', - 'color' => '#e52d27', - 'match' => 'https://youtube.com/', + 'url_rel' => 'nofollow', + 'icon' => 'um-faicon-youtube', + 'validate' => 'youtube_url', + 'url_text' => __( 'YouTube', 'ultimate-member' ), + 'advanced' => 'social', + 'color' => '#e52d27', + 'match' => array( + 'https://youtube.com/', + 'https://youtu.be/', + ), ), 'soundcloud' => array( diff --git a/includes/core/class-fields.php b/includes/core/class-fields.php index 88a6cb35..bb2e172c 100644 --- a/includes/core/class-fields.php +++ b/includes/core/class-fields.php @@ -86,9 +86,10 @@ if ( ! class_exists( 'um\core\Fields' ) ) { } foreach ( $social as $k => $arr ) { - if ( um_profile( $k ) ) { ?> + if ( um_profile( $k ) ) { + $match = is_array( $arr['match'] ) ? $arr['match'][0] : $arr['match']; ?> - diff --git a/includes/core/class-form.php b/includes/core/class-form.php index d6494d9b..ab12b1ca 100644 --- a/includes/core/class-form.php +++ b/includes/core/class-form.php @@ -657,19 +657,47 @@ if ( ! class_exists( 'um\core\Form' ) ) { $v = sanitize_text_field( $form[ $k ] ); // Make a proper social link - if ( ! empty( $v ) && ! strstr( $v, $f['match'] ) ) { - $domain = trim( strtr( $f['match'], array( - 'https://' => '', - 'http://' => '', - ) ), ' /' ); + if ( ! empty( $v ) ) { + $replace_match = is_array( $f['match'] ) ? $f['match'][0] : $f['match']; - if ( ! strstr( $v, $domain ) ) { - $v = $f['match'] . $v; - } else { - $v = 'https://' . trim( strtr( $v, array( - 'https://' => '', - 'http://' => '', - ) ), ' /' ); + $need_replace = false; + if ( is_array( $f['match'] ) ) { + $need_replace = true; + foreach ( $f['match'] as $arr_match ) { + if ( strstr( $v, $arr_match ) ) { + $need_replace = false; + } + } + } + + if ( ! is_array( $f['match'] ) || $need_replace ) { + if ( ! strstr( $v, $replace_match ) ) { + $domain = trim( + strtr( + $replace_match, + array( + 'https://' => '', + 'http://' => '', + ) + ), + ' /' + ); + + if ( ! strstr( $v, $domain ) ) { + $v = $replace_match . $v; + } else { + $v = 'https://' . trim( + strtr( + $v, + array( + 'https://' => '', + 'http://' => '', + ) + ), + ' /' + ); + } + } } } diff --git a/includes/core/um-actions-form.php b/includes/core/um-actions-form.php index 61531d60..15b1ae94 100644 --- a/includes/core/um-actions-form.php +++ b/includes/core/um-actions-form.php @@ -702,7 +702,7 @@ function um_submit_form_errors_hook_( $args ) { break; case 'youtube_url': - if ( ! UM()->validation()->is_url( $args[ $key ], 'youtube.com' ) ) { + if ( ! UM()->validation()->is_url( $args[ $key ], 'youtube.com' ) && ! UM()->validation()->is_url( $args[ $key ], 'youtu.be' ) ) { UM()->form()->add_error( $key, sprintf( __( 'Please enter a valid %s username or profile URL', 'ultimate-member' ), $array['label'] ) ); } break; From 8ff70c000322d18f8982583db0ddfd911bceb158 Mon Sep 17 00:00:00 2001 From: Nikita Sinelnikov Date: Mon, 24 Oct 2022 18:15:04 +0300 Subject: [PATCH 18/18] - updated readme files and prepare for release; --- README.md | 2 +- readme.txt | 25 +++++++++++++++++++++++-- ultimate-member.php | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c90329c9..d6e1431e 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ GNU Version 2 or Any Later Version ## Releases -[Official Release Version: 2.5.0](https://github.com/ultimatemember/ultimatemember/releases/tag/2.5.0). +[Official Release Version: 2.5.1](https://github.com/ultimatemember/ultimatemember/releases/tag/2.5.1). ## Changelog diff --git a/readme.txt b/readme.txt index 06ed0531..f53a6dca 100644 --- a/readme.txt +++ b/readme.txt @@ -7,7 +7,7 @@ Tags: community, member, membership, user-profile, user-registration Requires PHP: 5.6 Requires at least: 5.0 Tested up to: 6.0 -Stable tag: 2.5.0 +Stable tag: 2.5.1 License: GNU Version 2 or Any Later Version License URI: http://www.gnu.org/licenses/gpl-3.0.txt @@ -163,9 +163,30 @@ No, you do not need to use our plugin’s login or registration pages and can us * To learn more about version 2.1 please see this [docs](https://docs.ultimatemember.com/article/1512-upgrade-2-1-0) * UM2.1+ is a significant update to the Member Directories' code base from 2.0.x. Please make sure you take a full-site backup with restore point before updating the plugin -= 2.5.1: September xx, 2022 = += 2.5.1: October 26, 2022 = +* Enhancements: + - Added: Custom fields callbacks blacklist. Use `um_dropdown_options_source_blacklist` filter for adding your custom functions to the custom callbacks blacklist. By default there are all PHP internal functions. + +* Bugfixes: + + - Fixed: Posts' restriction that is based on term restriction settings + - Fixed: Issue with class name in checkbox and radio. Class name being 'activeright' instead of 'active right' + - Fixed: Admin upgrade scripts and upgrades pack validation + - Fixed: Directory traversal vulnerabilities + - Fixed: Destroying user sessions after changing "Approved" status to something else (e.g. deactivated) + - Fixed: Conflict when `wp_get_current_user()` not exists. Transferred restriction settings callbacks to `plugins_loaded` hook + - Fixed: Restriction post displaying when 404 is enabled and old restiction logic isn't active + - Fixed: PHP warning when nav menu is empty + - Fixed: Disable auto-login after user is registered by Administrator and UM Registration form + - Fixed: Some typos errors + - Fixed: Using an apostrophe symbols in emails for registration and login both + - Fixed: Sanitizing YouTube links. Applying both https://youtu.be/xxxxxxx and https://youtube.com/xxxxxxx links + +* Deprecated: + + - Removed: Outdated setting using in code (force_display_name_capitlized). Moved the functionality to extended [repo](https://github.com/ultimatemember/Extended/tree/main/um-capitalize-name#readme) = 2.5.0: August 17, 2022 = diff --git a/ultimate-member.php b/ultimate-member.php index a0f8ba9d..a1526ef6 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.5.1-alpha1 +Version: 2.5.1 Author: Ultimate Member Author URI: http://ultimatemember.com/ Text Domain: ultimate-member