diff --git a/.wordpress-org/icon-128x128.png b/.wordpress-org/icon-128x128.png index 95e491b6..6add64c7 100644 Binary files a/.wordpress-org/icon-128x128.png and b/.wordpress-org/icon-128x128.png differ diff --git a/.wordpress-org/icon-256x256.png b/.wordpress-org/icon-256x256.png index b2e8dbd1..b02cf1a5 100644 Binary files a/.wordpress-org/icon-256x256.png and b/.wordpress-org/icon-256x256.png differ diff --git a/README.md b/README.md index 09b1cb0a..635bd8b9 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ GNU Version 2 or Any Later Version ## Releases -[Official Release Version: 2.3.2](https://github.com/ultimatemember/ultimatemember/releases/tag/2.3.2). +[Official Release Version: 2.4.0](https://github.com/ultimatemember/ultimatemember/releases/tag/2.4.0). ## Changelog diff --git a/includes/admin/core/class-admin-settings.php b/includes/admin/core/class-admin-settings.php index bd8e0b67..92824b72 100644 --- a/includes/admin/core/class-admin-settings.php +++ b/includes/admin/core/class-admin-settings.php @@ -763,6 +763,12 @@ if ( ! class_exists( 'um\admin\core\Admin_Settings' ) ) { 'blocked_words' => array( 'sanitize' => 'textarea', ), + 'allowed_choice_callbacks' => array( + 'sanitize' => 'textarea', + ), + 'allow_url_redirect_confirm' => array( + 'sanitize' => 'bool', + ), 'admin_email' => array( 'sanitize' => 'text', ), @@ -1289,6 +1295,18 @@ if ( ! class_exists( 'um\admin\core\Admin_Settings' ) ) { 'label' => __( 'Blacklist Words (Enter one word per line)', 'ultimate-member' ), 'tooltip' => __( 'This option lets you specify blacklist of words to prevent anyone from signing up with such a word as their username', 'ultimate-member' ), ), + array( + 'id' => 'allowed_choice_callbacks', + 'type' => 'textarea', + 'label' => __( 'Allowed Choice Callbacks (Enter one PHP function per line)', 'ultimate-member' ), + 'tooltip' => __( 'This option lets you specify the choice callback functions to prevent anyone from using 3rd-party functions that may put your site at risk.', 'ultimate-member' ), + ), + array( + 'id' => 'allow_url_redirect_confirm', + 'type' => 'checkbox', + 'label' => __( 'Allow external link redirect confirm', 'ultimate-member' ), + 'tooltip' => __( 'Using JS.confirm alert when you go to an external link.', 'ultimate-member' ), + ), ), ), ), diff --git a/includes/admin/core/packages/2.4.0/functions.php b/includes/admin/core/packages/2.4.0/functions.php new file mode 100644 index 00000000..2e498db9 --- /dev/null +++ b/includes/admin/core/packages/2.4.0/functions.php @@ -0,0 +1,50 @@ +admin()->check_ajax_nonce(); + + um_maybe_unset_time_limit(); + + $functions = array(); + // hardcoded for UM:Woocommerce function + if ( function_exists( 'um_woo_directory_get_states' ) ) { + $functions[] = 'um_woo_directory_get_states'; + } + + $custom_fields = get_option( 'um_fields', array() ); + foreach ( $custom_fields as $custom_field ) { + if ( array_key_exists( 'custom_dropdown_options_source', $custom_field ) && function_exists( $custom_field['custom_dropdown_options_source'] ) ) { + $functions[] = $custom_field['custom_dropdown_options_source']; + } + } + + $forms_query = new WP_Query; + $forms = $forms_query->query( array( + 'post_type' => 'um_form', + 'posts_per_page' => -1, + 'fields' => 'ids', + ) ); + + foreach ( $forms as $form_id ) { + $forms_fields = get_post_meta( $form_id, '_um_custom_fields', true ); + if ( ! is_array( $forms_fields ) ) { + continue; + } + + foreach ( $forms_fields as $key => $field ) { + if ( array_key_exists( 'custom_dropdown_options_source', $field ) && function_exists( $field['custom_dropdown_options_source'] ) ) { + $functions[] = $field['custom_dropdown_options_source']; + } + } + } + + $functions = array_unique( $functions ); + $functions = implode( "\r\n", $functions ); + UM()->options()->update( 'allowed_choice_callbacks', $functions ); + + // delete temporarily option for fields upgrade + update_option( 'um_last_version_upgrade', '2.4.0' ); + + wp_send_json_success( array( 'message' => __( 'Custom callback functions whitelisted for 2.4.0 version.', 'ultimate-member' ) ) ); +} diff --git a/includes/admin/core/packages/2.4.0/hooks.php b/includes/admin/core/packages/2.4.0/hooks.php new file mode 100644 index 00000000..1a2371a1 --- /dev/null +++ b/includes/admin/core/packages/2.4.0/hooks.php @@ -0,0 +1,5 @@ + 'choice_callbacks240', +); diff --git a/includes/admin/core/packages/2.4.0/init.php b/includes/admin/core/packages/2.4.0/init.php new file mode 100644 index 00000000..adccb90e --- /dev/null +++ b/includes/admin/core/packages/2.4.0/init.php @@ -0,0 +1,30 @@ + + + + diff --git a/includes/class-config.php b/includes/class-config.php index 0cc2a9b9..5938a261 100644 --- a/includes/class-config.php +++ b/includes/class-config.php @@ -555,6 +555,8 @@ if ( ! class_exists( 'um\Config' ) ) { 'reset_password_limit_number' => 3, 'blocked_emails' => '', 'blocked_words' => 'admin' . "\r\n" . 'administrator' . "\r\n" . 'webmaster' . "\r\n" . 'support' . "\r\n" . 'staff', + 'allowed_choice_callbacks' => '', + 'allow_url_redirect_confirm' => 1, 'default_avatar' => '', 'default_cover' => '', 'disable_profile_photo_upload' => 0, diff --git a/includes/core/class-fields.php b/includes/core/class-fields.php index f6a1f7dd..55db4a71 100644 --- a/includes/core/class-fields.php +++ b/includes/core/class-fields.php @@ -144,6 +144,20 @@ if ( ! class_exists( 'um\core\Fields' ) ) { $fields[ $id ] = $args; + if ( array_key_exists( 'custom_dropdown_options_source', $args ) && function_exists( $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 ); + + UM()->options()->update( 'allowed_choice_callbacks', $allowed_callbacks ); + } + unset( $fields[ $id ]['in_row'] ); unset( $fields[ $id ]['in_sub_row'] ); unset( $fields[ $id ]['in_column'] ); @@ -185,6 +199,20 @@ if ( ! class_exists( 'um\core\Fields' ) ) { $fields[ $id ] = $args; + if ( array_key_exists( 'custom_dropdown_options_source', $args ) && function_exists( $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 ); + + UM()->options()->update( 'allowed_choice_callbacks', $allowed_callbacks ); + } + // for group field only if ( $args['type'] == 'group' ) { $fields[ $id ]['in_group'] = ''; diff --git a/includes/core/class-form.php b/includes/core/class-form.php index d70c989f..6e47d9c5 100644 --- a/includes/core/class-form.php +++ b/includes/core/class-form.php @@ -114,6 +114,39 @@ if ( ! class_exists( 'um\core\Form' ) ) { $arr_options['status'] = 'success'; $arr_options['post'] = $_POST; + // Callback validation + if ( empty( $_POST['child_callback'] ) ) { + $arr_options['status'] = 'error'; + $arr_options['message'] = __( 'Wrong callback.', 'ultimate-member' ); + + wp_send_json( $arr_options ); + } + + $ajax_source_func = sanitize_text_field( $_POST['child_callback'] ); + + if ( ! function_exists( $ajax_source_func ) ) { + $arr_options['status'] = 'error'; + $arr_options['message'] = __( 'Wrong callback.', 'ultimate-member' ); + + wp_send_json( $arr_options ); + } + + $allowed_callbacks = UM()->options()->get( 'allowed_choice_callbacks' ); + if ( empty( $allowed_callbacks ) ) { + $arr_options['status'] = 'error'; + $arr_options['message'] = __( 'This is not possible for security reasons.', 'ultimate-member' ); + wp_send_json( $arr_options ); + } + + $allowed_callbacks = array_map( 'rtrim', explode( "\n", $allowed_callbacks ) ); + + if ( ! in_array( $ajax_source_func, $allowed_callbacks, 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'] ); } @@ -122,37 +155,34 @@ if ( ! class_exists( 'um\core\Form' ) ) { $arr_options['fields'] = $form_fields; if ( isset( $arr_options['post']['members_directory'] ) && 'yes' === $arr_options['post']['members_directory'] ) { - $ajax_source_func = $_POST['child_callback']; - if ( function_exists( $ajax_source_func ) ) { - global $wpdb; + global $wpdb; - $values_array = $wpdb->get_col( - $wpdb->prepare( - "SELECT DISTINCT meta_value - FROM $wpdb->usermeta - WHERE meta_key = %s AND - meta_value != ''", - $arr_options['post']['child_name'] - ) - ); + $values_array = $wpdb->get_col( + $wpdb->prepare( + "SELECT DISTINCT meta_value + FROM $wpdb->usermeta + WHERE meta_key = %s AND + meta_value != ''", + $arr_options['post']['child_name'] + ) + ); - if ( ! empty( $values_array ) ) { - $parent_dropdown = isset( $arr_options['field']['parent_dropdown_relationship'] ) ? $arr_options['field']['parent_dropdown_relationship'] : ''; - $arr_options['items'] = call_user_func( $ajax_source_func, $parent_dropdown ); + if ( ! empty( $values_array ) ) { + $parent_dropdown = isset( $arr_options['field']['parent_dropdown_relationship'] ) ? $arr_options['field']['parent_dropdown_relationship'] : ''; + $arr_options['items'] = call_user_func( $ajax_source_func, $parent_dropdown ); - if ( array_keys( $arr_options['items'] ) !== range( 0, count( $arr_options['items'] ) - 1 ) ) { - // array with dropdown items is associative - $arr_options['items'] = array_intersect_key( array_map( 'trim', $arr_options['items'] ), array_flip( $values_array ) ); - } else { - // array with dropdown items has sequential numeric keys, starting from 0 and there are intersected values with $values_array - $arr_options['items'] = array_intersect( $arr_options['items'], $values_array ); - } + if ( array_keys( $arr_options['items'] ) !== range( 0, count( $arr_options['items'] ) - 1 ) ) { + // array with dropdown items is associative + $arr_options['items'] = array_intersect_key( array_map( 'trim', $arr_options['items'] ), array_flip( $values_array ) ); } else { - $arr_options['items'] = array(); + // array with dropdown items has sequential numeric keys, starting from 0 and there are intersected values with $values_array + $arr_options['items'] = array_intersect( $arr_options['items'], $values_array ); } - - wp_send_json( $arr_options ); + } else { + $arr_options['items'] = array(); } + + wp_send_json( $arr_options ); } else { /** * UM hook @@ -184,9 +214,6 @@ if ( ! class_exists( 'um\core\Form' ) ) { } if ( ! empty( $_POST['child_callback'] ) && isset( $form_fields[ $_POST['child_name'] ] ) ) { - - $ajax_source_func = $_POST['child_callback']; - // If the requested callback function is added in the form or added in the field option, execute it with call_user_func. if ( isset( $form_fields[ $_POST['child_name'] ]['custom_dropdown_options_source'] ) && ! empty( $form_fields[ $_POST['child_name'] ]['custom_dropdown_options_source'] ) && @@ -194,9 +221,7 @@ if ( ! class_exists( 'um\core\Form' ) ) { $arr_options['field'] = $form_fields[ $_POST['child_name'] ]; - if ( function_exists( $ajax_source_func ) ) { - $arr_options['items'] = call_user_func( $ajax_source_func, $arr_options['field']['parent_dropdown_relationship'] ); - } + $arr_options['items'] = call_user_func( $ajax_source_func, $arr_options['field']['parent_dropdown_relationship'] ); } else { $arr_options['status'] = 'error'; $arr_options['message'] = __( 'This is not possible for security reasons.', 'ultimate-member' ); diff --git a/includes/core/class-profile.php b/includes/core/class-profile.php index 5e704bb2..21d6196b 100644 --- a/includes/core/class-profile.php +++ b/includes/core/class-profile.php @@ -422,6 +422,15 @@ if ( ! class_exists( 'um\core\Profile' ) ) { $data['in_profile_meta'] = true; $value = um_filtered_value( $key, $data ); + if ( 'description' === $key ) { + if ( UM()->options()->get( 'profile_show_html_bio' ) ) { + $res = make_clickable( wpautop( wp_kses_post( $value ) ) ); + } else { + $res = esc_html( $value ); + } + + $value = nl2br( $res ); + } if ( ! $value && ( ! array_key_exists( 'type', $data ) || ! in_array( $data['type'], $fields_without_metakey ) ) ) { continue; } diff --git a/includes/core/class-shortcodes.php b/includes/core/class-shortcodes.php index 010945a3..1f122743 100644 --- a/includes/core/class-shortcodes.php +++ b/includes/core/class-shortcodes.php @@ -357,15 +357,17 @@ if ( ! class_exists( 'um\core\Shortcodes' ) ) { function um_loggedin( $args = array(), $content = "" ) { ob_start(); - $defaults = array( - 'lock_text' => __( 'This content has been restricted to logged in users only. Please login to view this content.', 'ultimate-member' ), - 'show_lock' => 'yes', + $args = shortcode_atts( + array( + 'lock_text' => __( 'This content has been restricted to logged in users only. Please login to view this content.', 'ultimate-member' ), + 'show_lock' => 'yes', + ), + $args, + 'um_loggedin' ); - $args = wp_parse_args( $args, $defaults ); - if ( ! is_user_logged_in() ) { - if ( $args['show_lock'] == 'no' ) { + if ( 'no' === $args['show_lock'] ) { echo ''; } else { $args['lock_text'] = $this->convert_locker_tags( $args['lock_text'] ); @@ -380,7 +382,7 @@ if ( ! class_exists( 'um\core\Shortcodes' ) ) { } $output = ob_get_clean(); - + return htmlspecialchars_decode( $output, ENT_NOQUOTES ); } diff --git a/includes/core/um-filters-fields.php b/includes/core/um-filters-fields.php index b62854ab..47ad3478 100644 --- a/includes/core/um-filters-fields.php +++ b/includes/core/um-filters-fields.php @@ -429,7 +429,7 @@ function um_profile_field_filter_hook__( $value, $data, $type = '' ) { $value = str_replace('http://https://','https://',$value); $onclick_alert = ''; - if ( $value !== wp_validate_redirect( $value ) ) { + if ( UM()->options()->get( 'allow_url_redirect_confirm' ) && $value !== wp_validate_redirect( $value ) ) { $onclick_alert = ' onclick="return confirm( \'' . sprintf( __( 'This link leads to a 3rd-party website. Make sure the link is safe and you really want to go to this website: `%s`', 'ultimate-member' ), $value ) . '\' );"'; } diff --git a/includes/core/um-filters-navmenu.php b/includes/core/um-filters-navmenu.php index 137ba0dc..2390734f 100644 --- a/includes/core/um-filters-navmenu.php +++ b/includes/core/um-filters-navmenu.php @@ -11,6 +11,9 @@ if ( ! is_admin() ) { * @return array */ function um_add_custom_message_to_menu( $sorted_menu_items, $args ) { + if ( empty( $sorted_menu_items ) ) { + return $sorted_menu_items; + } if ( is_user_logged_in() ) { um_fetch_user( get_current_user_id() ); diff --git a/readme.txt b/readme.txt index fd0ed656..00c23da9 100644 --- a/readme.txt +++ b/readme.txt @@ -6,8 +6,8 @@ Donate link: Tags: community, member, membership, user-profile, user-registration Requires PHP: 5.6 Requires at least: 5.0 -Tested up to: 5.9 -Stable tag: 2.3.2 +Tested up to: 6.0 +Stable tag: 2.4.0 License: GNU Version 2 or Any Later Version License URI: http://www.gnu.org/licenses/gpl-3.0.txt @@ -163,8 +163,20 @@ 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.3.3: May xx, 2022 = += 2.4.0: May 25, 2022 = +* Enhancements: + + - Added: "Allow external link redirect confirm" setting for the displaying JS.confirm alert before redirect to external link from User Profile links + - Added: "Allowed Choice Callbacks" setting for the security enhancements + +* Bugfixes: + + - Fixed: PHP warning when nav menu is empty + - Fixed: Security issue related to the User Description field + - Fixed: Security issue related to the [um_loggedin] shortcode + - Fixed: Using $current_screen without checking for existence + - Fixed: `remove_unused_uploads()` function for some PHP installations = 2.3.2: April 21, 2022 = diff --git a/ultimate-member.php b/ultimate-member.php index 945c5671..fe128cec 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.3.3-alpha +Version: 2.4.0 Author: Ultimate Member Author URI: http://ultimatemember.com/ Text Domain: ultimate-member