Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions includes/Checker/Checks/Plugin_Repo/Personal_Data_Eraser_Check.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php
/**
* Class Personal_Data_Eraser_Check.
*
* @package plugin-check
*/

namespace WordPress\Plugin_Check\Checker\Checks\Plugin_Repo;

use WordPress\Plugin_Check\Checker\Check_Categories;
use WordPress\Plugin_Check\Checker\Check_Result;
use WordPress\Plugin_Check\Checker\Checks\Abstract_File_Check;
use WordPress\Plugin_Check\Traits\Amend_Check_Result;
use WordPress\Plugin_Check\Traits\Stable_Check;

/**
* Check to detect personal data handling without a registered eraser callback.
*
* Plugins that collect or store personal data are expected to register an
* eraser callback via the `wp_privacy_personal_data_erasers` filter so that
* WordPress's built-in Personal Data Removal tool can delete the plugin's
* data on behalf of a user who submits a removal request.
*
* @since 1.3.0
* @link https://developer.wordpress.org/plugins/privacy/adding-the-personal-data-eraser-to-your-plugin/
*/
class Personal_Data_Eraser_Check extends Abstract_File_Check {

use Amend_Check_Result;
use Stable_Check;

/**
* Regex pattern that matches common personal-data storage API calls.
*
* Matches function calls that are strong indicators that a plugin is
* collecting or storing personal data about users.
*
* @since 1.3.0
* @var string
*/
const PERSONAL_DATA_PATTERN = '/\b(?:add_user_meta|update_user_meta|add_comment_meta|update_comment_meta|\$wpdb\s*->\s*(?:insert|update|replace))\s*\(/';

/**
* Regex pattern that matches registration of a personal data eraser.
*
* Matches add_filter() calls that hook into the wp_privacy_personal_data_erasers
* filter to register a data eraser callback.
*
* @since 1.3.0
* @var string
*/
const ERASER_REGISTRATION_PATTERN = '/add_filter\s*\(\s*[\'"]wp_privacy_personal_data_erasers[\'"]/';

/**
* Gets the categories for the check.
*
* Every check must have at least one category.
*
* @since 1.3.0
*
* @return array The categories for the check.
*/
public function get_categories() {
return array( Check_Categories::CATEGORY_PLUGIN_REPO );
}

/**
* Amends the given result by running the check on the given list of files.
*
* @since 1.3.0
*
* @param Check_Result $result The check result to amend, including the plugin context to check.
* @param array $files List of absolute file paths.
*/
protected function check_files( Check_Result $result, array $files ) {
$php_files = self::filter_files_by_extension( $files, 'php' );

$this->check_for_missing_eraser( $result, $php_files );
}

/**
* Checks whether the plugin handles personal data but omits the eraser filter.
*
* The check is intentionally a two-step process:
* 1. Confirm the plugin has at least one personal-data storage call.
* 2. Only then verify whether it registers the eraser filter.
*
* This avoids false positives for plugins that do not touch personal data at all.
*
* @since 1.3.0
*
* @param Check_Result $result The check result to amend.
* @param array $php_files List of absolute PHP file paths.
*/
protected function check_for_missing_eraser( Check_Result $result, array $php_files ) {
// Step 1: detect personal data signals across all plugin PHP files.
$signal_file = self::file_preg_match( self::PERSONAL_DATA_PATTERN, $php_files );

if ( false === $signal_file ) {
// No personal data handling detected — nothing to warn about.
return;
}

// Step 2: check if the plugin already registers a personal data eraser.
$has_eraser = self::file_preg_match( self::ERASER_REGISTRATION_PATTERN, $php_files );

if ( false !== $has_eraser ) {
// Eraser is registered — no issue.
return;
}

// Personal data is handled but no eraser is registered: emit a warning.
$this->add_result_warning_for_file(
$result,
__( 'Personal data was detected in this plugin but no data eraser has been registered. Plugins that store personal data should implement a data eraser via the <code>wp_privacy_personal_data_erasers</code> filter so that site administrators can fulfill data removal requests.', 'plugin-check' ),
'missing_personal_data_eraser',
$signal_file,
0,
0,
'https://developer.wordpress.org/plugins/privacy/adding-the-personal-data-eraser-to-your-plugin/',
5
);
}

/**
* Gets the description for the check.
*
* Every check must have a short description explaining what the check does.
*
* @since 1.3.0
*
* @return string Description.
*/
public function get_description(): string {
return __( 'Detects plugins that store personal data without registering a personal data eraser for GDPR compliance.', 'plugin-check' );
}

/**
* Gets the documentation URL for the check.
*
* Every check must have a URL with further information about the check.
*
* @since 1.3.0
*
* @return string The documentation URL.
*/
public function get_documentation_url(): string {
return 'https://developer.wordpress.org/plugins/privacy/adding-the-personal-data-eraser-to-your-plugin/';
}
}
1 change: 1 addition & 0 deletions includes/Checker/Default_Check_Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ private function register_default_checks() {
'minified_files' => new Checks\Plugin_Repo\Minified_Files_Check(),
'direct_file_access' => new Checks\Plugin_Repo\Direct_File_Access_Check(),
'external_admin_menu_links' => new Checks\Plugin_Repo\External_Admin_Menu_Links_Check(),
'personal_data_eraser' => new Checks\Plugin_Repo\Personal_Data_Eraser_Check(),
'wp_functions_compatibility' => new Checks\Plugin_Repo\WP_Functions_Compatibility_Check(),
)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* Plugin Name: Test Plugin Personal Data Eraser With Errors
* Plugin URI: https://github.com/WordPress/plugin-check
* Description: Test plugin for the Personal Data Eraser check — stores user meta but does not register a data eraser.
* Requires at least: 6.3
* Requires PHP: 7.4
* Version: 1.0.0
* Author: WordPress Performance Team
* Author URI: https://make.wordpress.org/performance/
* License: GPLv2 or later
* License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* Text Domain: test-plugin-personal-data-eraser-errors
*
* @package test-plugin-personal-data-eraser-errors
*/

