diff --git a/includes/class-newspack.php b/includes/class-newspack.php index 1b41a53b9d..3bde777a1f 100644 --- a/includes/class-newspack.php +++ b/includes/class-newspack.php @@ -105,9 +105,9 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/reader-activation/sync/class-woocommerce.php'; include_once NEWSPACK_ABSPATH . 'includes/reader-activation/sync/class-contact-sync.php'; include_once NEWSPACK_ABSPATH . 'includes/reader-activation/sync/class-contact-sync-admin.php'; + include_once NEWSPACK_ABSPATH . 'includes/reader-activation/class-comment-display-name.php'; include_once NEWSPACK_ABSPATH . 'includes/class-action-scheduler.php'; include_once NEWSPACK_ABSPATH . 'includes/reader-activation/class-integrations.php'; - \Newspack\Reader_Activation\Integrations::init(); include_once NEWSPACK_ABSPATH . 'includes/reader-activation/class-promoted-fields.php'; include_once NEWSPACK_ABSPATH . 'includes/reader-activation/class-session-hydration.php'; include_once NEWSPACK_ABSPATH . 'includes/data-events/class-utils.php'; diff --git a/includes/reader-activation/class-comment-display-name.php b/includes/reader-activation/class-comment-display-name.php new file mode 100644 index 0000000000..c968be5999 --- /dev/null +++ b/includes/reader-activation/class-comment-display-name.php @@ -0,0 +1,122 @@ +ID ); + } + + /** + * Validate and save the display name before a comment is inserted. + * + * Runs on `preprocess_comment` so the updated display name is used as the + * comment author, rather than the stale email-derived name WordPress read + * from the user profile earlier in the request. + * + * @param array $commentdata Comment data. + * @return array Comment data with updated comment_author. + */ + public static function validate_display_name( $commentdata ) { + if ( ! self::should_prompt() ) { + return $commentdata; + } + + $display_name = isset( $_POST['comment_display_name'] ) ? \sanitize_text_field( \wp_unslash( $_POST['comment_display_name'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing + + if ( empty( $display_name ) ) { + \wp_die( + esc_html__( 'Please enter a display name.', 'newspack-plugin' ), + esc_html__( 'Comment Submission Failure', 'newspack-plugin' ), + [ 'back_link' => true ] + ); + } + + $user = \wp_get_current_user(); + $email = $user->user_email; + if ( + Reader_Activation::generate_user_nicename( $email ) === $display_name || + Reader_Activation::strip_email_domain( $email ) === $display_name + ) { + \wp_die( + esc_html__( 'Please choose a display name that is not derived from your email address.', 'newspack-plugin' ), + esc_html__( 'Comment Submission Failure', 'newspack-plugin' ), + [ 'back_link' => true ] + ); + } + + // Update the user profile. + $user_data = [ + 'ID' => $user->ID, + 'display_name' => $display_name, + ]; + + $name_parts = explode( ' ', $display_name, 2 ); + $user_data['first_name'] = $name_parts[0]; + $user_data['last_name'] = $name_parts[1] ?? ''; + + \wp_update_user( $user_data ); + + // Override the comment author so this comment uses the new name. + $commentdata['comment_author'] = $display_name; + + return $commentdata; + } + + /** + * Render the display name field before the comment form submit button. + * + * @param string $submit_field The submit field HTML. + * @return string The submit field HTML, with display name field prepended if needed. + */ + public static function render_display_name_field( $submit_field ) { + if ( ! self::should_prompt() ) { + return $submit_field; + } + + $field = '

' + . '' + . '' + . '

