Personal Data Exporters and Erasers
This is more like an advanced tutorial, I mean all we are going to do here is to add and customise the code. So, if you are just looking for more basic information about the WordPress personal data tools, I recommend to watch this video first:
[youtube_embed]https://www.youtube.com/embed/rYy1rEiJZsQ[/youtube_embed]
Register your Personal Data Exporter
Let’s talk a little bit about Tools > Export Personal Data. It is build to help you in a situation when somebody who used your website for a while requested all his personal data collected by you.
As I said in the video before, everything becomes very simple with the new Personal Data Exporter tool introduced in WordPress 4.9.6.
But what if you have your custom plugin or just the code in your theme – doesn’t matter, which operates your users personal data? This personal data won’t be added to the exported file automatically. What to do in this case? Example:

Is there a way to export this additional personal data? Manually? Surely no.
Below is the solution – I’m going to share with you how to extend WordPress default exporter and eraser tools for that test Orders custom post type.
add_filter( 'wp_privacy_personal_data_exporters', 'misha_register_exporter', 10);
function misha_register_exporter( $exporters_array ) {
$exporters_array['misha_exporter'] = array(
'exporter_friendly_name' => 'Misha exporter', // isn't shown anywhere
'callback' => 'misha_exporter_function', // name of the callback function which is below
);
return $exporters_array;
}
/*
* Callback function which processes the export
*/
function misha_exporter_function( $email_address, $iteration = 1 ) {
$iteration = (int) $iteration;
$export_items = array();
if( $orders = get_posts( array(
'post_type' => 'my_order',
'posts_per_page' => 100, // how much to process each time
'paged' => $iteration,
'meta_key' => 'customer_email',
'meta_value' => $email_address
) ) ) {
foreach ( (array) $orders as $order ){
// here you can specify the fields, that exist in any way
$data = array(
array(
'name' => 'Order ID',
'value' => $order->ID
),
array(
'name' => 'Full Name',
'value' => get_post_meta( $order->ID, 'customer_name', true )
),
array(
'name' => 'Email',
'value' => $email_address
),
array(
'name' => 'Amount',
'value' => get_post_meta( $order->ID, 'order_amount', true )
)
);
// let's say that it is the additional field, which is not always populated
if( $country = get_post_meta( $order->ID, 'customer_country', true ) ) {
$data[] = array(
'name' => 'Country',
'value' => $country
);
}
$export_items[] = array(
'group_id' => 'orders',
'group_label' => 'Orders',
'item_id' => 'order-'.$order->ID,
'data' => $data
);
}
}
// Tell core if we have more orders to work on still
$done = count( $orders ) < 100;
return array(
'data' => $export_items,
'done' => $done,
);
}
Some notes for the above code:
- If you use this code in your custom plugin, it is best practice if the key of the array and the slug of your plugin will match (line 4).
$iteration
parameter is likepage
when you query posts. We need this param in case a user has large amount of data in the database, let’s say more than 100 orders. Sometimes it could cause a slow page load or even a timeout error if you try to process all the data at once (lines 16, 23).- Let’s look at this condition
$done = count( $orders ) < 100;
It is very simple actually. Remember when a query returns 5 posts (for example) with the posts_per_page parameter is 5, in most cases it means that there are more posts.
Please note also, that the report will be created only when you click a “Download Personal Data” button or email the data.
Once the export file is created, another action hook will be fired:
add_action( 'wp_privacy_personal_data_export_file_created', 'misha_export_file_created', 20, 4 );
function misha_export_file_created( $archive_pathname, $archive_url, $html_report_pathname, $request_id ) {
// do stuff
}
Read below about wp_get_user_request_data( $request_id );
as well.
Exported files directory. How to change it?
By default WordPress stores all the files with the exported personal data in {UPLOADS DIR}/wp-personal-data-exports
.
- You can use
wp_privacy_exports_dir()
to get the full path of the personal data exports directory.There is
wp_privacy_exports_dir
filter hook inside this function:add_filter( 'wp_privacy_exports_dir', 'misha_change_exports_dir' ); function misha_change_exports_dir( $export_dir ) { // you can do all the stuff with wp_upload_dir() here but I think it will be enough just to str_replace() it return str_replace( 'wp-personal-data-exports', 'gdpr-exports', $export_dir ); }
- Or you can also use
wp_privacy_exports_url()
to get the full absolute URL of the exports directory. This function also haswp_privacy_exports_url
filter hook.
If you want to change your export directory, just run the above function for both wp_privacy_exports_dir
and wp_privacy_exports_url
filter hooks.
Exported files expiration period
WordPress has a scheduled action wp_privacy_delete_old_export_files
now which runs every hour and it can not be changed.
There is a function connected to the hook, which checks the privacy exports directory for the files and removes anything older than 3 days.
3 days period can be changed with wp_privacy_export_expiration
filter hook. For example below I change it to 1 day.
add_filter('wp_privacy_export_expiration', 'misha_custom_exp_period' );
function misha_custom_exp_period( $seconds ) {
return 86400; // One day
}
Remember, that since WordPress 3.5.0 you can use time constants like DAY_IN_SECONDS
, HOUR_IN_SECONDS
etc.
Register your Personal Data Erasers
In most cases you do not have to remove personal data, it would be enough just to anonymise it. Following my previous example with exporting orders, I created a personal data eraser for the WordPress tool below.
add_filter( 'wp_privacy_personal_data_erasers', 'misha_register_my_eraser', 10 );
function misha_register_my_eraser( $erasers ) {
$erasers['misha_eraser'] = array(
'eraser_friendly_name' => 'Misha eraser', // anything
'callback' => 'misha_eraser_function', // callback
);
return $erasers;
}
/*
* This callback function processes erasure requests
*/
function misha_eraser_function( $email_address, $iteration = 1 ) {
$iteration = (int) $iteration;
$items_removed = false;
if( $orders = get_posts(
array(
'post_type' => 'my_order',
'posts_per_page' => 100, // how much order do you want to process for a single term
'paged' => $iteration,
'meta_key' => 'customer_email',
'meta_value' => $email_address
)
) ) {
foreach ( (array) $orders as $order ){
// you can remove orders completely if you do not need them
//wp_delete_post( $order->ID, true ); // parameter true means permanent deletion without trash
// you can delete or anonymise the fields
delete_post_meta($order->ID, 'customer_email');
update_post_meta($order->ID, 'customer_name','Anonymous');
// if something was changed or removed in this iteration, set to "true"
$items_removed = true;
}
}
$done = count( $orders ) < $iteration;
return array(
'items_removed' => $items_removed,
'items_retained' => false,
'messages' => array(''), // you can add any custom message to be shown in /wp-admin/
'done' => $done,
);
}
One more hook…
Once all the erasure operations have been completed, wp_privacy_personal_data_erased
action hook will be fired. By the way the user notifications are also connected to this hook. Example:
add_action( 'wp_privacy_personal_data_erased', 'misha_do_smth_after_data_erasure' );
function misha_do_smth_after_data_erasure( $request_id ) {
// do something
}
By the way you can use wp_get_user_request_data( $request_id );
to get all the request data as an object. Example:
$request_data = wp_get_user_request_data( $request_id );
// print_r( $request_data);
echo $request->email;
As always, if you have any question or suggestion, welcome to comments 👇

Misha Rudrastyh
Hey guys and welcome to my website. For more than 10 years I've been doing my best to share with you some superb WordPress guides and tips for free.
Need some developer help? Contact me
I want to run Multiple WordPress websites with different theme and plugin using one database where the content remains the same in all websites.
1- site.com – Main site
2- siteb.com – Should use site.com database except for the theme & plugin
3- sitec.com – Should use site.com database except for the theme & plugin
Is there any way this can be done?
Hi Misha,
Sorry, Its a off topic comment.
I am trying and didn’t found any solution about so i asked here.
Thanks
Hi,
What about using WordPress Multisite? and
switch_to_blog()
.Thanks
Hi, Thanks for this usefull tutorial. I’ve got one question. Could it be, that only posts with status “published” will be exported? I need export for all posts from my custom post type, also on status review or special to my post type “expired”.
Is this possible?
Ahhhh, found the solution by myself.
Within IF statement: