diff --git a/adminpages/member-edit.php b/adminpages/member-edit.php index dd7d5d89da..af4fad088f 100644 --- a/adminpages/member-edit.php +++ b/adminpages/member-edit.php @@ -14,6 +14,7 @@ function pmpro_member_edit_get_panels() { $panels[] = new PMPro_Member_Edit_Panel_Memberships(); $panels[] = new PMPro_Member_Edit_Panel_Subscriptions(); $panels[] = new PMPro_Member_Edit_Panel_Orders(); + $panels[] = new PMPro_Member_Edit_Panel_Member_Notes(); $panels[] = new PMPro_Member_Edit_Panel_TOS(); // Add user fields panels. diff --git a/adminpages/member-edit/pmpro-class-member-edit-panel-member-notes.php b/adminpages/member-edit/pmpro-class-member-edit-panel-member-notes.php new file mode 100644 index 0000000000..d0f4c59784 --- /dev/null +++ b/adminpages/member-edit/pmpro-class-member-edit-panel-member-notes.php @@ -0,0 +1,298 @@ + by : `. The string is parsed for display and rebuilt + * on add/delete. Pre-existing free-form `user_notes` content that doesn't + * match the header pattern still displays as one or more "legacy" entries and + * can be trashed like any other note. + */ +class PMPro_Member_Edit_Panel_Member_Notes extends PMPro_Member_Edit_Panel { + /** + * Set up the panel. + */ + public function __construct() { + $this->slug = 'member-notes'; + $this->title = __( 'Member Notes', 'paid-memberships-pro' ); + + // The panel handles its own action buttons. No top-level submit. + $this->submit_text = ''; + + // Show success/error messages from prior actions on this panel. + if ( ! empty( $_REQUEST['user_id'] ) && ! empty( $_REQUEST['member_notes_action'] ) ) { + $action = sanitize_key( $_REQUEST['member_notes_action'] ); + if ( $action === 'added' ) { + pmpro_setMessage( __( 'Member note added.', 'paid-memberships-pro' ), 'pmpro_success' ); + } elseif ( $action === 'deleted' ) { + pmpro_setMessage( __( 'Member note deleted.', 'paid-memberships-pro' ), 'pmpro_success' ); + } + } + } + + /** + * Parse the user's `user_notes` string into structured records. + * + * Each record: { index, date, author, note, legacy }. Records are returned + * in stored order (oldest first); `index` is the position in the stored + * string and is what `delete_note()` uses. + * + * @param int $user_id + * @return array + */ + protected static function parse_notes( $user_id ) { + $user_id = (int) $user_id; + if ( $user_id <= 0 ) { + return array(); + } + + $string = (string) get_user_meta( $user_id, 'user_notes', true ); + if ( '' === trim( $string ) ) { + return array(); + } + + $chunks = preg_split( "/\n\s*\n/", $string ); + $notes = array(); + $index = 0; + foreach ( $chunks as $chunk ) { + $chunk = trim( $chunk ); + if ( '' === $chunk ) { + continue; + } + + if ( preg_match( '/^(.+?) by (.+?): (.*)$/s', $chunk, $matches ) ) { + $notes[] = array( + 'index' => $index, + 'date' => $matches[1], + 'author' => $matches[2], + 'note' => $matches[3], + 'legacy' => false, + ); + } else { + $notes[] = array( + 'index' => $index, + 'date' => '', + 'author' => '', + 'note' => $chunk, + 'legacy' => true, + ); + } + $index++; + } + + return $notes; + } + + /** + * Append a new note to the user's `user_notes` string. + * + * @param int $user_id + * @param string $note + * @return bool + */ + protected static function add_note( $user_id, $note ) { + $user_id = (int) $user_id; + if ( $user_id <= 0 ) { + return false; + } + + // Sanitize and collapse internal blank lines so the `\n\n` separator stays unambiguous. + $note = trim( wp_kses_post( (string) $note ) ); + $note = preg_replace( "/\n\s*\n+/", "\n", $note ); + if ( '' === $note ) { + return false; + } + + $current_user = wp_get_current_user(); + $author_name = $current_user && $current_user->ID ? $current_user->display_name : __( 'System', 'paid-memberships-pro' ); + $author_name = trim( str_replace( array( ' by ', ': ' ), ' ', $author_name ) ); + if ( '' === $author_name ) { + $author_name = __( 'Unknown', 'paid-memberships-pro' ); + } + + $date_display = wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ); + $entry = sprintf( '%1$s by %2$s: %3$s', $date_display, $author_name, $note ); + + $existing = (string) get_user_meta( $user_id, 'user_notes', true ); + $new = '' === trim( $existing ) ? $entry : ( rtrim( $existing ) . "\n\n" . $entry ); + + update_user_meta( $user_id, 'user_notes', $new ); + return true; + } + + /** + * Remove the note at `$index` (its position in the stored string). + * + * @param int $user_id + * @param int $index + * @return bool + */ + protected static function delete_note( $user_id, $index ) { + $user_id = (int) $user_id; + $index = (int) $index; + if ( $user_id <= 0 || $index < 0 ) { + return false; + } + + $notes = self::parse_notes( $user_id ); + if ( ! isset( $notes[ $index ] ) ) { + return false; + } + + $kept_strings = array(); + foreach ( $notes as $n ) { + if ( $n['index'] === $index ) { + continue; + } + if ( ! empty( $n['legacy'] ) ) { + $kept_strings[] = $n['note']; + } else { + $kept_strings[] = sprintf( '%1$s by %2$s: %3$s', $n['date'], $n['author'], $n['note'] ); + } + } + update_user_meta( $user_id, 'user_notes', implode( "\n\n", $kept_strings ) ); + return true; + } + + /** + * Display the panel contents. + */ + protected function display_panel_contents() { + $user = self::get_user(); + if ( empty( $user->ID ) ) { + return; + } + + // Reverse so newest is shown first while keeping each note's `index` + // aligned with its position in the stored string for delete. + $notes = array_reverse( self::parse_notes( $user->ID ) ); + ?> +

+ +

+ + +

+ + + + + + + + + + ID ) ) { + return; + } + + // Delete a note. The hidden value is the note's index in the stored string. + if ( isset( $_POST['pmpro_delete_member_note'] ) && '' !== $_POST['pmpro_delete_member_note'] ) { + self::delete_note( $user->ID, (int) $_POST['pmpro_delete_member_note'] ); + + wp_safe_redirect( add_query_arg( array( + 'page' => 'pmpro-member', + 'user_id' => $user->ID, + 'pmpro_member_edit_panel' => $this->slug, + 'member_notes_action' => 'deleted', + ), admin_url( 'admin.php' ) ) ); + exit; + } + + // Add a note. + if ( ! empty( $_POST['pmpro_add_member_note_submit'] ) ) { + $note_text = isset( $_POST['pmpro_member_note_text'] ) ? wp_unslash( $_POST['pmpro_member_note_text'] ) : ''; + self::add_note( $user->ID, $note_text ); + + wp_safe_redirect( add_query_arg( array( + 'page' => 'pmpro-member', + 'user_id' => $user->ID, + 'pmpro_member_edit_panel' => $this->slug, + 'member_notes_action' => 'added', + ), admin_url( 'admin.php' ) ) ); + exit; + } + } +} diff --git a/adminpages/member-edit/pmpro-class-member-edit-panel-user-info.php b/adminpages/member-edit/pmpro-class-member-edit-panel-user-info.php index a97927061d..9a80f515f5 100644 --- a/adminpages/member-edit/pmpro-class-member-edit-panel-user-info.php +++ b/adminpages/member-edit/pmpro-class-member-edit-panel-user-info.php @@ -53,8 +53,7 @@ protected function display_panel_contents() { $user_login = ! empty( $_POST['user_login'] ) ? sanitize_user( $_POST['user_login'] ) : ''; $user_email = ! empty( $_POST['email'] ) ? stripslashes( sanitize_email( $_POST['email'] ) ) : ''; $first_name = ! empty( $_POST['first_name'] ) ? stripslashes( sanitize_text_field( $_POST['first_name'] ) ): ''; - $last_name = ! empty( $_POST['last_name'] ) ? stripslashes( sanitize_text_field( $_POST['last_name'] ) ) : ''; - $user_notes = ! empty( $_POST['user_notes'] ) ? stripslashes( sanitize_textarea_field( $_POST['user_notes'] ) ) : ''; + $last_name = ! empty( $_POST['last_name'] ) ? stripslashes( sanitize_text_field( $_POST['last_name'] ) ) : ''; // If we are edting a user, get the user information. $user = self::get_user(); @@ -64,7 +63,6 @@ protected function display_panel_contents() { $first_name = $user->first_name; $last_name = $user->last_name; $role = current( $user->roles ); - $user_notes = $user->user_notes; } else { // We are creating a new user. // Enqueue core WordPress script for passwords: generate, visibility, and strength check. @@ -182,13 +180,6 @@ protected function display_panel_contents() { - - - - -

