Skip to content

feat(privacy): add check for missing wp_privacy_personal_data_exporters registration - #1292

Open
faisalahammad wants to merge 7 commits into
WordPress:trunkfrom
faisalahammad:feature/1251-personal-data-exporter-check
Open

faisalahammad wants to merge 7 commits into
WordPress:trunkfrom
faisalahammad:feature/1251-personal-data-exporter-check

Conversation

@faisalahammad

@faisalahammad faisalahammad commented May 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new static check, Personal_Data_Exporter_Check, that warns plugin authors when their plugin stores personal data (user meta, comment meta, or direct DB writes) but does not register an exporter callback via the wp_privacy_personal_data_exporters filter. Since WordPress 4.9.6, plugins handling personal data are expected to hook into the Personal Data Export tool so site admins can fulfill GDPR data export requests.

Fixes #1251

Changes

includes/Checker/Checks/Plugin_Repo/Personal_Data_Exporter_Check.php (new)

A token-based static file check (token_get_all(), so comments and string literals are ignored) with a two-step flow:

  1. Scan PHP files for personal-data signals (add_user_meta, update_user_meta, add_comment_meta, update_comment_meta, and $wpdb->insert/update/replace). Files under the plugin's own tests/ directory are excluded.
  2. Only if signals are found, check for add_filter( 'wp_privacy_personal_data_exporters', ... ) — if absent, emit a severity-5 warning (missing_personal_data_exporter).

The check is marked experimental, so it only runs with --include-experimental. This keeps the default run quiet because common non-personal uses of these functions (view counters, plugin settings, caches) may otherwise produce false positives.

includes/Checker/Default_Check_Repository.php

Registers the check under the Plugin_Repo category so it runs alongside other plugin directory compliance checks:

'personal_data_exporter'     => new Checks\Plugin_Repo\Personal_Data_Exporter_Check(),

Testing

Test 1: Plugin stores user meta, no exporter registered (expects warning)

  1. Install and activate the test plugin that calls update_user_meta() with no exporter filter
  2. Run Plugin Check on it with --include-experimental
  3. Result: Warning with code missing_personal_data_exporter appears ✅

Test 2: Plugin stores user meta, exporter registered (expects clean)

  1. Install and activate the test plugin that calls update_user_meta() and registers add_filter( 'wp_privacy_personal_data_exporters', ... )
  2. Run Plugin Check on it with --include-experimental
  3. Result: No missing_personal_data_exporter warning ✅

Test 3: Plugin has no personal data handling (expects clean)

  1. Install and activate a plugin with no user meta / comment meta / DB writes
  2. Run Plugin Check on it with --include-experimental
  3. Result: No missing_personal_data_exporter warning ✅

Test 4: Plugin uses $wpdb->insert() with no exporter registered (expects warning)

  1. Install and activate the test plugin that calls $wpdb->insert() with no exporter filter
  2. Result: missing_personal_data_exporter warning appears ✅

PHPUnit tests added at tests/phpunit/tests/Checker/Checks/Personal_Data_Exporter_Check_Tests.php covering all four scenarios.

AI Usage Disclosure

  • This PR includes AI-assisted code or content

AI tools were used: Claude Code assisted with the token-scanner implementation, applying review feedback (replacing the initial regex with a token-based scanner, experimental opt-in, test-path exclusions), and writing/updating the PHPUnit test coverage.

Open WordPress Playground Preview Open WordPress Playground Preview

Add a new static check that warns plugin authors when their plugin
handles personal data (user meta, comment meta, direct DB writes)
but does not register a callback via the wp_privacy_personal_data_exporters
filter.

- New check class: Personal_Data_Exporter_Check
- Registered in Default_Check_Repository under 'personal_data_exporter'
- PHPUnit test class with three test cases
- Test data plugins (with and without exporter registration)

Fixes WordPress#1251
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: faisalahammad <faisalahammad@git.wordpress.org>
Co-authored-by: AndriusBurba <andriusburba94@git.wordpress.org>
Co-authored-by: masteradhoc <masteradhoc@git.wordpress.org>
Co-authored-by: dknauss <dpknauss@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@AndriusBurba

Copy link
Copy Markdown

Thanks for tackling this, @faisalahammad! I tested the branch locally in wp-env against a few plugins. The update_user_meta happy paths work as expected. A few findings:

$wpdb detection never fires. The PERSONAL_DATA_PATTERN starts the alternation with \b, but $ is a non-word character, so \b\$wpdb only matches when $wpdb is preceded by a word character, which never happens in normal code. preg_match on $wpdb->insert( returns no match (it matches only in a contrived case like foo$wpdb->insert(). So direct DB writes, which are called out in both the description and #1251, aren't detected at all. The PHPUnit tests don't catch this because none of them exercise the $wpdb path. One fix is to move the \b onto just the function name group, e.g. '/(?:\b(?:add_user_meta|update_user_meta|add_comment_meta|update_comment_meta)|\$wpdb\s*->\s*(?:insert|update|replace))\s*\(/'.

False positives from matching raw text. Because the check runs regex over file contents rather than tokens: (a) a plugin that mentions update_user_meta() only in a code comment gets warned; (b) a real plugin in my test set was flagged solely because of update_comment_meta() calls inside its tests/ directory, with no runtime usage at all. Could the check be token based, and/or skip dev and test files?

Scope. Even after the $wpdb fix, calls like $wpdb->insert or update_user_meta are extremely common for non-personal data (view counters, plugin settings, caches), so a stable default warning could be quite noisy. Given that, would it make sense to start this as an experimental or opt-in check?

Minor: the @since tags should reference the next release (trunk is currently 2.0.0, so likely 2.1.0) rather than 1.3.0, and docs/checks.md needs a row for the new check.

- switch to token scanner to skip comments and match $wpdb correctly
- add experimental trait to avoid noisy stable warnings
- exclude plugin's own tests/ directory
- fix @SInCE tags to 2.0.0
- add docs/checks.md row and new wpdb-insert test case
@faisalahammad

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. Pushed fixes for everything you flagged.

Big ones:

  • replaced the regex with a token scanner so comments are skipped and $wpdb->insert/update/replace matches properly
  • check is now experimental, so it only runs with --include-experimental flag. helps with the noise concern for real plugins
  • added path anchored tests/ exclusion that filters the plugin's own tests folder without breaking the runner's own tests/ path

Minor:

  • @SInCE bumped from 1.3.0 to 2.0.0 to match trunk
  • added row in docs/checks.md

Also added a 4th PHPUnit test for the $wpdb->insert case and a matching testdata plugin.

All 476 phpunit tests pass, phpcs clean, phpstan clean, coderabbit review clean.

Let me know if anything else needs a look.

- extract is_personal_data_function_call helper to reduce NPath complexity
- extract is_wpdb_method_call helper to reduce NPath complexity
- extract is_exporter_filter_registration helper to reduce NPath complexity

Errors fixed:
- PHPMD NPathComplexity: find_file_with_personal_data_signal() from 3154 to < 50
- PHPMD NPathComplexity: plugin_registers_exporter() from 1300 to < 50
- PHPMD CyclomaticComplexity: find_file_with_personal_data_signal() from 21 to < 10

PHP 7.4+ compatible. All CI checks passing.
@masteradhoc

Copy link
Copy Markdown

Hey @AndriusBurba - did you had the chance to recheck this PR once again?

@dknauss

dknauss commented Aug 9, 2026

Copy link
Copy Markdown

The move to a token_get_all() scanner reads well, and gating on --include-experimental seems right for signals this broad.

These were probably not intended for inclusion in the diff:

  • plugin-check-pr-1292.zip
  • wp-tests-config.php
  • TESTING_INSTRUCTIONS.md

Separately, #1293 implements the eraser half of this and still uses the original regex — including the \b\$wpdb bug @AndriusBurba caught here, which means it never detects direct DB writes. Landing this PR first and porting the scanner across would save repeating the fix.

@faisalahammad

Copy link
Copy Markdown
Contributor Author

Thanks @dknauss — all three stray files are now removed from the diff:

  • plugin-check-pr-1292.zip
  • wp-tests-config.php
  • TESTING_INSTRUCTIONS.md

Removed in 29cf85e8 (fix(privacy): remove stray committed files, bump @SInCE to 2.1.0). Working tree is clean; only the intended checker change and these deletions remain vs trunk.

Agreed on #1293 — landing this token-scanner version first and porting it across to the eraser half would retire the \b$wpdb bug there without redoing the fix. Happy to help with that follow-up if useful.

@faisalahammad

Copy link
Copy Markdown
Contributor Author

Quick follow-up @AndriusBurba — in my earlier reply I noted @since at 2.0.0, but you were right that the next release is 2.1.0. That's now corrected: all 18 @since tags in Personal_Data_Exporter_Check.php are bumped 2.0.0 → 2.1.0 (29cf85e8), matching trunk's convention for new checks.

Recap of the state vs your review, all in the current head 29cf85e8:

  • Token scanner replaces the regex, so comments/strings are skipped and $wpdb->insert/update/replace matches correctly
  • Experimental opt-in (--include-experimental), so default runs stay quiet
  • Plugin's own tests/ directory excluded
  • docs/checks.md row added for personal_data_exporter
  • New PHPUnit test + testdata plugin for the $wpdb->insert() path

Happy to adjust if anything else needs a look.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental privacy check that warns when plugins store personal data without registering a WordPress personal-data exporter.

Changes:

  • Implements token-based personal-data and exporter detection.
  • Registers and documents the new check.
  • Adds PHPUnit fixtures covering key scenarios.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
includes/Checker/Checks/Plugin_Repo/Personal_Data_Exporter_Check.php Implements the check.
includes/Checker/Default_Check_Repository.php Registers the check.
docs/checks.md Documents the check.
tests/phpunit/tests/Checker/Checks/Personal_Data_Exporter_Check_Tests.php Adds PHPUnit coverage.
tests/phpunit/testdata/plugins/test-plugin-personal-data-exporter-with-errors/load.php Provides missing-exporter fixture.
tests/phpunit/testdata/plugins/test-plugin-personal-data-exporter-without-errors/load.php Provides registered-exporter fixture.
tests/phpunit/testdata/plugins/test-plugin-personal-data-exporter-with-wpdb-insert/load.php Provides direct-database-write fixture.
Suppressed comments (1)

includes/Checker/Checks/Plugin_Repo/Personal_Data_Exporter_Check.php:326

  • $open_paren can be null, but it is passed to the typed helper before this condition checks it. An incomplete file ending with add_filter therefore crashes the entire check with a TypeError. Validate the opening parenthesis before looking up the argument.
		$open_paren = $this->get_next_significant_token_index( $tokens, $index );
		$arg_index  = $this->get_next_significant_token_index( $tokens, $open_paren );

		if ( null === $open_paren || '(' !== $tokens[ $open_paren ] || null === $arg_index ) {

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread includes/Checker/Checks/Plugin_Repo/Personal_Data_Exporter_Check.php Outdated
Comment thread includes/Checker/Checks/Plugin_Repo/Personal_Data_Exporter_Check.php Outdated
Comment thread includes/Checker/Checks/Plugin_Repo/Personal_Data_Exporter_Check.php Outdated
Comment thread includes/Checker/Checks/Plugin_Repo/Personal_Data_Exporter_Check.php Outdated
- fix tests/ exclusion match on plugin directory path
- skip comments when finding next significant token
- match fully-qualified function calls across PHP 7.4/8.0
- require '(' for wpdb method calls (reject property reads)
- null-check token lookups before use
@faisalahammad

Copy link
Copy Markdown
Contributor Author

All 5 Copilot review findings addressed in 180ea519. Summary of fixes:

  • filter_out_test_paths() now matches tests/ correctly (was using dirname() on the plugin directory path, which prevented the exclusion from ever firing).
  • get_next_significant_token_index() now skips comments, matching the previous-token helper, so update_user_meta /*note*/ (...) and add_filter /*note*/ (...) are detected correctly.
  • Added is_name_token() helper that detects both T_STRING and the guarded T_NAME_FULLY_QUALIFIED (PHP 7.4 safe via defined() + constant()). Fully-qualified calls like \update_user_meta() and \add_filter() now match on any PHP version.
  • is_wpdb_method_call() now requires an opening parenthesis after the method name, so a property read like $wpdb->insert; no longer counts as a DB write.
  • Each token lookup result is null-checked before use, so a file ending at $wpdb no longer throws a TypeError.

All quality gates pass: phpcs 0 errors, phpstan 0 errors, PHPUnit 496/496 (including 4 Personal_Data_Exporter_Check tests), CodeRabbit 0 findings.

Ready for re-review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Privacy: Add check for wp_privacy_personal_data_exporters filter (GDPR personal data export)

5 participants