diff --git a/includes/admin/class-secure.php b/includes/admin/class-secure.php index 89405b5f..65f8ec79 100644 --- a/includes/admin/class-secure.php +++ b/includes/admin/class-secure.php @@ -298,8 +298,8 @@ if ( ! class_exists( 'um\admin\Secure' ) ) { array( 'id' => 'secure_allowed_redirect_hosts', 'type' => 'textarea', - 'label' => __( 'Allowed hosts for redirect (one host per line)', 'ultimate-member' ), - 'description' => __( 'Extend allowed hosts for redirects', 'ultimate-member' ), + 'label' => __( 'Allowed hosts for safe redirect (one host per line)', 'ultimate-member' ), + 'description' => __( 'Extend allowed hosts for frontend pages redirects', 'ultimate-member' ), ), ) ); diff --git a/includes/admin/core/class-admin-settings.php b/includes/admin/core/class-admin-settings.php index 349a5c2a..d8a7cb4e 100644 --- a/includes/admin/core/class-admin-settings.php +++ b/includes/admin/core/class-admin-settings.php @@ -962,6 +962,9 @@ if ( ! class_exists( 'um\admin\core\Admin_Settings' ) ) { 'secure_notify_admins_banned_accounts__interval' => array( 'sanitize' => 'key', ), + 'secure_allowed_redirect_hosts' => array( + 'sanitize' => 'textarea', + ), ) ); diff --git a/includes/class-config.php b/includes/class-config.php index 26259166..67a45e66 100644 --- a/includes/class-config.php +++ b/includes/class-config.php @@ -589,6 +589,7 @@ if ( ! class_exists( 'um\Config' ) ) { 'banned_capabilities' => array( 'manage_options', 'promote_users', 'level_10' ), 'secure_notify_admins_banned_accounts' => false, 'secure_notify_admins_banned_accounts__interval' => 'instant', + 'secure_allowed_redirect_hosts' => '', ); add_filter( 'um_get_tabs_from_config', '__return_true' ); diff --git a/includes/core/class-login.php b/includes/core/class-login.php index e4d2576d..c4b4af83 100644 --- a/includes/core/class-login.php +++ b/includes/core/class-login.php @@ -61,7 +61,22 @@ if ( ! class_exists( 'um\core\Login' ) ) { } if ( empty( $args['_wpnonce'] ) || ! wp_verify_nonce( $args['_wpnonce'], 'um_login_form' ) ) { - // @todo add hookdocs + /** + * Filters URL for redirect if login form nonce isn't verified. + * + * @param {string} $error_url URL for redirect if login form nonce isn't verified. + * + * @return {string} URL for redirect. + * + * @since 2.0 + * @hook um_login_invalid_nonce_redirect_url + * + * @example Change URL for redirect if login form nonce isn't verified. + * function my_um_login_invalid_nonce_redirect_url( $error_url ) { + * return '{your_custom_url}'; + * } + * add_filter( 'um_login_invalid_nonce_redirect_url', 'my_um_login_invalid_nonce_redirect_url' ); + */ $url = apply_filters( 'um_login_invalid_nonce_redirect_url', add_query_arg( array( 'err' => 'invalid_nonce' ) ) ); um_safe_redirect( $url ); exit; diff --git a/includes/core/class-logout.php b/includes/core/class-logout.php index e93f6538..30999d41 100644 --- a/includes/core/class-logout.php +++ b/includes/core/class-logout.php @@ -79,29 +79,25 @@ if ( ! class_exists( 'um\core\Logout' ) ) { wp_destroy_current_session(); wp_logout(); session_unset(); - exit( wp_safe_redirect( home_url() ) ); + wp_safe_redirect( home_url() ); + exit; } else { /** - * UM hook + * Filters URL for redirect after logout. * - * @type filter - * @title um_logout_redirect_url - * @description Change redirect URL after logout - * @input_vars - * [{"var":"$url","type":"string","desc":"Redirect URL"}, - * {"var":"$id","type":"int","desc":"User ID"}] - * @change_log - * ["Since: 2.0"] - * @usage - * - * @example - * Change URL for redirect after logout. + * function my_logout_redirect_url( $logout_redirect_url, $user_id ) { + * return '{your_custom_url}'; * } - * ?> + * add_filter( 'um_logout_redirect_url', 'my_logout_redirect_url', 10, 2 ); */ $redirect_url = apply_filters( 'um_logout_redirect_url', um_user( 'logout_redirect_url' ), um_user( 'ID' ) ); wp_destroy_current_session(); @@ -111,7 +107,8 @@ if ( ! class_exists( 'um\core\Logout' ) ) { } } else { add_filter( 'wp_safe_redirect_fallback', array( &$this, 'safe_redirect_default' ), 10, 2 ); - exit( wp_safe_redirect( home_url() ) ); + wp_safe_redirect( home_url() ); + exit; } } diff --git a/includes/core/class-password.php b/includes/core/class-password.php index 8c2f2167..c83b715b 100644 --- a/includes/core/class-password.php +++ b/includes/core/class-password.php @@ -239,7 +239,7 @@ if ( ! class_exists( 'um\core\Password' ) ) { if ( isset( $_GET['hash'] ) && isset( $_GET['login'] ) ) { $value = sprintf( '%s:%s', wp_unslash( $_GET['login'] ), wp_unslash( $_GET['hash'] ) ); $this->setcookie( $rp_cookie, $value ); - + // Not `um_safe_redirect()` because password-reset page is predefined page and is situated on the same host. wp_safe_redirect( remove_query_arg( array( 'hash', 'login' ) ) ); exit; } diff --git a/includes/core/class-register.php b/includes/core/class-register.php index f968dc72..de6c4fe6 100644 --- a/includes/core/class-register.php +++ b/includes/core/class-register.php @@ -55,7 +55,22 @@ if ( ! class_exists( 'um\core\Register' ) ) { } if ( empty( $args['_wpnonce'] ) || ! wp_verify_nonce( $args['_wpnonce'], 'um_register_form' ) ) { - // @todo add hookdocs + /** + * Filters URL for redirect if register form nonce isn't verified. + * + * @param {string} $error_url URL for redirect if register form nonce isn't verified. + * + * @return {string} URL for redirect. + * + * @since 2.0 + * @hook um_register_invalid_nonce_redirect_url + * + * @example Change URL for redirect if register form nonce isn't verified. + * function my_um_register_invalid_nonce_redirect_url( $error_url ) { + * return '{your_custom_url}'; + * } + * add_filter( 'um_register_invalid_nonce_redirect_url', 'my_um_register_invalid_nonce_redirect_url' ); + */ $url = apply_filters( 'um_register_invalid_nonce_redirect_url', add_query_arg( array( 'err' => 'invalid_nonce' ) ) ); um_safe_redirect( $url ); exit; diff --git a/includes/core/um-actions-login.php b/includes/core/um-actions-login.php index 947142bc..ff68968b 100644 --- a/includes/core/um-actions-login.php +++ b/includes/core/um-actions-login.php @@ -140,11 +140,13 @@ function um_submit_form_errors_hook_logincheck( $submitted_data, $form_data ) { case 'awaiting_email_confirmation': case 'rejected': um_reset_user(); + // Not `um_safe_redirect()` because UM()->permalinks()->get_current_url() is situated on the same host. wp_safe_redirect( add_query_arg( 'err', esc_attr( $status ), UM()->permalinks()->get_current_url() ) ); exit; } if ( isset( $form_data['form_id'] ) && absint( $form_data['form_id'] ) === absint( UM()->shortcodes()->core_login_form() ) && UM()->form()->errors && ! isset( $_POST[ UM()->honeypot ] ) ) { + // Not `um_safe_redirect()` because predefined login page is situated on the same host. wp_safe_redirect( um_get_core_page( 'login' ) ); exit; } @@ -224,12 +226,14 @@ function um_user_login( $submitted_data ) { // Role redirect $after_login = um_user( 'after_login' ); if ( empty( $after_login ) ) { + // Not `um_safe_redirect()` because predefined user profile page is situated on the same host. wp_safe_redirect( um_user_profile_url() ); exit; } switch ( $after_login ) { case 'redirect_admin': + // Not `um_safe_redirect()` because is redirected to wp-admin. wp_safe_redirect( admin_url() ); exit; case 'redirect_url': @@ -255,10 +259,12 @@ function um_user_login( $submitted_data ) { um_safe_redirect( $redirect_url ); exit; case 'refresh': + // Not `um_safe_redirect()` because UM()->permalinks()->get_current_url() is situated on the same host. wp_safe_redirect( UM()->permalinks()->get_current_url() ); exit; case 'redirect_profile': default: + // Not `um_safe_redirect()` because predefined user profile page is situated on the same host. wp_safe_redirect( um_user_profile_url() ); exit; } diff --git a/includes/core/um-actions-profile.php b/includes/core/um-actions-profile.php index 7a6944dc..f5b38c34 100644 --- a/includes/core/um-actions-profile.php +++ b/includes/core/um-actions-profile.php @@ -508,6 +508,7 @@ function um_user_edit_profile( $args, $form_data ) { // Finally redirect to profile. $url = um_user_profile_url( $user_id ); $url = apply_filters( 'um_update_profile_redirect_after', $url, $user_id, $args ); + // Not `um_safe_redirect()` because predefined user profile page is situated on the same host. wp_safe_redirect( um_edit_my_profile_cancel_uri( $url ) ); exit; } diff --git a/includes/core/um-actions-register.php b/includes/core/um-actions-register.php index 89098d95..d8617175 100644 --- a/includes/core/um-actions-register.php +++ b/includes/core/um-actions-register.php @@ -146,32 +146,70 @@ add_action( 'um_registration_complete', 'um_send_registration_notification' ); function um_check_user_status( $user_id, $args, $form_data = null ) { $status = um_user( 'account_status' ); /** - * UM hook + * Fires after complete UM user registration. + * Where $status can be equal to 'approved', 'checkmail' or 'pending'. * - * @type action - * @title um_post_registration_{$status}_hook - * @description After complete UM user registration. - * @input_vars - * [{"var":"$user_id","type":"int","desc":"User ID"}, - * {"var":"$args","type":"array","desc":"Form data"}] - * @change_log - * ["Since: 2.0"] - * @usage add_action( 'um_post_registration_{$status}_hook', 'function_name', 10, 2 ); - * @example - * Make a custom action after complete UM user registration when user get an approved status. + * function my_um_post_registration( $user_id, $submitted_data, $form_data ) { * // your code here * } - * ?> + * add_action( 'um_post_registration_approved_hook', 'my_um_post_registration', 10, 3 ); + * @example Make a custom action after complete UM user registration when user requires email activation. + * function my_um_post_registration( $user_id, $submitted_data, $form_data ) { + * // your code here + * } + * add_action( 'um_post_registration_checkmail_hook', 'my_um_post_registration', 10, 3 ); + * @example Make a custom action after complete UM user registration when user requires admin review. + * function my_um_post_registration( $user_id, $submitted_data, $form_data ) { + * // your code here + * } + * add_action( 'um_post_registration_pending_hook', 'my_um_post_registration', 10, 3 ); */ - do_action( "um_post_registration_{$status}_hook", $user_id, $args ); + do_action( "um_post_registration_{$status}_hook", $user_id, $args, $form_data ); if ( is_null( $form_data ) || is_admin() ) { return; } - do_action( "track_{$status}_user_registration" ); + /** + * Fires after complete UM user registration. Only for the frontend action which is run before autologin and redirects. + * Where $status can be equal to 'approved', 'checkmail' or 'pending'. + * + * @since 1.3.x + * @since 2.6.8 Added $user_id, $submitted_data, $form_data arguments. + * + * @hook track_{$status}_user_registration + * + * @param {int} $user_id User ID. Since 2.6.8 + * @param {array} $submitted_data Registration form submitted data. Since 2.6.8 + * @param {array} $form_data Form data. Since 2.6.8 + * + * @example Make a custom action after complete UM user registration when user get an approved status. + * function my_um_post_registration( $user_id, $submitted_data, $form_data ) { + * // your code here + * } + * add_action( 'track_approved_user_registration', 'my_um_post_registration', 10, 3 ); + * @example Make a custom action after complete UM user registration when user requires email activation. + * function my_um_post_registration( $user_id, $submitted_data, $form_data ) { + * // your code here + * } + * add_action( 'track_checkmail_user_registration', 'my_um_post_registration', 10, 3 ); + * @example Make a custom action after complete UM user registration when user requires admin review. + * function my_um_post_registration( $user_id, $submitted_data, $form_data ) { + * // your code here + * } + * add_action( 'track_pending_user_registration', 'my_um_post_registration', 10, 3 ); + */ + do_action( "track_{$status}_user_registration", $user_id, $args, $form_data ); if ( 'approved' === $status ) { // Check if user is logged in because there can be the customized way when through 'um_registration_for_loggedin_users' hook the registration is enabled for the logged-in users (e.g. Administrator). @@ -182,72 +220,60 @@ function um_check_user_status( $user_id, $args, $form_data = null ) { UM()->user()->generate_profile_slug( $user_id ); /** - * UM hook + * Fires after complete UM user registration and autologin. * - * @type action - * @title um_registration_after_auto_login - * @description After complete UM user registration and autologin. - * @input_vars - * [{"var":"$user_id","type":"int","desc":"User ID"}] - * @change_log - * ["Since: 2.0"] - * @usage add_action( 'um_registration_after_auto_login', 'function_name', 10, 1 ); - * @example - * Make a custom action after complete UM user registration and autologin. + * function my_um_registration_after_auto_login( $user_id ) { * // your code here * } - * ?> + * add_action( 'um_registration_after_auto_login', 'my_um_registration_after_auto_login' ); */ do_action( 'um_registration_after_auto_login', $user_id ); // Priority redirect if ( isset( $args['redirect_to'] ) ) { um_safe_redirect( urldecode( $args['redirect_to'] ) ); - exit; } um_fetch_user( $user_id ); if ( 'redirect_url' === um_user( 'auto_approve_act' ) && '' !== um_user( 'auto_approve_url' ) ) { - um_safe_redirect( um_user( 'auto_approve_url' )); - exit; + um_safe_redirect( um_user( 'auto_approve_url' ) ); } if ( 'redirect_profile' === um_user( 'auto_approve_act' ) ) { + // Not `um_safe_redirect()` because predefined user profile page is situated on the same host. wp_safe_redirect( um_user_profile_url() ); exit; } } else { if ( 'redirect_url' === um_user( $status . '_action' ) && '' !== um_user( $status . '_url' ) ) { /** - * UM hook + * Filters the redirect URL for pending user after registration. * - * @type filter - * @title um_registration_pending_user_redirect - * @description Change redirect URL for pending user after registration - * @input_vars - * [{"var":"$url","type":"string","desc":"Redirect URL"}, - * {"var":"$status","type":"string","desc":"User status"}, - * {"var":"$user_id","type":"int","desc":"User ID"}] - * @change_log - * ["Since: 2.0"] - * @usage - * - * @example - * Change redirect URL for pending user after registration. * function my_registration_pending_user_redirect( $url, $status, $user_id ) { * // your code here * return $url; * } - * ?> + * add_filter( 'um_registration_pending_user_redirect', 'my_registration_pending_user_redirect', 10, 3 ); */ $redirect_url = apply_filters( 'um_registration_pending_user_redirect', um_user( $status . '_url' ), $status, um_user( 'ID' ) ); - um_safe_redirect( $redirect_url ); - exit; } if ( 'show_message' === um_user( $status . '_action' ) && '' !== um_user( $status . '_message' ) ) { @@ -256,7 +282,7 @@ function um_check_user_status( $user_id, $args, $form_data = null ) { // Add only priority role to URL. $url = add_query_arg( 'um_role', esc_attr( um_user( 'role' ) ), $url ); $url = add_query_arg( 'um_form_id', esc_attr( $form_data['form_id'] ), $url ); - + // Not `um_safe_redirect()` because UM()->permalinks()->get_current_url() is situated on the same host. wp_safe_redirect( $url ); exit; } @@ -714,6 +740,7 @@ function um_form_register_redirect() { $page_id = UM()->options()->get( UM()->options()->get_core_page_id( 'register' ) ); $register_post = get_post( $page_id ); if ( ! empty( $register_post ) ) { + // Not `um_safe_redirect()` because predefined register page is situated on the same host. wp_safe_redirect( get_permalink( $page_id ) ); exit(); } diff --git a/includes/frontend/class-secure.php b/includes/frontend/class-secure.php index d242c54a..fc5efe32 100644 --- a/includes/frontend/class-secure.php +++ b/includes/frontend/class-secure.php @@ -129,6 +129,7 @@ if ( ! class_exists( 'um\frontend\Secure' ) ) { if ( UM()->options()->get( 'lock_register_forms' ) ) { $login_url = add_query_arg( 'notice', 'maintenance', um_get_core_page( 'login' ) ); nocache_headers(); + // Not `um_safe_redirect()` because predefined login page is situated on the same host. wp_safe_redirect( $login_url ); exit; } @@ -144,6 +145,7 @@ if ( ! class_exists( 'um\frontend\Secure' ) ) { $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' ) ); + // Not `um_safe_redirect()` because predefined login page is situated on the same host. wp_safe_redirect( $login_url ); exit; } @@ -241,6 +243,7 @@ if ( ! class_exists( 'um\frontend\Secure' ) ) { $redirect = apply_filters( 'um_secure_blocked_user_redirect_immediately', true ); if ( $redirect ) { $login_url = add_query_arg( 'err', 'inactive', um_get_core_page( 'login' ) ); + // Not `um_safe_redirect()` because predefined login page is situated on the same host. wp_safe_redirect( $login_url ); exit; } diff --git a/includes/um-short-functions.php b/includes/um-short-functions.php index 7d1bbdb6..a8e1f5c4 100644 --- a/includes/um-short-functions.php +++ b/includes/um-short-functions.php @@ -2843,16 +2843,15 @@ function um_is_amp( $check_theme_support = true ) { } /** - * UM safe redirect + * UM safe redirect. By default, you can be redirected only to WordPress installation Home URL. Fallback URL is wp-admin URL. + * But it can be changed through filters and extended by UM Setting "Allowed hosts for safe redirect (one host per line)" and filter `um_wp_safe_redirect_fallback`. * - * @since 2.6.9 + * @since 2.6.8 * * @param string $url redirect URL. - * - * @return string */ function um_safe_redirect( $url ) { - add_filter( 'allowed_redirect_hosts', 'um_allowed_redirect_hosts', 10, 1 ); + add_filter( 'allowed_redirect_hosts', 'um_allowed_redirect_hosts' ); add_filter( 'wp_safe_redirect_fallback', 'um_wp_safe_redirect_fallback', 10, 2 ); wp_safe_redirect( $url ); @@ -2862,21 +2861,19 @@ function um_safe_redirect( $url ) { /** * UM allowed hosts * - * @since 2.6.9 + * @since 2.6.8 * - * @param array $hosts allowed hosts. + * @param array $hosts Allowed hosts. * * @return array */ function um_allowed_redirect_hosts( $hosts ) { - $hosts = UM()->options()->get( 'secure_allowed_redirect_hosts' ); - - $hosts = explode( "\n", $hosts ); - $hosts = array_unique( $hosts ); + $secure_hosts = UM()->options()->get( 'secure_allowed_redirect_hosts' ); + $secure_hosts = explode( "\n", $secure_hosts ); + $secure_hosts = array_unique( $secure_hosts ); $additional_hosts = array(); - - foreach ( $hosts as $key => $host ) { + foreach ( $secure_hosts as $host ) { if ( '' !== trim( $host ) ) { $host = trim( $host ); $host = str_replace( array( 'http://', 'https://' ), '', $host ); @@ -2887,26 +2884,28 @@ function um_allowed_redirect_hosts( $hosts ) { } if ( strpos( $host, 'www.' ) !== false ) { - if ( ! in_array( str_replace( array( 'www.' ), '', $host ), $additional_hosts, true ) ) { - $additional_hosts[] = str_replace( array( 'www.' ), '', $host ); + $strip_www = str_replace( 'www.', '', $host ); + if ( ! in_array( $strip_www, $additional_hosts, true ) ) { + $additional_hosts[] = $strip_www; } } else { - if ( ! in_array( 'www.' . $host, $additional_hosts, true ) ) { - $additional_hosts[] = 'www.' . $host; + $added_www = 'www.' . $host; + if ( ! in_array( $added_www, $additional_hosts, true ) ) { + $additional_hosts[] = $added_www; } } } } /** - * Filters change allowed hosts. + * Filters change allowed hosts. When `wp_safe_redirect()` function is used for the Ultimate Member frontend redirects. * - * @since 2.6.9 + * @since 2.6.8 * @hook um_allowed_redirect_hosts * - * @param {array} $additional_hosts allowed hosts. - * @param {array} $hosts default hosts. + * @param {array} $additional_hosts Allowed hosts. + * @param {array} $hosts Default hosts. * - * @return {array} allowed hosts. + * @return {array} Allowed hosts. * * @example Change allowed hosts. * function my_um_allowed_redirect_hosts( $additional_hosts, $hosts ) { @@ -2916,33 +2915,32 @@ function um_allowed_redirect_hosts( $hosts ) { * add_filter( 'um_allowed_redirect_hosts', 'my_um_allowed_redirect_hosts', 10, 2 ); */ $additional_hosts = apply_filters( 'um_allowed_redirect_hosts', $additional_hosts, $hosts ); - - $allowed_hosts = array_merge( $hosts, $additional_hosts ); - - return $allowed_hosts; + return array_merge( $hosts, $additional_hosts ); } /** * UM fallback redirect URL * - * @since 2.6.9 + * @since 2.6.8 * - * @param string $url fallback URL. - * @param string $status redirect status. + * @param string $url Fallback URL. + * @param string $status Redirect status. * * @return string */ function um_wp_safe_redirect_fallback( $url, $status ) { /** - * Filters change fallback URL. + * Filters change fallback URL. When `wp_safe_redirect()` function is used for the Ultimate Member frontend redirects. + * It's `home_url()` by default. * - * @since 2.6.9 + * @since 2.6.8 * @hook um_wp_safe_redirect_fallback * - * @param {string} $url fallback URL. - * @param {string} $status status. + * @param {string} $url UM Fallback URL. + * @param {string} $default_fallback Default fallback URL. + * @param {string} $status Redirect status. * - * @return {string} fallback URL. + * @return {string} Fallback URL. * * @example Change fallback URL. * function my_um_wp_safe_redirect_fallback( $url, $status ) { @@ -2951,7 +2949,5 @@ function um_wp_safe_redirect_fallback( $url, $status ) { * } * add_filter( 'um_wp_safe_redirect_fallback', 'my_um_wp_safe_redirect_fallback', 10, 2 ); */ - $url = apply_filters( 'um_wp_safe_redirect_fallback', home_url( '/' ), $status ); - - return $url; + return apply_filters( 'um_wp_safe_redirect_fallback', home_url( '/' ), $url, $status ); } diff --git a/readme.txt b/readme.txt index 5522615d..a3e229bf 100644 --- a/readme.txt +++ b/readme.txt @@ -166,7 +166,7 @@ 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.6.8: July 14, 2023 = += 2.6.8: July 19, 2023 = * Enhancements: @@ -175,6 +175,7 @@ IMPORTANT: PLEASE UPDATE THE PLUGIN TO AT LEAST VERSION 2.6.7 IMMEDIATELY. VERSI - Added: `um_edit_profile_url` hook for force changing user profile edit URL - Added: Additional hook attributes to 'um_reset_password_errors_hook' and 'um_reset_password_process_hook' - Added: $form_data attribute to 'um_before_save_registration_details' hook + - Added: `um_safe_redirect()` function for handle `wp_safe_redirect()` function with new the "Allowed hosts for safe redirect" setting - Updated: [Hooks Documentation v2](https://ultimatemember.github.io/ultimatemember/hooks/) * Bugfixes: diff --git a/ultimate-member.php b/ultimate-member.php index b71fb2e8..32a0ccba 100644 --- a/ultimate-member.php +++ b/ultimate-member.php @@ -3,7 +3,7 @@ Plugin Name: Ultimate Member Plugin URI: http://ultimatemember.com/ Description: The easiest way to create powerful online communities and beautiful user profiles with WordPress -Version: 2.6.8-rc.1 +Version: 2.6.8 Author: Ultimate Member Author URI: http://ultimatemember.com/ Text Domain: ultimate-member