diff --git a/includes/admin/assets/js/um-admin-secure.js b/includes/admin/assets/js/um-admin-secure.js new file mode 100644 index 00000000..c4bc45d8 --- /dev/null +++ b/includes/admin/assets/js/um-admin-secure.js @@ -0,0 +1,78 @@ +jQuery(document).on("ready", function(){ + + const scan_results_wrapper = jQuery(".um-secure-scan-results"); + const scan_button_elem = jQuery(".um-secure-scan-content"); + const scan_capabilities = jQuery("input[data-field_id^='banned_capabilities']"); + + var UM_Secure = { + init: function() { + scan_results_wrapper.css({ + 'margin-top': '10px', + 'padding': '10px', + 'padding-bottom': '10px', + 'background-color': '#fff', + 'display': 'block', + 'max-height': '200px', + 'height': '500px', + 'overflow-y': 'scroll', + }); + + scan_button_elem.on("click", function(e){ + UM_Secure.effect(); + e.preventDefault(); + var me = jQuery(this); + me.prop("disabled", true); + scan_results_wrapper.empty(); + + UM_Secure.log( wp.i18n.__( 'Scanning site..', 'ultimate-member' ) ); + + UM_Secure.ajax(''); + + }); + scan_capabilities.on("change", function(){ + scan_button_elem.after( ' ' + wp.i18n.__( 'You can start the scan now but you must save the settings to apply the selected capabilities after the scan is complete.', 'ultimate-member' ) + '' ); + scan_capabilities.off("change"); + }) + }, + ajax: function( last_capability ) { + let checkedCaps = []; + let checkedCapsInputs = scan_results_wrapper.parents('.um-form-table').find('input[type="checkbox"][data-field_id^="banned_capabilities_"]:checked'); + checkedCapsInputs.each(function (){ + checkedCaps.push( jQuery(this).data('field_id').replace('banned_capabilities_', '') ); + }); + + var request = { + nonce: um_admin_scripts.nonce, + capabilities: checkedCaps, + last_scanned_capability: last_capability, + }; + + wp.ajax.send('um_secure_scan_affected_users', { + data: request, + success: function (response) { + if ( ! response.completed ) { + UM_Secure.ajax( response.last_scanned_capability ); + UM_Secure.log( response.message ); + } else if ( response.completed ) { + scan_results_wrapper.empty(); + UM_Secure.log( response.recommendations ); + scan_results_wrapper.find('.current').removeClass('current'); + scan_button_elem.removeAttr('disabled'); + } + }, + }); + }, + log: function( str ) { + scan_results_wrapper.find('.current').removeClass('current'); + scan_results_wrapper.append( '' + str + '
' ); + }, + effect: function() { + var blink = function(){ + scan_results_wrapper.find(".current").fadeTo(100, 0.1).fadeTo(200, 1.0); + }; + setInterval(blink, 1000); + } + }; + + UM_Secure.init(); +}); diff --git a/includes/admin/class-admin.php b/includes/admin/class-admin.php index d9131416..91ff1609 100644 --- a/includes/admin/class-admin.php +++ b/includes/admin/class-admin.php @@ -88,6 +88,11 @@ if ( ! class_exists( 'um\admin\Admin' ) ) { add_filter( 'post_updated_messages', array( &$this, 'post_updated_messages' ) ); } + public function includes() { + $this->notices(); + $this->secure(); + } + function init_variables() { $this->role_meta = apply_filters( @@ -1076,7 +1081,6 @@ if ( ! class_exists( 'um\admin\Admin' ) ) { return $value; } - /** * @param $value * @@ -1087,6 +1091,15 @@ if ( ! class_exists( 'um\admin\Admin' ) ) { return $value; } + /** + * @param $value + * + * @return array + */ + public function sanitize_wp_capabilities_assoc( $value ) { + $value = array_map( 'sanitize_key', array_filter( $value ) ); + return $value; + } /** * Sanitize role meta fields when wp-admin form has been submitted @@ -2056,17 +2069,28 @@ if ( ! class_exists( 'um\admin\Admin' ) ) { return $parent_file; } - /** * @since 2.0 * * @return core\Admin_Notices() */ - function notices() { + public function notices() { if ( empty( UM()->classes['admin_notices'] ) ) { UM()->classes['admin_notices'] = new core\Admin_Notices(); } return UM()->classes['admin_notices']; } + + /** + * @since 2.6.8 + * + * @return Secure + */ + public function secure() { + if ( empty( UM()->classes['um\admin\secure'] ) ) { + UM()->classes['um\admin\secure'] = new Secure(); + } + return UM()->classes['um\admin\secure']; + } } } diff --git a/includes/admin/class-secure.php b/includes/admin/class-secure.php new file mode 100644 index 00000000..5bd1289d --- /dev/null +++ b/includes/admin/class-secure.php @@ -0,0 +1,376 @@ +admin_enqueue()->js_url . 'um-admin-secure.js', array( 'jquery' ), UM_VERSION, true ); + wp_enqueue_script( 'um_admin_secure' ); + } + + /** + * Filter users by Register Date + * + * @since 2.6.8 + * @param object $query WP query `pre_get_users` + */ + public function filter_users_by_date_registered( $query ) { + global $pagenow; + if ( is_admin() && 'users.php' === $pagenow ) { + // phpcs:disable WordPress.Security.NonceVerification + $date_from = isset( $_GET['um_secure_date_from'] ) ? $_GET['um_secure_date_from'] : null; + $date_to = isset( $_GET['um_secure_date_to'] ) ? $_GET['um_secure_date_to'] : null; + // phpcs:enable WordPress.Security.NonceVerification + if ( ! $date_to ) { + $query->set( + 'date_query', + array( + 'after' => human_time_diff( $date_from, strtotime( current_time( 'mysql' ) ) ) . ' ago', + 'inclusive' => true, + ) + ); + } elseif ( $date_from && $date_to ) { + $query->set( + 'date_query', + array( + 'after' => human_time_diff( $date_from, strtotime( current_time( 'mysql' ) ) ) . ' ago', + 'before' => human_time_diff( $date_to, strtotime( current_time( 'mysql' ) ) ) . ' ago', + 'inclusive' => true, + ) + ); + } + } + + return $query; + } + + /** + * Handle secure actions. + * + * @since 2.6.8 + */ + public function admin_init() { + global $wpdb; + // Dismiss admin notice after the first visit to Secure settings page. + if ( isset( $_REQUEST['page'] ) && isset( $_REQUEST['tab'] ) && + 'um_options' === sanitize_key( $_REQUEST['page'] ) && 'secure' === sanitize_key( $_REQUEST['tab'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification + UM()->admin()->notices()->dismiss( 'secure_settings' ); + } + + if ( isset( $_REQUEST['um_secure_expire_all_sessions'] ) && ! wp_doing_ajax() ) { + if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'um-secure-expire-session-nonce' ) || ! current_user_can( 'manage_options' ) ) { + // This nonce is not valid or current logged-in user has no administrative rights. + wp_die( esc_html__( 'Security check', 'ultimate-member' ) ); + } + + /** + * Destroy all user sessions except the current logged-in user. + */ + $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->usermeta . ' WHERE meta_key="session_tokens" AND user_id != %d', get_current_user_id() ) ); + + if ( UM()->options()->get( 'display_login_form_notice' ) ) { + global $wpdb; + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->usermeta} WHERE user_id != %d AND ( meta_key = 'um_secure_has_reset_password' OR meta_key = 'um_secure_has_reset_password__timestamp' )", + get_current_user_id() + ) + ); + } + + wp_safe_redirect( add_query_arg( 'update', 'um_secure_expire_sessions', wp_get_referer() ) ); + exit; + } + + if ( isset( $_REQUEST['um_secure_restore_account'], $_REQUEST['user_id'] ) && ! wp_doing_ajax() ) { + $user_id = absint( $_REQUEST['user_id'] ); + if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'um-security-restore-account-nonce-' . $user_id ) || ! current_user_can( 'manage_options' ) ) { + // This nonce is not valid or current logged-in user has no administrative rights. + wp_die( esc_html__( 'Security check', 'ultimate-member' ) ); + } + + $user = get_userdata( $user_id ); + if ( ! $user ) { + wp_die( esc_html__( 'Invalid user.', 'ultimate-member' ) ); + } + + um_fetch_user( $user_id ); + $metadata = get_user_meta( $user_id, 'um_user_blocked__metadata', true ); + $user->update_user_level_from_caps(); + + // Restore Roles. + if ( isset( $metadata['roles'] ) ) { + foreach ( $metadata['roles'] as $role ) { + $user->add_role( $role ); + } + } + // Restore Account Status. + if ( isset( $metadata['account_status'] ) ) { + UM()->user()->set_status( $metadata['account_status'] ); + } + + // Delete blocked meta. + delete_user_meta( $user_id, 'um_user_blocked__metadata' ); + delete_user_meta( $user_id, 'um_user_blocked' ); + delete_user_meta( $user_id, 'um_user_blocked__timestamp' ); + + // Don't need to reset a password. + if ( UM()->options()->get( 'display_login_form_notice' ) ) { + update_user_meta( $user_id, 'um_secure_has_reset_password', true ); + update_user_meta( $user_id, 'um_secure_has_reset_password__timestamp', current_time( 'mysql' ) ); + } + + // Clear Cache. + UM()->user()->remove_cache( $user_id ); + um_reset_user(); + wp_safe_redirect( add_query_arg( 'update', 'um_secure_restore', wp_get_referer() ) ); + exit; + } + } + + /** + * Register Secure Settings + * + * @since 2.6.8 + * + * @param array $settings + * @return array + */ + public function add_settings( $settings ) { + $nonce = wp_create_nonce( 'um-secure-expire-session-nonce' ); + $count_users = count_users(); + + $banned_capabilities = array(); + $banned_admin_capabilities = UM()->common()->secure()->get_banned_capabilities_list(); + foreach ( $banned_admin_capabilities as $cap ) { + $banned_capabilities[ $cap ] = $cap; + } + + $disabled_capabilities = UM()->options()->get_default( 'banned_capabilities' ); + $disabled_capabilities_text = '' . implode( ', ', $disabled_capabilities ) . ''; + + $scanner_content = ''; + $scanner_content .= ''; + $scanner_content .= esc_html__( 'Last scan:', 'ultimate-member' ) . ' '; + $scan_status = get_option( 'um_secure_scan_status' ); + $last_scanned_time = get_option( 'um_secure_last_time_scanned' ); + if ( ! empty( $last_scanned_time ) ) { + $scanner_content .= human_time_diff( strtotime( $last_scanned_time ), strtotime( current_time( 'mysql' ) ) ) . ' ' . esc_html__( 'ago', 'ultimate-member' ); + if ( 'started' === $scan_status ) { + $scanner_content .= ' - ' . esc_html__( 'Not Completed.', 'ultimate-member' ); + } + } else { + $scanner_content .= esc_html__( 'Not Scanned yet.', 'ultimate-member' ); + } + $scanner_content .= ''; + + $secure_fields = array( + array( + 'id' => 'banned_capabilities', + 'type' => 'multi_checkbox', + 'multi' => true, + 'assoc' => true, + 'checkbox_key' => true, + 'columns' => 2, + 'options_disabled' => $disabled_capabilities, + 'options' => $banned_capabilities, + 'label' => __( 'Banned Administrative Capabilities', 'ultimate-member' ), + // translators: %s are disabled default capabilities that are enabled by default. + 'description' => sprintf( __( 'All the above are default Administrator & Super Admin capabilities. When someone tries to inject capabilities to the Account, Profile & Register forms submission, it will be flagged with this option. The %s capabilities are locked to ensure no users will be created with these capabilities.', 'ultimate-member' ), $disabled_capabilities_text ), + ), + array( + 'id' => 'secure_scan_affected_users', + 'type' => 'info_text', + 'label' => __( 'Scanner', 'ultimate-member' ), + 'value' => $scanner_content, + 'description' => __( 'Scan your site to check for vulnerabilities prior to Ultimate Member version 2.6.7 and get recommendations to secure your site.', 'ultimate-member' ), + ), + array( + 'id' => 'lock_register_forms', + 'type' => 'checkbox', + 'label' => __( 'Lock All Register Forms', 'ultimate-member' ), + 'description' => __( 'This prevents all users from registering with Ultimate Member on your site.', 'ultimate-member' ), + ), + array( + 'id' => 'display_login_form_notice', + 'type' => 'checkbox', + 'label' => __( 'Display Login form notice to reset passwords', 'ultimate-member' ), + 'description' => __( 'Enforces users to reset their passwords( one-time ) and prevent from entering old password.', 'ultimate-member' ), + ), + ); + + $count_users_exclude_me = $count_users['total_users'] - 1; + if ( $count_users_exclude_me > 0 ) { + $secure_fields[] = array( + 'id' => 'force_reset_passwords', + 'type' => 'info_text', + 'label' => __( 'Expire All Users Sessions', 'ultimate-member' ), + // translators: %d is the users count. + 'value' => '' . esc_html( sprintf( __( 'Logout Users (%d)', 'ultimate-member' ), $count_users_exclude_me ) ) . '', + 'description' => __( 'This will log out all users on your site and forces them to reset passwords
when "Display Login form notice to reset passwords" is enabled/checked.', 'ultimate-member' ), + ); + } + + $secure_fields = array_merge( + $secure_fields, + array( + array( + 'id' => 'secure_ban_admins_accounts', + 'type' => 'checkbox', + 'label' => __( 'Enable ban for administrative capabilities', 'ultimate-member' ), + 'description' => __( ' When someone tries to inject capabilities to the Account, Profile & Register forms submission, it will be banned.', 'ultimate-member' ), + ), + array( + 'id' => 'secure_notify_admins_banned_accounts', + 'type' => 'checkbox', + 'label' => __( 'Notify Administrators', 'ultimate-member' ), + 'description' => __( 'When enabled, All administrators will be notified when someone has suspicious activities in the Account, Profile & Register forms.', 'ultimate-member' ), + 'conditional' => array( 'secure_ban_admins_accounts', '=', 1 ), + ), + array( + 'id' => 'secure_notify_admins_banned_accounts__interval', + 'type' => 'select', + 'options' => array( + 'instant' => __( 'Send Immediately', 'ultimate-member' ), + 'hourly' => __( 'Hourly', 'ultimate-member' ), + 'daily' => __( 'Daily', 'ultimate-member' ), + ), + 'label' => __( 'Notification Schedule', 'ultimate-member' ), + 'conditional' => array( 'secure_notify_admins_banned_accounts', '=', 1 ), + ), + ) + ); + + $settings['secure'] = array( + 'title' => __( 'Secure', 'ultimate-member' ), + 'fields' => $secure_fields, + ); + + return $settings; + } + + /** + * Append blocked status to the `account_status` column rows. + * + * @param string $val Default column row value. + * @param string $column_name Current column name. + * @param int $user_id User ID in loop. + * + * @since 2.6.8 + * + * @return string + */ + public function add_restore_account( $val, $column_name, $user_id ) { + if ( 'account_status' === $column_name ) { + um_fetch_user( $user_id ); + $is_blocked = um_user( 'um_user_blocked' ); + $account_status = um_user( 'account_status' ); + if ( ! empty( $is_blocked ) && in_array( $account_status, array( 'rejected', 'inactive' ), true ) ) { + $datetime = um_user( 'um_user_blocked__timestamp' ); + $val .= '
' . esc_html__( 'Blocked Due to Suspicious Activity', 'ultimate-member' ) . '
'; + $nonce = wp_create_nonce( 'um-security-restore-account-nonce-' . $user_id ); + $restore_account_url = admin_url( 'users.php?user_id=' . $user_id . '&um_secure_restore_account=1&_wpnonce=' . $nonce ); + $action = ' · ' . esc_html__( 'Restore Account', 'ultimate-member' ) . ''; + if ( ! empty( $datetime ) ) { + $val .= '
' . human_time_diff( strtotime( $datetime ), strtotime( current_time( 'mysql' ) ) ) . ' ' . __( 'ago', 'ultimate-member' ) . '' . $action . '
'; + } + } + um_reset_user(); + } + return $val; + } + + /** + * + */ + public function check_secure_changes() { + if ( isset( $_POST['um_options']['display_login_form_notice'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification + $current_option_value = UM()->options()->get( 'display_login_form_notice' ); + if ( empty( $current_option_value ) ) { + return; + } + + if ( empty( $_POST['um_options']['display_login_form_notice'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification + $this->need_flush_meta = true; + } + } + } + + /** + * + */ + public function on_settings_save() { + if ( isset( $_POST['um_options']['display_login_form_notice'] ) && ! empty( $this->need_flush_meta ) ) { //phpcs:ignore WordPress.Security.NonceVerification + global $wpdb; + $wpdb->query( + "DELETE FROM {$wpdb->usermeta} WHERE meta_key = 'um_secure_has_reset_password' OR meta_key = 'um_secure_has_reset_password__timestamp'" + ); + } + + if ( isset( $_POST['um_options']['secure_notify_admins_banned_accounts'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification + if ( ! empty( $_POST['um_options']['secure_notify_admins_banned_accounts'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification + UM()->options()->update( 'suspicious-activity_on', 1 ); + } else { + UM()->options()->update( 'suspicious-activity_on', 0 ); + } + } + } + } +} diff --git a/includes/admin/core/class-admin-forms-settings.php b/includes/admin/core/class-admin-forms-settings.php index fb115a66..ba52b817 100644 --- a/includes/admin/core/class-admin-forms-settings.php +++ b/includes/admin/core/class-admin-forms-settings.php @@ -54,4 +54,4 @@ if ( ! class_exists( 'um\admin\core\Admin_Forms_Settings' ) ) { } } -} \ No newline at end of file +} diff --git a/includes/admin/core/class-admin-forms.php b/includes/admin/core/class-admin-forms.php index 92355732..21bb22d9 100644 --- a/includes/admin/core/class-admin-forms.php +++ b/includes/admin/core/class-admin-forms.php @@ -117,13 +117,14 @@ if ( ! class_exists( 'um\admin\core\Admin_Forms' ) ) { $data['value'] = esc_attr( $data['value'] ); } - if( in_array( $data['type'], array('info_text') ) ){ + if ( 'info_text' === $data['type'] ) { $arr_kses = array( 'a' => array( - 'href' => array(), - 'title' => array(), - 'target' => array(), - 'class' => array(), + 'href' => array(), + 'title' => array(), + 'target' => array(), + 'class' => array(), + 'onclick' => array(), ), 'button' => array( 'class' => array(), @@ -1185,15 +1186,14 @@ if ( ! class_exists( 'um\admin\core\Admin_Forms' ) ) { * @return bool|string */ function render_multi_checkbox( $field_data ) { - if ( empty( $field_data['id'] ) ) { return false; } $id = ( ! empty( $this->form_data['prefix_id'] ) ? $this->form_data['prefix_id'] : '' ) . '_' . $field_data['id']; - $class = ! empty( $field_data['class'] ) ? $field_data['class'] : ''; - $class .= ! empty( $field_data['size'] ) ? $field_data['size'] : 'um-long-field'; + $class = ! empty( $field_data['class'] ) ? $field_data['class'] : ''; + $class .= ! empty( $field_data['size'] ) ? $field_data['size'] : 'um-long-field'; $class_attr = ' class="um-forms-field ' . esc_attr( $class ) . '" '; $name = $field_data['id']; @@ -1204,19 +1204,27 @@ if ( ! class_exists( 'um\admin\core\Admin_Forms' ) ) { $values = array(); } - $i = 0; + $i = 0; $html = ''; $columns = ( ! empty( $field_data['columns'] ) && is_numeric( $field_data['columns'] ) ) ? $field_data['columns'] : 1; while ( $i < $columns ) { - $per_page = ceil( count( $field_data['options'] ) / $columns ); - $section_fields_per_page = array_slice( $field_data['options'], $i*$per_page, $per_page, true ); - $html .= ''; + $per_page = ceil( count( $field_data['options'] ) / $columns ); + $section_fields_per_page = array_slice( $field_data['options'], $i * $per_page, $per_page, true ); + $html .= ''; foreach ( $section_fields_per_page as $k => $title ) { - $id_attr = ' id="' . esc_attr( $id . '_' . $k ) . '" '; + $id_attr = ' id="' . esc_attr( $id . '_' . $k ) . '" '; $for_attr = ' for="' . esc_attr( $id . '_' . $k ) . '" '; - $name_attr = ' name="' . $name . '[' . $k . ']" '; + + if ( ! empty( $field_data['assoc'] ) ) { + $name_attr = ' name="' . esc_attr( $name ) . '[]" '; + $value_attr = ' value="' . esc_attr( $k ) . '" '; + } else { + $name_attr = ' name="' . esc_attr( $name ) . '[' . esc_attr( $k ) . ']" '; + $value_attr = ' value="1" '; + } + $disabed_attr = ''; $data = array( 'field_id' => $field_data['id'] . '_' . $k, @@ -1228,14 +1236,18 @@ if ( ! class_exists( 'um\admin\core\Admin_Forms' ) ) { $data_attr = ''; foreach ( $data as $key => $value ) { - if ( $value == 'checkbox_key' ) { + if ( 'checkbox_key' === $value ) { $value = $k; } $data_attr .= ' data-' . $key . '="' . esc_attr( $value ) . '" '; } + if ( isset( $field_data['options_disabled'] ) && in_array( $k, $field_data['options_disabled'], true ) ) { + $disabed_attr = 'disabled="disabled"'; + } + $html .= ""; } diff --git a/includes/admin/core/class-admin-notices.php b/includes/admin/core/class-admin-notices.php index d19e13da..f1ad8a7c 100644 --- a/includes/admin/core/class-admin-notices.php +++ b/includes/admin/core/class-admin-notices.php @@ -1,9 +1,9 @@ future_changed(); + $this->common_secure(); + /** * UM hook * @@ -79,6 +83,12 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { do_action( 'um_admin_create_notices' ); } + public function create_list_for_screen() { + if ( UM()->admin()->is_um_screen() ) { + $this->secure_settings(); + } + } + /** * @return array @@ -511,8 +521,14 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { case 'err_users_updated': $messages[0]['err_content'] = __( 'Super administrators cannot be modified.', 'ultimate-member' ); - $messages[1]['content'] = __( 'Other users have been updated.', 'ultimate-member' ); - + $messages[1]['content'] = __( 'Other users have been updated.', 'ultimate-member' ); + break; + case 'um_secure_expire_sessions': + $messages[0]['content'] = __( 'All users sessions have been successfully destroyed.', 'ultimate-member' ); + break; + case 'um_secure_restore': + $messages[0]['content'] = __( 'Account has been successfully restored.', 'ultimate-member' ); + break; } if ( ! empty( $messages ) ) { @@ -715,7 +731,6 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { ), 2 ); } - /** * Check Templates Versions notice */ @@ -746,26 +761,119 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { } } + /** + * First time installed Secure settings. + */ + public function secure_settings() { + ob_start(); + ?> +

+
+ +

+

+ + +

+ add_notice( + 'secure_settings', + array( + 'class' => 'warning', + 'message' => $message, + 'dismissible' => true, + ), + 1 + ); + } - function dismiss_notice() { + public function common_secure() { + if ( UM()->options()->get( 'lock_register_forms' ) ) { + ob_start(); + ?> +

+ Settings > Secure > Lock All Register Forms.', 'ultimate-member' ); ?> +

+ add_notice( + 'common_secure_register', + array( + 'class' => 'warning', + 'message' => $message, + 'dismissible' => true, + ), + 1 + ); + } + + if ( UM()->options()->get( 'display_login_form_notice' ) ) { + ob_start(); + ?> +

+ Settings > Secure > Display Login form notice to reset passwords.', 'ultimate-member' ); ?> +

+ add_notice( + 'common_secure_password_reset', + array( + 'class' => 'warning', + 'message' => $message, + 'dismissible' => true, + ), + 1 + ); + } + + if ( UM()->options()->get( 'secure_ban_admins_accounts' ) ) { + ob_start(); + ?> +

+ Settings > Secure > Enable ban for administrative capabilities.', 'ultimate-member' ); ?> +

+ add_notice( + 'common_secure_suspicious_activity', + array( + 'class' => 'warning', + 'message' => $message, + 'dismissible' => true, + ), + 1 + ); + } + } + + public function dismiss_notice() { UM()->admin()->check_ajax_nonce(); if ( empty( $_POST['key'] ) ) { wp_send_json_error( __( 'Wrong Data', 'ultimate-member' ) ); } - $hidden_notices = get_option( 'um_hidden_admin_notices', array() ); - if ( ! is_array( $hidden_notices ) ) { - $hidden_notices = array(); - } - - $hidden_notices[] = sanitize_key( $_POST['key'] ); - - update_option( 'um_hidden_admin_notices', $hidden_notices ); + $this->dismiss( sanitize_key( $_POST['key'] ) ); wp_send_json_success(); } + /** + * Dismiss notice by key. + * + * @param string $key + * + * @return void + */ + public function dismiss( $key ) { + $hidden_notices = get_option( 'um_hidden_admin_notices', array() ); + if ( ! is_array( $hidden_notices ) ) { + $hidden_notices = array(); + } + $hidden_notices[] = $key; + update_option( 'um_hidden_admin_notices', $hidden_notices ); + } function force_dismiss_notice() { if ( ! empty( $_REQUEST['um_dismiss_notice'] ) && ! empty( $_REQUEST['um_admin_nonce'] ) ) { diff --git a/includes/admin/core/class-admin-settings.php b/includes/admin/core/class-admin-settings.php index 455f85de..33a9d833 100644 --- a/includes/admin/core/class-admin-settings.php +++ b/includes/admin/core/class-admin-settings.php @@ -947,6 +947,21 @@ if ( ! class_exists( 'um\admin\core\Admin_Settings' ) ) { 'uninstall_on_delete' => array( 'sanitize' => 'bool', ), + 'lock_register_forms' => array( + 'sanitize' => 'bool', + ), + 'display_login_form_notice' => array( + 'sanitize' => 'bool', + ), + 'banned_capabilities' => array( + 'sanitize' => array( UM()->admin(), 'sanitize_wp_capabilities_assoc' ), + ), + 'secure_notify_admins_banned_accounts' => array( + 'sanitize' => 'bool', + ), + 'secure_notify_admins_banned_accounts__interval' => array( + 'sanitize' => 'key', + ), ) ); diff --git a/includes/ajax/class-init.php b/includes/ajax/class-init.php new file mode 100644 index 00000000..4772a48c --- /dev/null +++ b/includes/ajax/class-init.php @@ -0,0 +1,38 @@ +secure(); + } + + /** + * @since 2.6.8 + * + * @return Secure + */ + public function secure() { + if ( empty( UM()->classes['um\ajax\secure'] ) ) { + UM()->classes['um\ajax\secure'] = new Secure(); + } + return UM()->classes['um\ajax\secure']; + } + } +} diff --git a/includes/ajax/class-secure.php b/includes/ajax/class-secure.php new file mode 100644 index 00000000..30576a3d --- /dev/null +++ b/includes/ajax/class-secure.php @@ -0,0 +1,451 @@ +options()->get( 'banned_capabilities' ); + } + + $proceed = false; + $completed = false; + $message = ''; + $scanned_cap = ''; + $user_affected = false; + $count_users = 0; + + $last_element = end( $arr_banned_caps ); + + foreach ( $arr_banned_caps as $cap ) { + + if ( empty( $last_scanned_capability ) ) { + $proceed = true; + } else { + if ( $last_scanned_capability === $cap ) { // if this was the last capability, skip this and proceed the next loop. + $proceed = true; + continue; + } + } + + if ( ! $proceed ) { + continue; + } + + $args = array( + 'capability' => $cap, + 'role__not_in' => array( 'administrator' ), + 'fields' => 'ids', + ); + $wp_user_query = new WP_User_Query( $args ); + $count_users = $wp_user_query->get_total(); + if ( $count_users <= 0 ) { + $message = '`' . esc_html( $cap ) . '` ' . esc_html__( ' is safe ' ) . ''; + } else { + $user_affected = true; + $message = '`' . esc_html( $cap ) . '` ' . sprintf( /* translators: has affected %d user account */ _n( 'has affected %d user account.', ' has affected %d user accounts.', $count_users, 'ultimate-member' ), $count_users ) . ''; + } + + if ( $last_element === $cap ) { + $completed = true; + update_option( 'um_secure_scan_status', 'completed' ); + } + + $scanned_cap = $cap; + + break; + } + + if ( $user_affected ) { + $scan_details['affected_caps'][] = $scanned_cap; + + $scan_details['scanned_caps'][ $scanned_cap ] = array( + 'total_affected_users' => $count_users, + 'users' => $wp_user_query->get_results(), + ); + + if ( ! isset( $scan_details['scanned_caps']['total_all_cap_flagged'] ) ) { + $scan_details['scanned_caps']['total_all_cap_flagged'] = 1; + } else { + ++$scan_details['scanned_caps']['total_all_cap_flagged']; + } + } + + if ( ! isset( $scan_details['scanned_caps']['total_all_affected_users'] ) ) { + $scan_details['scanned_caps']['total_all_affected_users'] = absint( $count_users ); + } else { + $scan_details['scanned_caps']['total_all_affected_users'] = absint( $scan_details['scanned_caps']['total_all_affected_users'] ) + absint( $count_users ); + } + + update_option( 'um_secure_scanned_details', $scan_details ); + + wp_send_json_success( + array( + 'last_scanned_capability' => $scanned_cap, + 'message' => $message, + 'completed' => $completed, + 'recommendations' => $completed ? wp_kses( $this->scan_recommendations(), UM()->get_allowed_html( 'templates' ) ) : '', + ) + ); + } + + /** + * Recommendations after the scan completed. + * + * @return string + */ + public function scan_recommendations() { + global $wp_roles, $wpdb; + $br = '
'; + $check = ''; + $flag = ''; + $warning = ''; + + $all_plugins = get_plugins(); + $active_plugins = apply_filters( 'active_plugins', get_option( 'active_plugins' ) ); + + $content = '-----' . $br . $br; + + $suspicious_accounts = new WP_User_Query( + array( + 'relation' => 'AND', + 'number' => -1, + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => 'submitted', + 'value' => sprintf( ':"%s";', $wpdb->prefix . '_capabilities' ), + 'compare' => 'LIKE', + ), + array( + 'key' => 'submitted', + 'value' => sprintf( ':"%s";', $wpdb->prefix . '_user_level' ), + 'compare' => 'LIKE', + ), + array( + 'key' => 'submitted', + 'value' => sprintf( '.*%s";', '_capabilities' ), + 'compare' => 'REGEXP', + ), + array( + 'key' => 'submitted', + 'value' => sprintf( '.*%s";', '_user_level' ), + 'compare' => 'REGEXP', + ), + ), + ) + ); + + $suspicious_accounts_count = $suspicious_accounts->get_total(); + $susp_accounts = $suspicious_accounts->get_results(); + $arr_dates_registered = array(); + $arr_suspected_accounts = array(); + + /** + * Disable and Kickout Suspicious accounts. + */ + if ( $suspicious_accounts_count > 0 ) { + if ( ! empty( $susp_accounts ) ) { + foreach ( $susp_accounts as $user ) { + + $arr_suspected_accounts[] = $user->ID; + $arr_dates_registered[] = $user->user_registered; + + if ( $user->__get( 'um_user_blocked' ) ) { + continue; + } + + UM()->common()->secure()->revoke_caps( $user ); + // Get an instance of WP_User_Meta_Session_Tokens + $sessions_manager = WP_Session_Tokens::get_instance( $user->ID ); + // Remove all the session data for all users. + $sessions_manager->destroy_all(); + } + } + + $arr_dates_in_timestamp = array_map( 'strtotime', $arr_dates_registered ); + + $oldest_date = min( $arr_dates_in_timestamp ); + $newest_date = max( $arr_dates_in_timestamp ); + + $might_affected_users = new WP_User_Query( + array( + 'number' => -1, + 'exclude' => $arr_suspected_accounts, + 'date_query' => array( + 'after' => gmdate( 'F d, Y', strtotime( '-1 day', $oldest_date ) ), + 'before' => gmdate( 'F d, Y', strtotime( '+1 day', $newest_date ) ), + ), + ) + ); + + } + + /** + * Get Site Health's Total issues to resolve + */ + $get_issues = get_transient( 'health-check-site-status-result' ); + + $issue_counts = array(); + + if ( false !== $get_issues ) { + $issue_counts = json_decode( $get_issues, true ); + } + + if ( ! is_array( $issue_counts ) || ! $issue_counts ) { + $issue_counts = array( + 'recommended' => 0, + 'critical' => 0, + ); + } + + $site_health_issues_total = $issue_counts['recommended'] + $issue_counts['critical']; + + $content .= '
' . ( $suspicious_accounts_count > 0 ? $warning : $check ) . __( 'Scan Complete.', 'ultimate-member' ) . '
'; + + $option = get_option( 'um_secure_scanned_details', $susp_accounts ); + $scan_details = $option['scanned_caps']; + + if ( $suspicious_accounts_count > 0 ) { + update_option( 'um_secure_found_suspicious_accounts', true ); + $content .= $br . $flag . 'Suspcious Accounts Detected!
'; + $content .= $br . __( 'We have found ', 'ultimate-member' ) . '' . /* translators: %s suspcious account */ sprintf( _n( '%s suspcious account', '%s suspcious accounts', $suspicious_accounts_count, 'ultimate-member' ), $suspicious_accounts_count ) . ' ' . __( 'created on your site via Ultimate Member Forms.', 'ultimate-member' ); + $content .= $br . __( 'We\'ve temporarily disabled the suspcious account(s) for you to take actions.', 'ultimate-member' ); + + if ( $might_affected_users->get_total() > 0 ) { + $od = gmdate( 'F d, Y', $oldest_date ); + $nd = gmdate( 'F d, Y', $newest_date ); + if ( $od !== $nd ) { + $date_registered = $od . ' to ' . $nd; + } else { + $date_registered = $od; + } + $content .= $br . $br . __( 'Also, We\'ve found ', 'ultimate-member' ) . '' . /* translators: %s suspcious account */ sprintf( _n( '%s account', '%s accounts', $might_affected_users->get_total(), 'ultimate-member' ), $might_affected_users->get_total() ) . ' ' . sprintf( _n( 'created on %s when the suspicious account was created.', 'created on %s when the suspicious accounts were created.', $suspicious_accounts_count, 'ultimate-member' ), $date_registered ); + } + } else { + $content .= $br . 'Suspcious Accounts
'; + $content .= $br . $check . ' No suspicious accounts found.
'; + } + + $content .= $br . $br . __( 'PLEASE READ OUR RECOMMENDATIONS BELOW: ', 'ultimate-member' ) . $br; + + $content .= $br . '
WARNING: Ensure that you\'ve created a full backup of your site as your restoration point before changing anything on your site with our recommendations.
'; + if ( $suspicious_accounts_count > 0 ) { + $lock_register_forms_url = admin_url( 'admin.php?page=um_options&tab=secure&um_secure_lock_register_forms=1&_wpnonce=' . wp_create_nonce( 'um_secure_lock_register_forms' ) ); + $content .= $br . '1. Please temporarily lock all your active Register forms. Click here to lock them now. You can unblock the Register forms later. Just go to Ultimate Member > Settings > Secure > uncheck the option "Lock All Register Forms".'; + $content .= $br . $br; + $suspicious_accounts_url = admin_url( 'users.php?um_status=inactive' ); + + if ( $might_affected_users->get_total() > 0 ) { + $od = gmdate( 'F d, Y', $oldest_date ); + $nd = gmdate( 'F d, Y', $newest_date ); + if ( $od !== $nd ) { + $suspicious_accounts_url = admin_url( 'users.php?um_secure_date_from=' . $oldest_date . '&um_secure_date_to=' . $newest_date ); + } else { + $suspicious_accounts_url = admin_url( 'users.php?um_secure_date_from=' . $oldest_date ); + } + } + + $content .= '2. Review all suspicious accounts and delete them completely. Click here to review accounts.'; + $content .= $br . $br; + + $nonce = wp_create_nonce( 'um-secure-expire-session-nonce' ); + $destroy_all_sessions_url = admin_url( '?um_secure_expire_all_sessions=1&_wpnonce=' . esc_attr( $nonce ) . '&except_me=1' ); + $content .= '4. If accounts are suspicious to you, please destroy all user sessions to logout active users on your site. Click here to Destroy Sessions now'; + + $content .= $br . $br; + $content .= '4. Run a complete scan on your site using third-party Security plugins such as WPScan/Jetpack Protect or WordFence Security.'; + + $content .= $br . $br; + $nonce = wp_create_nonce( 'um-secure-enable-reset-pass-nonce' ); + $reset_pass_sessions_url = admin_url( '?um_secure_enable_reset_password=1&_wpnonce=' . esc_attr( $nonce ) . '&except_me=1' ); + + $content .= '5. Force users to Reset their Passwords. Click here to enable this option. When this option is enabled, users will be asked to reset their passwords(one-time) on the next login in the UM Login form.'; + $content .= $br . $br; + + $content .= '6. Once your site is secured, please create or enable Daily Backups of your server/site. You can contact your hosting provider to assist you on this matter.'; + $content .= $br . $br; + + $content .= '👇 MORE RECOMMENDATIONS BELOW.'; + } + + $content .= $br . $br . 'Review & Resolve Issues with Site Health Check tool'; + $content .= $br . __( 'Site Health is a tool in WordPress that helps you monitor how your site is doing. It shows critical information about your WordPress configuration and items that require your attention.', 'ultimate-member' ); + if ( $site_health_issues_total > 0 ) { + $content .= $br . $flag . sprintf( /* translators: %d issue in the Site Health status */ _n( 'There\s %d issue in the Site Health status', 'There are %d issues in the Site Health status', $site_health_issues_total ), $site_health_issues_total ); + $content .= ': Review Site Health Status'; + } else { + $content .= $br . $check . __( 'There are no issues found in the Site Health status', 'ultimate-member' ); + } + + $content .= $br . $br . 'Default WP Register Form'; + if ( get_option( 'users_can_register' ) ) { + $content .= $br . $flag . 'The default WordPress Register form is enabled. If you\'re getting Spam User Registrations, we recommend that you enable a Challenge-Response plugin such as our Ultimate Member - ReCaptcha extension.'; + $content .= $br; + } else { + $content .= $br . $check . 'The default WordPress Register form is disabled.' . $br; + } + + $content .= $br . 'Block Disposable Email Addresses/Domains'; + if ( empty( UM()->options()->get( 'blocked_emails' ) ) ) { + $content .= $br . $flag . 'You are not blocking email addresses or disposable email domains that are mostly used for Spam Account Registrations. You can get the list of disposable email domains from this repository and then add them to Blocked Email Addresses options.'; + $content .= $br; + } else { + $content .= $br . $check . 'Blocked Emails option is already set.'; + $content .= $br; + } + + $content .= $br . 'Manage User Roles & Capabilities'; + if ( absint( $scan_details['total_all_affected_users'] ) > 0 ) { + $count_flagged_caps = $scan_details['total_all_cap_flagged']; + $count_users = $scan_details['total_all_affected_users']; + $affected_caps = $option['affected_caps']; + $affected_roles = array(); + + $all_roles = $wp_roles->roles; + $editable_roles = apply_filters( 'editable_roles', $all_roles ); + foreach ( $affected_caps as $cap ) { + foreach ( $editable_roles as $role_key => $role ) { + if ( in_array( $cap, array_keys( $role['capabilities'] ), true ) ) { + $affected_roles[ $role_key ] = $role['name']; + } + } + } + $content .= $br . $flag . 'We have found ' . sprintf( /* translators: */ _n( ' %d user account', ' %d user accounts ', $count_users, 'ultimate-member' ), $count_users ); + $content .= sprintf( /* translators: */ _n( ' affected by %d capability selected in the Banned Administrative Capabilities.', ' affected by one of the %d capabilities selected in the Banned Administrative Capabilities.', $count_flagged_caps, 'ultimate-member' ), $count_flagged_caps ); + + $content .= $br . '- ' . implode( '
- ', $affected_caps ); + + $content .= $br . $br . 'The flagged capabilities are related to the following roles: ' . $br . ' - ' . implode( '
- ', array_values( $affected_roles ) ); + + $content .= $br . $br . 'The affected user accounts will be flagged as suspicious when they update their Profile/Account. If you are not using these capabilities, you may remove them from the roles in the User Role settings. If the roles are not created via Ultimate Member > User Roles, you can use a third-party plugin to modify the role capability.'; + $content .= $br . $br . 'We strongly recommend that you never assign roles with the same capabilities as your administrators for your members/users and that may allow them to access the admin-side features and functionalities of your WordPress site.'; + } else { + $content .= $br . $check . 'Roles & Capabilities are all secured. No users are using the same capabilities as your administrators.'; + } + + $content .= $br . $br . 'Require Strong Passwords'; + if ( ! UM()->options()->get( 'require_strongpass' ) ) { + $content .= $br . $flag . 'We recommend that you enable and require "Strong Password" feature for all the Register, Reset Password & Account forms.'; + $content .= $br . ' Click here to enable.'; + } else { + $content .= $br . $check . 'Your forms are already configured to require of using strong passwords.'; + } + + $content .= $br . $br . 'Secure Site\'s Connection'; + if ( ! isset( $_SERVER['HTTPS'] ) || 'on' !== $_SERVER['HTTPS'] ) { + $content .= $br . $flag . 'Your site cannot provide a secure connection. Please contact your hosting provider to enable SSL certifications on your server.'; + } else { + $content .= $br . $check . 'Your site provides a secure connection with SSL.'; + } + + $content .= $br . $br . 'Install Challenge-Response plugin to Login & Register Forms'; + if ( ! array_key_exists( 'um-recaptcha/um-recaptcha.php', $all_plugins ) ) { + $content .= $br . $flag . 'We recommend that you install and enable ReCaptcha to your Reset Password, Login & Register forms.'; + } else { + if ( in_array( 'um-recaptcha/um-recaptcha.php', $active_plugins, true ) ) { + $content .= $br . $check . 'Ultimate Member ReCaptcha is actived.'; + $um_forms = get_posts( 'post_type=um_form&numberposts=-1&fields=ids' ); + foreach ( $um_forms as $fid ) { + switch ( get_post_meta( $fid, '_um_mode', true ) ) { + case 'register': + $has_captcha = absint( get_post_meta( $fid, '_um_register_g_recaptcha_status', true ) ); + $content .= $br . '  - Register: ' . get_the_title( $fid ) . ' recaptcha ' . ( 1 === $has_captcha ? ' is enabled ' . $check : 'is disabled ' . $flag ); + break; + case 'login': + $has_captcha = absint( get_post_meta( $fid, '_um_login_g_recaptcha_status', true ) ); + $content .= $br . '  - Login: ' . get_the_title( $fid ) . ' recaptcha ' . ( 1 === $has_captcha ? ' is enabled ' . $check : 'is disabled ' . $flag ); + break; + } + } + $reset_pass_form = absint( UM()->options()->get( 'g_recaptcha_password_reset' ) ); + $content .= $br . '  - Reset Password Form\'s recaptcha ' . ( 1 === $reset_pass_form ? ' is enabled ' . $check : 'is disabled ' . $flag ); + + } elseif ( array_key_exists( 'um-recaptcha/um-recaptcha.php', $all_plugins ) ) { + $content .= $br . $flag . 'Ultimate Member ReCaptcha is installed but not activated.'; + } else { + $content .= $br . $flag . 'We recommend that you install and enable ReCaptcha to Login & Register forms.'; + } + } + + $update_plugins = get_site_transient( 'update_plugins' ); + $update_themes = get_site_transient( 'update_themes' ); + $update_wp_core = get_site_transient( 'update_core' ); + global $wp_version; + $content .= $br . $br . 'Keep Themes & Plugins up to date.'; + $content .= $br . __( 'It is important that you update your themes/plugins if the theme/plugin creators update is aimed at fixing security, bug and vulnerability issues. It is not a good idea to ignore available updates as this may give hackers an advantage when trying to access your website.', 'ultimate-member' ); + + if ( isset( $update_plugins->response ) && ! empty( $update_plugins->response ) ) { + $content .= $br . $br . $flag . sprintf( /* translators: */ _n( 'There\'s %d plugin that requires an update.', 'There are %d plugins that require updates', count( $update_plugins->response ), 'ultimate-member' ), count( $update_plugins->response ) ) . ' Update Plugins Now'; + foreach ( $update_plugins->response as $plugin_name => $data ) { + $content .= $br . '  - ' . $plugin_name; + } + } else { + $content .= $br . $br . $check . __( 'Plugins are up to date.', 'ultimate-member' ); + } + + if ( isset( $update_themes->response ) && ! empty( $update_themes->response ) ) { + $content .= $br . $br . $flag . sprintf( /* translators: */ _n( 'There\'s %d theme that requires an update.', 'There are %d themes that require updates', count( $update_plugins->response ), 'ultimate-member' ), count( $update_plugins->response ) ) . ' Update Themes Now'; + foreach ( $update_themes->response as $theme_name => $data ) { + $content .= $br . '  - ' . $theme_name; + } + } else { + $content .= $br . $br . $check . __( 'Themes are up to date.', 'ultimate-member' ); + } + + if ( isset( $update_themes->current ) && $wp_version !== $update_themes->current ) { + $content .= $br . $br . $flag . __( 'There\'s a new version of WordPress.', 'ultimate-member' ) . 'Update WordPress Now'; + } else { + $content .= $br . $br . $check . __( 'You\'re using the latest version of WordPress', 'ultimate-member' ) . '(' . esc_attr( $wp_version ) . ')'; + } + + $content .= $br . $br . __( 'That\'s all. If you have any recommendation on how to secure your site or have questions, please contact us on our feedback page. ', 'ultimate-member' ); + + update_option( 'um_secure_scan_result_content', $content ); + + return $content; + } +} diff --git a/includes/class-config.php b/includes/class-config.php index 903b4515..26259166 100644 --- a/includes/class-config.php +++ b/includes/class-config.php @@ -195,7 +195,6 @@ if ( ! class_exists( 'um\Config' ) ) { '_um_secondary_color', ); - /** * UM hook * @@ -496,10 +495,18 @@ if ( ! class_exists( 'um\Config' ) ) { 'body' => '{display_name} has just deleted their {site_name} account.', 'description' => __('Whether to receive notification when an account is deleted','ultimate-member'), 'recipient' => 'admin' - ) + ), + 'suspicious-activity' => array( + 'key' => 'suspicious-activity', + 'title' => __( 'Secure: Suspicious Account Activity', 'ultimate-member' ), + 'subject' => __( '[{site_name}] Suspicious Account Activity', 'ultimate-member' ), + 'body' => 'This is to inform you that there are suspicious activities with the following accounts: {user_profile_link}', + 'description' => __( 'Whether to receive notification when suspicious account activity is detected.', 'ultimate-member' ), + 'recipient' => 'admin', + 'default_active' => true, + ), ) ); - //settings defaults $this->settings_defaults = array( 'restricted_access_post_metabox' => array( 'post' => 1, 'page' => 1 ), @@ -559,9 +566,9 @@ if ( ! class_exists( 'um\Config' ) ) { 'form_asterisk' => 0, 'profile_title' => '{display_name} | {site_name}', 'profile_desc' => '{display_name} is on {site_name}. Join {site_name} to view {display_name}\'s profile', - 'admin_email' => get_bloginfo('admin_email'), - 'mail_from' => get_bloginfo('name'), - 'mail_from_addr' => get_bloginfo('admin_email'), + 'admin_email' => get_bloginfo( 'admin_email' ), + 'mail_from' => get_bloginfo( 'name' ), + 'mail_from_addr' => get_bloginfo( 'admin_email' ), 'email_html' => 1, 'image_orientation_by_exif' => 0, 'image_compression' => 60, @@ -576,6 +583,12 @@ if ( ! class_exists( 'um\Config' ) ) { 'profile_show_html_bio' => 0, 'profile_noindex' => 0, 'activation_link_expiry_time' => '', + 'lock_register_forms' => false, + 'display_login_form_notice' => false, + 'secure_ban_admins_accounts' => false, + 'banned_capabilities' => array( 'manage_options', 'promote_users', 'level_10' ), + 'secure_notify_admins_banned_accounts' => false, + 'secure_notify_admins_banned_accounts__interval' => 'instant', ); add_filter( 'um_get_tabs_from_config', '__return_true' ); diff --git a/includes/class-functions.php b/includes/class-functions.php index 0a1aed80..dabefd4e 100644 --- a/includes/class-functions.php +++ b/includes/class-functions.php @@ -568,6 +568,9 @@ if ( ! class_exists( 'UM_Functions' ) ) { 'charoff' => true, 'valign' => true, ), + 'a' => array( + 'onclick' => array(), + ), ); break; case 'templates': diff --git a/includes/class-init.php b/includes/class-init.php index 3c839c9a..2cf9fdf6 100644 --- a/includes/class-init.php +++ b/includes/class-init.php @@ -527,8 +527,10 @@ if ( ! class_exists( 'UM' ) ) { } //run setup - $this->common()->create_post_types(); + $this->common()->cpt()->create_post_types(); $this->setup()->run_setup(); + + $this->cron()->schedule_events(); } @@ -549,10 +551,11 @@ if ( ! class_exists( 'UM' ) ) { */ public function includes() { - $this->common(); + $this->common()->includes(); $this->access(); if ( $this->is_request( 'ajax' ) ) { + $this->ajax()->includes(); $this->admin(); $this->ajax_init(); $this->admin_ajax_hooks(); @@ -565,6 +568,7 @@ if ( ! class_exists( 'UM' ) ) { $this->plugin_updater(); $this->theme_updater(); } elseif ( $this->is_request( 'admin' ) ) { + $this->admin()->includes(); $this->admin(); $this->admin_menu(); $this->admin_upgrade(); @@ -572,7 +576,6 @@ if ( ! class_exists( 'UM' ) ) { $this->columns(); $this->admin_enqueue(); $this->metabox(); - $this->admin()->notices(); $this->users(); $this->dragdrop(); $this->admin_gdpr(); @@ -580,6 +583,7 @@ if ( ! class_exists( 'UM' ) ) { $this->plugin_updater(); $this->theme_updater(); } elseif ( $this->is_request( 'frontend' ) ) { + $this->frontend()->includes(); $this->enqueue(); $this->account(); $this->password(); @@ -607,6 +611,7 @@ if ( ! class_exists( 'UM' ) ) { $this->gdpr(); $this->member_directory(); $this->blocks(); + $this->secure(); //if multisite networks active if ( is_multisite() ) { @@ -649,7 +654,6 @@ if ( ! class_exists( 'UM' ) ) { return $this->classes['blocks']; } - /** * Get extension API * @@ -688,19 +692,43 @@ if ( ! class_exists( 'UM' ) ) { return $this->classes[ $key ]; } + /** + * @since 2.6.8 + * + * @return um\ajax\Init + */ + public function ajax() { + if ( empty( $this->classes['um\ajax\init'] ) ) { + $this->classes['um\ajax\init'] = new um\ajax\Init(); + } + + return $this->classes['um\ajax\init']; + } /** * @since 2.0 + * @since 2.6.8 changed namespace and class content. * - * @return um\core\Common() + * @return um\common\Init */ - function common() { - if ( empty( $this->classes['common'] ) ) { - $this->classes['common'] = new um\core\Common(); + public function common() { + if ( empty( $this->classes['um\common\init'] ) ) { + $this->classes['um\common\init'] = new um\common\Init(); } - return $this->classes['common']; + return $this->classes['um\common\init']; } + /** + * @since 2.6.8 + * + * @return um\frontend\Init + */ + public function frontend() { + if ( empty( $this->classes['um\frontend\init'] ) ) { + $this->classes['um\frontend\init'] = new um\frontend\Init(); + } + return $this->classes['um\frontend\init']; + } /** * @since 2.0 @@ -760,7 +788,6 @@ if ( ! class_exists( 'UM' ) ) { new um\core\AJAX_Common(); } - /** * @since 2.0.30 */ @@ -775,9 +802,9 @@ if ( ! class_exists( 'UM' ) ) { /** * @since 2.0 * - * @return um\admin\Admin() + * @return um\admin\Admin */ - function admin() { + public function admin() { if ( empty( $this->classes['admin'] ) ) { $this->classes['admin'] = new um\admin\Admin(); } diff --git a/includes/common/class-cpt.php b/includes/common/class-cpt.php new file mode 100644 index 00000000..fe3828f1 --- /dev/null +++ b/includes/common/class-cpt.php @@ -0,0 +1,92 @@ + array( + 'name' => __( 'Forms', 'ultimate-member' ), + 'singular_name' => __( 'Form', 'ultimate-member' ), + 'add_new' => __( 'Add New', 'ultimate-member' ), + 'add_new_item' => __( 'Add New Form', 'ultimate-member' ), + 'edit_item' => __( 'Edit Form', 'ultimate-member' ), + 'not_found' => __( 'You did not create any forms yet', 'ultimate-member' ), + 'not_found_in_trash' => __( 'Nothing found in Trash', 'ultimate-member' ), + 'search_items' => __( 'Search Forms', 'ultimate-member' ), + ), + 'capabilities' => array( + 'edit_post' => 'manage_options', + 'read_post' => 'manage_options', + 'delete_post' => 'manage_options', + 'edit_posts' => 'manage_options', + 'edit_others_posts' => 'manage_options', + 'delete_posts' => 'manage_options', + 'publish_posts' => 'manage_options', + 'read_private_posts' => 'manage_options', + ), + 'show_ui' => true, + 'show_in_menu' => false, + 'public' => false, + 'show_in_rest' => true, + 'supports' => array( 'title' ), + ) + ); + + if ( UM()->options()->get( 'members_page' ) ) { + register_post_type( + 'um_directory', + array( + 'labels' => array( + 'name' => __( 'Member Directories', 'ultimate-member' ), + 'singular_name' => __( 'Member Directory', 'ultimate-member' ), + 'add_new' => __( 'Add New', 'ultimate-member' ), + 'add_new_item' => __( 'Add New Member Directory', 'ultimate-member' ), + 'edit_item' => __( 'Edit Member Directory', 'ultimate-member' ), + 'not_found' => __( 'You did not create any member directories yet', 'ultimate-member' ), + 'not_found_in_trash' => __( 'Nothing found in Trash', 'ultimate-member' ), + 'search_items' => __( 'Search Member Directories', 'ultimate-member' ), + ), + 'capabilities' => array( + 'edit_post' => 'manage_options', + 'read_post' => 'manage_options', + 'delete_post' => 'manage_options', + 'edit_posts' => 'manage_options', + 'edit_others_posts' => 'manage_options', + 'delete_posts' => 'manage_options', + 'publish_posts' => 'manage_options', + 'read_private_posts' => 'manage_options', + ), + 'show_ui' => true, + 'show_in_menu' => false, + 'public' => false, + 'show_in_rest' => true, + 'supports' => array( 'title' ), + ) + ); + } + } + } +} diff --git a/includes/common/class-init.php b/includes/common/class-init.php new file mode 100644 index 00000000..b33146f1 --- /dev/null +++ b/includes/common/class-init.php @@ -0,0 +1,64 @@ +cpt()->hooks(); + $this->screen(); + $this->secure()->hooks(); + } + + /** + * @since 2.6.8 + * + * @return CPT + */ + public function cpt() { + if ( empty( UM()->classes['um\common\cpt'] ) ) { + UM()->classes['um\common\cpt'] = new CPT(); + } + return UM()->classes['um\common\cpt']; + } + + /** + * @since 2.6.8 + * + * @return Screen + */ + public function screen() { + if ( empty( UM()->classes['um\common\screen'] ) ) { + UM()->classes['um\common\screen'] = new Screen(); + } + return UM()->classes['um\common\screen']; + } + + /** + * @since 2.6.8 + * + * @return Secure + */ + public function secure() { + if ( empty( UM()->classes['um\common\secure'] ) ) { + UM()->classes['um\common\secure'] = new Secure(); + } + return UM()->classes['um\common\secure']; + } + } +} diff --git a/includes/common/class-screen.php b/includes/common/class-screen.php new file mode 100644 index 00000000..494cdae8 --- /dev/null +++ b/includes/common/class-screen.php @@ -0,0 +1,44 @@ +options()->get( 'secure_ban_admins_accounts' ) ) { + return; + } + + if ( UM()->options()->get( 'secure_notify_admins_banned_accounts' ) ) { + $notification_interval = UM()->options()->get( 'secure_notify_admins_banned_accounts__interval' ); + if ( 'instant' === $notification_interval ) { + return; + } + + if ( 'hourly' === $notification_interval ) { + add_action( 'um_hourly_scheduled_events', array( $this, 'notify_administrators_hourly' ) ); + } elseif ( 'daily' === $notification_interval ) { + add_action( 'um_daily_scheduled_events', array( $this, 'notify_administrators_daily' ) ); + } + } + } + + /** + * Notify Administrators hourly - Suspicious activities in an hour + * + * @since 2.6.8 + */ + public function notify_administrators_hourly() { + $user_ids = get_users( + array( + 'fields' => 'ids', + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => 'um_user_blocked__timestamp', + 'value' => gmdate( 'Y-m-d H:i:s', strtotime( '-1 hour' ) ), + 'compare' => '>=', + 'type' => 'DATETIME', + ), + ), + ) + ); + + $this->send_notification( $user_ids ); + } + + /** + * Notify Administrators daily - Today's suspicious activity + * + * @since 2.6.8 + */ + public function notify_administrators_daily() { + $user_ids = get_users( + array( + 'fields' => 'ids', + 'relation' => 'AND', + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => 'um_user_blocked__timestamp', + 'value' => gmdate( 'Y-m-d H:i:s', strtotime( '-1 day' ) ), + 'compare' => '>=', + 'type' => 'DATE', + ), + array( + 'key' => 'um_user_blocked__timestamp', + 'value' => gmdate( 'Y-m-d H:i:s', strtotime( 'now' ) ), + 'compare' => '<=', + 'type' => 'DATE', + ), + ), + ) + ); + + $this->send_notification( $user_ids ); + } + + public function send_notification( $user_ids ) { + $banned_profile_links = ''; + foreach ( $user_ids as $uid ) { + um_fetch_user( $uid ); + $banned_profile_links .= UM()->user()->get_profile_link( $uid ) . ' ' . um_user( 'account_status' ) . '
'; + } + um_reset_user(); + + $emails = um_multi_admin_email(); + if ( ! empty( $emails ) ) { + foreach ( $emails as $email ) { + UM()->mail()->send( + $email, + 'suspicious-activity', + array( + 'admin' => true, + 'tags' => array( + '{banned_profile_links}', + ), + 'tags_replace' => array( + $banned_profile_links, + ), + ) + ); + } + } + } + + /** + * Get the banned capabilities list. + * + * @return array + */ + public function get_banned_capabilities_list() { + /** + * Filters the banned capabilities for UM Register forms. + * + * @param {array} $capabilities WordPress Administrative Capabilities. + * + * @return {array} Banned admin capabilities. + * + * @since 2.6.8 + * @hook um_secure_register_form_banned_capabilities + * + * @example Added `read` capability as banned. + * function my_banned_capabilities( $capabilities ) { + * $capabilities[] = 'read'; + * return $capabilities; + * } + * add_filter( 'um_secure_register_form_banned_capabilities', 'my_banned_capabilities' ); + */ + $banned_admin_capabilities = apply_filters( + 'um_secure_register_form_banned_capabilities', + array( + 'create_sites', + 'delete_sites', + 'manage_network', + 'manage_sites', + 'manage_network_users', + 'manage_network_plugins', + 'manage_network_themes', + 'manage_network_options', + 'upgrade_network', + 'setup_network', + 'activate_plugins', + 'edit_dashboard', + 'edit_theme_options', + 'export', + 'import', + 'list_users', + 'remove_users', + 'switch_themes', + 'customize', + 'delete_site', + 'update_core', + 'update_plugins', + 'update_themes', + 'install_plugins', + 'install_themes', + 'delete_themes', + 'delete_plugins', + 'edit_plugins', + 'edit_themes', + 'edit_files', + 'edit_users', + 'add_users', + 'create_users', + 'delete_users', + 'level_10', + 'manage_options', + 'promote_users', + ) + ); + return $banned_admin_capabilities; + } + + /** + * Revoke Caps & Mark rejected as suspicious + * + * @param WP_User $user + * + * @since 2.6.8 + */ + public function revoke_caps( $user ) { + $user_agent = ''; + if ( isset( $_REQUEST['nonce'], $_REQUEST['action'] ) && 'um_secure_scan_affected_users' === $_REQUEST['action'] && wp_verify_nonce( $_REQUEST['nonce'], 'um-admin-nonce' ) && current_user_can( 'manage_options' ) ) { + $user_agent = __( 'Ultimate Member Scanner', 'ultimate-member' ); + } elseif ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) { + $user_agent = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ); + } + + um_fetch_user( $user->ID ); + + // Capture details. + $captured = array( + 'capabilities' => $user->allcaps, + 'submitted' => ! empty( UM()->form()->post_form ) ? UM()->form()->post_form : '', + 'roles' => $user->roles, + 'user_agent' => $user_agent, + 'account_status' => um_user( 'status' ), + ); + update_user_meta( $user->ID, 'um_user_blocked__metadata', $captured ); + + $user->remove_all_caps(); + $user->update_user_level_from_caps(); + + if ( is_user_logged_in() ) { + UM()->user()->set_status( 'inactive' ); + } else { + UM()->user()->set_status( 'rejected' ); + } + um_reset_user(); + update_user_meta( $user->ID, 'um_user_blocked', 'suspicious_activity' ); + update_user_meta( $user->ID, 'um_user_blocked__timestamp', current_time( 'mysql' ) ); + } + + /** + * Always add default banned capabilities. + * + * @param mixed $option_value + * + * @return mixed + * + * @since 2.6.8 + */ + public function add_default_capabilities( $option_value ) { + if ( is_array( $option_value ) ) { + $option_value = array_merge( $option_value, UM()->options()->get_default( 'banned_capabilities' ) ); + } + return $option_value; + } + } +} diff --git a/includes/core/class-common.php b/includes/core/class-common.php deleted file mode 100644 index 77f6e7f9..00000000 --- a/includes/core/class-common.php +++ /dev/null @@ -1,118 +0,0 @@ - array( - 'name' => __( 'Forms', 'ultimate-member' ), - 'singular_name' => __( 'Form', 'ultimate-member' ), - 'add_new' => __( 'Add New', 'ultimate-member' ), - 'add_new_item' => __( 'Add New Form', 'ultimate-member' ), - 'edit_item' => __( 'Edit Form', 'ultimate-member' ), - 'not_found' => __( 'You did not create any forms yet', 'ultimate-member' ), - 'not_found_in_trash' => __( 'Nothing found in Trash', 'ultimate-member' ), - 'search_items' => __( 'Search Forms', 'ultimate-member' ), - ), - 'capabilities' => array( - 'edit_post' => 'manage_options', - 'read_post' => 'manage_options', - 'delete_post' => 'manage_options', - 'edit_posts' => 'manage_options', - 'edit_others_posts' => 'manage_options', - 'delete_posts' => 'manage_options', - 'publish_posts' => 'manage_options', - 'read_private_posts' => 'manage_options', - ), - 'show_ui' => true, - 'show_in_menu' => false, - 'public' => false, - 'show_in_rest' => true, - 'supports' => array( 'title' ), - ) ); - - if ( UM()->options()->get( 'members_page' ) || ! get_option( 'um_options' ) ) { - - register_post_type( 'um_directory', array( - 'labels' => array( - 'name' => __( 'Member Directories', 'ultimate-member' ), - 'singular_name' => __( 'Member Directory', 'ultimate-member' ), - 'add_new' => __( 'Add New', 'ultimate-member' ), - 'add_new_item' => __( 'Add New Member Directory', 'ultimate-member' ), - 'edit_item' => __( 'Edit Member Directory', 'ultimate-member' ), - 'not_found' => __( 'You did not create any member directories yet', 'ultimate-member' ), - 'not_found_in_trash' => __( 'Nothing found in Trash', 'ultimate-member' ), - 'search_items' => __( 'Search Member Directories', 'ultimate-member' ), - ), - 'capabilities' => array( - 'edit_post' => 'manage_options', - 'read_post' => 'manage_options', - 'delete_post' => 'manage_options', - 'edit_posts' => 'manage_options', - 'edit_others_posts' => 'manage_options', - 'delete_posts' => 'manage_options', - 'publish_posts' => 'manage_options', - 'read_private_posts' => 'manage_options', - ), - 'show_ui' => true, - 'show_in_menu' => false, - 'public' => false, - 'show_in_rest' => true, - 'supports' => array( 'title' ), - ) ); - - } - - } - } -} \ No newline at end of file diff --git a/includes/core/class-cron.php b/includes/core/class-cron.php index 93a1083f..63b0ad34 100644 --- a/includes/core/class-cron.php +++ b/includes/core/class-cron.php @@ -1,122 +1,126 @@ - */ - $um_cron = apply_filters( 'um_cron_disable', false ); - if ( $um_cron ) { - return; - } - add_filter( 'cron_schedules', array( $this, 'add_schedules' ) ); - add_action( 'wp', array( $this, 'schedule_Events' ) ); + add_action( 'wp', array( $this, 'schedule_events' ) ); } + /** + * @return bool + */ + private function cron_disabled() { + /** + * Filters variable for disable Ultimate Member WP Cron actions. + * + * @since 2.0 + * @hook um_cron_disable + * + * @param {bool} $is_disabled Shortcode arguments. + * + * @return {bool} Do Cron actions are disabled? True for disable. + * + * @example Disable all Ultimate Member WP Cron actions. + * add_filter( 'um_cron_disable', '__return_true' ); + */ + return apply_filters( 'um_cron_disable', false ); + } /** + * Adds once weekly to the existing schedules. + * * @param array $schedules * * @return array */ public function add_schedules( $schedules = array() ) { + if ( $this->cron_disabled() ) { + return $schedules; + } - // Adds once weekly to the existing schedules. $schedules['weekly'] = array( 'interval' => 604800, - 'display' => __( 'Once Weekly', 'ultimate-member' ) + 'display' => __( 'Once Weekly', 'ultimate-member' ), ); return $schedules; } - /** * */ - public function schedule_Events() { + public function schedule_events() { + if ( $this->cron_disabled() ) { + return; + } + $this->weekly_events(); $this->daily_events(); $this->twicedaily_events(); $this->hourly_events(); } - /** * */ private function weekly_events() { + $sunday_start = wp_date( 'w' ); + $week_start = $sunday_start - absint( get_option( 'start_of_week' ) ); + $week_start_day = strtotime( '-' . $week_start . ' days' ); + $time = mktime( 0, 0, 0, wp_date( 'm', $week_start_day ), wp_date( 'd', $week_start_day ), wp_date( 'Y', $week_start_day ) ); if ( ! wp_next_scheduled( 'um_weekly_scheduled_events' ) ) { - wp_schedule_event( current_time( 'timestamp' ), 'weekly', 'um_weekly_scheduled_events' ); + wp_schedule_event( $time, 'weekly', 'um_weekly_scheduled_events' ); } } - /** * */ private function daily_events() { if ( ! wp_next_scheduled( 'um_daily_scheduled_events' ) ) { - wp_schedule_event( current_time( 'timestamp' ), 'daily', 'um_daily_scheduled_events' ); + $time = mktime( 0, 0, 0, wp_date( 'm' ), wp_date( 'd' ), wp_date( 'Y' ) ); + wp_schedule_event( $time, 'daily', 'um_daily_scheduled_events' ); } } - /** * */ private function twicedaily_events() { if ( ! wp_next_scheduled( 'um_twicedaily_scheduled_events' ) ) { - wp_schedule_event( current_time( 'timestamp' ), 'twicedaily', 'um_twicedaily_scheduled_events' ); + $time = mktime( 0, 0, 0, wp_date( 'm' ), wp_date( 'd' ), wp_date( 'Y' ) ); + wp_schedule_event( $time, 'twicedaily', 'um_twicedaily_scheduled_events' ); } } - /** * */ private function hourly_events() { if ( ! wp_next_scheduled( 'um_hourly_scheduled_events' ) ) { - wp_schedule_event( current_time( 'timestamp' ), 'hourly', 'um_hourly_scheduled_events' ); + $time = mktime( wp_date( 'H' ), 0, 0, wp_date( 'm' ), wp_date( 'd' ), wp_date( 'Y' ) ); + wp_schedule_event( $time, 'hourly', 'um_hourly_scheduled_events' ); } } - + /** + * Breaks all Ultimate Member registered schedule events. + */ public function unschedule_events() { wp_clear_scheduled_hook( 'um_weekly_scheduled_events' ); wp_clear_scheduled_hook( 'um_daily_scheduled_events' ); diff --git a/includes/core/class-options.php b/includes/core/class-options.php index 9c97bb7b..a453e1f9 100644 --- a/includes/core/class-options.php +++ b/includes/core/class-options.php @@ -114,12 +114,13 @@ if ( ! class_exists( 'um\core\Options' ) ) { * @use UM()->config() * * @param $option_id - * @return bool + * @return mixed */ function get_default( $option_id ) { $settings_defaults = UM()->config()->settings_defaults; - if ( ! isset( $settings_defaults[ $option_id ] ) ) + if ( ! isset( $settings_defaults[ $option_id ] ) ) { return false; + } return $settings_defaults[ $option_id ]; } diff --git a/includes/frontend/class-init.php b/includes/frontend/class-init.php new file mode 100644 index 00000000..21eff21a --- /dev/null +++ b/includes/frontend/class-init.php @@ -0,0 +1,38 @@ +secure(); + } + + /** + * @since 2.6.8 + * + * @return Secure + */ + public function secure() { + if ( empty( UM()->classes['um\frontend\secure'] ) ) { + UM()->classes['um\frontend\secure'] = new Secure(); + } + return UM()->classes['um\frontend\secure']; + } + } +} diff --git a/includes/frontend/class-secure.php b/includes/frontend/class-secure.php new file mode 100644 index 00000000..dfcb51de --- /dev/null +++ b/includes/frontend/class-secure.php @@ -0,0 +1,269 @@ +options()->get( 'secure_ban_admins_accounts' ) ) { + return; + } + + /** + * Checks the integrity of Current User's Capabilities + */ + add_action( 'um_after_save_registration_details', array( $this, 'secure_user_capabilities' ), 1 ); + add_action( 'um_after_save_registration_details', array( $this, 'maybe_set_whitelisted_password' ), 2 ); + if ( is_user_logged_in() && ! current_user_can( 'manage_options' ) ) { // Exclude current Logged-in Administrator from validation checks. + add_action( 'um_after_user_updated', array( $this, 'secure_user_capabilities' ), 1 ); + add_action( 'um_after_user_account_updated', array( $this, 'secure_user_capabilities' ), 1 ); + } + } + + /** + * Add Login notice for Reset Password + * + * @since 2.6.8 + */ + public function reset_password_notice() { + if ( ! UM()->options()->get( 'display_login_form_notice' ) ) { + return; + } + + // phpcs:disable WordPress.Security.NonceVerification + if ( ! isset( $_REQUEST['notice'] ) || 'expired_password' !== $_REQUEST['notice'] ) { + return; + } + // phpcs:enable WordPress.Security.NonceVerification + + echo "

"; + echo wp_kses( + sprintf( + // translators: One-time change requires you to reset your password + __( 'Important: Your password has expired. This (one-time) change requires you to reset your password. Please click here to reset your password via Email.', 'ultimate-member' ), + um_get_core_page( 'password-reset' ) + ), + array( + 'strong' => array(), + 'a' => array( + 'href' => array(), + ), + ) + ); + echo '

'; + } + + /** + * Add Login notice for Under Maintance + * + * @since 2.6.8 + */ + public function under_maintenance_notice() { + if ( ! UM()->options()->get( 'lock_register_forms' ) ) { + return; + } + + // phpcs:disable WordPress.Security.NonceVerification + if ( ! isset( $_GET['notice'] ) || 'maintenance' !== $_GET['notice'] ) { + return; + } + // phpcs:enable WordPress.Security.NonceVerification + + echo "

"; + echo wp_kses( + __( 'Important: This site is currently under maintenance. Please log in or check back soon.', 'ultimate-member' ), + array( + 'strong' => array(), + 'a' => array( + 'href' => array(), + ), + ) + ); + echo '

'; + } + + /** + * Block all UM Register form submissions. + * + * @since 2.6.8 + */ + public function block_register_forms() { + if ( UM()->options()->get( 'lock_register_forms' ) ) { + $login_url = add_query_arg( 'notice', 'maintenance', um_get_core_page( 'login' ) ); + nocache_headers(); + wp_safe_redirect( $login_url ); + exit; + } + } + + /** + * Validate when user has expired password + * + * @since 2.6.8 + */ + public function login_validate_expired_pass() { + if ( UM()->options()->get( 'display_login_form_notice' ) ) { + $expired_password_reset = get_user_meta( um_user( 'ID' ), 'um_secure_has_reset_password', true ); + if ( ! $expired_password_reset ) { + $login_url = add_query_arg( 'notice', 'expired_password', um_get_core_page( 'login' ) ); + wp_safe_redirect( $login_url ); + exit; + } + } + } + + /** + * Prevent users from using Old Passwords on UM Password Reset form. + * + * @param WP_Error $errors + * @param WP_User|WP_Error $user + * + * @since 2.6.8 + */ + public function avoid_old_password( $errors, $user ) { + if ( empty( $_POST['_um_password_change'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification + return; + } + + if ( ! UM()->options()->get( 'display_login_form_notice' ) ) { + return; + } + + if ( isset( $_REQUEST['user_password'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification + $new_user_pass = wp_unslash( $_REQUEST['user_password'] ); // phpcs:ignore WordPress.Security.NonceVerification + if ( wp_check_password( $new_user_pass, $user->data->user_pass, $user->ID ) ) { + UM()->form()->add_error( 'user_password', __( 'Your new password cannot be same as old password.', 'ultimate-member' ) ); + $errors->add( 'um_block_old_password', __( 'Your new password cannot be same as old password.', 'ultimate-member' ) ); + } else { + update_user_meta( $user->ID, 'um_secure_has_reset_password', true ); + update_user_meta( $user->ID, 'um_secure_has_reset_password__timestamp', current_time( 'mysql' ) ); + } + } + } + + /** + * Secure user capabilities and revoke administrative ones. + * + * @since 2.6.8 + * + * @param int $user_id + */ + public function secure_user_capabilities( $user_id ) { + global $wpdb; + $user = get_userdata( $user_id ); + if ( empty( $user ) ) { + return; + } + + // Fetch the WP_User object of our user. + um_fetch_user( $user_id ); + $has_admin_cap = false; + $arr_banned_caps = UM()->options()->get( 'banned_capabilities' ); + + if ( is_array( $arr_banned_caps ) ) { + foreach ( $arr_banned_caps as $cap ) { + /** + * When there's at least one administrator cap added to the user, + * immediately revoke caps and mark as rejected. + */ + if ( $user->has_cap( $cap ) ) { + $has_admin_cap = true; + break; + } + } + } + + if ( ! $has_admin_cap ) { + /** + * Double-check if *_user_level has been modified with the highest level + * when user has no administrative capabilities. + */ + $user_level = um_user( $wpdb->get_blog_prefix() . 'user_level' ); + if ( ! empty( $user_level ) && 10 === absint( $user_level ) ) { + $has_admin_cap = true; + } + } + + if ( $has_admin_cap ) { + UM()->common()->secure()->revoke_caps( $user ); + /** + * Notify Administrators Immediately + */ + if ( UM()->options()->get( 'secure_notify_admins_banned_accounts' ) ) { + $interval = UM()->options()->get( 'secure_notify_admins_banned_accounts__interval' ); + if ( 'instant' === $interval ) { + UM()->common()->secure()->send_notification( array( $user_id ) ); + } + } + + // Destroy Sessions & Redirect. + wp_destroy_current_session(); + wp_logout(); + session_unset(); + $redirect = apply_filters( 'um_secure_blocked_user_redirect_immediately', true ); + if ( $redirect ) { + $login_url = add_query_arg( 'err', 'inactive', um_get_core_page( 'login' ) ); + wp_safe_redirect( $login_url ); + exit; + } + } + } + + /** + * Set meta (no need to reset his password) if the user is a new registered. + * + * @since 2.6.8 + * + * @param int $user_id + */ + public function maybe_set_whitelisted_password( $user_id ) { + $user = get_userdata( $user_id ); + if ( empty( $user ) ) { + return; + } + + if ( UM()->options()->get( 'display_login_form_notice' ) ) { + update_user_meta( $user_id, 'um_secure_has_reset_password', true ); + update_user_meta( $user_id, 'um_secure_has_reset_password__timestamp', current_time( 'mysql' ) ); + } + } + } +} diff --git a/templates/email/suspicious-activity.php b/templates/email/suspicious-activity.php new file mode 100644 index 00000000..3f60c14b --- /dev/null +++ b/templates/email/suspicious-activity.php @@ -0,0 +1,36 @@ + +
+ +
+
{site_name}
+ +
+
+ +
+ +
This is to inform you that there are suspicious activities with the following account(s):
+
{banned_profile_links}
+
Due to that we have set each account(s) status to rejected or deactivated, revoked roles & destroyed the login session.
+ +
+ +
+ +
- Sent via Ultimate Member plugin.
+ +
+ +