diff --git a/ACTIONS.md b/ACTIONS.md index 83e4520c..f507c6c2 100644 --- a/ACTIONS.md +++ b/ACTIONS.md @@ -101,7 +101,10 @@ Fires within the Fictioneer user profile section in the WordPress `wp-admin/prof * $profile_user (WP_User) – The owner of the currently edited profile. **Hooked actions:** -* `fictioneer_admin_user_fields( $profile_user )` – User profile fields. Priority 5. +* `fictioneer_admin_profile_fields_fingerprint( $profile_user )` – User fingerprint field. Priority 5. +* `fictioneer_admin_profile_fields_flags( $profile_user )` – User flags. Priority 6. +* `fictioneer_admin_profile_fields_oauth( $profile_user )` – User OAuth connections. Priority 7. +* `fictioneer_admin_profile_fields_data_nodes( $profile_user )` – User data nodes. Priority 8. * `fictioneer_admin_profile_moderation( $profile_user )` – Moderation flags and message. Priority 10. * `fictioneer_admin_profile_author( $profile_user )` – Author page select, support message, and support links. Priority 20. * `fictioneer_admin_profile_oauth( $profile_user )` – OAuth 2.0 account binding IDs. Priority 30. diff --git a/includes/functions/_oauth.php b/includes/functions/_oauth.php index c51bbc38..ef883e5c 100644 --- a/includes/functions/_oauth.php +++ b/includes/functions/_oauth.php @@ -69,15 +69,15 @@ if ( ! function_exists( 'fictioneer_get_oauth_login_link' ) ) { * The login link will be escaped and returned with nonce, which is only relevant * for the first call. Subsequent calls follow the OAuth protocol for security. * If the OAuth for the channel is not properly set up or the feature disabled, - * and empty string will be returned instead. + * an empty string will be returned instead. * * @since Fictioneer 4.7 * - * @param string $channel The channel (discord, google, twitch, or patreon). - * @param string $content Content of the link. - * @param string|false $anchor Optional. An anchor for the return page. Default false. - * @param boolean $merge Optional. Whether to link the account to another. - * @param string $classes Optional. Additional CSS classes. + * @param string $channel The channel (discord, google, twitch, or patreon). + * @param string $content Content of the link. + * @param string|false $anchor Optional. An anchor for the return page. Default false. + * @param boolean $merge Optional. Whether to link the account to another. + * @param string $classes Optional. Additional CSS classes. * * @return string OAuth login link or empty string if disabled. */ diff --git a/includes/functions/users/_admin_profile.php b/includes/functions/users/_admin_profile.php index d40a6309..47fa4f2e 100644 --- a/includes/functions/users/_admin_profile.php +++ b/includes/functions/users/_admin_profile.php @@ -1,128 +1,201 @@ $notice ); + + // Redirect + wp_safe_redirect( add_query_arg( $notice, wp_get_referer() ) ); + + // Terminate + exit(); +} + +// ============================================================================= +// ADMIN PROFILE ACTIONS +// ============================================================================= + +/** + * Unset OAuth + * + * @since Fictioneer 5.2.5 + */ + +function fictioneer_admin_profile_unset_oauth() { + // Setup + $channel = sanitize_text_field( $_GET['channel'] ?? '' ); + + // Verify request + fictioneer_verify_admin_profile_action( "admin_oauth_unset_{$channel}" ); + + // Continue setup + $current_user_id = get_current_user_id(); + $profile_user_id = absint( $_GET['profile_user_id'] ?? 0 ); + $target_is_admin = fictioneer_is_admin( $profile_user_id ); + + // Guard admins + if ( $target_is_admin && $current_user_id !== $profile_user_id ) { + wp_die( __( 'Insufficient permissions.', 'fcnes' ) ); + } + + // Guard users + if ( $current_user_id !== $profile_user_id ) { + wp_die( __( 'Insufficient permissions.', 'fcnes' ) ); + } + + // Unset connection + delete_user_meta( $profile_user_id, "fictioneer_{$channel}_id_hash" ); + + // Finish + fictioneer_finish_admin_profile_action( "admin-profile-unset-oauth-{$channel}" ); +} +// Not conditional since removing your connection should always be allowed +add_action( 'admin_post_admin_profile_unset_oauth', 'fictioneer_admin_profile_unset_oauth' ); + +/** + * Cleat data node + * + * @since Fictioneer 5.2.5 + */ + +function fictioneer_admin_profile_clear_data_node() { + // Setup + $node = sanitize_text_field( $_GET['node'] ?? '' ); + + // Verify request + fictioneer_verify_admin_profile_action( "admin_clear_data_node_{$node}" ); + + // Continue setup + $current_user_id = get_current_user_id(); + $profile_user_id = absint( $_GET['profile_user_id'] ?? 0 ); + $target_is_admin = fictioneer_is_admin( $profile_user_id ); + $result = false; + + // Guard admins + if ( $target_is_admin && ( $current_user_id !== $profile_user_id ) ) { + wp_die( __( 'Insufficient permissions.', 'fcnes' ) ); + } + + // Guard users + if ( $current_user_id !== $profile_user_id ) { + wp_die( __( 'Insufficient permissions.', 'fcnes' ) ); + } + + // Clear data + switch ( $node ) { + case 'comments': + $result = fictioneer_soft_delete_user_comments( $profile_user_id ); + $result = is_array( $result ) ? $result['complete'] : $result; + break; + case 'comment-subscriptions': + $result = update_user_meta( $profile_user_id, 'fictioneer_comment_reply_validator', time() ); + break; + case 'follows': + $result = update_user_meta( $profile_user_id, 'fictioneer_user_follows', [] ); + update_user_meta( $profile_user_id, 'fictioneer_user_follows_cache', false ); + break; + case 'reminders': + $result = update_user_meta( $profile_user_id, 'fictioneer_user_reminders', [] ); + break; + case 'checkmarks': + $result = update_user_meta( $profile_user_id, 'fictioneer_user_checkmarks', [] ); + break; + case 'bookmarks': + // Bookmarks are only parsed client-side and stored as JSON string + $result = update_user_meta( $profile_user_id, 'fictioneer_bookmarks', '{}' ); + break; + } + + // Finish + if ( ! empty( $result ) ) { + fictioneer_finish_admin_profile_action( "admin-profile-cleared-data-node-{$node}" ); + } { + fictioneer_finish_admin_profile_action( "admin-profile-not-cleared-data-node-{$node}", 'failure' ); + } +} +// Not conditional since clearing your data nodes should always be allowed +add_action( 'admin_post_admin_profile_clear_data_node', 'fictioneer_admin_profile_clear_data_node' ); + // ============================================================================= // OUTPUT ADMIN PROFILE NOTICES // ============================================================================= +if ( ! defined( 'FICTIONEER_ADMIN_PROFILE_NOTICES' ) ) { + define( + 'FICTIONEER_ADMIN_PROFILE_NOTICES', + array( + 'admin-profile-unset-oauth-patreon' => __( 'Patreon connection successfully removed.', 'fictioneer' ), + 'admin-profile-unset-oauth-google' => __( 'Google connection successfully removed.', 'fictioneer' ), + 'admin-profile-unset-oauth-twitch' => __( 'Twitch connection successfully removed.', 'fictioneer' ), + 'admin-profile-unset-oauth-discord' => __( 'Discord connection successfully removed.', 'fictioneer' ), + 'admin-profile-not-cleared-data-node-comments' => __( 'Comments could not be cleared.', 'fictioneer' ), + 'admin-profile-not-cleared-data-node-comment-subscriptions' => __( 'Comment subscriptions could not be cleared.', 'fictioneer' ), + 'admin-profile-not-cleared-data-node-follows' => __( 'Follows could not be cleared.', 'fictioneer' ), + 'admin-profile-not-cleared-data-node-reminders' => __( 'Reminders could not be cleared.', 'fictioneer' ), + 'admin-profile-not-cleared-data-node-checkmarks' => __( 'Checkmarks could not be cleared.', 'fictioneer' ), + 'admin-profile-not-cleared-data-node-bookmarks' => __( 'Bookmarks could not be cleared.', 'fictioneer' ), + 'admin-profile-cleared-data-node-comments' => __( 'Comments successfully cleared.', 'fictioneer' ), + 'admin-profile-cleared-data-node-comment-subscriptions' => __( 'Comment subscriptions successfully cleared.', 'fictioneer' ), + 'admin-profile-not-cleared-data-node-follows' => __( 'Follows successfully cleared.', 'fictioneer' ), + 'admin-profile-cleared-data-node-reminders' => __( 'Reminders successfully cleared.', 'fictioneer' ), + 'admin-profile-cleared-data-node-checkmarks' => __( 'Checkmarks successfully cleared.', 'fictioneer' ), + 'admin-profile-cleared-data-node-bookmarks' => __( 'Bookmarks successfully cleared.', 'fictioneer' ), + 'oauth_already_linked' => __( 'Account already linked to another profile.', 'fictioneer' ), + 'oauth_merged_discord' => __( 'Discord account successfully linked', 'fictioneer' ), + 'oauth_merged_google' => __( 'Google account successfully linked.', 'fictioneer' ), + 'oauth_merged_twitch' => __( 'Twitch account successfully linked.', 'fictioneer' ), + 'oauth_merged_patreon' => __( 'Patreon account successfully linked.', 'fictioneer' ), + ) + ); +} + /** - * Output admin profile notices + * Output admin settings notices * - * @since Fictioneer 5.0 + * @since Fictioneer 5.2.5 */ function fictioneer_admin_profile_notices() { - // Setup - $action = $_GET['action'] ?? null; - $nonce = $_GET['fictioneer_nonce'] ?? null; - $failure = $_GET['failure'] ?? null; + // Get performed action $success = $_GET['success'] ?? null; + $failure = $_GET['failure'] ?? null; - // Abort if... - if ( ! $failure && ! $success && ( ! $action || ! $nonce ) ) return; - - // OAuth already linked - if ( $failure == 'oauth_already_linked' ) { - echo '

' . __( 'Account already linked to another profile.', 'fictioneer' ) . '

'; + // Has success notice? + if ( ! empty( $success ) && isset( FICTIONEER_ADMIN_PROFILE_NOTICES[ $success ] ) ) { + echo '

' . FICTIONEER_ADMIN_PROFILE_NOTICES[ $success ] . '

'; } - // Discord OAuth merged - if ( $success == 'oauth_merged_discord' ) { - echo '

' . __( 'Discord account successfully linked', 'fictioneer' ) . '

'; - } - - // Google OAuth merged - if ( $success == 'oauth_merged_google' ) { - echo '

' . __( 'Google account successfully linked.', 'fictioneer' ) . '

'; - } - - // Twitch OAuth merged - if ( $success == 'oauth_merged_twitch' ) { - echo '

' . __( 'Twitch account successfully linked.', 'fictioneer' ) . '

'; - } - - // Patreon OAuth merged - if ( $success == 'oauth_merged_patreon' ) { - echo '

' . __( 'Patreon account successfully linked.', 'fictioneer' ) . '

'; - } - - // Discord OAuth removed - if ( $action == 'oauth_unset_discord' && $nonce && wp_verify_nonce( $nonce, 'unset_oauth' ) ) { - echo '

' . __( 'Discord account successfully removed.', 'fictioneer' ) . '

'; - } - - // Google OAuth removed - if ( $action == 'oauth_unset_google' && $nonce && wp_verify_nonce( $nonce, 'unset_oauth' ) ) { - echo '

' . __( 'Google account successfully removed.', 'fictioneer' ) . '

'; - } - - // Twitch OAuth removed - if ( $action == 'oauth_unset_twitch' && $nonce && wp_verify_nonce( $nonce, 'unset_oauth' ) ) { - echo '

' . __( 'Twitch account successfully removed.', 'fictioneer' ) . '

'; - } - - // Patreon OAuth removed - if ( $action == 'oauth_unset_patreon' && $nonce && wp_verify_nonce( $nonce, 'unset_oauth' ) ) { - echo '

' . __( 'Patreon account successfully removed.', 'fictioneer' ) . '

'; - } - - // Comments cleared - if ( $action == 'clear_comments' && $nonce && wp_verify_nonce( $nonce, 'clear_data' ) ) { - echo '

' . __( 'Comments successfully cleared.', 'fictioneer' ) . '

'; - } - - // Comments NOT cleared - if ( $failure == 'clear_comments' ) { - echo '

' . __( 'Comments could not be (completely) cleared. Please try again later or contact an administrator.', 'fictioneer' ) . '

'; - } - - // Comment subscriptions cleared - if ( $action == 'clear_comment_subscriptions' && $nonce && wp_verify_nonce( $nonce, 'clear_data' ) ) { - echo '

' . __( 'Comment subscriptions successfully cleared.', 'fictioneer' ) . '

'; - } - - // Comment subscriptions NOT cleared - if ( $failure == 'clear_comment_subscriptions' ) { - echo '

' . __( 'Comment subscriptions could not be cleared.', 'fictioneer' ) . '

'; - } - - // Checkmarks cleared - if ( $action == 'clear_checkmarks' && $nonce && wp_verify_nonce( $nonce, 'clear_data' ) ) { - echo '

' . __( 'Checkmarks successfully cleared.', 'fictioneer' ) . '

'; - } - - // Checkmarks NOT cleared - if ( $failure == 'clear_checkmarks' ) { - echo '

' . __( 'Checkmarks could not be cleared.', 'fictioneer' ) . '

'; - } - - // Follows cleared - if ( $action == 'clear_follows' && $nonce && wp_verify_nonce( $nonce, 'clear_data' ) ) { - echo '

' . __( 'Follows successfully cleared.', 'fictioneer' ) . '

'; - } - - // Follows NOT cleared - if ( $failure == 'clear_follows' ) { - echo '

' . __( 'Follows could not be cleared.', 'fictioneer' ) . '

'; - } - - // Reminders cleared - if ( $action == 'clear_reminders' && $nonce && wp_verify_nonce( $nonce, 'clear_data' ) ) { - echo '

' . __( 'Reminders successfully cleared.', 'fictioneer' ) . '

'; - } - - // Reminders NOT cleared - if ( $failure == 'clear_reminders' ) { - echo '

' . __( 'Reminders could not be cleared.', 'fictioneer' ) . '

'; - } - - // Bookmarks cleared - if ( $action == 'clear_bookmarks' && $nonce && wp_verify_nonce( $nonce, 'clear_data' ) ) { - echo '

' . __( 'Bookmarks successfully cleared.', 'fictioneer' ) . '

'; - } - - // Bookmarks NOT cleared - if ( $failure == 'clear_bookmarks' ) { - echo '

' . __( 'Bookmarks could not be cleared.', 'fictioneer' ) . '

'; + // Has failure notice? + if ( ! empty( $failure ) && isset( FICTIONEER_ADMIN_PROFILE_NOTICES[ $failure ] ) ) { + echo '

' . FICTIONEER_ADMIN_PROFILE_NOTICES[ $failure ] . '

'; } } add_action( 'admin_notices', 'fictioneer_admin_profile_notices' ); @@ -141,11 +214,24 @@ add_action( 'admin_notices', 'fictioneer_admin_profile_notices' ); */ function fictioneer_custom_profile_fields( $profile_user ) { + // Setup + $moderation_message = get_the_author_meta( 'fictioneer_admin_moderation_message', $profile_user->ID ); + // Start HTML ---> ?>

- + ' . $moderation_message . '

'; + } + + // Hook to show fields + do_action( 'fictioneer_admin_user_sections', $profile_user ); + + ?>
ID === get_current_user_id(); - $editing_user_is_admin = fictioneer_is_admin( get_current_user_id() ); - $confirm_string = _x( 'delete', 'Prompt confirm deletion string.', 'fictioneer' ); +function fictioneer_admin_profile_fields_fingerprint( $profile_user ) { + // Setup + $sender_is_admin = fictioneer_is_admin( get_current_user_id() ); + $sender_is_owner = $profile_user->ID === get_current_user_id(); - // ... abort conditions... - if ( ! $editing_user_is_admin && ! $is_owner ) return; + // Guard + if ( ! $sender_is_admin && ! $sender_is_owner ) return; - // ... continue setup - $moderation_message = get_the_author_meta( 'fictioneer_admin_moderation_message', $profile_user->ID ); + // --- Start HTML ---> ?> + + + + +

+ + + ' . $moderation_message . '

'; - } +// ============================================================================= +// SHOW FLAGS +// ============================================================================= - // Display fingerprint --- Start HTML ---> ?> - - - - -

- - - ?> - - - -
-
-
+ + + $profile_user->ID, 'count' => true ) ); + $checkmarks = fictioneer_load_checkmarks( $profile_user ); + $checkmarks_count = count( $checkmarks['data'] ); + $checkmarks_chapters_count = fictioneer_count_chapter_checkmarks( $checkmarks ); + $follows = fictioneer_load_follows( $profile_user ); + $follows_count = count( $follows['data'] ); + $reminders = fictioneer_load_reminders( $profile_user ); + $reminders_count = count( $reminders['data'] ); + $bookmarks = get_user_meta( $profile_user->ID, 'fictioneer_bookmarks', true ) ?: null; + $sender_is_owner = $profile_user->ID === get_current_user_id(); + $confirmation_string = _x( 'delete', 'Prompt confirm deletion string.', 'fictioneer' ); + $notification_validator = get_user_meta( $profile_user->ID, 'fictioneer_comment_reply_validator', true ) ?: null; + $comment_subscriptions_count = 0; + $nodes = array( + ['comments', '', $comments_count] + ); + + if ( ! empty( $notification_validator ) ) { + $comment_subscriptions_count = get_comments( + array( + 'user_id' => $profile_user->ID, + 'meta_key' => 'fictioneer_send_notifications', + 'meta_value' => $notification_validator, + 'count' => true + ) + ); + } + + // Clear local bookmarks + if ( $success && $success === 'admin-profile-cleared-data-node-bookmarks' ) { + echo ""; + } + + // Comment subscriptions? + if ( get_option( 'fictioneer_enable_comment_notifications' ) && $comment_subscriptions_count > 0 ) { + $nodes[] = ['comment-subscriptions','', $comment_subscriptions_count]; + } + + // Follows? + if ( get_option( 'fictioneer_enable_follows' ) && $follows_count > 0 ) { + $nodes[] = ['follows', '', $follows_count]; + } + + // Reminders? + if ( get_option( 'fictioneer_enable_reminders' ) && $reminders_count > 0 ) { + $nodes[] = ['reminders', '', $reminders_count]; + } + + // Checkmarks? + if ( get_option( 'fictioneer_enable_checkmarks' ) && $checkmarks_count > 0 ) { + $nodes[] = ['checkmarks', '', "{$checkmarks_count}|{$checkmarks_chapters_count}"]; + } + + // Bookmarks? + if ( get_option( 'fictioneer_enable_bookmarks' ) && ! empty( $bookmarks ) && $bookmarks != '{}' ) { + $nodes[] = ['bookmarks', '', 0]; + } + + // Guard (only profile owner) + if ( ! $sender_is_owner ) return; + + // --- Start HTML ---> ?> + + + +

+ +

+
+ + $profile_user->ID, + 'node' => $node[0] + ), + wp_nonce_url( + admin_url( 'admin-post.php?action=admin_profile_clear_data_node' ), + "admin_clear_data_node_{$node[0]}", + 'fictioneer_nonce' + ) + ); + + $confirmation_message = sprintf( + __( 'Are you sure you want to clear your %s? This action is irreversible. Enter %s to confirm.', 'fictioneer' ), + str_replace( '-', ' ', $node[0] ), + strtoupper( $confirmation_string ) + ); + ?> +
+ + + + +
+ +
+ + +