-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Privacy API docs #48
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
# Privacy API | ||
|
||
The CMS provides extensible privacy tools including a default privacy policy page and hooks for additional code to suggest content for it, as well as the ability to register user data export and deletion callbacks. | ||
|
||
## Privacy Policy Page | ||
|
||
The privacy policy page is automatically generated as a draft with basic information related to the operation of the CMS along with headings for sections that should be filled out with more detailed information such as what analytics and trackers are running on the site. | ||
|
||
[The default page is available to edit here](internal://admin/post.php?post=3&action=edit). | ||
|
||
Before launching a site it is important to fill out this page and publish it. | ||
|
||
The standard content hierarchy is as follows: | ||
|
||
- Who we are | ||
- What personal data we collect and why we collect it | ||
- Comments | ||
- Media | ||
- Contact forms | ||
- Cookies | ||
- Embedded content from other websites | ||
- Analytics | ||
- Who we share your data with | ||
- How long we retain your data | ||
- What rights you have over your data | ||
- Where we send your data | ||
- Your contact information | ||
- Additional information | ||
- How we protect your data | ||
- What data breach procedures we have in place | ||
- What third parties we receive data from | ||
- What automated decision making and/or profiling we do with user data | ||
- Industry regulatory disclosure requirements | ||
|
||
### Using an Existing or Alternative Page | ||
|
||
Under the [Settings > Privacy admin menu item](internal://admin/privacy.php) it is possible to select which page you would like to use as the privacy policy page. | ||
|
||
![Privacy page settings screen](./assets/privacy-page-settings.png) | ||
|
||
The Privacy Policy page ID is stored as an option: | ||
|
||
```php | ||
$privacy_page_id = get_option( 'wp_page_for_privacy_policy' ); | ||
``` | ||
|
||
## Suggesting Privacy Policy Content | ||
|
||
**`wp_add_privacy_policy_content( string $identifier, string $policy_text )`** | ||
|
||
This function will provide an editor working on the privacy policy page with prompts for content they can add and the code or feature it relates to. The text should aim to answer one of the default headings described above. | ||
|
||
If you need to provide information that fits under multiple sections you can make multiple calls to this function to make it easier for editors to pull out the relevant content where needed. | ||
|
||
It is recommended to link out to any 3rd party privacy policies where relevant. | ||
|
||
```php | ||
// Must be called on the `admin_init` hook. | ||
add_action( 'admin_init', function () { | ||
$policy_text = sprintf( | ||
__( 'The Google Analytics feature collects information | ||
about your browser and how you interact with the website. | ||
If you are logged in this information may be associated with | ||
your account. You can learn more by visiting the | ||
<a href="%s">Google Privacy Policy</a> page.' | ||
), | ||
'https://policies.google.com/privacy?hl=en' | ||
); | ||
wp_add_privacy_policy_content( 'google-analytics', $policy_text ); | ||
} ); | ||
``` | ||
|
||
## Personal Data Exports | ||
|
||
When a user makes a request for an export of their personal data a confirmation request should be sent to them via the [Export Personal Data tool](internal://admin/tools.php?page=export_personal_data) by filling in their email address and clicking send. | ||
|
||
![Personal Data Export admin screen](./assets/data-export.png) | ||
|
||
Once they have confirmed their request a zip file is created and emailed to them. | ||
|
||
### Extending Personal Data Export | ||
|
||
By default the export will contain any data known to be associated with the requester's email address such as comments or posts. Some code may extend the platform in such a way that it cannot determine all the data associated with the user automatically. | ||
|
||
To extend the data export use the `wp_privacy_personal_data_exporters` filter: | ||
|
||
```php | ||
add_filter( 'wp_privacy_personal_data_exporters', function ( array $exporters ) : array { | ||
$exporters['form-responses'] = [ | ||
'exporter_friendly_name' => __( 'Form Response Exporter' ), | ||
'callback' => 'form_response_exporter', | ||
]; | ||
return $exporters; | ||
} ); | ||
``` | ||
|
||
The `form_response_exporter` callback function recieves the requester's email address and is responsible for collecting the custom data. | ||
|
||
The function should paginate results to avoid timeouts and performance issues. To help with this core checks the value of `done` in the returned array and if false will call the function again with the `$page` argument incremented by 1. | ||
|
||
The following example exports data from a hypothetical `form_responses` custom post type: | ||
|
||
```php | ||
function form_response_exporter( string $email, int $page = 1 ) : array { | ||
|
||
$export_items = []; | ||
|
||
// Fetch form responses from this email address. | ||
$responses = new WP_Query( [ | ||
'posts_per_page' => 300, | ||
'paged' => $page, | ||
'post_type' => 'form_responses', | ||
'meta_key' => 'email', | ||
'meta_value' => $meail, | ||
] ); | ||
|
||
foreach ( $responses->posts as $post ) { | ||
// Most item IDs should look like postType-postID | ||
// If you don't have a post, comment or other ID to work with, | ||
// use a unique value to avoid having this item's export | ||
// combined in the final report with other items of the same id. | ||
$item_id = sprintf( 'form-responses-%d', $post->ID ); | ||
|
||
// Core group IDs include 'comments', 'posts', etc. | ||
// But you can add your own group IDs as needed. | ||
$group_id = 'form-responses'; | ||
|
||
// Optional group label. Core provides these for core groups. | ||
// If you define your own group, the first exporter to | ||
// include a label will be used as the group label in the | ||
// final exported report. | ||
$group_label = __( 'Form Responses' ); | ||
|
||
// Add as many items in the item data array as needed. | ||
$data = [ | ||
[ | ||
'name' => __( 'Message' ), | ||
'value' => $post->post_content, | ||
], | ||
[ | ||
'name' => __( 'Interests' ), | ||
'value' => implode( ', ', get_post_meta( $post->ID, 'interests' ) ), | ||
], | ||
// ... more fields ... | ||
]; | ||
|
||
// Exported items must match this array structure. | ||
$export_items[] = [ | ||
'group_id' => $group_id, | ||
'group_label' => $group_label, | ||
'item_id' => $item_id, | ||
'data' => $data, | ||
]; | ||
} | ||
|
||
// Must return an array of data and a "done" flag. | ||
return [ | ||
'data' => $export_items, | ||
'done' => $responses->have_posts(), | ||
]; | ||
} | ||
``` | ||
|
||
## Erasing Personal Data | ||
|
||
Following a similar mechanism to the personal data export should a user request that their personal data be deleted you can send a confirmation email to them via the [Tools > Erase Personal Data page](internal://admin/tools.php?page=remove_personal_data). | ||
|
||
Once confirmed all known data associated with their email address will be deleted. Note the data deleted does not extend to any backups as those are not covered under "making a reasonable effort to remove all data" in accordance with GDPR. | ||
|
||
Should a site ever need to be restored from a backup it is important to go back through previous data removal requests and ensure the data was either already removed or to remove it again. | ||
|
||
### Extending Personal Data Erasers | ||
|
||
Again via a similar mechanism to the data export custom code can provide a means to remove personal data. | ||
|
||
A date deletion callback can be registered via the `wp_privacy_personal_data_erasers` filter: | ||
|
||
```php | ||
add_filter( 'wp_privacy_personal_data_erasers', function ( array $erasers ) : array { | ||
$erasers['form_response_eraser'] = [ | ||
'eraser_friendly_name' => __( 'Form Response Eraser' ), | ||
'callback' => 'form_response_eraser', | ||
]; | ||
return $erasers; | ||
} ); | ||
``` | ||
|
||
The following example shows the deletion of the hypothetical `form_responses` custom post type from earlier and takes the exact same arguments of email address and page: | ||
|
||
```php | ||
function form_response_eraser( string $email, $page = 1 ) : array { | ||
|
||
// Fetch form responses from this email address. | ||
$responses = new WP_Query( [ | ||
'posts_per_page' => 300, | ||
'paged' => $page, | ||
'post_type' => 'form_responses', | ||
'meta_key' => 'email', | ||
'meta_value' => $meail, | ||
] ); | ||
|
||
$items_removed = 0; | ||
$items_retained = 0; | ||
$messages = []; | ||
|
||
foreach ( $responses->posts as $post ) { | ||
// Force detete the post. | ||
$success = wp_delete_post( $post->ID, true ); | ||
|
||
if ( $success ) { | ||
$items_removed++; | ||
} else { | ||
$items_retained++; | ||
$messages[] = sprintf( __( 'Form Response ID %d could not be deleted.' ), $post->ID ); | ||
} | ||
} | ||
|
||
return [ | ||
// Number of items removed. | ||
'items_removed' => $items_removed, | ||
// Number of items retained. | ||
'items_retained' => $items_retained, | ||
// Optional messages for response. | ||
'messages' => $messages, | ||
// Whether we have more data to go through. | ||
'done' => $responses->have_posts(), | ||
]; | ||
} | ||
``` | ||
|
||
## Filters | ||
|
||
**`user_request_action_confirmed_message : string`** | ||
|
||
Allows modifying the confirmation email text sent to the user. | ||
|
||
**`wp_privacy_export_expiration : int`** | ||
|
||
Allows modifying the time in seconds to retain generated export files for. Defaults to 3 days. | ||
|
||
**`wp_privacy_personal_data_email_content : string`** | ||
|
||
Allows modifying the email text sent to users with their data export download link. | ||
|
||
## Capabilities | ||
|
||
The privacy features have some custom capablities allowing for non-admins or custom user roles to be able to manage personal data exports and deletion. | ||
|
||
**`erase_others_personal_data`** | ||
|
||
Controls whether the user can access the erase data screen under Tools in the admin. | ||
|
||
**`export_others_personal_data`** | ||
|
||
Controls whether the user can access the export data screen under Tools in the admin. | ||
|
||
**`manage_privacy_options`** | ||
|
||
Controls whether the user can set the privacy policy page under Settings > Privacy in the admin. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add text domains?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the sake of example code it's probably ok but can do