| 1 | # How to Connect Your Plugin to Core's New Personal Data Exporter
|
|---|
| 2 |
|
|---|
| 3 | ## Background
|
|---|
| 4 |
|
|---|
| 5 | In WordPress 4.9.x, new tools were added to make compliance easier with laws
|
|---|
| 6 | like the European Union's General Data Protection Regulation, or GDPR for
|
|---|
| 7 | short. Among the tools added is a Personal Data Export tool which supports
|
|---|
| 8 | exporting all the personal data for a given user in a ZIP file.
|
|---|
| 9 |
|
|---|
| 10 | In addition to the personal data stored in things like WordPress comments,
|
|---|
| 11 | plugins can also hook into the exporter feature to export the personal
|
|---|
| 12 | data they collect, whether it be in something like postmeta or even an
|
|---|
| 13 | entirely new Custom Post Type (CPT).
|
|---|
| 14 |
|
|---|
| 15 | ## How It Works
|
|---|
| 16 |
|
|---|
| 17 | The "key" for all the exports is the user's email address - this was chosen
|
|---|
| 18 | because it supports exporting personal data for both full-fledged registered
|
|---|
| 19 | users and also unregistered users (e.g. like a logged out commenter).
|
|---|
| 20 |
|
|---|
| 21 | However, since assembling a personal data export could be an intensive
|
|---|
| 22 | process and will likely contain sensitive data, we don't want to just
|
|---|
| 23 | generate it and email it to the requestor without confirming the request, so
|
|---|
| 24 | the admin-facing UX starts all requests by having the admin enter the username
|
|---|
| 25 | or email address making the request and then sends then a link to click
|
|---|
| 26 | to confirm their request.
|
|---|
| 27 |
|
|---|
| 28 | A list of requests and whether they have been confirmed is available to
|
|---|
| 29 | the administrator in the same user interface. Once a request has been
|
|---|
| 30 | confirmed, the admin can generate and download or directly email the
|
|---|
| 31 | personal data export ZIP file for the user.
|
|---|
| 32 |
|
|---|
| 33 | Inside the ZIP file the user receives, they will find a "mini website"
|
|---|
| 34 | with an index HTML page containing their personal data organized in
|
|---|
| 35 | groups (e.g. a group for comments, etc. )
|
|---|
| 36 |
|
|---|
| 37 | ## Design Internals
|
|---|
| 38 |
|
|---|
| 39 | Whether the admin downloads the personal data export ZIP file or sends
|
|---|
| 40 | it directly to the requestor, the way the personal data export is
|
|---|
| 41 | assembled is identical - and relies on hooking "exporter" callbacks to
|
|---|
| 42 | do the dirty work of collecting all the data for the export.
|
|---|
| 43 |
|
|---|
| 44 | When the admin clicks on the download or email link, an AJAX loop begins
|
|---|
| 45 | that iterates over all the exporters registered in the system, one at a time.
|
|---|
| 46 | In addition to exporters built into core, plugins can register their own
|
|---|
| 47 | exporter callbacks.
|
|---|
| 48 |
|
|---|
| 49 | The exporter callback interface is designed to be as simple as possible.
|
|---|
| 50 | A exporter callback receives the email address we are working with,
|
|---|
| 51 | and a page parameter as well. The page parameter (which starts at 1) is
|
|---|
| 52 | used to avoid plugins potentially causing timeouts by attempting to export
|
|---|
| 53 | all the personal data they've collected at once.
|
|---|
| 54 |
|
|---|
| 55 | The exporter callback replies with whatever data it has for that
|
|---|
| 56 | email address and page and whether it is done or not. If a exporter
|
|---|
| 57 | callback reports that it is not done, it will be called again (in a
|
|---|
| 58 | separate request) with the page parameter incremented by 1.
|
|---|
| 59 |
|
|---|
| 60 | Exporter callbacks are expected to return an array of items for the
|
|---|
| 61 | export. Each item contains an a group identifier for the group of which
|
|---|
| 62 | the item is a part (e.g. comments, posts, orders, etc.), an optional group
|
|---|
| 63 | label (translated), an item identifier (e.g. comment-133) and then an array of
|
|---|
| 64 | name, value pairs containing the data to be exported for that item.
|
|---|
| 65 |
|
|---|
| 66 | It is noteworthy that the value could be a media path, in which case the
|
|---|
| 67 | media file will be added to the exported ZIP file with a link in the
|
|---|
| 68 | "mini website" "index" HTML document to it.
|
|---|
| 69 |
|
|---|
| 70 | When all the exporters have been called to completion, core first assembles
|
|---|
| 71 | an "index" HTML document that serves as the heart of the export report.
|
|---|
| 72 | First, it walks the aggregate data and finds all the groups that core
|
|---|
| 73 | and plugins have identified.
|
|---|
| 74 |
|
|---|
| 75 | Then, for each group, it walks the data and finds all the items, using
|
|---|
| 76 | their item identifier to collect all the data for a given
|
|---|
| 77 | item (e.g. comment-133) from all the exporters into a single entry for the
|
|---|
| 78 | report. That way, core and plugins can all contribute data for the same
|
|---|
| 79 | item (e.g. a plugin may add location information to comments) and in the
|
|---|
| 80 | final export, all the data for a given item (e.g. comment 133) will be
|
|---|
| 81 | presented together.
|
|---|
| 82 |
|
|---|
| 83 | All of this is rendered into the HTML document and then the HTML document
|
|---|
| 84 | is zipped with any media attachments before being returned to the
|
|---|
| 85 | admin or emailed to the user. Exports are cached on the server for 1 day and
|
|---|
| 86 | then deleted.
|
|---|
| 87 |
|
|---|
| 88 | ## What to Do
|
|---|
| 89 |
|
|---|
| 90 | A plugin can register one or more exporters, but most plugins will only
|
|---|
| 91 | need one. Let's work from the example given above where a plugin adds
|
|---|
| 92 | location data for the commenter to comments.
|
|---|
| 93 |
|
|---|
| 94 | First, let's assume the plugin has used `add_comment_meta` to add location
|
|---|
| 95 | data using `meta_key`s of `latitude` and `longitude`
|
|---|
| 96 |
|
|---|
| 97 | The first thing the plugin needs to do is to create an exporter function
|
|---|
| 98 | that accepts an email address and a page, e.g.:
|
|---|
| 99 |
|
|---|
| 100 | ```
|
|---|
| 101 | function my_plugin_exporter( $email_address, $page = 1 ) {
|
|---|
| 102 | $number = 500; // Limit us to avoid timing out
|
|---|
| 103 | $page = (int) $page;
|
|---|
| 104 |
|
|---|
| 105 | $export_items = array();
|
|---|
| 106 |
|
|---|
| 107 | $comments = get_comments(
|
|---|
| 108 | array(
|
|---|
| 109 | 'author_email' => $email_address,
|
|---|
| 110 | 'number' => $number,
|
|---|
| 111 | 'paged' => $page,
|
|---|
| 112 | 'order_by' => 'comment_ID',
|
|---|
| 113 | 'order' => 'ASC',
|
|---|
| 114 | )
|
|---|
| 115 | );
|
|---|
| 116 |
|
|---|
| 117 | foreach ( (array) $comments as $comment ) {
|
|---|
| 118 | $latitude = get_comment_meta( $comment->comment_ID, 'latitude', true );
|
|---|
| 119 | $longitude = get_comment_meta( $comment->comment_ID, 'longitude', true );
|
|---|
| 120 |
|
|---|
| 121 | // Only add location data to the export if it is not empty
|
|---|
| 122 | if ( ! empty( $latitude ) ) {
|
|---|
| 123 | // Most item IDs should look like postType-postID
|
|---|
| 124 | // If you don't have a post, comment or other ID to work with,
|
|---|
| 125 | // use a unique value to avoid having this item's export
|
|---|
| 126 | // combined in the final report with other items of the same id
|
|---|
| 127 | $item_id = "comment-{$comment->comment_ID}";
|
|---|
| 128 |
|
|---|
| 129 | // Core group IDs include 'comments', 'posts', etc.
|
|---|
| 130 | // But you can add your own group IDs as needed
|
|---|
| 131 | $group_id = 'comments';
|
|---|
| 132 |
|
|---|
| 133 | // Optional group label. Core provides these for core groups.
|
|---|
| 134 | // If you define your own group, the first exporter to
|
|---|
| 135 | // include a label will be used as the group label in the
|
|---|
| 136 | // final exported report
|
|---|
| 137 | $group_label = __( 'Comments' );
|
|---|
| 138 |
|
|---|
| 139 | // Plugins can add as many items in the item data array as they want
|
|---|
| 140 | $data = array(
|
|---|
| 141 | array(
|
|---|
| 142 | 'name' => __( 'Commenter Latitude' ),
|
|---|
| 143 | 'value' => $latitude
|
|---|
| 144 | ),
|
|---|
| 145 | array(
|
|---|
| 146 | 'name' => __( 'Commenter Longitude' ),
|
|---|
| 147 | 'value' => $longitude
|
|---|
| 148 | )
|
|---|
| 149 | );
|
|---|
| 150 |
|
|---|
| 151 | $export_items[] = array(
|
|---|
| 152 | 'group_id' => $group_id,
|
|---|
| 153 | 'group_label' => $group_label,
|
|---|
| 154 | 'item_id' => $item_id,
|
|---|
| 155 | 'data' => $data,
|
|---|
| 156 | );
|
|---|
| 157 | }
|
|---|
| 158 | }
|
|---|
| 159 |
|
|---|
| 160 | // Tell core if we have more comments to work on still
|
|---|
| 161 | $done = count( $comments ) < $number;
|
|---|
| 162 |
|
|---|
| 163 | return array(
|
|---|
| 164 | 'data' => $export_items,
|
|---|
| 165 | 'done' => $done,
|
|---|
| 166 | );
|
|---|
| 167 | }
|
|---|
| 168 | ```
|
|---|
| 169 |
|
|---|
| 170 | The next thing the plugin needs to do is to register the callback by
|
|---|
| 171 | filtering the exporter array using the `wp_privacy_personal_data_exporters`
|
|---|
| 172 | filter.
|
|---|
| 173 |
|
|---|
| 174 | When registering you provide a friendly name for the export (to aid in
|
|---|
| 175 | debugging - this friendly name is not shown to anyone at this time)
|
|---|
| 176 | and the callback, e.g.
|
|---|
| 177 |
|
|---|
| 178 | ```
|
|---|
| 179 | function register_my_plugin_exporter( $exporters ) {
|
|---|
| 180 | $exporters[] = array(
|
|---|
| 181 | 'exporter_friendly_name' => __( 'Comment Location Plugin' ),
|
|---|
| 182 | 'callback' => 'my_plugin_exporter',
|
|---|
| 183 | );
|
|---|
| 184 | return $exporters;
|
|---|
| 185 | }
|
|---|
| 186 |
|
|---|
| 187 | add_filter(
|
|---|
| 188 | 'wp_privacy_personal_data_exporters',
|
|---|
| 189 | 'register_my_plugin_exporter',
|
|---|
| 190 | 10
|
|---|
| 191 | );
|
|---|
| 192 | ```
|
|---|
| 193 |
|
|---|
| 194 | And that's all there is to it! Your plugin will now provide data
|
|---|
| 195 | for the export!
|
|---|