run:R W Run
7.85 KB
2026-03-11 16:18:51
R W Run
3.54 KB
2026-03-11 16:18:51
R W Run
148.33 KB
2026-03-11 16:18:51
R W Run
11.45 KB
2026-03-11 16:18:51
R W Run
3.58 KB
2026-03-11 16:18:51
R W Run
2.53 KB
2026-03-11 16:18:51
R W Run
2.6 KB
2026-03-11 16:18:51
R W Run
6.59 KB
2026-03-11 16:18:51
R W Run
14.83 KB
2026-03-11 16:18:51
R W Run
21.18 KB
2026-03-11 16:18:51
R W Run
48.13 KB
2026-03-11 16:18:51
R W Run
4.07 KB
2026-03-11 16:18:51
R W Run
5.3 KB
2026-03-11 16:18:51
R W Run
8.28 KB
2026-03-11 16:18:51
R W Run
26.73 KB
2026-03-11 16:18:51
R W Run
2.8 KB
2026-03-11 16:18:51
R W Run
15.2 KB
2026-03-11 16:18:51
R W Run
192.08 KB
2026-03-11 16:18:51
R W Run
11.77 KB
2026-03-11 16:18:51
R W Run
3.2 KB
2026-03-11 16:18:51
R W Run
22.89 KB
2026-03-11 16:18:51
R W Run
12.77 KB
2026-03-11 16:18:51
R W Run
4.08 KB
2026-03-11 16:18:51
R W Run
26.27 KB
2026-03-11 16:18:51
R W Run
4.97 KB
2026-03-11 16:18:51
R W Run
5.57 KB
2026-03-11 16:18:51
R W Run
13.93 KB
2026-03-11 16:18:51
R W Run
4.09 KB
2026-03-11 16:18:51
R W Run
6.79 KB
2026-03-11 16:18:51
R W Run
60.45 KB
2026-03-11 16:18:51
R W Run
32.4 KB
2026-03-11 16:18:51
R W Run
18.24 KB
2026-03-11 16:18:51
R W Run
66.01 KB
2026-03-11 16:18:51
R W Run
23.84 KB
2026-03-11 16:18:51
R W Run
17.72 KB
2026-03-11 16:18:51
R W Run
22.71 KB
2026-03-11 16:18:51
R W Run
18.05 KB
2026-03-11 16:18:51
R W Run
22.76 KB
2026-03-11 16:18:51
R W Run
7.34 KB
2026-03-11 16:18:51
R W Run
4.51 KB
2026-03-11 16:18:51
R W Run
9.02 KB
2026-03-11 16:18:51
R W Run
1.46 KB
2026-03-11 16:18:51
R W Run
51.76 KB
2026-03-11 16:18:51
R W Run
25.29 KB
2026-03-11 16:18:51
R W Run
21.61 KB
2026-03-11 16:18:51
R W Run
27.77 KB
2026-03-11 16:18:51
R W Run
15.35 KB
2026-03-11 16:18:51
R W Run
24.54 KB
2026-03-11 16:18:51
R W Run
56.44 KB
2026-03-11 16:18:51
R W Run
1.42 KB
2026-03-11 16:18:51
R W Run
63.66 KB
2026-03-11 16:18:51
R W Run
31.9 KB
2026-03-11 16:18:51
R W Run
14.44 KB
2026-03-11 16:18:51
R W Run
36.47 KB
2026-03-11 16:18:51
R W Run
14 KB
2026-03-11 16:18:51
R W Run
121.89 KB
2026-03-11 16:18:51
R W Run
6.26 KB
2026-03-11 16:18:51
R W Run
20.73 KB
2026-03-11 16:18:51
R W Run
15.23 KB
2026-03-11 16:18:51
R W Run
10.14 KB
2026-03-11 16:18:51
R W Run
6.94 KB
2026-03-11 16:18:51
R W Run
1.44 KB
2026-03-11 16:18:51
R W Run
46.85 KB
2026-03-11 16:18:51
R W Run
18.61 KB
2026-03-11 16:18:51
R W Run
6.08 KB
2026-03-11 16:18:51
R W Run
20.06 KB
2026-03-11 16:18:51
R W Run
5.73 KB
2026-03-11 16:18:51
R W Run
68.18 KB
2026-03-11 16:18:51
R W Run
40.8 KB
2026-03-11 16:18:51
R W Run
1.44 KB
2026-03-11 16:18:51
R W Run
25.26 KB
2026-03-11 16:18:51
R W Run
95.94 KB
2026-03-11 16:18:51
R W Run
43.12 KB
2026-03-11 16:18:51
R W Run
41.73 KB
2026-03-11 16:18:51
R W Run
6.46 KB
2026-03-11 16:18:51
R W Run
3.71 KB
2026-03-11 16:18:51
R W Run
116.31 KB
2026-03-11 16:18:51
R W Run
9.39 KB
2026-03-11 16:18:51
R W Run
64.34 KB
2026-03-11 16:18:51
R W Run
44.73 KB
2026-03-11 16:18:51
R W Run
1.27 KB
2026-03-11 16:18:51
R W Run
3.68 KB
2026-03-11 16:18:51
R W Run
33.53 KB
2026-03-11 16:18:51
R W Run
48.84 KB
2026-03-11 16:18:51
R W Run
26.35 KB
2026-03-11 16:18:51
R W Run
1.12 KB
2026-03-11 16:18:51
R W Run
4.19 KB
2026-03-11 16:18:51
R W Run
38.19 KB
2026-03-11 16:18:51
R W Run
91.33 KB
2026-03-11 16:18:51
R W Run
80.39 KB
2026-03-11 16:18:51
R W Run
32.67 KB
2026-03-11 16:18:51
R W Run
16.18 KB
2026-03-11 16:18:51
R W Run
44.46 KB
2026-03-11 16:18:51
R W Run
6.23 KB
2026-03-11 16:18:51
R W Run
8.23 KB
2026-03-11 16:18:51
R W Run
96.96 KB
2026-03-11 16:18:51
R W Run
6.83 KB
2026-03-11 16:18:51
R W Run
46.62 KB
2026-03-11 16:18:51
R W Run
10.82 KB
2026-03-11 16:18:51
R W Run
68.86 KB
2026-03-11 16:18:51
R W Run
33.63 KB
2026-03-11 16:18:51
R W Run
113.3 KB
2026-03-11 16:18:51
R W Run
22.98 KB
2026-03-11 16:18:51
R W Run
10.66 KB
2026-03-11 16:18:51
R W Run
error_log
📄privacy-tools.php
1<?php
2/**
3 * WordPress Administration Privacy Tools API.
4 *
5 * @package WordPress
6 * @subpackage Administration
7 */
8
9/**
10 * Resend an existing request and return the result.
11 *
12 * @since 4.9.6
13 * @access private
14 *
15 * @param int $request_id Request ID.
16 * @return true|WP_Error Returns true if sending the email was successful, or a WP_Error object.
17 */
18function _wp_privacy_resend_request( $request_id ) {
19 $request_id = absint( $request_id );
20 $request = get_post( $request_id );
21
22 if ( ! $request || 'user_request' !== $request->post_type ) {
23 return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) );
24 }
25
26 $result = wp_send_user_request( $request_id );
27
28 if ( is_wp_error( $result ) ) {
29 return $result;
30 } elseif ( ! $result ) {
31 return new WP_Error( 'privacy_request_error', __( 'Unable to initiate confirmation for personal data request.' ) );
32 }
33
34 return true;
35}
36
37/**
38 * Marks a request as completed by the admin and logs the current timestamp.
39 *
40 * @since 4.9.6
41 * @access private
42 *
43 * @param int $request_id Request ID.
44 * @return int|WP_Error Request ID on success, or a WP_Error on failure.
45 */
46function _wp_privacy_completed_request( $request_id ) {
47 // Get the request.
48 $request_id = absint( $request_id );
49 $request = wp_get_user_request( $request_id );
50
51 if ( ! $request ) {
52 return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) );
53 }
54
55 update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() );
56
57 $result = wp_update_post(
58 array(
59 'ID' => $request_id,
60 'post_status' => 'request-completed',
61 )
62 );
63
64 return $result;
65}
66
67/**
68 * Handle list table actions.
69 *
70 * @since 4.9.6
71 * @access private
72 */
73function _wp_personal_data_handle_actions() {
74 if ( isset( $_POST['privacy_action_email_retry'] ) ) {
75 check_admin_referer( 'bulk-privacy_requests' );
76
77 $request_id = absint( current( array_keys( (array) wp_unslash( $_POST['privacy_action_email_retry'] ) ) ) );
78 $result = _wp_privacy_resend_request( $request_id );
79
80 if ( is_wp_error( $result ) ) {
81 add_settings_error(
82 'privacy_action_email_retry',
83 'privacy_action_email_retry',
84 $result->get_error_message(),
85 'error'
86 );
87 } else {
88 add_settings_error(
89 'privacy_action_email_retry',
90 'privacy_action_email_retry',
91 __( 'Confirmation request sent again successfully.' ),
92 'success'
93 );
94 }
95 } elseif ( isset( $_POST['action'] ) ) {
96 $action = ! empty( $_POST['action'] ) ? sanitize_key( wp_unslash( $_POST['action'] ) ) : '';
97
98 switch ( $action ) {
99 case 'add_export_personal_data_request':
100 case 'add_remove_personal_data_request':
101 check_admin_referer( 'personal-data-request' );
102
103 if ( ! isset( $_POST['type_of_action'], $_POST['username_or_email_for_privacy_request'] ) ) {
104 add_settings_error(
105 'action_type',
106 'action_type',
107 __( 'Invalid personal data action.' ),
108 'error'
109 );
110 }
111 $action_type = sanitize_text_field( wp_unslash( $_POST['type_of_action'] ) );
112 $username_or_email_address = sanitize_text_field( wp_unslash( $_POST['username_or_email_for_privacy_request'] ) );
113 $email_address = '';
114 $status = 'pending';
115
116 if ( ! isset( $_POST['send_confirmation_email'] ) ) {
117 $status = 'confirmed';
118 }
119
120 if ( ! in_array( $action_type, _wp_privacy_action_request_types(), true ) ) {
121 add_settings_error(
122 'action_type',
123 'action_type',
124 __( 'Invalid personal data action.' ),
125 'error'
126 );
127 }
128
129 if ( ! is_email( $username_or_email_address ) ) {
130 $user = get_user_by( 'login', $username_or_email_address );
131 if ( ! $user instanceof WP_User ) {
132 add_settings_error(
133 'username_or_email_for_privacy_request',
134 'username_or_email_for_privacy_request',
135 __( 'Unable to add this request. A valid email address or username must be supplied.' ),
136 'error'
137 );
138 } else {
139 $email_address = $user->user_email;
140 }
141 } else {
142 $email_address = $username_or_email_address;
143 }
144
145 if ( empty( $email_address ) ) {
146 break;
147 }
148
149 $request_id = wp_create_user_request( $email_address, $action_type, array(), $status );
150 $message = '';
151
152 if ( is_wp_error( $request_id ) ) {
153 $message = $request_id->get_error_message();
154 } elseif ( ! $request_id ) {
155 $message = __( 'Unable to initiate confirmation request.' );
156 }
157
158 if ( $message ) {
159 add_settings_error(
160 'username_or_email_for_privacy_request',
161 'username_or_email_for_privacy_request',
162 $message,
163 'error'
164 );
165 break;
166 }
167
168 if ( 'pending' === $status ) {
169 wp_send_user_request( $request_id );
170
171 $message = __( 'Confirmation request initiated successfully.' );
172 } elseif ( 'confirmed' === $status ) {
173 $message = __( 'Request added successfully.' );
174 }
175
176 if ( $message ) {
177 add_settings_error(
178 'username_or_email_for_privacy_request',
179 'username_or_email_for_privacy_request',
180 $message,
181 'success'
182 );
183 break;
184 }
185 }
186 }
187}
188
189/**
190 * Cleans up failed and expired requests before displaying the list table.
191 *
192 * @since 4.9.6
193 * @access private
194 */
195function _wp_personal_data_cleanup_requests() {
196 /** This filter is documented in wp-includes/user.php */
197 $expires = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS );
198
199 $requests_query = new WP_Query(
200 array(
201 'post_type' => 'user_request',
202 'posts_per_page' => -1,
203 'post_status' => 'request-pending',
204 'fields' => 'ids',
205 'date_query' => array(
206 array(
207 'column' => 'post_modified_gmt',
208 'before' => $expires . ' seconds ago',
209 ),
210 ),
211 )
212 );
213
214 $request_ids = $requests_query->posts;
215
216 foreach ( $request_ids as $request_id ) {
217 wp_update_post(
218 array(
219 'ID' => $request_id,
220 'post_status' => 'request-failed',
221 'post_password' => '',
222 )
223 );
224 }
225}
226
227/**
228 * Generate a single group for the personal data export report.
229 *
230 * @since 4.9.6
231 * @since 5.4.0 Added the `$group_id` and `$groups_count` parameters.
232 *
233 * @param array $group_data {
234 * The group data to render.
235 *
236 * @type string $group_label The user-facing heading for the group, e.g. 'Comments'.
237 * @type array $items {
238 * An array of group items.
239 *
240 * @type array $group_item_data {
241 * An array of name-value pairs for the item.
242 *
243 * @type string $name The user-facing name of an item name-value pair, e.g. 'IP Address'.
244 * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'.
245 * }
246 * }
247 * }
248 * @param string $group_id The group identifier.
249 * @param int $groups_count The number of all groups
250 * @return string The HTML for this group and its items.
251 */
252function wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id = '', $groups_count = 1 ) {
253 $group_id_attr = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id );
254
255 $group_html = '<h2 id="' . esc_attr( $group_id_attr ) . '">';
256 $group_html .= esc_html( $group_data['group_label'] );
257
258 $items_count = count( (array) $group_data['items'] );
259 if ( $items_count > 1 ) {
260 $group_html .= sprintf( ' <span class="count">(%d)</span>', $items_count );
261 }
262
263 $group_html .= '</h2>';
264
265 if ( ! empty( $group_data['group_description'] ) ) {
266 $group_html .= '<p>' . esc_html( $group_data['group_description'] ) . '</p>';
267 }
268
269 $group_html .= '<div>';
270
271 foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) {
272 $group_html .= '<table>';
273 $group_html .= '<tbody>';
274
275 foreach ( (array) $group_item_data as $group_item_datum ) {
276 $value = $group_item_datum['value'];
277 // If it looks like a link, make it a link.
278 if ( ! str_contains( $value, ' ' ) && ( str_starts_with( $value, 'http://' ) || str_starts_with( $value, 'https://' ) ) ) {
279 $value = '<a href="' . esc_url( $value ) . '">' . esc_html( $value ) . '</a>';
280 }
281
282 $group_html .= '<tr>';
283 $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>';
284 $group_html .= '<td>' . wp_kses( $value, 'personal_data_export' ) . '</td>';
285 $group_html .= '</tr>';
286 }
287
288 $group_html .= '</tbody>';
289 $group_html .= '</table>';
290 }
291
292 if ( $groups_count > 1 ) {
293 $group_html .= '<div class="return-to-top">';
294 $group_html .= '<a href="#top"><span aria-hidden="true">&uarr; </span> ' . esc_html__( 'Go to top' ) . '</a>';
295 $group_html .= '</div>';
296 }
297
298 $group_html .= '</div>';
299
300 return $group_html;
301}
302
303/**
304 * Generate the personal data export file.
305 *
306 * @since 4.9.6
307 *
308 * @param int $request_id The export request ID.
309 */
310function wp_privacy_generate_personal_data_export_file( $request_id ) {
311 if ( ! class_exists( 'ZipArchive' ) ) {
312 wp_send_json_error( __( 'Unable to generate personal data export file. ZipArchive not available.' ) );
313 }
314
315 // Get the request.
316 $request = wp_get_user_request( $request_id );
317
318 if ( ! $request || 'export_personal_data' !== $request->action_name ) {
319 wp_send_json_error( __( 'Invalid request ID when generating personal data export file.' ) );
320 }
321
322 $email_address = $request->email;
323
324 if ( ! is_email( $email_address ) ) {
325 wp_send_json_error( __( 'Invalid email address when generating personal data export file.' ) );
326 }
327
328 // Create the exports folder if needed.
329 $exports_dir = wp_privacy_exports_dir();
330 $exports_url = wp_privacy_exports_url();
331
332 if ( ! wp_mkdir_p( $exports_dir ) ) {
333 wp_send_json_error( __( 'Unable to create personal data export folder.' ) );
334 }
335
336 // Protect export folder from browsing.
337 $index_pathname = $exports_dir . 'index.php';
338 if ( ! file_exists( $index_pathname ) ) {
339 $file = fopen( $index_pathname, 'w' );
340 if ( false === $file ) {
341 wp_send_json_error( __( 'Unable to protect personal data export folder from browsing.' ) );
342 }
343 fwrite( $file, "<?php\n// Silence is golden.\n" );
344 fclose( $file );
345 }
346
347 $obscura = wp_generate_password( 32, false, false );
348 $file_basename = 'wp-personal-data-file-' . $obscura;
349 $html_report_filename = wp_unique_filename( $exports_dir, $file_basename . '.html' );
350 $html_report_pathname = wp_normalize_path( $exports_dir . $html_report_filename );
351 $json_report_filename = $file_basename . '.json';
352 $json_report_pathname = wp_normalize_path( $exports_dir . $json_report_filename );
353
354 /*
355 * Gather general data needed.
356 */
357
358 // Title.
359 $title = sprintf(
360 /* translators: %s: User's email address. */
361 __( 'Personal Data Export for %s' ),
362 $email_address
363 );
364
365 // First, build an "About" group on the fly for this report.
366 $about_group = array(
367 /* translators: Header for the About section in a personal data export. */
368 'group_label' => _x( 'About', 'personal data group label' ),
369 /* translators: Description for the About section in a personal data export. */
370 'group_description' => _x( 'Overview of export report.', 'personal data group description' ),
371 'items' => array(
372 'about-1' => array(
373 array(
374 'name' => _x( 'Report generated for', 'email address' ),
375 'value' => $email_address,
376 ),
377 array(
378 'name' => _x( 'For site', 'website name' ),
379 'value' => get_bloginfo( 'name' ),
380 ),
381 array(
382 'name' => _x( 'At URL', 'website URL' ),
383 'value' => get_bloginfo( 'url' ),
384 ),
385 array(
386 'name' => _x( 'On', 'date/time' ),
387 'value' => current_time( 'mysql' ),
388 ),
389 ),
390 ),
391 );
392
393 // And now, all the Groups.
394 $groups = get_post_meta( $request_id, '_export_data_grouped', true );
395 if ( is_array( $groups ) ) {
396 // Merge in the special "About" group.
397 $groups = array_merge( array( 'about' => $about_group ), $groups );
398 $groups_count = count( $groups );
399 } else {
400 if ( false !== $groups ) {
401 _doing_it_wrong(
402 __FUNCTION__,
403 /* translators: %s: Post meta key. */
404 sprintf( __( 'The %s post meta must be an array.' ), '<code>_export_data_grouped</code>' ),
405 '5.8.0'
406 );
407 }
408
409 $groups = null;
410 $groups_count = 0;
411 }
412
413 // Convert the groups to JSON format.
414 $groups_json = wp_json_encode( $groups );
415
416 if ( false === $groups_json ) {
417 $error_message = sprintf(
418 /* translators: %s: Error message. */
419 __( 'Unable to encode the personal data for export. Error: %s' ),
420 json_last_error_msg()
421 );
422
423 wp_send_json_error( $error_message );
424 }
425
426 /*
427 * Handle the JSON export.
428 */
429 $file = fopen( $json_report_pathname, 'w' );
430
431 if ( false === $file ) {
432 wp_send_json_error( __( 'Unable to open personal data export file (JSON report) for writing.' ) );
433 }
434
435 fwrite( $file, '{' );
436 fwrite( $file, '"' . $title . '":' );
437 fwrite( $file, $groups_json );
438 fwrite( $file, '}' );
439 fclose( $file );
440
441 /*
442 * Handle the HTML export.
443 */
444 $file = fopen( $html_report_pathname, 'w' );
445
446 if ( false === $file ) {
447 wp_send_json_error( __( 'Unable to open personal data export (HTML report) for writing.' ) );
448 }
449
450 fwrite( $file, "<!DOCTYPE html>\n" );
451 fwrite( $file, "<html>\n" );
452 fwrite( $file, "<head>\n" );
453 fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" );
454 fwrite( $file, "<style type='text/css'>" );
455 fwrite( $file, 'body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }' );
456 fwrite( $file, 'table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }' );
457 fwrite( $file, 'th { padding: 5px; text-align: left; width: 20%; }' );
458 fwrite( $file, 'td { padding: 5px; }' );
459 fwrite( $file, 'tr:nth-child(odd) { background-color: #fafafa; }' );
460 fwrite( $file, '.return-to-top { text-align: right; }' );
461 fwrite( $file, '</style>' );
462 fwrite( $file, '<title>' );
463 fwrite( $file, esc_html( $title ) );
464 fwrite( $file, '</title>' );
465 fwrite( $file, "</head>\n" );
466 fwrite( $file, "<body>\n" );
467 fwrite( $file, '<h1 id="top">' . esc_html__( 'Personal Data Export' ) . '</h1>' );
468
469 // Create TOC.
470 if ( $groups_count > 1 ) {
471 fwrite( $file, '<div id="table_of_contents">' );
472 fwrite( $file, '<h2>' . esc_html__( 'Table of Contents' ) . '</h2>' );
473 fwrite( $file, '<ul>' );
474 foreach ( (array) $groups as $group_id => $group_data ) {
475 $group_label = esc_html( $group_data['group_label'] );
476 $group_id_attr = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id );
477 $group_items_count = count( (array) $group_data['items'] );
478 if ( $group_items_count > 1 ) {
479 $group_label .= sprintf( ' <span class="count">(%d)</span>', $group_items_count );
480 }
481 fwrite( $file, '<li>' );
482 fwrite( $file, '<a href="#' . esc_attr( $group_id_attr ) . '">' . $group_label . '</a>' );
483 fwrite( $file, '</li>' );
484 }
485 fwrite( $file, '</ul>' );
486 fwrite( $file, '</div>' );
487 }
488
489 // Now, iterate over every group in $groups and have the formatter render it in HTML.
490 foreach ( (array) $groups as $group_id => $group_data ) {
491 fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id, $groups_count ) );
492 }
493
494 fwrite( $file, "</body>\n" );
495 fwrite( $file, "</html>\n" );
496 fclose( $file );
497
498 /*
499 * Now, generate the ZIP.
500 *
501 * If an archive has already been generated, then remove it and reuse the filename,
502 * to avoid breaking any URLs that may have been previously sent via email.
503 */
504 $error = false;
505
506 // This meta value is used from version 5.5.
507 $archive_filename = get_post_meta( $request_id, '_export_file_name', true );
508
509 // This one stored an absolute path and is used for backward compatibility.
510 $archive_pathname = get_post_meta( $request_id, '_export_file_path', true );
511
512 // If a filename meta exists, use it.
513 if ( ! empty( $archive_filename ) ) {
514 $archive_pathname = $exports_dir . $archive_filename;
515 } elseif ( ! empty( $archive_pathname ) ) {
516 // If a full path meta exists, use it and create the new meta value.
517 $archive_filename = basename( $archive_pathname );
518
519 update_post_meta( $request_id, '_export_file_name', $archive_filename );
520
521 // Remove the back-compat meta values.
522 delete_post_meta( $request_id, '_export_file_url' );
523 delete_post_meta( $request_id, '_export_file_path' );
524 } else {
525 // If there's no filename or full path stored, create a new file.
526 $archive_filename = $file_basename . '.zip';
527 $archive_pathname = $exports_dir . $archive_filename;
528
529 update_post_meta( $request_id, '_export_file_name', $archive_filename );
530 }
531
532 $archive_url = $exports_url . $archive_filename;
533
534 if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) {
535 wp_delete_file( $archive_pathname );
536 }
537
538 $zip = new ZipArchive();
539 if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) {
540 if ( ! $zip->addFile( $json_report_pathname, 'export.json' ) ) {
541 $error = __( 'Unable to archive the personal data export file (JSON format).' );
542 }
543
544 if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) {
545 $error = __( 'Unable to archive the personal data export file (HTML format).' );
546 }
547
548 $zip->close();
549
550 if ( ! $error ) {
551 /**
552 * Fires right after all personal data has been written to the export file.
553 *
554 * @since 4.9.6
555 * @since 5.4.0 Added the `$json_report_pathname` parameter.
556 *
557 * @param string $archive_pathname The full path to the export file on the filesystem.
558 * @param string $archive_url The URL of the archive file.
559 * @param string $html_report_pathname The full path to the HTML personal data report on the filesystem.
560 * @param int $request_id The export request ID.
561 * @param string $json_report_pathname The full path to the JSON personal data report on the filesystem.
562 */
563 do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id, $json_report_pathname );
564 }
565 } else {
566 $error = __( 'Unable to open personal data export file (archive) for writing.' );
567 }
568
569 // Remove the JSON file.
570 unlink( $json_report_pathname );
571
572 // Remove the HTML file.
573 unlink( $html_report_pathname );
574
575 if ( $error ) {
576 wp_send_json_error( $error );
577 }
578}
579
580/**
581 * Send an email to the user with a link to the personal data export file
582 *
583 * @since 4.9.6
584 *
585 * @param int $request_id The request ID for this personal data export.
586 * @return true|WP_Error True on success or `WP_Error` on failure.
587 */
588function wp_privacy_send_personal_data_export_email( $request_id ) {
589 // Get the request.
590 $request = wp_get_user_request( $request_id );
591
592 if ( ! $request || 'export_personal_data' !== $request->action_name ) {
593 return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) );
594 }
595
596 // Localize message content for user; fallback to site default for visitors.
597 if ( ! empty( $request->user_id ) ) {
598 $switched_locale = switch_to_user_locale( $request->user_id );
599 } else {
600 $switched_locale = switch_to_locale( get_locale() );
601 }
602
603 /** This filter is documented in wp-includes/functions.php */
604 $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );
605 $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration );
606
607 $exports_url = wp_privacy_exports_url();
608 $export_file_name = get_post_meta( $request_id, '_export_file_name', true );
609 $export_file_url = $exports_url . $export_file_name;
610
611 $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
612 $site_url = home_url();
613
614 /**
615 * Filters the recipient of the personal data export email notification.
616 * Should be used with great caution to avoid sending the data export link to the wrong email.
617 *
618 * @since 5.3.0
619 *
620 * @param string $request_email The email address of the notification recipient.
621 * @param WP_User_Request $request The request that is initiating the notification.
622 */
623 $request_email = apply_filters( 'wp_privacy_personal_data_email_to', $request->email, $request );
624
625 $email_data = array(
626 'request' => $request,
627 'expiration' => $expiration,
628 'expiration_date' => $expiration_date,
629 'message_recipient' => $request_email,
630 'export_file_url' => $export_file_url,
631 'sitename' => $site_name,
632 'siteurl' => $site_url,
633 );
634
635 /* translators: Personal data export notification email subject. %s: Site title. */
636 $subject = sprintf( __( '[%s] Personal Data Export' ), $site_name );
637
638 /**
639 * Filters the subject of the email sent when an export request is completed.
640 *
641 * @since 5.3.0
642 *
643 * @param string $subject The email subject.
644 * @param string $sitename The name of the site.
645 * @param array $email_data {
646 * Data relating to the account action email.
647 *
648 * @type WP_User_Request $request User request object.
649 * @type int $expiration The time in seconds until the export file expires.
650 * @type string $expiration_date The localized date and time when the export file expires.
651 * @type string $message_recipient The address that the email will be sent to. Defaults
652 * to the value of `$request->email`, but can be changed
653 * by the `wp_privacy_personal_data_email_to` filter.
654 * @type string $export_file_url The export file URL.
655 * @type string $sitename The site name sending the mail.
656 * @type string $siteurl The site URL sending the mail.
657 * }
658 */
659 $subject = apply_filters( 'wp_privacy_personal_data_email_subject', $subject, $site_name, $email_data );
660
661 /* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */
662 $email_text = __(
663 'Howdy,
664
665Your request for an export of personal data has been completed. You may
666download your personal data by clicking on the link below. For privacy
667and security, we will automatically delete the file on ###EXPIRATION###,
668so please download it before then.
669
670###LINK###
671
672Regards,
673All at ###SITENAME###
674###SITEURL###'
675 );
676
677 /**
678 * Filters the text of the email sent with a personal data export file.
679 *
680 * The following strings have a special meaning and will get replaced dynamically:
681 *
682 * - `###EXPIRATION###` The date when the URL will be automatically deleted.
683 * - `###LINK###` URL of the personal data export file for the user.
684 * - `###SITENAME###` The name of the site.
685 * - `###SITEURL###` The URL to the site.
686 *
687 * @since 4.9.6
688 * @since 5.3.0 Introduced the `$email_data` array.
689 *
690 * @param string $email_text Text in the email.
691 * @param int $request_id The request ID for this personal data export.
692 * @param array $email_data {
693 * Data relating to the account action email.
694 *
695 * @type WP_User_Request $request User request object.
696 * @type int $expiration The time in seconds until the export file expires.
697 * @type string $expiration_date The localized date and time when the export file expires.
698 * @type string $message_recipient The address that the email will be sent to. Defaults
699 * to the value of `$request->email`, but can be changed
700 * by the `wp_privacy_personal_data_email_to` filter.
701 * @type string $export_file_url The export file URL.
702 * @type string $sitename The site name sending the mail.
703 * @type string $siteurl The site URL sending the mail.
704 */
705 $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id, $email_data );
706
707 $content = str_replace( '###EXPIRATION###', $expiration_date, $content );
708 $content = str_replace( '###LINK###', sanitize_url( $export_file_url ), $content );
709 $content = str_replace( '###EMAIL###', $request_email, $content );
710 $content = str_replace( '###SITENAME###', $site_name, $content );
711 $content = str_replace( '###SITEURL###', sanitize_url( $site_url ), $content );
712
713 $headers = '';
714
715 /**
716 * Filters the headers of the email sent with a personal data export file.
717 *
718 * @since 5.4.0
719 *
720 * @param string|array $headers The email headers.
721 * @param string $subject The email subject.
722 * @param string $content The email content.
723 * @param int $request_id The request ID.
724 * @param array $email_data {
725 * Data relating to the account action email.
726 *
727 * @type WP_User_Request $request User request object.
728 * @type int $expiration The time in seconds until the export file expires.
729 * @type string $expiration_date The localized date and time when the export file expires.
730 * @type string $message_recipient The address that the email will be sent to. Defaults
731 * to the value of `$request->email`, but can be changed
732 * by the `wp_privacy_personal_data_email_to` filter.
733 * @type string $export_file_url The export file URL.
734 * @type string $sitename The site name sending the mail.
735 * @type string $siteurl The site URL sending the mail.
736 * }
737 */
738 $headers = apply_filters( 'wp_privacy_personal_data_email_headers', $headers, $subject, $content, $request_id, $email_data );
739
740 $mail_success = wp_mail( $request_email, $subject, $content, $headers );
741
742 if ( $switched_locale ) {
743 restore_previous_locale();
744 }
745
746 if ( ! $mail_success ) {
747 return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) );
748 }
749
750 return true;
751}
752
753/**
754 * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file.
755 *
756 * @since 4.9.6
757 *
758 * @see 'wp_privacy_personal_data_export_page'
759 *
760 * @param array $response The response from the personal data exporter for the given page.
761 * @param int $exporter_index The index of the personal data exporter. Begins at 1.
762 * @param string $email_address The email address of the user whose personal data this is.
763 * @param int $page The page of personal data for this exporter. Begins at 1.
764 * @param int $request_id The request ID for this personal data export.
765 * @param bool $send_as_email Whether the final results of the export should be emailed to the user.
766 * @param string $exporter_key The slug (key) of the exporter.
767 * @return array The filtered response.
768 */
769function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) {
770 /* Do some simple checks on the shape of the response from the exporter.
771 * If the exporter response is malformed, don't attempt to consume it - let it
772 * pass through to generate a warning to the user by default Ajax processing.
773 */
774 if ( ! is_array( $response ) ) {
775 return $response;
776 }
777
778 if ( ! array_key_exists( 'done', $response ) ) {
779 return $response;
780 }
781
782 if ( ! array_key_exists( 'data', $response ) ) {
783 return $response;
784 }
785
786 if ( ! is_array( $response['data'] ) ) {
787 return $response;
788 }
789
790 // Get the request.
791 $request = wp_get_user_request( $request_id );
792
793 if ( ! $request || 'export_personal_data' !== $request->action_name ) {
794 wp_send_json_error( __( 'Invalid request ID when merging personal data to export.' ) );
795 }
796
797 $export_data = array();
798
799 // First exporter, first page? Reset the report data accumulation array.
800 if ( 1 === $exporter_index && 1 === $page ) {
801 update_post_meta( $request_id, '_export_data_raw', $export_data );
802 } else {
803 $accumulated_data = get_post_meta( $request_id, '_export_data_raw', true );
804
805 if ( $accumulated_data ) {
806 $export_data = $accumulated_data;
807 }
808 }
809
810 // Now, merge the data from the exporter response into the data we have accumulated already.
811 $export_data = array_merge( $export_data, $response['data'] );
812 update_post_meta( $request_id, '_export_data_raw', $export_data );
813
814 // If we are not yet on the last page of the last exporter, return now.
815 /** This filter is documented in wp-admin/includes/ajax-actions.php */
816 $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );
817 $is_last_exporter = count( $exporters ) === $exporter_index;
818 $exporter_done = $response['done'];
819 if ( ! $is_last_exporter || ! $exporter_done ) {
820 return $response;
821 }
822
823 // Last exporter, last page - let's prepare the export file.
824
825 // First we need to re-organize the raw data hierarchically in groups and items.
826 $groups = array();
827 foreach ( (array) $export_data as $export_datum ) {
828 $group_id = $export_datum['group_id'];
829 $group_label = $export_datum['group_label'];
830
831 $group_description = '';
832 if ( ! empty( $export_datum['group_description'] ) ) {
833 $group_description = $export_datum['group_description'];
834 }
835
836 if ( ! array_key_exists( $group_id, $groups ) ) {
837 $groups[ $group_id ] = array(
838 'group_label' => $group_label,
839 'group_description' => $group_description,
840 'items' => array(),
841 );
842 }
843
844 $item_id = $export_datum['item_id'];
845 if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) {
846 $groups[ $group_id ]['items'][ $item_id ] = array();
847 }
848
849 $old_item_data = $groups[ $group_id ]['items'][ $item_id ];
850 $merged_item_data = array_merge( $export_datum['data'], $old_item_data );
851 $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data;
852 }
853
854 // Then save the grouped data into the request.
855 delete_post_meta( $request_id, '_export_data_raw' );
856 update_post_meta( $request_id, '_export_data_grouped', $groups );
857
858 /**
859 * Generate the export file from the collected, grouped personal data.
860 *
861 * @since 4.9.6
862 *
863 * @param int $request_id The export request ID.
864 */
865 do_action( 'wp_privacy_personal_data_export_file', $request_id );
866
867 // Clear the grouped data now that it is no longer needed.
868 delete_post_meta( $request_id, '_export_data_grouped' );
869
870 // If the destination is email, send it now.
871 if ( $send_as_email ) {
872 $mail_success = wp_privacy_send_personal_data_export_email( $request_id );
873 if ( is_wp_error( $mail_success ) ) {
874 wp_send_json_error( $mail_success->get_error_message() );
875 }
876
877 // Update the request to completed state when the export email is sent.
878 _wp_privacy_completed_request( $request_id );
879 } else {
880 // Modify the response to include the URL of the export file so the browser can fetch it.
881 $exports_url = wp_privacy_exports_url();
882 $export_file_name = get_post_meta( $request_id, '_export_file_name', true );
883 $export_file_url = $exports_url . $export_file_name;
884
885 if ( ! empty( $export_file_url ) ) {
886 $response['url'] = $export_file_url;
887 }
888 }
889
890 return $response;
891}
892
893/**
894 * Mark erasure requests as completed after processing is finished.
895 *
896 * This intercepts the Ajax responses to personal data eraser page requests, and
897 * monitors the status of a request. Once all of the processing has finished, the
898 * request is marked as completed.
899 *
900 * @since 4.9.6
901 *
902 * @see 'wp_privacy_personal_data_erasure_page'
903 *
904 * @param array $response The response from the personal data eraser for
905 * the given page.
906 * @param int $eraser_index The index of the personal data eraser. Begins
907 * at 1.
908 * @param string $email_address The email address of the user whose personal
909 * data this is.
910 * @param int $page The page of personal data for this eraser.
911 * Begins at 1.
912 * @param int $request_id The request ID for this personal data erasure.
913 * @return array The filtered response.
914 */
915function wp_privacy_process_personal_data_erasure_page( $response, $eraser_index, $email_address, $page, $request_id ) {
916 /*
917 * If the eraser response is malformed, don't attempt to consume it; let it
918 * pass through, so that the default Ajax processing will generate a warning
919 * to the user.
920 */
921 if ( ! is_array( $response ) ) {
922 return $response;
923 }
924
925 if ( ! array_key_exists( 'done', $response ) ) {
926 return $response;
927 }
928
929 if ( ! array_key_exists( 'items_removed', $response ) ) {
930 return $response;
931 }
932
933 if ( ! array_key_exists( 'items_retained', $response ) ) {
934 return $response;
935 }
936
937 if ( ! array_key_exists( 'messages', $response ) ) {
938 return $response;
939 }
940
941 // Get the request.
942 $request = wp_get_user_request( $request_id );
943
944 if ( ! $request || 'remove_personal_data' !== $request->action_name ) {
945 wp_send_json_error( __( 'Invalid request ID when processing personal data to erase.' ) );
946 }
947
948 /** This filter is documented in wp-admin/includes/ajax-actions.php */
949 $erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() );
950 $is_last_eraser = count( $erasers ) === $eraser_index;
951 $eraser_done = $response['done'];
952
953 if ( ! $is_last_eraser || ! $eraser_done ) {
954 return $response;
955 }
956
957 _wp_privacy_completed_request( $request_id );
958
959 /**
960 * Fires immediately after a personal data erasure request has been marked completed.
961 *
962 * @since 4.9.6
963 *
964 * @param int $request_id The privacy request post ID associated with this request.
965 */
966 do_action( 'wp_privacy_personal_data_erased', $request_id );
967
968 return $response;
969}
970
Ui Ux Design – Teachers Night Out

Get in Touch

© 2024 Teachers Night Out. All Rights Reserved.