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 ---> ?>
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 )
+ );
+ ?>
+
+
+
+ |
+
+