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
1 change: 1 addition & 0 deletions docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@
| 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/) |
| 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/) |
| non_blocking_scripts | performance | Checks whether scripts and styles are enqueued using a recommended loading strategy. | [Learn more](https://developer.wordpress.org/plugins/) |
| 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/) |
139 changes: 139 additions & 0 deletions includes/Checker/Checks/Plugin_Repo/Privacy_Policy_Check.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php
/**
* Class Privacy_Policy_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 that plugins handling personal data call wp_add_privacy_policy_content().
*
* Plugins that collect, use, store, or transmit personal data to a third party
* are required by WordPress.org guidelines to suggest privacy policy text to site
* administrators via wp_add_privacy_policy_content(). This check detects common
* personal-data-handling patterns and warns if that function is not used.
*
* @since 1.7.0
*/
class Privacy_Policy_Check extends Abstract_File_Check {

use Amend_Check_Result;
use Stable_Check;

/**
* Regex patterns that indicate a plugin may handle personal data.
*
* Each pattern is accompanied by a human-readable label used in the
* warning message to help plugin authors understand why the check fired.
*
* @since 1.7.0
* @var array<string, string>
*/
const PERSONAL_DATA_PATTERNS = array(
'wp_remote_post\s*\(' => 'wp_remote_post()',
'wp_remote_get\s*\(' => 'wp_remote_get()',
'setcookie\s*\(' => 'setcookie()',
'\$_COOKIE\b' => '$_COOKIE',
'wp_set_auth_cookie\s*\(' => 'wp_set_auth_cookie()',
);

/**
* Gets the categories for the check.
*
* Every check must have at least one category.
*
* @since 1.7.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.7.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' );

if ( empty( $php_files ) ) {
return;
}

// First, detect whether the plugin already calls wp_add_privacy_policy_content().
$has_privacy_call = (bool) self::file_preg_match(
'#\bwp_add_privacy_policy_content\s*\(#',
$php_files
);

// If the plugin already registers privacy policy content, nothing to warn about.
if ( $has_privacy_call ) {
return;
}

// Check for each personal-data-handling pattern.
foreach ( self::PERSONAL_DATA_PATTERNS as $pattern => $label ) {
$matches = array();
$matched_file = self::file_preg_match( '#' . $pattern . '#', $php_files, $matches );

if ( $matched_file ) {
$this->add_result_warning_for_file(
$result,
sprintf(
/* translators: %s: The detected function or variable name indicating personal data usage. */
__( '<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' ),
'<code>' . esc_html( $label ) . '</code>'
),
'missing_privacy_policy_content',
$result->plugin()->main_file(),
0,
0,
'https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/',
5
);

// One warning per plugin is sufficient — avoid duplicate messages.
return;
}
}
}

/**
* Gets the description for the check.
*
* Every check must have a short description explaining what the check does.
*
* @since 1.7.0
*
* @return string Description.
*/
public function get_description(): string {
return __( 'Checks that plugins handling personal data call wp_add_privacy_policy_content() to suggest privacy policy text to site administrators.', 'plugin-check' );
}

/**
* Gets the documentation URL for the check.
*
* Every check must have a URL with further information about the check.
*
* @since 1.7.0
*
* @return string The documentation URL.
*/
public function get_documentation_url(): string {
return __( 'https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/', 'plugin-check' );
}
}
1 change: 1 addition & 0 deletions includes/Checker/Default_Check_Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ private function register_default_checks() {
'direct_file_access' => new Checks\Plugin_Repo\Direct_File_Access_Check(),
'external_admin_menu_links' => new Checks\Plugin_Repo\External_Admin_Menu_Links_Check(),
'wp_functions_compatibility' => new Checks\Plugin_Repo\WP_Functions_Compatibility_Check(),
'privacy_policy' => new Checks\Plugin_Repo\Privacy_Policy_Check(),
)
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
* Plugin Name: Test Plugin Privacy Policy No Signals
* Plugin URI: https://github.com/WordPress/plugin-check
* Description: A test plugin that does not handle personal data at all — no privacy check signals present.
* Requires at least: 6.0
* 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-privacy-policy-no-signals
*
* @package test-plugin-privacy-policy-no-signals
*/