'; + + return $field . $submit_field; + } +} +Comment_Display_Name::init(); diff --git a/includes/reader-activation/class-integrations.php b/includes/reader-activation/class-integrations.php index ff2b89d609..18fb915efd 100644 --- a/includes/reader-activation/class-integrations.php +++ b/includes/reader-activation/class-integrations.php @@ -546,3 +546,4 @@ public static function run_health_checks() { } } } +Integrations::init(); diff --git a/tests/unit-tests/comment-display-name.php b/tests/unit-tests/comment-display-name.php new file mode 100644 index 0000000000..742f5b8544 --- /dev/null +++ b/tests/unit-tests/comment-display-name.php @@ -0,0 +1,231 @@ +create_generic_reader(); + + $output = Comment_Display_Name::render_display_name_field( '' ); + + $this->assertStringContainsString( 'name="comment_display_name"', $output ); + $this->assertStringContainsString( 'required', $output ); + $this->assertStringContainsString( '', $output ); + + wp_delete_user( $user_id ); + } + + /** + * Test that the display name field does not render for a reader with a custom display name. + */ + public function test_does_not_render_for_custom_display_name() { + $user_id = $this->create_generic_reader(); + wp_update_user( + [ + 'ID' => $user_id, + 'display_name' => 'Jane Doe', + ] + ); + + $output = Comment_Display_Name::render_display_name_field( '' ); + + $this->assertEquals( '', $output ); + + wp_delete_user( $user_id ); + } + + /** + * Test that the display name field does not render for non-reader users. + */ + public function test_does_not_render_for_non_reader() { + $admin_id = wp_insert_user( + [ + 'user_login' => 'test-admin', + 'user_pass' => wp_generate_password(), + 'user_email' => 'admin@example.com', + 'role' => 'administrator', + ] + ); + wp_set_current_user( $admin_id ); + + $output = Comment_Display_Name::render_display_name_field( '' ); + + $this->assertEquals( '', $output ); + + wp_delete_user( $admin_id ); + } + + /** + * Test that the display name field does not render for logged-out users. + */ + public function test_does_not_render_for_logged_out() { + wp_set_current_user( 0 ); + + $output = Comment_Display_Name::render_display_name_field( '' ); + + $this->assertEquals( '', $output ); + } + + /** + * Test that validation rejects an empty display name. + */ + public function test_validate_rejects_empty_display_name() { + $user_id = $this->create_generic_reader(); + $_POST['comment_display_name'] = ''; + + $commentdata = [ + 'comment_post_ID' => $this->factory->post->create(), + 'user_id' => $user_id, + ]; + + $this->expectException( WPDieException::class ); + Comment_Display_Name::validate_display_name( $commentdata ); + + wp_delete_user( $user_id ); + unset( $_POST['comment_display_name'] ); + } + + /** + * Test that validation rejects a display name that matches the generic pattern. + */ + public function test_validate_rejects_generic_display_name() { + $user_id = $this->create_generic_reader( 'john.smith@example.com' ); + $_POST['comment_display_name'] = 'john.smith'; + + $commentdata = [ + 'comment_post_ID' => $this->factory->post->create(), + 'user_id' => $user_id, + ]; + + $this->expectException( WPDieException::class ); + Comment_Display_Name::validate_display_name( $commentdata ); + + wp_delete_user( $user_id ); + unset( $_POST['comment_display_name'] ); + } + + /** + * Test that validation passes with a valid display name and sets comment_author. + */ + public function test_validate_accepts_valid_display_name() { + $user_id = $this->create_generic_reader(); + $_POST['comment_display_name'] = 'Jane Doe'; + + $commentdata = [ + 'comment_post_ID' => $this->factory->post->create(), + 'user_id' => $user_id, + ]; + + $result = Comment_Display_Name::validate_display_name( $commentdata ); + $this->assertEquals( 'Jane Doe', $result['comment_author'] ); + + wp_delete_user( $user_id ); + unset( $_POST['comment_display_name'] ); + } + + /** + * Test that validation passes for non-reader users without the field. + */ + public function test_validate_skips_non_reader() { + $admin_id = wp_insert_user( + [ + 'user_login' => 'test-admin-2', + 'user_pass' => wp_generate_password(), + 'user_email' => 'admin2@example.com', + 'role' => 'administrator', + ] + ); + wp_set_current_user( $admin_id ); + + $commentdata = [ + 'comment_post_ID' => $this->factory->post->create(), + 'user_id' => $admin_id, + ]; + + $result = Comment_Display_Name::validate_display_name( $commentdata ); + $this->assertEquals( $commentdata, $result ); + + wp_delete_user( $admin_id ); + } + + /** + * Test that validation saves the display name to the user profile and updates comment_author. + */ + public function test_validate_saves_display_name() { + $user_id = $this->create_generic_reader(); + $_POST['comment_display_name'] = 'Jane Doe'; + + $commentdata = [ + 'comment_post_ID' => $this->factory->post->create(), + 'user_id' => $user_id, + 'comment_author' => 'jane-doe', // Generic name set by WP before preprocess_comment. + ]; + + $result = Comment_Display_Name::validate_display_name( $commentdata ); + + // Comment author should be updated. + $this->assertEquals( 'Jane Doe', $result['comment_author'] ); + + // User profile should be updated. + $user = get_userdata( $user_id ); + $this->assertEquals( 'Jane Doe', $user->display_name ); + $this->assertEquals( 'Jane', $user->first_name ); + $this->assertEquals( 'Doe', $user->last_name ); + $this->assertFalse( Reader_Activation::reader_has_generic_display_name( $user_id ) ); + + wp_delete_user( $user_id ); + unset( $_POST['comment_display_name'] ); + } + + /** + * Test that validation handles single-word display names. + */ + public function test_validate_saves_single_word_name() { + $user_id = $this->create_generic_reader(); + $_POST['comment_display_name'] = 'Madonna'; + + $commentdata = [ + 'comment_post_ID' => $this->factory->post->create(), + 'user_id' => $user_id, + 'comment_author' => 'jane-doe', + ]; + + $result = Comment_Display_Name::validate_display_name( $commentdata ); + + $this->assertEquals( 'Madonna', $result['comment_author'] ); + + $user = get_userdata( $user_id ); + $this->assertEquals( 'Madonna', $user->display_name ); + $this->assertFalse( Reader_Activation::reader_has_generic_display_name( $user_id ) ); + + wp_delete_user( $user_id ); + unset( $_POST['comment_display_name'] ); + } +}