- - ID ) && ! empty( $user->ID ) ) { ?> @@ -326,10 +317,6 @@ public function save() { } } - // Add other user meta - $user_notes = ! empty( $_POST['user_notes'] ) ? sanitize_textarea_field( $_POST['user_notes'] ) : ''; - update_user_meta( $user_id, 'user_notes', $user_notes ); - // Save the avatar field if applicable. $avatar_error = ''; if ( function_exists( 'pmpro_save_avatar_field' ) ) { diff --git a/paid-memberships-pro.php b/paid-memberships-pro.php index 6177a07ca0..bcef46dd17 100644 --- a/paid-memberships-pro.php +++ b/paid-memberships-pro.php @@ -105,6 +105,7 @@ require_once( PMPRO_DIR . '/adminpages/member-edit/pmpro-class-member-edit-panel-memberships.php' ); require_once( PMPRO_DIR . '/adminpages/member-edit/pmpro-class-member-edit-panel-subscriptions.php' ); require_once( PMPRO_DIR . '/adminpages/member-edit/pmpro-class-member-edit-panel-orders.php' ); +require_once( PMPRO_DIR . '/adminpages/member-edit/pmpro-class-member-edit-panel-member-notes.php' ); require_once( PMPRO_DIR . '/adminpages/member-edit/pmpro-class-member-edit-panel-tos.php' ); require_once( PMPRO_DIR . '/adminpages/member-edit/pmpro-class-member-edit-panel-user-fields.php' ); require_once( PMPRO_DIR . '/adminpages/member-edit/pmpro-class-member-edit-panel-email-log.php' );