From b9d7d3d7c80e4e0e97340684d57412c65c9bef80 Mon Sep 17 00:00:00 2001 From: Erick Danzer Date: Thu, 13 Nov 2025 20:21:04 -0700 Subject: [PATCH 1/6] Forms: use first/last name for author --- .../contact-form/class-feedback-author.php | 62 +++++++++++++++++++ .../forms/src/contact-form/class-feedback.php | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/projects/packages/forms/src/contact-form/class-feedback-author.php b/projects/packages/forms/src/contact-form/class-feedback-author.php index 25d0614fd5721..06a087a7b5b05 100644 --- a/projects/packages/forms/src/contact-form/class-feedback-author.php +++ b/projects/packages/forms/src/contact-form/class-feedback-author.php @@ -48,6 +48,64 @@ public function __construct( $name = '', $email = '', $url = '' ) { $this->url = $url; } + /** + * Compute author name from either submission data (array + form) or stored fields (Feedback). + * + * Rules: + * - If First/Last name fields exist (by known ids), return "First Last" trimmed. + * - Otherwise, return the single Name field value. + * - Applies the pre_comment_author_name filter and standard sanitization. + * + * @param Feedback|array $source Submission array or Feedback instance. + * @param Contact_Form|null $form The form object (required when $source is submission array). + * @return string Filtered display name. + */ + public static function get_computed_author_name( $source, $form = null ) { + $is_from_feedback_object = is_object( $source ) && $source instanceof Feedback; + $final_name = ''; + + if ( $is_from_feedback_object ) { + $first_name = $source->get_field_value_by_form_field_id( 'first-name' ); + $last_name = $source->get_field_value_by_form_field_id( 'last-name' ); + $full_name = trim( $first_name . ' ' . $last_name ); + $single_name = ''; + if ( method_exists( $source, 'get_fields' ) ) { + foreach ( $source->get_fields() as $field ) { + if ( method_exists( $field, 'get_type' ) && $field->get_type() === 'name' ) { + $single_name = $field->get_render_value(); + break; + } + } + } + + // Return the full name if it exists, otherwise return the single name. + $final_name = $full_name ? $full_name : $single_name; + } elseif ( is_array( $source ) ) { + $first_name = isset( $source['first-name'] ) ? wp_unslash( $source['first-name'] ) : ''; + $last_name = isset( $source['last-name'] ) ? wp_unslash( $source['last-name'] ) : ''; + $full_name = trim( $first_name . ' ' . $last_name ); + + $single_name = ''; + if ( $form && method_exists( $form, 'get_field_ids' ) ) { + $field_ids = $form->get_field_ids(); + if ( isset( $field_ids['name'] ) && isset( $source[ $field_ids['name'] ] ) ) { + $single_name = wp_unslash( $source[ $field_ids['name'] ] ); + } + } + + // Return the full name if it exists, otherwise return the single name. + $final_name = $full_name ? $full_name : $single_name; + } + + // Apply standard filtering/sanitization used for author names. + return Contact_Form_Plugin::strip_tags( + stripslashes( + /** This filter is already documented in core/wp-includes/comment-functions.php */ + apply_filters( 'pre_comment_author_name', addslashes( $final_name ) ) + ) + ); + } + /** * Create a Feedback_Author instance from the submission data. * @@ -74,6 +132,10 @@ public static function from_submission( $post_data, $form ) { * @return string Filter value for the author information. */ private static function get_computed_author_info( $post_data, $type, $filter, $form ) { + // Handle name specially to support First/Last variations. + if ( $type === 'name' ) { + return self::get_computed_author_name( $post_data, $form ); + } $field_ids = $form->get_field_ids(); if ( isset( $field_ids[ $type ] ) ) { $key = $field_ids[ $type ]; diff --git a/projects/packages/forms/src/contact-form/class-feedback.php b/projects/packages/forms/src/contact-form/class-feedback.php index b3bb98c4ad36f..02262f5f25574 100644 --- a/projects/packages/forms/src/contact-form/class-feedback.php +++ b/projects/packages/forms/src/contact-form/class-feedback.php @@ -242,7 +242,7 @@ private function load_from_post( WP_Post $feedback_post ) { $this->notification_recipients = $parsed_content['notification_recipients'] ?? array(); $this->author_data = new Feedback_Author( - $this->get_first_field_of_type( 'name', 'pre_comment_author_name' ), + Feedback_Author::get_computed_author_name( $this ), $this->get_first_field_of_type( 'email', 'pre_comment_author_email' ), $this->get_first_field_of_type( 'url', 'pre_comment_author_url' ) ); From e4785d3a497d30ff76077b1ca70de05dd9db8c85 Mon Sep 17 00:00:00 2001 From: Erick Danzer Date: Thu, 13 Nov 2025 20:22:33 -0700 Subject: [PATCH 2/6] changelog --- .../forms/changelog/update-forms-author-first-last-name | 4 ++++ .../jetpack/changelog/update-forms-author-first-last-name | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 projects/packages/forms/changelog/update-forms-author-first-last-name create mode 100644 projects/plugins/jetpack/changelog/update-forms-author-first-last-name diff --git a/projects/packages/forms/changelog/update-forms-author-first-last-name b/projects/packages/forms/changelog/update-forms-author-first-last-name new file mode 100644 index 0000000000000..a08b7c5b2f7a1 --- /dev/null +++ b/projects/packages/forms/changelog/update-forms-author-first-last-name @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Forms: use first/last name for author. diff --git a/projects/plugins/jetpack/changelog/update-forms-author-first-last-name b/projects/plugins/jetpack/changelog/update-forms-author-first-last-name new file mode 100644 index 0000000000000..288e7051beade --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-forms-author-first-last-name @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Forms: use first/last name for author. From 4b85c957e31f8ec1056a7d8a830ef1dde74abcbb Mon Sep 17 00:00:00 2001 From: Erick Danzer Date: Fri, 14 Nov 2025 11:18:47 -0700 Subject: [PATCH 3/6] Forms: add feedback first/last name handling --- .../contact-form/class-feedback-author.php | 123 ++++++++---------- .../forms/src/contact-form/class-feedback.php | 24 +++- 2 files changed, 78 insertions(+), 69 deletions(-) diff --git a/projects/packages/forms/src/contact-form/class-feedback-author.php b/projects/packages/forms/src/contact-form/class-feedback-author.php index 06a087a7b5b05..d64a6157b271f 100644 --- a/projects/packages/forms/src/contact-form/class-feedback-author.php +++ b/projects/packages/forms/src/contact-form/class-feedback-author.php @@ -36,74 +36,34 @@ class Feedback_Author { private $url; /** - * Constructor for Feedback_Author. + * The first name of the author. * - * @param string $name The name of the author. - * @param string $email The email of the author. - * @param string $url The URL of the author. + * @var string */ - public function __construct( $name = '', $email = '', $url = '' ) { - $this->name = $name; - $this->email = $email; - $this->url = $url; - } + private $first_name = ''; /** - * Compute author name from either submission data (array + form) or stored fields (Feedback). - * - * Rules: - * - If First/Last name fields exist (by known ids), return "First Last" trimmed. - * - Otherwise, return the single Name field value. - * - Applies the pre_comment_author_name filter and standard sanitization. + * The last name of the author. * - * @param Feedback|array $source Submission array or Feedback instance. - * @param Contact_Form|null $form The form object (required when $source is submission array). - * @return string Filtered display name. + * @var string */ - public static function get_computed_author_name( $source, $form = null ) { - $is_from_feedback_object = is_object( $source ) && $source instanceof Feedback; - $final_name = ''; - - if ( $is_from_feedback_object ) { - $first_name = $source->get_field_value_by_form_field_id( 'first-name' ); - $last_name = $source->get_field_value_by_form_field_id( 'last-name' ); - $full_name = trim( $first_name . ' ' . $last_name ); - $single_name = ''; - if ( method_exists( $source, 'get_fields' ) ) { - foreach ( $source->get_fields() as $field ) { - if ( method_exists( $field, 'get_type' ) && $field->get_type() === 'name' ) { - $single_name = $field->get_render_value(); - break; - } - } - } + private $last_name = ''; - // Return the full name if it exists, otherwise return the single name. - $final_name = $full_name ? $full_name : $single_name; - } elseif ( is_array( $source ) ) { - $first_name = isset( $source['first-name'] ) ? wp_unslash( $source['first-name'] ) : ''; - $last_name = isset( $source['last-name'] ) ? wp_unslash( $source['last-name'] ) : ''; - $full_name = trim( $first_name . ' ' . $last_name ); - - $single_name = ''; - if ( $form && method_exists( $form, 'get_field_ids' ) ) { - $field_ids = $form->get_field_ids(); - if ( isset( $field_ids['name'] ) && isset( $source[ $field_ids['name'] ] ) ) { - $single_name = wp_unslash( $source[ $field_ids['name'] ] ); - } - } - - // Return the full name if it exists, otherwise return the single name. - $final_name = $full_name ? $full_name : $single_name; - } - - // Apply standard filtering/sanitization used for author names. - return Contact_Form_Plugin::strip_tags( - stripslashes( - /** This filter is already documented in core/wp-includes/comment-functions.php */ - apply_filters( 'pre_comment_author_name', addslashes( $final_name ) ) - ) - ); + /** + * Constructor for Feedback_Author. + * + * @param string $name The name of the author. + * @param string $email The email of the author. + * @param string $url The URL of the author. + * @param string $first_name The first name of the author. + * @param string $last_name The last name of the author. + */ + public function __construct( $name = '', $email = '', $url = '', $first_name = '', $last_name = '' ) { + $this->name = $name; + $this->email = $email; + $this->url = $url; + $this->first_name = $first_name; + $this->last_name = $last_name; } /** @@ -114,10 +74,14 @@ public static function get_computed_author_name( $source, $form = null ) { * @return Feedback_Author The Feedback_Author instance. */ public static function from_submission( $post_data, $form ) { + $first = isset( $post_data['first-name'] ) ? wp_unslash( $post_data['first-name'] ) : ''; + $last = isset( $post_data['last-name'] ) ? wp_unslash( $post_data['last-name'] ) : ''; return new self( self::get_computed_author_info( $post_data, 'name', 'pre_comment_author_name', $form ), self::get_computed_author_info( $post_data, 'email', 'pre_comment_author_email', $form ), - self::get_computed_author_info( $post_data, 'url', 'pre_comment_author_url', $form ) + self::get_computed_author_info( $post_data, 'url', 'pre_comment_author_url', $form ), + $first, + $last ); } @@ -132,10 +96,6 @@ public static function from_submission( $post_data, $form ) { * @return string Filter value for the author information. */ private static function get_computed_author_info( $post_data, $type, $filter, $form ) { - // Handle name specially to support First/Last variations. - if ( $type === 'name' ) { - return self::get_computed_author_name( $post_data, $form ); - } $field_ids = $form->get_field_ids(); if ( isset( $field_ids[ $type ] ) ) { $key = $field_ids[ $type ]; @@ -167,7 +127,8 @@ private static function get_computed_author_info( $post_data, $type, $filter, $f * @return string The display name of the author. */ public function get_display_name(): string { - return empty( $this->name ) ? $this->email : $this->name; + $name = $this->get_name(); + return empty( $name ) ? $this->email : $name; } /** @@ -187,6 +148,16 @@ public function get_avatar_url(): string { * @return string The name of the author. */ public function get_name() { + if ( $this->first_name && $this->last_name ) { + $raw = trim( $this->first_name . ' ' . $this->last_name ); + return Contact_Form_Plugin::strip_tags( + stripslashes( + /** This filter is already documented in core/wp-includes/comment-functions.php */ + apply_filters( 'pre_comment_author_name', addslashes( $raw ) ) + ) + ); + } + // This name value is filtered upstream when class is instantiated. return $this->name; } @@ -207,4 +178,22 @@ public function get_email() { public function get_url() { return $this->url; } + + /** + * Get the first name of the author (if provided separately). + * + * @return string + */ + public function get_first_name() { + return $this->first_name; + } + + /** + * Get the last name of the author (if provided separately). + * + * @return string + */ + public function get_last_name() { + return $this->last_name; + } } diff --git a/projects/packages/forms/src/contact-form/class-feedback.php b/projects/packages/forms/src/contact-form/class-feedback.php index 02262f5f25574..e38d25103e017 100644 --- a/projects/packages/forms/src/contact-form/class-feedback.php +++ b/projects/packages/forms/src/contact-form/class-feedback.php @@ -242,9 +242,11 @@ private function load_from_post( WP_Post $feedback_post ) { $this->notification_recipients = $parsed_content['notification_recipients'] ?? array(); $this->author_data = new Feedback_Author( - Feedback_Author::get_computed_author_name( $this ), + $this->get_first_field_of_type( 'name', 'pre_comment_author_name' ), $this->get_first_field_of_type( 'email', 'pre_comment_author_email' ), - $this->get_first_field_of_type( 'url', 'pre_comment_author_url' ) + $this->get_first_field_of_type( 'url', 'pre_comment_author_url' ), + $this->get_field_value_by_form_field_id( 'first-name' ), + $this->get_field_value_by_form_field_id( 'last-name' ) ); $this->comment_content = $this->get_first_field_of_type( 'textarea' ); @@ -729,6 +731,24 @@ public function get_author_name() { return $this->author_data->get_name(); } + /** + * Get the author's first name of a feedback entry. + * + * @return string + */ + public function get_author_first_name() { + return $this->author_data->get_first_name(); + } + + /** + * Get the author's last name of a feedback entry. + * + * @return string + */ + public function get_author_last_name() { + return $this->author_data->get_last_name(); + } + /** * Get the author email of a feedback entry. * From f827e98372cddffc26cb244fab424e6a32a67f86 Mon Sep 17 00:00:00 2001 From: Erick Danzer Date: Fri, 14 Nov 2025 12:36:48 -0700 Subject: [PATCH 4/6] Add tests --- .../php/contact-form/Feedback_Author_Test.php | 28 +++++++++++++++++++ .../tests/php/contact-form/Feedback_Test.php | 25 +++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/projects/packages/forms/tests/php/contact-form/Feedback_Author_Test.php b/projects/packages/forms/tests/php/contact-form/Feedback_Author_Test.php index 560086eea4507..09a1119520ad6 100644 --- a/projects/packages/forms/tests/php/contact-form/Feedback_Author_Test.php +++ b/projects/packages/forms/tests/php/contact-form/Feedback_Author_Test.php @@ -20,6 +20,34 @@ #[CoversClass( Feedback_Author::class )] class Feedback_Author_Test extends BaseTestCase { + /** + * Minimal: combining first and last name yields full name in name/display. + */ + public function test_combined_first_last_in_name_and_display() { + $author = new Feedback_Author( '', 'john@example.com', '', 'John', 'Doe' ); + + $this->assertEquals( 'John Doe', $author->get_name() ); + $this->assertEquals( 'John Doe', $author->get_display_name() ); + } + + /** + * Minimal: getters for first and last name return raw values. + */ + public function test_first_last_getters() { + $author = new Feedback_Author( '', '', '', 'Alice', 'Smith' ); + $this->assertSame( 'Alice', $author->get_first_name() ); + $this->assertSame( 'Smith', $author->get_last_name() ); + } + + /** + * Minimal: when only one of first/last is present, fall back to single name. + */ + public function test_partial_first_or_last_falls_back_to_single_name() { + $author = new Feedback_Author( 'Single Name', 's@example.com', '', 'Bob', '' ); + $this->assertEquals( 'Single Name', $author->get_name() ); + $this->assertEquals( 'Single Name', $author->get_display_name() ); + } + /** * Test constructor with all parameters. */ diff --git a/projects/packages/forms/tests/php/contact-form/Feedback_Test.php b/projects/packages/forms/tests/php/contact-form/Feedback_Test.php index a0c861580ba28..00c0f6f3f8773 100644 --- a/projects/packages/forms/tests/php/contact-form/Feedback_Test.php +++ b/projects/packages/forms/tests/php/contact-form/Feedback_Test.php @@ -3140,4 +3140,29 @@ public function test_get_country_flag() { remove_filter( 'jetpack_get_country_from_ip', $filter_callback, 10 ); } + + /** + * Minimal: submission with first-name/last-name sets author name and first/last getters. + */ + public function test_author_first_last_on_submission() { + // Form can be minimal; we rely on explicit first/last ids in post data. + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Message' type='textarea' required='1'/]" + ); + + $post_data = array( + 'first-name' => 'Jane', + 'last-name' => 'Doe', + ); + + $response = Feedback::from_submission( $post_data, $form ); + + $this->assertEquals( 'Jane Doe', $response->get_author_name(), 'Author name should combine first and last' ); + $this->assertSame( 'Jane', $response->get_author_first_name(), 'First name getter should return raw first name' ); + $this->assertSame( 'Doe', $response->get_author_last_name(), 'Last name getter should return raw last name' ); + } } From c79802eccdeff7df7b3ebe2faffcc003097f54d3 Mon Sep 17 00:00:00 2001 From: Erick Danzer Date: Mon, 17 Nov 2025 09:17:10 -0700 Subject: [PATCH 5/6] Fix tests --- .../forms/tests/php/contact-form/Feedback_Test.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/projects/packages/forms/tests/php/contact-form/Feedback_Test.php b/projects/packages/forms/tests/php/contact-form/Feedback_Test.php index 00c0f6f3f8773..1b3f96f9844d5 100644 --- a/projects/packages/forms/tests/php/contact-form/Feedback_Test.php +++ b/projects/packages/forms/tests/php/contact-form/Feedback_Test.php @@ -3145,18 +3145,22 @@ public function test_get_country_flag() { * Minimal: submission with first-name/last-name sets author name and first/last getters. */ public function test_author_first_last_on_submission() { - // Form can be minimal; we rely on explicit first/last ids in post data. $form = new Contact_Form( array( 'title' => 'Test Form', 'description' => 'This is a test form.', ), - "[contact-field label='Message' type='textarea' required='1'/]" + " + [contact-field label='First name' type='name' id='first-name'/] + [contact-field label='Last name' type='name' id='last-name'/] + [contact-field label='Email' type='email' id='email'/] + " ); $post_data = array( 'first-name' => 'Jane', 'last-name' => 'Doe', + 'email' => 'jane@example.com', ); $response = Feedback::from_submission( $post_data, $form ); From cf7ff094c58d66aa1734252786f9abecbc3ae9ca Mon Sep 17 00:00:00 2001 From: Erick Danzer Date: Tue, 18 Nov 2025 08:58:44 -0700 Subject: [PATCH 6/6] Update tests --- .../php/contact-form/Feedback_Author_Test.php | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/projects/packages/forms/tests/php/contact-form/Feedback_Author_Test.php b/projects/packages/forms/tests/php/contact-form/Feedback_Author_Test.php index 09a1119520ad6..94917b8e61a8b 100644 --- a/projects/packages/forms/tests/php/contact-form/Feedback_Author_Test.php +++ b/projects/packages/forms/tests/php/contact-form/Feedback_Author_Test.php @@ -28,26 +28,45 @@ public function test_combined_first_last_in_name_and_display() { $this->assertEquals( 'John Doe', $author->get_name() ); $this->assertEquals( 'John Doe', $author->get_display_name() ); + $this->assertSame( 'John', $author->get_first_name() ); + $this->assertSame( 'Doe', $author->get_last_name() ); + } + /** + * Minimal: when only one of first/last is present, fall back to single name. + */ + public function test_partial_first_or_last_falls_back_to_single_name() { + $author = new Feedback_Author( 'Single Name', 's@example.com', '', 'Bob', '' ); + $this->assertEquals( 'Single Name', $author->get_name() ); + $this->assertEquals( 'Single Name', $author->get_display_name() ); } /** - * Minimal: getters for first and last name return raw values. + * When both first/last are present and differ from single name, combined name takes precedence. */ - public function test_first_last_getters() { - $author = new Feedback_Author( '', '', '', 'Alice', 'Smith' ); - $this->assertSame( 'Alice', $author->get_first_name() ); - $this->assertSame( 'Smith', $author->get_last_name() ); + public function test_first_last_override_single_name_when_both_present() { + $author = new Feedback_Author( 'Some Other Name', 'x@example.com', '', 'Alice', 'Jones' ); + $this->assertEquals( 'Alice Jones', $author->get_name() ); + $this->assertEquals( 'Alice Jones', $author->get_display_name() ); } /** - * Minimal: when only one of first/last is present, fall back to single name. + * When only last name is provided and single name exists, fall back to single name. */ - public function test_partial_first_or_last_falls_back_to_single_name() { - $author = new Feedback_Author( 'Single Name', 's@example.com', '', 'Bob', '' ); + public function test_only_lastname_with_single_name_falls_back_to_single() { + $author = new Feedback_Author( 'Single Name', 'x@example.com', '', '', 'Jones' ); $this->assertEquals( 'Single Name', $author->get_name() ); $this->assertEquals( 'Single Name', $author->get_display_name() ); } + /** + * When only last name is provided and single name is missing, fall back to email in display. + */ + public function test_only_lastname_without_single_name_falls_back_to_email() { + $author = new Feedback_Author( '', 'x@example.com', '', '', 'Jones' ); + $this->assertSame( '', $author->get_name() ); + $this->assertEquals( 'x@example.com', $author->get_display_name() ); + } + /** * Test constructor with all parameters. */