diff --git a/.wordpress-org/blueprints/blueprint.json b/.wordpress-org/blueprints/blueprint.json index 0860d56d..d4f5cf9f 100644 --- a/.wordpress-org/blueprints/blueprint.json +++ b/.wordpress-org/blueprints/blueprint.json @@ -15,7 +15,7 @@ "step": "installPlugin", "pluginZipFile": { "resource": "url", - "url": "https:\/\/downloads.wordpress.org\/plugin\/ultimate-member.2.9.0.zip" + "url": "https:\/\/downloads.wordpress.org\/plugin\/ultimate-member.2.9.1.zip" }, "options": { "activate": true diff --git a/README.md b/README.md index 11318199..f7e6d75e 100644 --- a/README.md +++ b/README.md @@ -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.9.0](https://github.com/ultimatemember/ultimatemember/releases/tag/2.9.0). +[Official Release Version: 2.9.1](https://github.com/ultimatemember/ultimatemember/releases/tag/2.9.1). ## Changelog diff --git a/changelog.txt b/changelog.txt index 49946b34..991b03a3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,20 @@ == Changelog == += 2.9.1 November 15, 2024 = + +* Enhancements: + + - Added: `um_image_upload_validation` hook for 3rd-party validation during upload images + +* Bugfixes: + + - Fixed: "Load textdomain just in time" issue + - Fixed: Capabilities checking in the wp-admin > Users list table + - Fixed: File/image upload on the role specific profile form + - Fixed: Issues when the form's custom fields meta has a wrong format + - Fixed: Validation of the "Registration Default Role" slug + - Fixed: Allowed query variables via registered REST API class only when REST_REQUEST is defined + = 2.9.0 November 12, 2024 = * Enhancements: diff --git a/includes/action-scheduler/class-init.php b/includes/action-scheduler/class-init.php index e3f55f36..2e27a790 100644 --- a/includes/action-scheduler/class-init.php +++ b/includes/action-scheduler/class-init.php @@ -42,14 +42,7 @@ if ( ! class_exists( 'um\action_scheduler\Init' ) ) { */ public function __construct() { if ( ! $this->can_be_active() ) { - UM()->admin()->notices()->add_notice( - 'um-action-scheduler', - array( - 'class' => 'notice-warning is-dismissible', - // translators: %1$s - Plugin name, %1$s - Plugin Version - 'message' => '

' . sprintf( __( '%1$s %2$s The file needed to enable the Action Scheduler is missing. The plugin will continue to function as it did before, but without the new benefits offered by the Action Scheduler.', 'ultimate-member' ), UM_PLUGIN_NAME, UM_VERSION ) . '

', - ) - ); + add_action( 'init', array( $this, 'add_notice' ) ); } else { add_filter( 'um_settings_structure', array( $this, 'add_setting' ) ); @@ -60,6 +53,17 @@ if ( ! class_exists( 'um\action_scheduler\Init' ) ) { } } + public function add_notice() { + UM()->admin()->notices()->add_notice( + 'um-action-scheduler', + array( + 'class' => 'notice-warning is-dismissible', + // translators: %1$s - Plugin name, %1$s - Plugin Version + 'message' => '

' . sprintf( __( '%1$s %2$s The file needed to enable the Action Scheduler is missing. The plugin will continue to function as it did before, but without the new benefits offered by the Action Scheduler.', 'ultimate-member' ), UM_PLUGIN_NAME, UM_VERSION ) . '

', + ) + ); + } + /** * Adds the Action Scheduler setting to Ultimate Member feature settings * diff --git a/includes/admin/core/class-admin-builder.php b/includes/admin/core/class-admin-builder.php index 243b4c33..e0b49050 100644 --- a/includes/admin/core/class-admin-builder.php +++ b/includes/admin/core/class-admin-builder.php @@ -366,8 +366,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Builder' ) ) { * * @return string */ - function get_fields_by_row( $row_id ) { - + public function get_fields_by_row( $row_id ) { if ( empty( $this->global_fields ) || ! is_array( $this->global_fields ) ) { $this->global_fields = array(); } @@ -379,10 +378,9 @@ if ( ! class_exists( 'um\admin\core\Admin_Builder' ) ) { } } - return ( isset ( $results ) ) ? $results : ''; + return isset( $results ) ? $results : ''; } - /** * Get fields by sub row * diff --git a/includes/admin/core/class-admin-navmenu.php b/includes/admin/core/class-admin-navmenu.php index d4a423e7..fef9914a 100644 --- a/includes/admin/core/class-admin-navmenu.php +++ b/includes/admin/core/class-admin-navmenu.php @@ -23,10 +23,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Navmenu' ) ) { * Admin_Navmenu constructor. */ public function __construct() { - self::$fields = array( - 'um_nav_public' => __( 'Display Mode', 'ultimate-member' ), - 'um_nav_roles' => __( 'By Role', 'ultimate-member' ), - ); + add_action( 'init', array( &$this, 'set_variables' ) ); add_action( 'customize_controls_print_footer_scripts', array( &$this, '_wp_template' ) ); add_action( 'wp_update_nav_menu_item', array( &$this, '_save' ), 10, 3 ); @@ -36,6 +33,12 @@ if ( ! class_exists( 'um\admin\core\Admin_Navmenu' ) ) { // add_action( 'wp_nav_menu_item_custom_fields_customize_template', array( $this, 'wp_nav_menu_item_custom_fields_customize_template' ), 20, 5 ); } + public function set_variables() { + self::$fields = array( + 'um_nav_public' => __( 'Display Mode', 'ultimate-member' ), + 'um_nav_roles' => __( 'By Role', 'ultimate-member' ), + ); + } /** * Fires just before the move buttons of a nav menu item in the menu editor. diff --git a/includes/class-init.php b/includes/class-init.php index b498a084..6fe5a4fb 100644 --- a/includes/class-init.php +++ b/includes/class-init.php @@ -204,8 +204,8 @@ if ( ! class_exists( 'UM' ) ) { $this->is_filtering = 0; $this->honeypot = 'um_request'; - // textdomain loading - add_action( 'init', array( &$this, 'localize' ), 0 ); + // @todo investigate permanently delete https://make.wordpress.org/core/2024/10/21/i18n-improvements-6-7/#Enhanced-support-for-only-using-PHP-translation-files + add_action( 'init', array( &$this, 'localize' ), 0 ); // textdomain loading // include UM classes $this->includes(); @@ -243,33 +243,33 @@ if ( ! class_exists( 'UM' ) ) { * * @since 2.8.5 WordPress native functions are used to make this function clear. */ - public function localize() { - $default_domain = dirname( plugin_basename( UM_PLUGIN ) ); - /** - * Filters the plugin's textdomain. - * - * @param {string} $domain Plugin's textdomain. - * - * @return {string} Maybe changed plugin's textdomain. - * - * @since 1.3.x - * @hook um_language_textdomain - * - * @example Change UM language locale. - * function my_um_language_textdomain( $domain ) { - * $domain = 'ultimate-member-custom'; - * return $domain; - * } - * add_filter( 'um_language_textdomain', 'my_um_language_textdomain' ); - */ - $domain = apply_filters( 'um_language_textdomain', $default_domain ); - - // Unload textdomain if it has already loaded. - if ( is_textdomain_loaded( $domain ) ) { - unload_textdomain( $domain, true ); - } - load_plugin_textdomain( $domain, false, $default_domain . '/languages' ); - } +// public function localize() { +// $default_domain = dirname( plugin_basename( UM_PLUGIN ) ); +// /** +// * Filters the plugin's textdomain. +// * +// * @param {string} $domain Plugin's textdomain. +// * +// * @return {string} Maybe changed plugin's textdomain. +// * +// * @since 1.3.x +// * @hook um_language_textdomain +// * +// * @example Change UM language locale. +// * function my_um_language_textdomain( $domain ) { +// * $domain = 'ultimate-member-custom'; +// * return $domain; +// * } +// * add_filter( 'um_language_textdomain', 'my_um_language_textdomain' ); +// */ +// $domain = apply_filters( 'um_language_textdomain', $default_domain ); +// +// // Unload textdomain if it has already loaded. +// if ( is_textdomain_loaded( $domain ) ) { +// unload_textdomain( $domain, true ); +// } +// load_plugin_textdomain( $domain, false, $default_domain . '/languages' ); +// } /** * 1.3.x active extensions deactivate for properly running 2.0.x AJAX upgrades @@ -548,7 +548,6 @@ if ( ! class_exists( 'UM' ) ) { $this->password(); $this->rewrite(); $this->mail(); - $this->rest_api(); $this->shortcodes(); $this->roles(); $this->user(); @@ -565,11 +564,15 @@ if ( ! class_exists( 'UM' ) ) { $this->blocks(); $this->secure(); - //if multisite networks active + // If multisite networks active if ( is_multisite() ) { $this->multisite(); } + // Call only when REST_API request + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { + $this->rest_api(); + } } @@ -1001,14 +1004,12 @@ if ( ! class_exists( 'UM' ) ) { return $this->classes['config']; } - /** * @since 2.0 * * @return um\core\rest\API_v1|um\core\rest\API_v2 */ - function rest_api() { - + public function rest_api() { $api_version = $this->options()->get( 'rest_api_version' ); if ( empty( $this->classes['rest_api'] ) ) { @@ -1024,7 +1025,6 @@ if ( ! class_exists( 'UM' ) ) { return $this->classes['rest_api']; } - /** * @since 2.0 * diff --git a/includes/core/class-fields.php b/includes/core/class-fields.php index b781b30d..2aa7a3b3 100644 --- a/includes/core/class-fields.php +++ b/includes/core/class-fields.php @@ -1,6 +1,8 @@ query()->get_attr( 'custom_fields', $form_id ); - $field_scope = UM()->builtin()->predefined_fields; + $fields = UM()->query()->get_attr( 'custom_fields', $form_id ); + if ( empty( $fields ) || ! is_array( $fields ) ) { + $fields = array(); + } if ( ! isset( $fields[ $global_id ] ) ) { + $field_scope = UM()->builtin()->predefined_fields; + $count = 1; if ( ! empty( $fields ) ) { $count = count( $fields ) + 1; @@ -1624,7 +1630,7 @@ if ( ! class_exists( 'um\core\Fields' ) ) { * @param $key * * @return mixed - * @throws \Exception + * @throws Exception */ public function get_field( $key ) { $fields = $this->get_fields(); @@ -2161,7 +2167,7 @@ if ( ! class_exists( 'um\core\Fields' ) ) { * @param array $args * * @return string|null - * @throws \Exception + * @throws Exception */ public function edit_field( $key, $data, $rule = false, $args = array() ) { global $_um_profile_id; @@ -4030,7 +4036,6 @@ if ( ! class_exists( 'um\core\Fields' ) ) { return $arr; } - /** * Get fields in row * @@ -4038,8 +4043,8 @@ if ( ! class_exists( 'um\core\Fields' ) ) { * * @return string */ - function get_fields_by_row( $row_id ) { - if ( ! isset( $this->get_fields ) ) { + public function get_fields_by_row( $row_id ) { + if ( ! isset( $this->get_fields ) || ! is_array( $this->get_fields ) ) { return ''; } @@ -4049,10 +4054,9 @@ if ( ! class_exists( 'um\core\Fields' ) ) { } } - return ( isset ( $results ) ) ? $results : ''; + return isset( $results ) ? $results : ''; } - /** * Get fields by sub row * @@ -4119,7 +4123,7 @@ if ( ! class_exists( 'um\core\Fields' ) ) { * @param array $args * * @return string|null - * @throws \Exception + * @throws Exception */ public function display( $mode, $args ) { $output = null; @@ -4289,7 +4293,7 @@ if ( ! class_exists( 'um\core\Fields' ) ) { * @param bool $rule * * @return string|null - * @throws \Exception + * @throws Exception */ public function view_field( $key, $data, $rule = false ) { if ( '_um_last_login' === $key ) { @@ -4644,7 +4648,6 @@ if ( ! class_exists( 'um\core\Fields' ) ) { return apply_filters( "um_view_field_output_" . $data['type'], $data ); } - /** * Display fields ( view mode ) * @@ -4652,7 +4655,7 @@ if ( ! class_exists( 'um\core\Fields' ) ) { * @param array $args * * @return string|null - * @throws \Exception + * @throws Exception */ public function display_view( $mode, $args ) { $output = null; @@ -4664,7 +4667,7 @@ if ( ! class_exists( 'um\core\Fields' ) ) { $this->set_mode = $mode; $this->set_id = absint( $this->global_args['form_id'] ); - $this->field_icons = ( isset( $this->global_args['icons'] ) ) ? $this->global_args['icons'] : 'label'; + $this->field_icons = isset( $this->global_args['icons'] ) ? $this->global_args['icons'] : 'label'; // start output here $this->get_fields = $this->get_fields(); @@ -4694,8 +4697,7 @@ if ( ! class_exists( 'um\core\Fields' ) ) { } } - if ( ! empty( $this->get_fields ) ) { - + if ( ! empty( $this->get_fields ) && is_array( $this->get_fields ) ) { // find rows foreach ( $this->get_fields as $key => $array ) { if ( isset( $array['type'] ) && 'row' === $array['type'] ) { diff --git a/includes/core/class-files.php b/includes/core/class-files.php index 8f5389cc..a288c45b 100644 --- a/includes/core/class-files.php +++ b/includes/core/class-files.php @@ -376,6 +376,8 @@ if ( ! class_exists( 'um\core\Files' ) ) { wp_send_json_error( esc_js( __( 'You have no permission to edit this user through this form', 'ultimate-member' ) ) ); } + $post_data['role'] = maybe_unserialize( $post_data['role'] ); + if ( is_array( $post_data['role'] ) ) { if ( ! count( array_intersect( $post_data['role'], $current_user_roles ) ) ) { wp_send_json_error( esc_js( __( 'You have no permission to edit this user through this form', 'ultimate-member' ) ) ); @@ -448,141 +450,172 @@ if ( ! class_exists( 'um\core\Files' ) ) { $ret['error'] = null; $ret = array(); - /** - * Filters image upload checking nonce. - * - * @param {bool} $verify_nonce Verify nonce marker. Default true. - * - * @return {bool} Verify nonce marker. - * - * @since 1.3.x - * @hook um_image_upload_nonce - * - * @example Disable checking nonce on image upload. - * function my_image_upload_nonce( $verify_nonce ) { - * // your code here - * $verify_nonce = false; - * return $verify_nonce; - * } - * add_filter( 'um_image_upload_nonce', 'my_image_upload_nonce' ); - */ - $um_image_upload_nonce = apply_filters( 'um_image_upload_nonce', true ); - if ( $um_image_upload_nonce ) { - $timestamp = absint( $_POST['timestamp'] ); - $nonce = sanitize_text_field( $_POST['_wpnonce'] ); - if ( ! wp_verify_nonce( $nonce, "um_upload_nonce-{$timestamp}" ) && is_user_logged_in() ) { - // This nonce is not valid. - $ret['error'] = esc_html__( 'Invalid nonce', 'ultimate-member' ); - wp_send_json_error( $ret ); - } + if ( empty( $_POST['key'] ) ) { + $ret['error'] = esc_html__( 'Invalid image key', 'ultimate-member' ); + wp_send_json_error( $ret ); } + $id = sanitize_text_field( $_POST['key'] ); $user_id = empty( $_POST['user_id'] ) ? null : absint( $_POST['user_id'] ); - if ( $user_id && is_user_logged_in() && ! UM()->roles()->um_current_user_can( 'edit', $user_id ) ) { - $ret['error'] = esc_html__( 'You have no permission to edit this user', 'ultimate-member' ); - wp_send_json_error( $ret ); - } - if ( $user_id && ! is_user_logged_in() ) { - $ret['error'] = esc_html__( 'Please login to edit this user', 'ultimate-member' ); - wp_send_json_error( $ret ); - } + /** + * Filters the custom validation marker for 3rd-party uploader. + * + * @param {bool} $custom_validation Custom validation marker. Is null by default. Keep null for UM core validation. + * @param {string} $id Uploader field key. + * @param {int} $user_id User ID. + * + * @return {bool} Custom validation marker. + * + * @since 2.9.1 + * @hook um_image_upload_validation + * + * @example Custom validation. + * function my_um_image_upload_validation( $custom_validation, $id, $user_id ) { + * // your code here + * $ret['error'] = esc_html__( 'Error code', 'ultimate-member' ); + * wp_send_json_error( $ret ); + * return true; + * } + * add_filter( 'um_image_upload_validation', 'my_um_image_upload_validation', 10, 3 ); + */ + $custom_validation = apply_filters( 'um_image_upload_validation', null, $id, $user_id ); + if ( is_null( $custom_validation ) ) { + /** + * Filters image upload checking nonce. + * + * @param {bool} $verify_nonce Verify nonce marker. Default true. + * + * @return {bool} Verify nonce marker. + * + * @since 1.3.x + * @hook um_image_upload_nonce + * + * @example Disable checking nonce on image upload. + * function my_image_upload_nonce( $verify_nonce ) { + * // your code here + * $verify_nonce = false; + * return $verify_nonce; + * } + * add_filter( 'um_image_upload_nonce', 'my_image_upload_nonce' ); + */ + $um_image_upload_nonce = apply_filters( 'um_image_upload_nonce', true ); + if ( $um_image_upload_nonce ) { + $timestamp = absint( $_POST['timestamp'] ); + $nonce = sanitize_text_field( $_POST['_wpnonce'] ); + if ( ! wp_verify_nonce( $nonce, "um_upload_nonce-{$timestamp}" ) && is_user_logged_in() ) { + // This nonce is not valid. + $ret['error'] = esc_html__( 'Invalid nonce', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + } - $form_id = absint( $_POST['set_id'] ); - $mode = sanitize_key( $_POST['set_mode'] ); - - UM()->fields()->set_id = $form_id; - UM()->fields()->set_mode = $mode; - - if ( ! is_user_logged_in() && 'profile' === $mode ) { - $ret['error'] = esc_html__( 'You have no permission to edit user profile', 'ultimate-member' ); - wp_send_json_error( $ret ); - } - - if ( null !== $user_id && 'register' === $mode ) { - $ret['error'] = esc_html__( 'User has to be empty on registration', 'ultimate-member' ); - wp_send_json_error( $ret ); - } - - $form_post = get_post( $form_id ); - // Invalid post ID. Maybe post doesn't exist. - if ( empty( $form_post ) ) { - $ret['error'] = esc_html__( 'Invalid form ID', 'ultimate-member' ); - wp_send_json_error( $ret ); - } - - if ( 'um_form' !== $form_post->post_type ) { - $ret['error'] = esc_html__( 'Invalid form post type', 'ultimate-member' ); - wp_send_json_error( $ret ); - } - - $form_status = get_post_status( $form_id ); - if ( 'publish' !== $form_status ) { - $ret['error'] = esc_html__( 'Invalid form status', 'ultimate-member' ); - wp_send_json_error( $ret ); - } - - $post_data = UM()->query()->post_data( $form_id ); - if ( ! array_key_exists( 'mode', $post_data ) || $mode !== $post_data['mode'] ) { - $ret['error'] = esc_html__( 'Invalid form type', 'ultimate-member' ); - wp_send_json_error( $ret ); - } - - // For profiles only. - if ( 'profile' === $mode && ! empty( $post_data['use_custom_settings'] ) && ! empty( $post_data['role'] ) ) { - // Option "Apply custom settings to this form". Option "Make this profile form role-specific". - // Show the first Profile Form with role selected, don't show profile forms below the page with other role-specific setting. - $current_user_roles = UM()->roles()->get_all_user_roles( $user_id ); - if ( empty( $current_user_roles ) ) { - $ret['error'] = esc_html__( 'You have no permission to edit this user through this form', 'ultimate-member' ); + if ( $user_id && is_user_logged_in() && ! UM()->roles()->um_current_user_can( 'edit', $user_id ) ) { + $ret['error'] = esc_html__( 'You have no permission to edit this user', 'ultimate-member' ); wp_send_json_error( $ret ); } - if ( is_array( $post_data['role'] ) ) { - if ( ! count( array_intersect( $post_data['role'], $current_user_roles ) ) ) { + if ( $user_id && ! is_user_logged_in() ) { + $ret['error'] = esc_html__( 'Please login to edit this user', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + + $form_id = absint( $_POST['set_id'] ); + $mode = sanitize_key( $_POST['set_mode'] ); + + UM()->fields()->set_id = $form_id; + UM()->fields()->set_mode = $mode; + + if ( ! is_user_logged_in() && 'profile' === $mode ) { + $ret['error'] = esc_html__( 'You have no permission to edit user profile', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + + if ( null !== $user_id && 'register' === $mode ) { + $ret['error'] = esc_html__( 'User has to be empty on registration', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + + $form_post = get_post( $form_id ); + // Invalid post ID. Maybe post doesn't exist. + if ( empty( $form_post ) ) { + $ret['error'] = esc_html__( 'Invalid form ID', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + + if ( 'um_form' !== $form_post->post_type ) { + $ret['error'] = esc_html__( 'Invalid form post type', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + + $form_status = get_post_status( $form_id ); + if ( 'publish' !== $form_status ) { + $ret['error'] = esc_html__( 'Invalid form status', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + + $post_data = UM()->query()->post_data( $form_id ); + if ( ! array_key_exists( 'mode', $post_data ) || $mode !== $post_data['mode'] ) { + $ret['error'] = esc_html__( 'Invalid form type', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + + // For profiles only. + if ( 'profile' === $mode && ! empty( $post_data['use_custom_settings'] ) && ! empty( $post_data['role'] ) ) { + // Option "Apply custom settings to this form". Option "Make this profile form role-specific". + // Show the first Profile Form with role selected, don't show profile forms below the page with other role-specific setting. + $current_user_roles = UM()->roles()->get_all_user_roles( $user_id ); + if ( empty( $current_user_roles ) ) { $ret['error'] = esc_html__( 'You have no permission to edit this user through this form', 'ultimate-member' ); wp_send_json_error( $ret ); } - } elseif ( ! in_array( $post_data['role'], $current_user_roles, true ) ) { - $ret['error'] = esc_html__( 'You have no permission to edit this user through this form', 'ultimate-member' ); - wp_send_json_error( $ret ); - } - } - $id = sanitize_text_field( $_POST['key'] ); + $post_data['role'] = maybe_unserialize( $post_data['role'] ); - if ( ! array_key_exists( 'custom_fields', $post_data ) || empty( $post_data['custom_fields'] ) ) { - $ret['error'] = esc_html__( 'Invalid form fields', 'ultimate-member' ); - wp_send_json_error( $ret ); - } - - $custom_fields = maybe_unserialize( $post_data['custom_fields'] ); - if ( ! is_array( $custom_fields ) || ! array_key_exists( $id, $custom_fields ) ) { - if ( ! ( 'profile' === $mode && in_array( $id, array( 'cover_photo', 'profile_photo' ), true ) ) ) { - $ret['error'] = esc_html__( 'Invalid field metakey', 'ultimate-member' ); - wp_send_json_error( $ret ); - } - } - - if ( 'profile' === $mode ) { - if ( in_array( $id, array( 'cover_photo', 'profile_photo' ), true ) ) { - if ( 'profile_photo' === $id ) { - $disable_photo_uploader = empty( $post_data['use_custom_settings'] ) ? UM()->options()->get( 'disable_profile_photo_upload' ) : $post_data['disable_photo_upload']; - if ( $disable_photo_uploader ) { - $ret['error'] = esc_html__( 'You have no permission to edit this field', 'ultimate-member' ); - wp_send_json_error( $ret ); - } - } else { - $cover_enabled_uploader = empty( $post_data['use_custom_settings'] ) ? UM()->options()->get( 'profile_cover_enabled' ) : $post_data['cover_enabled']; - if ( ! $cover_enabled_uploader ) { - $ret['error'] = esc_html__( 'You have no permission to edit this field', 'ultimate-member' ); + if ( is_array( $post_data['role'] ) ) { + if ( ! count( array_intersect( $post_data['role'], $current_user_roles ) ) ) { + $ret['error'] = esc_html__( 'You have no permission to edit this user through this form', 'ultimate-member' ); wp_send_json_error( $ret ); } + } elseif ( ! in_array( $post_data['role'], $current_user_roles, true ) ) { + $ret['error'] = esc_html__( 'You have no permission to edit this user through this form', 'ultimate-member' ); + wp_send_json_error( $ret ); } - } elseif ( ! um_can_edit_field( $custom_fields[ $id ] ) ) { - $ret['error'] = esc_html__( 'You have no permission to edit this field', 'ultimate-member' ); + } + + if ( ! array_key_exists( 'custom_fields', $post_data ) || empty( $post_data['custom_fields'] ) ) { + $ret['error'] = esc_html__( 'Invalid form fields', 'ultimate-member' ); wp_send_json_error( $ret ); } + + $custom_fields = maybe_unserialize( $post_data['custom_fields'] ); + if ( ! is_array( $custom_fields ) || ! array_key_exists( $id, $custom_fields ) ) { + if ( ! ( 'profile' === $mode && in_array( $id, array( 'cover_photo', 'profile_photo' ), true ) ) ) { + $ret['error'] = esc_html__( 'Invalid field metakey', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + } + + if ( 'profile' === $mode ) { + if ( in_array( $id, array( 'cover_photo', 'profile_photo' ), true ) ) { + if ( 'profile_photo' === $id ) { + $disable_photo_uploader = empty( $post_data['use_custom_settings'] ) ? UM()->options()->get( 'disable_profile_photo_upload' ) : $post_data['disable_photo_upload']; + if ( $disable_photo_uploader ) { + $ret['error'] = esc_html__( 'You have no permission to edit this field', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + } else { + $cover_enabled_uploader = empty( $post_data['use_custom_settings'] ) ? UM()->options()->get( 'profile_cover_enabled' ) : $post_data['cover_enabled']; + if ( ! $cover_enabled_uploader ) { + $ret['error'] = esc_html__( 'You have no permission to edit this field', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + } + } elseif ( ! um_can_edit_field( $custom_fields[ $id ] ) ) { + $ret['error'] = esc_html__( 'You have no permission to edit this field', 'ultimate-member' ); + wp_send_json_error( $ret ); + } + } } if ( isset( $_FILES[ $id ]['name'] ) ) { @@ -706,6 +739,8 @@ if ( ! class_exists( 'um\core\Files' ) ) { wp_send_json_error( $ret ); } + $post_data['role'] = maybe_unserialize( $post_data['role'] ); + if ( is_array( $post_data['role'] ) ) { if ( ! count( array_intersect( $post_data['role'], $current_user_roles ) ) ) { $ret['error'] = esc_html__( 'You have no permission to edit this user through this form', 'ultimate-member' ); diff --git a/includes/core/class-form.php b/includes/core/class-form.php index ad692d84..1cd407ed 100644 --- a/includes/core/class-form.php +++ b/includes/core/class-form.php @@ -1075,7 +1075,9 @@ if ( ! class_exists( 'um\core\Form' ) ) { $global_role = get_option( 'default_role' ); // WP Global settings $um_global_role = UM()->options()->get( 'register_role' ); // UM Settings Global settings - if ( ! empty( $um_global_role ) ) { + + $existing_roles = array_keys( wp_roles()->roles ); + if ( ! empty( $um_global_role ) && in_array( $um_global_role, $existing_roles, true ) ) { $global_role = $um_global_role; // Form Global settings } @@ -1089,7 +1091,7 @@ if ( ! class_exists( 'um\core\Form' ) ) { $role = get_post_meta( $post_id, "_um_{$mode}_role", true ); } - if ( empty( $role ) ) { // custom role is default, return default role's slug + if ( empty( $role ) || ! in_array( $role, $existing_roles, true ) ) { // custom role is default, return default role's slug $role = $global_role; } diff --git a/includes/core/class-roles-capabilities.php b/includes/core/class-roles-capabilities.php index 76fda611..6fde9d99 100644 --- a/includes/core/class-roles-capabilities.php +++ b/includes/core/class-roles-capabilities.php @@ -687,7 +687,6 @@ if ( ! class_exists( 'um\core\Roles_Capabilities' ) ) { return $roles; } - /** * Current user can * @@ -696,55 +695,48 @@ if ( ! class_exists( 'um\core\Roles_Capabilities' ) ) { * * @return bool|int */ - function um_current_user_can( $cap, $user_id ) { + public function um_current_user_can( $cap, $user_id ) { if ( ! is_user_logged_in() ) { return false; } + $user_id = absint( $user_id ); // typecast + $return = 1; - um_fetch_user( get_current_user_id() ); + if ( get_current_user_id() !== um_user( 'ID' ) ) { + $temp_id = um_user( 'ID' ); + um_fetch_user( get_current_user_id() ); + } $current_user_roles = $this->get_all_user_roles( $user_id ); switch( $cap ) { case 'edit': - - if ( get_current_user_id() == $user_id ) { - if ( um_user( 'can_edit_profile' ) ) { - $return = 1; - } else { - $return = 0; - } - } else { - - if ( ! um_user( 'can_access_private_profile' ) && UM()->user()->is_private_profile( $user_id ) ) { - $return = 0; - } else { - if ( ! um_user( 'can_edit_everyone' ) ) { - $return = 0; - } else { - if ( um_user( 'can_edit_roles' ) && ( empty( $current_user_roles ) || count( array_intersect( $current_user_roles, um_user( 'can_edit_roles' ) ) ) <= 0 ) ) { - $return = 0; - } else { - $return = 1; - } - } - } + if ( get_current_user_id() === $user_id && ! um_user( 'can_edit_profile' ) ) { + $return = 0; + } elseif ( ! um_user( 'can_access_private_profile' ) && UM()->user()->is_private_profile( $user_id ) ) { + $return = 0; + } elseif ( ! um_user( 'can_edit_everyone' ) ) { + $return = 0; + } elseif ( um_user( 'can_edit_roles' ) && ( empty( $current_user_roles ) || count( array_intersect( $current_user_roles, um_user( 'can_edit_roles' ) ) ) <= 0 ) ) { + $return = 0; } - break; case 'delete': - if ( ! um_user( 'can_delete_everyone' ) ) + if ( ! um_user( 'can_delete_everyone' ) ) { $return = 0; - elseif ( um_user( 'can_delete_roles' ) && ( empty( $current_user_roles ) || count( array_intersect( $current_user_roles, um_user( 'can_delete_roles' ) ) ) <= 0 ) ) + } elseif ( um_user( 'can_delete_roles' ) && ( empty( $current_user_roles ) || count( array_intersect( $current_user_roles, um_user( 'can_delete_roles' ) ) ) <= 0 ) ) { $return = 0; + } break; } - um_fetch_user( $user_id ); + if ( ! empty( $temp_id ) ) { + um_fetch_user( $temp_id ); + } return $return; } diff --git a/includes/core/um-actions-profile.php b/includes/core/um-actions-profile.php index 3810847a..bb3eb6c8 100644 --- a/includes/core/um-actions-profile.php +++ b/includes/core/um-actions-profile.php @@ -1188,8 +1188,8 @@ function um_profile_header( $args ) { * } * ?> */ - do_action( 'um_after_profile_name_inline', $args ); ?> - + do_action( 'um_after_profile_name_inline', $args, um_user( 'ID' ) ); + ?> @@ -1215,7 +1215,7 @@ function um_profile_header( $args ) { * } * ?> */ - do_action( 'um_after_profile_header_name_args', $args ); + do_action( 'um_after_profile_header_name_args', $args, um_user( 'ID' ) ); /** * UM hook * diff --git a/includes/core/um-filters-avatars.php b/includes/core/um-filters-avatars.php index 509ab9ad..8c5b7fbc 100644 --- a/includes/core/um-filters-avatars.php +++ b/includes/core/um-filters-avatars.php @@ -14,37 +14,44 @@ function um_avatar_defaults( $avatar_defaults ) { } add_filter( 'avatar_defaults', 'um_avatar_defaults', 99999 ); - /** - * Get user UM avatars + * Get user UM avatars. * @param string $avatar * @param string $id_or_email * @param string $size * @param string $avatar_class * @param string $default * @param string $alt - * @hooks filter `get_avatar` * @return string returns avatar in image html elements */ function um_get_avatar( $avatar = '', $id_or_email='', $size = '96', $avatar_class = '', $default = '', $alt = '' ) { - if ( is_numeric($id_or_email) ) + if ( is_numeric( $id_or_email ) ) { $user_id = (int) $id_or_email; - elseif ( is_string( $id_or_email ) && ( $user = get_user_by( 'email', $id_or_email ) ) ) + } elseif ( is_string( $id_or_email ) && ( $user = get_user_by( 'email', $id_or_email ) ) ) { $user_id = $user->ID; - elseif ( is_object( $id_or_email ) && ! empty( $id_or_email->user_id ) ) + } elseif ( is_object( $id_or_email ) && ! empty( $id_or_email->user_id ) ) { $user_id = (int) $id_or_email->user_id; - if ( empty( $user_id ) ) - return $avatar; + } - um_fetch_user( $user_id ); + if ( empty( $user_id ) ) { + return $avatar; + } + + if ( $user_id !== um_user( 'ID' ) ) { + $temp_id = um_user( 'ID' ); + um_fetch_user( $user_id ); + } $avatar = um_user( 'profile_photo', $size ); + if ( ! empty( $temp_id ) ) { + um_fetch_user( $temp_id ); + } + return $avatar; } add_filter( 'get_avatar', 'um_get_avatar', 99999, 5 ); - if ( ! function_exists( 'um_filter_get_avatar_url' ) ) { /** @@ -69,4 +76,4 @@ if ( ! function_exists( 'um_filter_get_avatar_url' ) ) { // hooked in the get_avatar_data function add_filter( 'get_avatar_url', 'um_filter_get_avatar_url', 20, 3 ); -} \ No newline at end of file +} diff --git a/includes/um-short-functions.php b/includes/um-short-functions.php index 51e1e287..4cfd3c74 100644 --- a/includes/um-short-functions.php +++ b/includes/um-short-functions.php @@ -2169,7 +2169,10 @@ function um_get_default_avatar_uri() { function um_get_user_avatar_data( $user_id = '', $size = '96' ) { if ( empty( $user_id ) ) { $user_id = um_user( 'ID' ); - } else { + } + + if ( $user_id !== um_user( 'ID' ) ) { + $temp_id = um_user( 'ID' ); um_fetch_user( $user_id ); } @@ -2198,12 +2201,12 @@ function um_get_user_avatar_data( $user_id = '', $size = '96' ) { } $gravatar_type = UM()->options()->get( 'use_um_gravatar_default_builtin_image' ); - if ( $gravatar_type == 'default' ) { + if ( 'default' === $gravatar_type ) { if ( UM()->options()->get( 'use_um_gravatar_default_image' ) ) { $data['url'] = add_query_arg( 'd', $data['default'], $data['url'] ); } else { $default = get_option( 'avatar_default', 'mystery' ); - if ( $default == 'gravatar_default' ) { + if ( 'gravatar_default' === $default ) { $default = ''; } $data['url'] = add_query_arg( 'd', $default, $data['url'] ); @@ -2212,63 +2215,63 @@ function um_get_user_avatar_data( $user_id = '', $size = '96' ) { $data['url'] = add_query_arg( 'd', $gravatar_type, $data['url'] ); } - $data['type'] = 'gravatar'; + $data['type'] = 'gravatar'; $data['class'] .= ' um-avatar-gravatar'; } else { - $data['url'] = $data['default']; - $data['type'] = 'default'; + $data['url'] = $data['default']; + $data['type'] = 'default'; $data['class'] .= ' um-avatar-default'; } - /** - * UM hook + * Filters the user avatar URL. * - * @type filter - * @title um_user_avatar_url_filter - * @description Change user avatar URL - * @input_vars - * [{"var":"$avatar_uri","type":"string","desc":"Avatar URL"}, - * {"var":"$user_id","type":"int","desc":"User ID"}] - * @change_log - * ["Since: 2.0"] - * @usage add_filter( 'um_user_avatar_url_filter', 'function_name', 10, 2 ); - * @example - * Change the user avatar URL. + * function my_um_user_avatar_url_filter( $url, $user_id, $data ) { * // your code here - * return $avatar_uri; + * $url = 'some_url'; + * return $url; * } - * ?> + * add_filter( 'um_user_avatar_url_filter', 'my_um_user_avatar_url_filter', 10, 3 ); */ $data['url'] = apply_filters( 'um_user_avatar_url_filter', $data['url'], $user_id, $data ); /** - * UM hook + * Filters the user avatar image `alt` argument. * - * @type filter - * @title um_avatar_image_alternate_text - * @description Change user display name on um_user function profile photo - * @input_vars - * [{"var":"$display_name","type":"string","desc":"User Display Name"}] - * @change_log - * ["Since: 2.0"] - * @usage add_filter( 'um_avatar_image_alternate_text', 'function_name', 10, 1 ); - * @example - * Change the user avatar URL. + * function my_um_avatar_image_alternate_text( $alt, $data ) { * // your code here - * return $display_name; + * $alt = 'some_alt'; + * return $alt; * } - * ?> + * add_filter( 'um_avatar_image_alternate_text', 'my_um_avatar_image_alternate_text', 10, 2 ); */ - $data['alt'] = apply_filters( "um_avatar_image_alternate_text", um_user( "display_name" ), $data ); + $data['alt'] = apply_filters( 'um_avatar_image_alternate_text', um_user( 'display_name' ), $data ); + + if ( ! empty( $temp_id ) ) { + um_fetch_user( $temp_id ); + } return $data; } - /** * get user avatar url * @@ -2282,7 +2285,6 @@ function um_get_user_avatar_url( $user_id = '', $size = '96' ) { return $data['url']; } - /** * default cover * diff --git a/readme.txt b/readme.txt index 0e3811d0..afa20aee 100644 --- a/readme.txt +++ b/readme.txt @@ -6,7 +6,7 @@ Tags: community, member, membership, user-profile, user-registration Requires PHP: 5.6 Requires at least: 5.5 Tested up to: 6.7 -Stable tag: 2.9.0 +Stable tag: 2.9.1 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.txt @@ -166,6 +166,21 @@ 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.9.1 2024-11-15 = + +**Enhancements** + +* Added: `um_image_upload_validation` hook for 3rd-party validation during upload images + +**Bugfixes** + +* Fixed: "Load textdomain just in time" issue +* Fixed: Capabilities checking in the wp-admin > Users list table +* Fixed: File/image upload on the role specific profile form +* Fixed: Issues when the form's custom fields meta has a wrong format +* Fixed: Validation of the "Registration Default Role" slug +* Fixed: Allowed query variables via registered REST API class only when REST_REQUEST is defined + = 2.9.0 2024-11-12 = **Enhancements** @@ -182,292 +197,6 @@ IMPORTANT: PLEASE UPDATE THE PLUGIN TO AT LEAST VERSION 2.6.7 IMMEDIATELY. VERSI **Cached and optimized/minified assets(JS/CSS) must be flushed/re-generated after upgrade** -= 2.8.9 2024-10-14 = - -**Enhancements** - -* Added: Using PHP tidy extension (if it's active) to make HTML textarea value clear -* Added: `um_tidy_config` filter hook for setting PHP tidy config -* Tweak: Avoid using force `set_status()` function. -* Tweak: Properly using `UM()->common()->users()->get_status( $user_id )` instead of `um_user( 'account_status' )` -* Tweak: Properly using `UM()->common()->users()->get_status( $user_id, 'formatted' )` instead of `um_user( 'account_status_name' )` -* Tweak: Properly using `um_user( 'status' )` for getting user role setting while registration - -**Bugfixes** - -* Fixed: UM tipsy removing inside .um-page selector (e.g. tipsy init from um-modal) -* Fixed: Rollback using `