diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 4dece3f2e9..5e0d238eeb 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -3006,6 +3006,52 @@ function wp_user_personal_data_exporter( $email_address ) { } } + // Get the list of reserved names. + $reserved_names = array_values( $user_props_to_export ); + + /** + * Filter to extend the Users profile data for the privacy exporter. + * + * @since 5.4.0 + * + * @param array $additional_user_profile_data { + * An array of name-value pairs of additional user data items. Default empty array. + * + * @type string $name The user-facing name of an item name-value pair,e.g. 'IP Address'. + * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'. + * } + * @param WP_User $user The user whose data is being exported. + * @param string[] $reserved_names An array of reserved names. Any item in `$additional_user_data` + * that uses one of these for its `name` will not be included in the export. + */ + $_extra_data = apply_filters( 'wp_privacy_additional_user_profile_data', array(), $user, $reserved_names ); + + if ( is_array( $_extra_data ) && ! empty( $_extra_data ) ) { + // Remove items that use reserved names. + $extra_data = array_filter( + $_extra_data, + function( $item ) use ( $reserved_names ) { + return ! in_array( $item['name'], $reserved_names ); + } + ); + + if ( count( $extra_data ) !== count( $_extra_data ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: wp_privacy_additional_user_profile_data */ + __( 'Filter %s returned items with reserved names.' ), + 'wp_privacy_additional_user_profile_data' + ), + '5.4.0' + ); + } + + if ( ! empty( $extra_data ) ) { + $user_data_to_export = array_merge( $user_data_to_export, $extra_data ); + } + } + $data_to_export[] = array( 'group_id' => 'user', 'group_label' => __( 'User' ), diff --git a/tests/phpunit/tests/user.php b/tests/phpunit/tests/user.php index 7fb561c7df..6935b07067 100644 --- a/tests/phpunit/tests/user.php +++ b/tests/phpunit/tests/user.php @@ -1794,4 +1794,133 @@ class Tests_User extends WP_UnitTestCase { $this->assertEquals( 'Last Login', $actual['data'][1]['data'][3]['name'] ); $this->assertEquals( 'January 29, 2020 09:13 AM', $actual['data'][1]['data'][3]['value'] ); } + + /** + * Testing the `wp_privacy_additional_user_profile_data` filter works. + * + * @ticket 47509 + */ + function test_filter_wp_privacy_additional_user_profile_data() { + $test_user = new WP_User( self::$contrib_id ); + + add_filter( 'wp_privacy_additional_user_profile_data', array( $this, 'export_additional_user_profile_data' ) ); + + $actual = wp_user_personal_data_exporter( $test_user->user_email ); + + remove_filter( 'wp_privacy_additional_user_profile_data', array( $this, 'export_additional_user_profile_data' ) ); + + $this->assertTrue( $actual['done'] ); + + // Number of exported users. + $this->assertSame( 1, count( $actual['data'] ) ); + + // Number of exported user properties (the 11 core properties, + // plus 1 additional from the filter). + $this->assertSame( 12, count( $actual['data'][0]['data'] ) ); + + // Check that the item added by the filter was retained. + $this->assertSame( + 1, + count( + wp_list_filter( + $actual['data'][0]['data'], + array( + 'name' => 'Test Additional Data Name', + 'value' => 'Test Additional Data Value', + ) + ) + ) + ); + + // _doing_wrong() should be called because the filter callback + // adds a item with a 'name' that is the same as one generated by core. + $this->setExpectedIncorrectUsage( 'wp_user_personal_data_exporter' ); + add_filter( 'wp_privacy_additional_user_profile_data', array( $this, 'export_additional_user_profile_data_with_dup_name' ) ); + + $actual = wp_user_personal_data_exporter( $test_user->user_email ); + + remove_filter( 'wp_privacy_additional_user_profile_data', array( $this, 'export_additional_user_profile_data' ) ); + + $this->assertTrue( $actual['done'] ); + + // Number of exported users. + $this->assertSame( 1, count( $actual['data'] ) ); + + // Number of exported user properties + // (the 11 core properties, plus 1 additional from the filter). + $this->assertSame( 12, count( $actual['data'][0]['data'] ) ); + + // Check that the duplicate 'name' => 'User ID' was stripped. + $this->assertSame( + 1, + count( + wp_list_filter( + $actual['data'][0]['data'], + array( + 'name' => 'User ID', + ) + ) + ) + ); + + // Check that the item added by the filter was retained. + $this->assertSame( + 1, + count( + wp_list_filter( + $actual['data'][0]['data'], + array( + 'name' => 'Test Additional Data Name', + 'value' => 'Test Additional Data Value', + ) + ) + ) + ); + } + + /** + * Filter callback to add additional profile data to the User Group on Export Requests. + * + * @ticket 47509 + * + * @return array $additional_profile_data The additional user data. + */ + public function export_additional_user_profile_data() { + $additional_profile_data = array( + // This item should be retained and included in the export. + array( + 'name' => 'Test Additional Data Name', + 'value' => 'Test Additional Data Value', + ), + ); + + return $additional_profile_data; + } + + /** + * Filter callback to add additional profile data to the User Group on Export Requests. + * + * This callback should generate a `_doing_it_wrong()`. + * + * @ticket 47509 + * + * @return array $additional_profile_data The additional user data. + */ + public function export_additional_user_profile_data_with_dup_name() { + $additional_profile_data = array( + // This item should be stripped out by wp_user_personal_data_exporter() + // because it's 'name' duplicates one exported by core. + array( + 'name' => 'User ID', + 'value' => 'Some User ID', + ), + // This item should be retained and included in the export. + array( + 'name' => 'Test Additional Data Name', + 'value' => 'Test Additional Data Value', + ), + ); + + return $additional_profile_data; + } }