Skip to content

Commit 571cfdf

Browse files
committed
feat(plugin-repo): add privacy policy content check
Add a new Privacy_Policy_Check that warns when a plugin uses personal-data-handling APIs but does not call wp_add_privacy_policy_content(). WordPress.org guidelines require plugins that collect, store, or transmit personal data to a third party to suggest privacy policy text to site administrators via this function. The check scans PHP files for signals indicating potential personal data handling: - wp_remote_post() / wp_remote_get() (external data transmission) - setcookie() / $_COOKIE (cookie-based tracking) - wp_set_auth_cookie() (authentication cookies) If any signal is detected and wp_add_privacy_policy_content() is not called anywhere in the plugin, a single warning is emitted on the plugin's main file pointing to the official WordPress privacy developer documentation. Plugins with no signals are completely unaffected by this check. Fixes #1249
1 parent 0107eca commit 571cfdf

7 files changed

Lines changed: 310 additions & 0 deletions

File tree

docs/checks.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@
3535
| enqueued_styles_scope | performance | Checks whether any stylesheets are loaded on all pages, which is usually not desirable and can lead to performance issues. | [Learn more](https://developer.wordpress.org/plugins/) |
3636
| enqueued_scripts_scope | performance | Checks whether any scripts are loaded on all pages, which is usually not desirable and can lead to performance issues. | [Learn more](https://developer.wordpress.org/plugins/) |
3737
| non_blocking_scripts | performance | Checks whether scripts and styles are enqueued using a recommended loading strategy. | [Learn more](https://developer.wordpress.org/plugins/) |
38+
| privacy_policy | plugin_repo | Checks that plugins handling personal data call wp_add_privacy_policy_content() to suggest privacy policy text to site administrators. | [Learn more](https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/) |
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
/**
3+
* Class Privacy_Policy_Check.
4+
*
5+
* @package plugin-check
6+
*/
7+
8+
namespace WordPress\Plugin_Check\Checker\Checks\Plugin_Repo;
9+
10+
use WordPress\Plugin_Check\Checker\Check_Categories;
11+
use WordPress\Plugin_Check\Checker\Check_Result;
12+
use WordPress\Plugin_Check\Checker\Checks\Abstract_File_Check;
13+
use WordPress\Plugin_Check\Traits\Amend_Check_Result;
14+
use WordPress\Plugin_Check\Traits\Stable_Check;
15+
16+
/**
17+
* Check that plugins handling personal data call wp_add_privacy_policy_content().
18+
*
19+
* Plugins that collect, use, store, or transmit personal data to a third party
20+
* are required by WordPress.org guidelines to suggest privacy policy text to site
21+
* administrators via wp_add_privacy_policy_content(). This check detects common
22+
* personal-data-handling patterns and warns if that function is not used.
23+
*
24+
* @since 1.7.0
25+
*/
26+
class Privacy_Policy_Check extends Abstract_File_Check {
27+
28+
use Amend_Check_Result;
29+
use Stable_Check;
30+
31+
/**
32+
* Regex patterns that indicate a plugin may handle personal data.
33+
*
34+
* Each pattern is accompanied by a human-readable label used in the
35+
* warning message to help plugin authors understand why the check fired.
36+
*
37+
* @since 1.7.0
38+
* @var array<string, string>
39+
*/
40+
const PERSONAL_DATA_PATTERNS = array(
41+
'wp_remote_post\s*\(' => 'wp_remote_post()',
42+
'wp_remote_get\s*\(' => 'wp_remote_get()',
43+
'setcookie\s*\(' => 'setcookie()',
44+
'\$_COOKIE\b' => '$_COOKIE',
45+
'wp_set_auth_cookie\s*\(' => 'wp_set_auth_cookie()',
46+
);
47+
48+
/**
49+
* Gets the categories for the check.
50+
*
51+
* Every check must have at least one category.
52+
*
53+
* @since 1.7.0
54+
*
55+
* @return array The categories for the check.
56+
*/
57+
public function get_categories() {
58+
return array( Check_Categories::CATEGORY_PLUGIN_REPO );
59+
}
60+
61+
/**
62+
* Amends the given result by running the check on the given list of files.
63+
*
64+
* @since 1.7.0
65+
*
66+
* @param Check_Result $result The check result to amend, including the plugin context to check.
67+
* @param array $files List of absolute file paths.
68+
*/
69+
protected function check_files( Check_Result $result, array $files ) {
70+
$php_files = self::filter_files_by_extension( $files, 'php' );
71+
72+
if ( empty( $php_files ) ) {
73+
return;
74+
}
75+
76+
// First, detect whether the plugin already calls wp_add_privacy_policy_content().
77+
$has_privacy_call = (bool) self::file_preg_match(
78+
'#\bwp_add_privacy_policy_content\s*\(#',
79+
$php_files
80+
);
81+
82+
// If the plugin already registers privacy policy content, nothing to warn about.
83+
if ( $has_privacy_call ) {
84+
return;
85+
}
86+
87+
// Check for each personal-data-handling pattern.
88+
foreach ( self::PERSONAL_DATA_PATTERNS as $pattern => $label ) {
89+
$matches = array();
90+
$matched_file = self::file_preg_match( '#' . $pattern . '#', $php_files, $matches );
91+
92+
if ( $matched_file ) {
93+
$this->add_result_warning_for_file(
94+
$result,
95+
sprintf(
96+
/* translators: %s: The detected function or variable name indicating personal data usage. */
97+
__( '<strong>Missing privacy policy content registration.</strong><br>The plugin uses %s which may involve handling personal data, but does not call wp_add_privacy_policy_content(). Plugins that collect, store, or transmit personal data should suggest privacy policy text to site administrators.', 'plugin-check' ),
98+
'<code>' . esc_html( $label ) . '</code>'
99+
),
100+
'missing_privacy_policy_content',
101+
$result->plugin()->main_file(),
102+
0,
103+
0,
104+
'https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/',
105+
5
106+
);
107+
108+
// One warning per plugin is sufficient — avoid duplicate messages.
109+
return;
110+
}
111+
}
112+
}
113+
114+
/**
115+
* Gets the description for the check.
116+
*
117+
* Every check must have a short description explaining what the check does.
118+
*
119+
* @since 1.7.0
120+
*
121+
* @return string Description.
122+
*/
123+
public function get_description(): string {
124+
return __( 'Checks that plugins handling personal data call wp_add_privacy_policy_content() to suggest privacy policy text to site administrators.', 'plugin-check' );
125+
}
126+
127+
/**
128+
* Gets the documentation URL for the check.
129+
*
130+
* Every check must have a URL with further information about the check.
131+
*
132+
* @since 1.7.0
133+
*
134+
* @return string The documentation URL.
135+
*/
136+
public function get_documentation_url(): string {
137+
return __( 'https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/', 'plugin-check' );
138+
}
139+
}

includes/Checker/Default_Check_Repository.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ private function register_default_checks() {
102102
'direct_file_access' => new Checks\Plugin_Repo\Direct_File_Access_Check(),
103103
'external_admin_menu_links' => new Checks\Plugin_Repo\External_Admin_Menu_Links_Check(),
104104
'wp_functions_compatibility' => new Checks\Plugin_Repo\WP_Functions_Compatibility_Check(),
105+
'privacy_policy' => new Checks\Plugin_Repo\Privacy_Policy_Check(),
105106
)
106107
);
107108

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
/**
3+
* Plugin Name: Test Plugin Privacy Policy No Signals
4+
* Plugin URI: https://github.com/WordPress/plugin-check
5+
* Description: A test plugin that does not handle personal data at all — no privacy check signals present.
6+
* Requires at least: 6.0
7+
* Requires PHP: 7.4
8+
* Version: 1.0.0
9+
* Author: WordPress Performance Team
10+
* Author URI: https://make.wordpress.org/performance/
11+
* License: GPLv2 or later
12+
* License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
13+
* Text Domain: test-plugin-privacy-policy-no-signals
14+
*
15+
* @package test-plugin-privacy-policy-no-signals
16+
*/
17+
18+
/**
19+
* Outputs a greeting message in the admin footer.
20+
*
21+
* @return void
22+
*/
23+
function test_plugin_privacy_no_signals_greet() {
24+
echo '<p>' . esc_html__( 'Hello from Test Plugin!', 'test-plugin-privacy-policy-no-signals' ) . '</p>';
25+
}
26+
27+
add_action( 'admin_footer', 'test_plugin_privacy_no_signals_greet' );
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
/**
3+
* Plugin Name: Test Plugin Privacy Policy With Errors
4+
* Plugin URI: https://github.com/WordPress/plugin-check
5+
* Description: A test plugin that handles personal data but does not call wp_add_privacy_policy_content().
6+
* Requires at least: 6.0
7+
* Requires PHP: 7.4
8+
* Version: 1.0.0
9+
* Author: WordPress Performance Team
10+
* Author URI: https://make.wordpress.org/performance/
11+
* License: GPLv2 or later
12+
* License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
13+
* Text Domain: test-plugin-privacy-policy-with-errors
14+
*
15+
* @package test-plugin-privacy-policy-with-errors
16+
*/
17+
18+
// Sends data to an external service — indicates potential personal data handling.
19+
function test_plugin_privacy_send_data() {
20+
$response = wp_remote_post(
21+
'https://example-analytics.com/collect',
22+
array(
23+
'body' => array(
24+
'email' => get_option( 'admin_email' ),
25+
),
26+
)
27+
);
28+
29+
return $response;
30+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
/**
3+
* Plugin Name: Test Plugin Privacy Policy Without Errors
4+
* Plugin URI: https://github.com/WordPress/plugin-check
5+
* Description: A test plugin that handles personal data AND correctly calls wp_add_privacy_policy_content().
6+
* Requires at least: 6.0
7+
* Requires PHP: 7.4
8+
* Version: 1.0.0
9+
* Author: WordPress Performance Team
10+
* Author URI: https://make.wordpress.org/performance/
11+
* License: GPLv2 or later
12+
* License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
13+
* Text Domain: test-plugin-privacy-policy-without-errors
14+
*
15+
* @package test-plugin-privacy-policy-without-errors
16+
*/
17+
18+
// Registers suggested privacy policy content — satisfies the check.
19+
add_action(
20+
'admin_init',
21+
function () {
22+
wp_add_privacy_policy_content(
23+
'Test Plugin Privacy Policy Without Errors',
24+
__( 'This plugin sends data to an external analytics service. No personally identifiable information is transmitted.', 'test-plugin-privacy-policy-without-errors' )
25+
);
26+
}
27+
);
28+
29+
// Sends data to an external service.
30+
function test_plugin_privacy_no_errors_send_data() {
31+
$response = wp_remote_post(
32+
'https://example-analytics.com/collect',
33+
array(
34+
'body' => array(
35+
'site_url' => get_site_url(),
36+
),
37+
)
38+
);
39+
40+
return $response;
41+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
/**
3+
* Tests for the Privacy_Policy_Check class.
4+
*
5+
* @package plugin-check
6+
*/
7+
8+
use WordPress\Plugin_Check\Checker\Check_Context;
9+
use WordPress\Plugin_Check\Checker\Check_Result;
10+
use WordPress\Plugin_Check\Checker\Checks\Plugin_Repo\Privacy_Policy_Check;
11+
12+
class Privacy_Policy_Check_Tests extends WP_UnitTestCase {
13+
14+
/**
15+
* Tests that a plugin using wp_remote_post() without wp_add_privacy_policy_content()
16+
* receives a warning.
17+
*/
18+
public function test_run_with_errors() {
19+
$check = new Privacy_Policy_Check();
20+
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-privacy-policy-with-errors/load.php' );
21+
$check_result = new Check_Result( $check_context );
22+
23+
$check->run( $check_result );
24+
25+
$warnings = $check_result->get_warnings();
26+
27+
$this->assertNotEmpty( $warnings );
28+
29+
// Warning must be on the plugin's main file.
30+
$this->assertArrayHasKey( 'load.php', $warnings );
31+
32+
// Verify the expected warning code is present.
33+
$this->assertCount( 1, wp_list_filter( $warnings['load.php'][0][0], array( 'code' => 'missing_privacy_policy_content' ) ) );
34+
}
35+
36+
/**
37+
* Tests that a plugin using wp_remote_post() WITH wp_add_privacy_policy_content()
38+
* does not receive any warnings.
39+
*/
40+
public function test_run_without_errors() {
41+
$check = new Privacy_Policy_Check();
42+
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-privacy-policy-without-errors/load.php' );
43+
$check_result = new Check_Result( $check_context );
44+
45+
$check->run( $check_result );
46+
47+
$warnings = $check_result->get_warnings();
48+
$errors = $check_result->get_errors();
49+
50+
$this->assertEmpty( $warnings );
51+
$this->assertEmpty( $errors );
52+
}
53+
54+
/**
55+
* Tests that a plugin with no personal-data-handling patterns does not receive
56+
* any warnings, even if it does not call wp_add_privacy_policy_content().
57+
*/
58+
public function test_run_with_no_signals() {
59+
$check = new Privacy_Policy_Check();
60+
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-privacy-policy-no-signals/load.php' );
61+
$check_result = new Check_Result( $check_context );
62+
63+
$check->run( $check_result );
64+
65+
$warnings = $check_result->get_warnings();
66+
$errors = $check_result->get_errors();
67+
68+
$this->assertEmpty( $warnings );
69+
$this->assertEmpty( $errors );
70+
}
71+
}

0 commit comments

Comments
 (0)