/**
* Saves a custom preference for a user.
*
* @param int $user_id User ID.
*/
function test_pdel_save_user_preference( $user_id ) {
update_user_meta( $user_id, 'test_pdel_preference', 'some_value' );
}
add_action( 'user_register', 'test_pdel_save_user_preference' );
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php
/**
* Plugin Name: Test Plugin Personal Data Eraser Without Errors
* Plugin URI: https://github.com/WordPress/plugin-check
* Description: Test plugin for the Personal Data Eraser check — stores user meta AND registers a data eraser.
* Requires at least: 6.3
* Requires PHP: 7.4
* Version: 1.0.0
* Author: WordPress Performance Team
* Author URI: https://make.wordpress.org/performance/
* License: GPLv2 or later
* License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* Text Domain: test-plugin-personal-data-eraser-ok
*
* @package test-plugin-personal-data-eraser-ok
*/

/**
* Saves a custom preference for a user.
*
* @param int $user_id User ID.
*/
function test_pdel_ok_save_user_preference( $user_id ) {
update_user_meta( $user_id, 'test_pdel_ok_preference', 'some_value' );
}
add_action( 'user_register', 'test_pdel_ok_save_user_preference' );

/**
* Registers the personal data eraser.
*
* @param array $erasers An array of personal data erasers.
* @return array Updated erasers array.
*/
function test_pdel_ok_register_eraser( $erasers ) {
$erasers['test-pdel-ok'] = array(
'eraser_friendly_name' => __( 'Test PDEL OK Plugin Data', 'test-plugin-personal-data-eraser-ok' ),
'callback' => 'test_pdel_ok_eraser',
);
return $erasers;
}
add_filter( 'wp_privacy_personal_data_erasers', 'test_pdel_ok_register_eraser' );

/**
* Erases personal data for a user.
*
* @param string $email_address Email address of the user.
* @param int $page Pagination page number.
* @return array Erasure status array.
*/
function test_pdel_ok_eraser( $email_address, $page = 1 ) {
$user = get_user_by( 'email', $email_address );
if ( ! $user ) {
return array(
'items_removed' => false,
'items_retained' => false,
'messages' => array(),
'done' => true,
);
}

$removed = delete_user_meta( $user->ID, 'test_pdel_ok_preference' );

return array(
'items_removed' => $removed,
'items_retained' => false,
'messages' => array(),
'done' => true,
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php
/**
* Tests for the Personal_Data_Eraser_Check class.
*
* @package plugin-check
*/

use WordPress\Plugin_Check\Checker\Check_Context;
use WordPress\Plugin_Check\Checker\Check_Result;
use WordPress\Plugin_Check\Checker\Checks\Plugin_Repo\Personal_Data_Eraser_Check;

class Personal_Data_Eraser_Check_Tests extends WP_UnitTestCase {

public function test_plugin_with_personal_data_but_no_eraser_triggers_warning() {
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-personal-data-eraser-with-errors/load.php' );
$check_result = new Check_Result( $check_context );

$check = new Personal_Data_Eraser_Check();
$check->run( $check_result );

$warnings = $check_result->get_warnings();

$this->assertNotEmpty( $warnings );

$found = false;
foreach ( $warnings as $file_warnings ) {
foreach ( $file_warnings as $line_warnings ) {
foreach ( $line_warnings as $col_warnings ) {
foreach ( $col_warnings as $warning ) {
if ( isset( $warning['code'] ) && 'missing_personal_data_eraser' === $warning['code'] ) {
$found = true;
break 4;
}
}
}
}
}

$this->assertTrue( $found, 'Expected missing_personal_data_eraser warning was not found.' );
}

public function test_plugin_with_personal_data_and_eraser_has_no_warning() {
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-personal-data-eraser-without-errors/load.php' );
$check_result = new Check_Result( $check_context );

$check = new Personal_Data_Eraser_Check();
$check->run( $check_result );

$found = false;
foreach ( $check_result->get_warnings() as $file_warnings ) {
foreach ( $file_warnings as $line_warnings ) {
foreach ( $line_warnings as $col_warnings ) {
foreach ( $col_warnings as $warning ) {
if ( isset( $warning['code'] ) && 'missing_personal_data_eraser' === $warning['code'] ) {
$found = true;
break 4;
}
}
}
}
}

$this->assertFalse( $found, 'Unexpected missing_personal_data_eraser warning was found.' );
}

public function test_plugin_with_no_personal_data_has_no_warning() {
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-safe-redirect/load.php' );
$check_result = new Check_Result( $check_context );

$check = new Personal_Data_Eraser_Check();
$check->run( $check_result );

$found = false;
foreach ( $check_result->get_warnings() as $file_warnings ) {
foreach ( $file_warnings as $line_warnings ) {
foreach ( $line_warnings as $col_warnings ) {
foreach ( $col_warnings as $warning ) {
if ( isset( $warning['code'] ) && 'missing_personal_data_eraser' === $warning['code'] ) {
$found = true;
break 4;
}
}
}
}
}

$this->assertFalse( $found, 'Unexpected missing_personal_data_eraser warning on a plugin with no personal data.' );
}
}
Loading