From 42c05abfac2ebf0d335e7f0627648d1f0b3b1492 Mon Sep 17 00:00:00 2001
From: Paul Biron <paul@sparrowhawkcomputing.com>
Date: Sat, 25 Jan 2020 13:46:47 -0700
Subject: [PATCH] add a filter in wp_user_personal_data_exporter() to make it
 easier to include additional user meta in a personal data export.  Includes
 improved unit test.

---
 src/wp-includes/user.php     |  49 ++++++++++++-
 tests/phpunit/tests/user.php | 135 +++++++++++++++++++++++++++++++++++
 2 files changed, 183 insertions(+), 1 deletion(-)

diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php
index a1478679f6..f84bade569 100644
--- a/src/wp-includes/user.php
+++ b/src/wp-includes/user.php
@@ -3003,10 +3003,57 @@ function wp_user_personal_data_exporter( $email_address ) {
 		}
 	}
 
+	// get the list of reserved names.
+	$reserved_names = array_values( $user_prop_to_export );
+
+	/**
+	 * Filter to extend the Users personal data for the privacy exporter.
+	 *
+	 * @since 5.4.0
+	 *
+	 * @param array    $additional_user_data {
+	 *     An array of name-value pairs of additional user data items.  Default: the 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 it's `name` will not be included in the export.
+	 */
+	$_extra_data = apply_filters( 'wp_privacy_additional_user_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_data */
+					__( 'Filter %s returned items with reserved names.' ),
+					'<code>wp_privacy_additional_user_data</code>'
+				),
+				'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' ),
-		'group_description' => __( 'User&#8217;s profile data.' ),
+		'group_description' => __( 'User&#8217;s meta data.' ),
 		'item_id'           => "user-{$user->ID}",
 		'data'              => $user_data_to_export,
 	);
diff --git a/tests/phpunit/tests/user.php b/tests/phpunit/tests/user.php
index 74c533b69d..3330e24447 100644
--- a/tests/phpunit/tests/user.php
+++ b/tests/phpunit/tests/user.php
@@ -1671,4 +1671,139 @@ class Tests_User extends WP_UnitTestCase {
 		// Number of exported user properties.
 		$this->assertSame( 11, count( $actual['data'][0]['data'] ) );
 	}
+
+	/**
+	 * Testing the `wp_privacy_additional_user_data` filter works.
+	 *
+	 * @since 5.4.0
+	 *
+	 * @ticket 47509
+	 */
+	function test_filter_wp_privacy_additional_user_data() {
+		$test_user = new WP_User( self::$contrib_id );
+
+		add_filter( 'wp_privacy_additional_user_data', array( $this, 'export_additional_user_data' ) );
+
+		$actual = wp_user_personal_data_exporter( $test_user->user_email );
+
+		remove_filter( 'wp_privacy_additional_user_data', array( $this, 'export_additional_user_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_data', array( $this, 'export_additional_user_data_with_dup_name' ) );
+
+		$actual = wp_user_personal_data_exporter( $test_user->user_email );
+
+		remove_filter( 'wp_privacy_additional_user_data', array( $this, 'export_additional_user_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 data to the User Group on Export Requests.
+	 *
+	 * @since 5.4.0
+	 *
+	 * @ticket 47509
+	 *
+	 * @return array $additional_data The additional User data.
+	 */
+	public function export_additional_user_data() {
+		$additional_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_data;
+	}
+
+	/**
+	 * Filter callback to add additional data to the User Group on Export Requests.
+	 *
+	 * This callback should generate a `_doing_it_wrong()`.
+	 *
+	 * @since 5.4.0
+	 *
+	 * @ticket 47509
+	 *
+	 * @return array $additional_data The additional User data.
+	 */
+	public function export_additional_user_data_with_dup_name() {
+		$additional_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_data;
+	}
 }
-- 
2.19.0.windows.1