/**
* Outputs a greeting message in the admin footer.
*
* @return void
*/
function test_plugin_privacy_no_signals_greet() {
echo '<p>' . esc_html__( 'Hello from Test Plugin!', 'test-plugin-privacy-policy-no-signals' ) . '</p>';
}

add_action( 'admin_footer', 'test_plugin_privacy_no_signals_greet' );
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/**
* Plugin Name: Test Plugin Privacy Policy With Errors
* Plugin URI: https://github.com/WordPress/plugin-check
* Description: A test plugin that handles personal data but does not register privacy policy content.
* Requires at least: 6.0
* 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-privacy-policy-with-errors
*
* @package test-plugin-privacy-policy-with-errors
*/

// Sends data to an external service — indicates potential personal data handling.
function test_plugin_privacy_send_data() {
$response = wp_remote_post(
'https://example-analytics.com/collect',
array(
'body' => array(
'email' => get_option( 'admin_email' ),
),
)
);

return $response;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/**
* Plugin Name: Test Plugin Privacy Policy Without Errors
* Plugin URI: https://github.com/WordPress/plugin-check
* Description: A test plugin that handles personal data AND correctly calls wp_add_privacy_policy_content().
* Requires at least: 6.0
* 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-privacy-policy-without-errors
*
* @package test-plugin-privacy-policy-without-errors
*/

// Registers suggested privacy policy content — satisfies the check.
add_action(
'admin_init',
function () {
wp_add_privacy_policy_content(
'Test Plugin Privacy Policy Without Errors',
__( 'This plugin sends data to an external analytics service. No personally identifiable information is transmitted.', 'test-plugin-privacy-policy-without-errors' )
);
}
);

// Sends data to an external service.
function test_plugin_privacy_no_errors_send_data() {
$response = wp_remote_post(
'https://example-analytics.com/collect',
array(
'body' => array(
'site_url' => get_site_url(),
),
)
);

return $response;
}
71 changes: 71 additions & 0 deletions tests/phpunit/tests/Checker/Checks/Privacy_Policy_Check_Tests.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php
/**
* Tests for the Privacy_Policy_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\Privacy_Policy_Check;

class Privacy_Policy_Check_Tests extends WP_UnitTestCase {

/**
* Tests that a plugin using wp_remote_post() without wp_add_privacy_policy_content()
* receives a warning.
*/
public function test_run_with_errors() {
$check = new Privacy_Policy_Check();
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-privacy-policy-with-errors/load.php' );
$check_result = new Check_Result( $check_context );

$check->run( $check_result );

$warnings = $check_result->get_warnings();

$this->assertNotEmpty( $warnings );

// Warning must be on the plugin's main file.
$this->assertArrayHasKey( 'load.php', $warnings );

// Verify the expected warning code is present.
$this->assertCount( 1, wp_list_filter( $warnings['load.php'][0][0], array( 'code' => 'missing_privacy_policy_content' ) ) );
}

/**
* Tests that a plugin using wp_remote_post() WITH wp_add_privacy_policy_content()
* does not receive any warnings.
*/
public function test_run_without_errors() {
$check = new Privacy_Policy_Check();
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-privacy-policy-without-errors/load.php' );
$check_result = new Check_Result( $check_context );

$check->run( $check_result );

$warnings = $check_result->get_warnings();
$errors = $check_result->get_errors();

$this->assertEmpty( $warnings );
$this->assertEmpty( $errors );
}

/**
* Tests that a plugin with no personal-data-handling patterns does not receive
* any warnings, even if it does not call wp_add_privacy_policy_content().
*/
public function test_run_with_no_signals() {
$check = new Privacy_Policy_Check();
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-privacy-policy-no-signals/load.php' );
$check_result = new Check_Result( $check_context );

$check->run( $check_result );

$warnings = $check_result->get_warnings();
$errors = $check_result->get_errors();

$this->assertEmpty( $warnings );
$this->assertEmpty( $errors );
}
}
Loading