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! |
---|