{"slug":"wp-privacy-personal-data","title":"wp-privacy-personal-data","summary":"Implement or audit WordPress plugin privacy integration with","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-16T14:52:29.601439Z","repo":{"url":"https://github.com/Lonsdale201/wp-agent-skills","stars":22,"forks":2,"license":"MIT","updatedAt":"2026-09-26T23:03:36Z"},"bodyHtml":"<hr>\n<h2>name: wp-privacy-personal-data\ndescription: Implement or audit WordPress plugin privacy integration with\nprivacy-policy suggestions, personal-data exporters, and personal-data\nerasers. Covers wp_add_privacy_policy_content,\nwp_privacy_personal_data_exporters, wp_privacy_personal_data_erasers,\npaged callback contracts, retained-data messages, idempotent erasure,\nre-identification closure, collection-time validity, custom tables/meta/remote\nsystems, retention, and multisite scope. Use when\na plugin stores email addresses, IPs, user identifiers, profiles, form\nsubmissions, logs, analytics, orders, messages, or other personal data.\nmetadata:\nwp-skills-author: \"Soczó Kristóf\"\nwp-skills-contact: \"mailto:lonsdale201@hotmail.com\"\nwp-skills-plugin: \"wordpress\"\nwp-skills-plugin-version-tested: \"4.9.6 - 7.1\"\nwp-skills-wp-version-tested: \"7.1\"\nwp-skills-php-min: \"7.4\"\nwp-skills-last-updated: \"2026-08-20\"</h2>\n<h1>WordPress Personal Data Privacy Integration</h1>\n<p>Use WordPress' Privacy tools so an administrator can export and erase data\nowned by a plugin from Tools &gt; Export Personal Data and Tools &gt; Erase Personal\nData. This is an engineering contract, not legal advice or a claim of GDPR\ncompliance.</p>\n<h2>Start with a data inventory</h2>\n<p>Before writing callbacks, trace every personal-data store and transfer:</p>\n<ul>\n<li>options, user/post/term/comment meta, CPTs, attachments, custom tables;</li>\n<li>logs, queues, transients, caches, generated exports, and backups;</li>\n<li>data keyed by email, user ID, order/customer ID, IP, cookie ID, or token;</li>\n<li>remote providers and webhooks controlled by the plugin;</li>\n<li>retention reason, expiry, and whether deletion would break legal records.</li>\n</ul>\n<p>Exporters and erasers must cover the same inventory. Do not assume deleting a\n<code>WP_User</code> removes plugin tables, files, remote copies, or denormalized logs.</p>\n<h2>Verify the collection boundary</h2>\n<p>Capture personal data only after the host workflow has established that the\nsubmission or business event is valid. Early hooks such as\n<code>rest_request_before_callbacks</code> and priority-zero <code>wp_ajax_nopriv_*</code> observers\nrun before the target plugin's permission callback or action callback, including\nits callback-level nonce, spam, business validation, and success logic. The REST\nfilter runs after core authentication and registered argument validation, but it\ncan still persist data from requests the endpoint later rejects.</p>\n<p>Prefer the integration's documented post-success hook and verify the exact\nform/list/action plus consent/feature state. Sanitization does not turn a failed\nsubmission into a valid collection event. Test invalid, spam, unauthorized, and\ndownstream-failure paths and assert that they create no durable profile data.</p>\n<h2>Suggest privacy-policy text</h2>\n<p>Register factual, conditional text from <code>admin_init</code>. Describe what is\ncollected, why, retention, recipients, and user choices. Do not paste a legal\nguarantee or silently edit the site's published policy.</p>\n<pre><code>add_action( 'admin_init', static function (): void {\n    wp_add_privacy_policy_content(\n        __( 'My Plugin', 'myplugin' ),\n        wp_kses_post(\n            '&lt;p class=\"privacy-policy-tutorial\"&gt;' .\n            __( 'Suggested text: describe the data this plugin actually stores and sends.', 'myplugin' ) .\n            '&lt;/p&gt;'\n        )\n    );\n} );\n</code></pre>\n<p>Keep the suggestion synchronized with feature flags. If telemetry or a remote\nintegration is optional, say when it is active and what leaves the site.</p>\n<h2>Register a personal-data exporter</h2>\n<p>The filter receives all exporters. Add a stable, plugin-prefixed key and return\nthe array. The callback accepts an email and a 1-based page number.</p>\n<pre><code>add_filter( 'wp_privacy_personal_data_exporters', static function ( array $exporters ): array {\n    $exporters['myplugin-submissions'] = array(\n        'exporter_friendly_name' =&gt; __( 'My Plugin submissions', 'myplugin' ),\n        'callback'               =&gt; 'myplugin_export_personal_data',\n    );\n    return $exporters;\n} );\n\nfunction myplugin_export_personal_data( string $email, int $page = 1 ): array {\n    $per_page = 100;\n    $page     = max( 1, $page );\n    $rows     = MyPlugin_Submission_Repository::find_by_email(\n        sanitize_email( $email ),\n        $per_page,\n        ( $page - 1 ) * $per_page\n    );\n    $data = array();\n\n    foreach ( $rows as $row ) {\n        $data[] = array(\n            'group_id'          =&gt; 'myplugin-submissions',\n            'group_label'       =&gt; __( 'Form submissions', 'myplugin' ),\n            'group_description' =&gt; __( 'Submissions stored by My Plugin.', 'myplugin' ),\n            'item_id'           =&gt; 'submission-' . (int) $row-&gt;id,\n            'data'              =&gt; array(\n                array( 'name' =&gt; __( 'Email', 'myplugin' ), 'value' =&gt; (string) $row-&gt;email ),\n                array( 'name' =&gt; __( 'Message', 'myplugin' ), 'value' =&gt; (string) $row-&gt;message ),\n                array( 'name' =&gt; __( 'Created', 'myplugin' ), 'value' =&gt; (string) $row-&gt;created_at_gmt ),\n            ),\n        );\n    }\n\n    return array(\n        'data' =&gt; $data,\n        'done' =&gt; count( $rows ) &lt; $per_page,\n    );\n}\n</code></pre>\n<p>Rules:</p>\n<ul>\n<li>Page results deterministically with a stable primary-key order.</li>\n<li>Keep batches bounded; callbacks run through repeated admin AJAX requests.</li>\n<li>Export user-facing values, not raw rows, secrets, hashes, or internal ACLs.</li>\n<li>Use stable <code>group_id</code> and unique <code>item_id</code>; translate labels, not IDs.</li>\n<li>Include records found by email and by the matching user ID where relevant.</li>\n<li>Return exactly <code>array( 'data' =&gt; array, 'done' =&gt; bool )</code> or <code>WP_Error</code>.</li>\n</ul>\n<h2>Register a personal-data eraser</h2>\n<pre><code>add_filter( 'wp_privacy_personal_data_erasers', static function ( array $erasers ): array {\n    $erasers['myplugin-submissions'] = array(\n        'eraser_friendly_name' =&gt; __( 'My Plugin submissions', 'myplugin' ),\n        'callback'             =&gt; 'myplugin_erase_personal_data',\n    );\n    return $erasers;\n} );\n\nfunction myplugin_erase_personal_data( string $email, int $page = 1 ): array {\n    $result = MyPlugin_Submission_Repository::anonymize_next_batch(\n        sanitize_email( $email ),\n        100\n    );\n\n    return array(\n        'items_removed'  =&gt; $result-&gt;changed &gt; 0,\n        'items_retained' =&gt; $result-&gt;retained &gt; 0,\n        'messages'       =&gt; array_map( 'sanitize_text_field', $result-&gt;messages ),\n        'done'           =&gt; ! $result-&gt;has_more,\n    );\n}\n</code></pre>\n<p>The eraser response must contain all four keys. <code>items_removed</code> also covers\nsuccessful anonymization. <code>items_retained</code> means personal data was found but\nkept; explain why in <code>messages</code> without leaking the retained data itself.</p>\n<h2>Erasure design rules</h2>\n<ul>\n<li>Make erasure idempotent. Re-running it must be safe and converge.</li>\n<li>Prefer anonymization when a business/legal record must remain; sever user\nlinks and remove direct identifiers that are not required.</li>\n<li>Close every re-identification path, not only the main row. Delete or\nirreversibly detach crosswalk/profile rows keyed by user ID, email, cookie ID,\nsession ID, device fingerprint, order/customer ID, captcha/consent token, or\ndeterministic hash. An anonymized entity is still identifiable when a\nsecondary table can attach the same browser/account again on its next visit.</li>\n<li>Derive erasure fields from the same versioned registry/schema used by every\ndata-producing integration. A hand-written generic PII list easily misses\nnamespaces such as <code>form_email</code> or provider-specific address fields.</li>\n<li>Do not use offset pagination over a result set whose matching rows disappear\nas they are erased: page 2 can skip records. Re-query the next first batch,\nor use a stable cursor/processed marker and calculate <code>has_more</code> explicitly.</li>\n<li>Do not report <code>done =&gt; true</code> while matching erasable rows remain.</li>\n<li>Clean object caches and secondary indexes after direct custom-table writes.</li>\n<li>If a remote processor fails, retain locally required retry state and return a\nclear message; never claim complete erasure when one controlled store failed.</li>\n<li>Never erase another user's data from an unverified public endpoint. Core's\nprivacy request UI owns confirmation, capabilities, and nonces.</li>\n</ul>\n<h2>Retention and lifecycle</h2>\n<p>Privacy erasure, uninstall, and routine retention are different operations:</p>\n<ul>\n<li>Erasure targets one confirmed data subject and may retain required records.</li>\n<li>Retention jobs remove expired data for everyone according to policy.</li>\n<li>Uninstall removes plugin-owned configuration/data only when the product's\nuninstall policy says so; it is not a substitute for subject erasure.</li>\n</ul>\n<p>WordPress 7.1 schedules <code>wp_privacy_personal_data_cleanup_requests</code> daily. Core\nmarks expired <code>request-pending</code> <code>user_request</code> posts as <code>request-failed</code> and\nclears their confirmation key; it does not delete completed requests or\nplugin-owned personal data. Expiry follows <code>user_request_key_expiration</code>\n(normally one day), and the cleanup compares the site-local <code>post_modified</code>\ncolumn because the relative date is evaluated in the site timezone.</p>\n<p>Do not duplicate this cron event or treat it as a plugin retention engine.\nWP-Cron is traffic-dependent, so operationally critical retention still needs\nmonitoring and a safe idempotent catch-up path. If filtering\n<code>user_request_key_expiration</code>, test confirmation links and scheduled cleanup\ntogether; the filter changes both sides of the lifecycle.</p>\n<p>Do not keep personal data in autoloaded options or permanent transients. Give\ntemporary exports/logs an expiry and cleanup path. Backups are usually managed\noutside plugin callbacks; document their retention instead of pretending the\nplugin erased them synchronously.</p>\n<h2>Multisite and tests</h2>\n<p>Decide whether data belongs to one site, network tables, or both. Core runs a\nsite's registered callbacks in that site's context; do not silently iterate an\nentire network without network authorization and a documented contract.</p>\n<p>Test at least:</p>\n<ol>\n<li>no matching data;</li>\n<li>one row and more than one full batch;</li>\n<li>registered user plus guest records sharing the email;</li>\n<li>retained and partially failed records;</li>\n<li>repeated erasure calls;</li>\n<li>multisite scope and deleted-user records;</li>\n<li>export output for accidental secrets/internal fields;</li>\n<li>every integration-specific PII namespace and every profile/crosswalk type;</li>\n<li>failed/spam/unauthorized submissions create no durable data;</li>\n<li>a post-erasure request with the old cookie/fingerprint cannot reattach the\nanonymized entity.</li>\n</ol>\n<h2>Critical rules</h2>\n<ul>\n<li>Inventory first; exporter and eraser coverage must match actual storage.</li>\n<li>Keep callbacks paged, deterministic, idempotent, and truthful about <code>done</code>.</li>\n<li>Erase the re-identification graph and integration-specific PII namespaces,\nnot just direct fields on the primary entity.</li>\n<li>Do not equate user deletion, uninstall, or DB-row deletion with complete\npersonal-data erasure.</li>\n<li>Do not expose passwords, auth tokens, secret meta, or unrelated users' data.</li>\n<li>Add privacy-policy suggestions for collection and third-party transfers.</li>\n</ul>\n<h2>Cross-references</h2>\n<ul>\n<li>Use <strong><code>wp-security-audit</code></strong> for request handlers and authorization.</li>\n<li>Use <strong><code>wp-settings-storage-audit</code></strong> for retention/autoload storage choices.</li>\n</ul>\n<h2>Core references</h2>\n<ul>\n<li><code>wp-admin/includes/plugin.php</code>: <code>wp_add_privacy_policy_content()</code>.</li>\n<li><code>wp-admin/includes/ajax-actions.php</code>: exporter/eraser response validation.</li>\n<li><code>wp-includes/comment.php</code>: core paged exporter and eraser examples.</li>\n<li><code>wp-admin/includes/privacy-tools.php</code>: export/erasure processing pipeline.</li>\n<li><code>wp-includes/functions.php</code>: scheduled request cleanup and old export-file cleanup.</li>\n</ul>\n<h2>References</h2>\n<ul>\n<li>Official documentation: <a href=\"https://developer.wordpress.org/plugins/privacy/adding-the-personal-data-exporter-to-your-plugin/\">https://developer.wordpress.org/plugins/privacy/adding-the-personal-data-exporter-to-your-plugin/</a></li>\n<li>Official documentation: <a href=\"https://developer.wordpress.org/plugins/privacy/adding-the-personal-data-eraser-to-your-plugin/\">https://developer.wordpress.org/plugins/privacy/adding-the-personal-data-eraser-to-your-plugin/</a></li>\n<li>Official documentation: <a href=\"https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/\">https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/</a></li>\n</ul>\n","files":[{"path":"agents/openai.yaml","sizeBytes":252,"isText":true},{"path":"SKILL.md","sizeBytes":11942,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-16T14:59:32.820985Z","sha256":"F47A9185AA79171CAF697B4449D47A1CAF681236E8DDABEF342BD55A65DF9CE9","sizeBytes":5132},"review":null,"source":{"repositoryUrl":"https://github.com/Lonsdale201/wp-agent-skills","path":"wordpress/wp-privacy-personal-data","license":"MIT","commit":"c51b571a259f0c4b5f5c0a3bc50ed580c6851f98","subtreeSha":"A10ABFC891A372A4FEABD69660B20F8456D1212D95AA2CB0AA5495AC75211FDD","lastSyncedAt":"2026-09-29T23:33:03.303675Z"},"reviewedAt":"2026-09-16T15:21:28.749468Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/wordpress/wp-privacy-personal-data"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart"},{"target":"git","command":"git clone https://github.com/Lonsdale201/wp-agent-skills.git"}]}