| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 */ 18 function _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 */ 46 function _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 */ 73 function _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 */ 195 function _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 /* 208 * The local-time column must be used here, not post_modified_gmt: 209 * WP_Date_Query resolves relative date strings like "N seconds ago" 210 * in the site's timezone, so comparing against the GMT column would 211 * shift the expiry window by the site's UTC offset. 212 */ 213 'column' => 'post_modified', 214 'before' => $expires . ' seconds ago', 215 ), 216 ), 217 ) 218 ); 219 220 $request_ids = $requests_query->posts; 221 222 foreach ( $request_ids as $request_id ) { 223 wp_update_post( 224 array( 225 'ID' => $request_id, 226 'post_status' => 'request-failed', 227 'post_password' => '', 228 ) 229 ); 230 } 231 } 232 233 /** 234 * Generate a single group for the personal data export report. 235 * 236 * @since 4.9.6 237 * @since 5.4.0 Added the `$group_id` and `$groups_count` parameters. 238 * 239 * @param array $group_data { 240 * The group data to render. 241 * 242 * @type string $group_label The user-facing heading for the group, e.g. 'Comments'. 243 * @type array $items { 244 * An array of group items. 245 * 246 * @type array $group_item_data { 247 * An array of name-value pairs for the item. 248 * 249 * @type string $name The user-facing name of an item name-value pair, e.g. 'IP Address'. 250 * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'. 251 * } 252 * } 253 * } 254 * @param string $group_id The group identifier. 255 * @param int $groups_count The number of all groups 256 * @return string The HTML for this group and its items. 257 */ 258 function wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id = '', $groups_count = 1 ) { 259 $group_id_attr = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id ); 260 261 $group_html = '<h2 id="' . esc_attr( $group_id_attr ) . '">'; 262 $group_html .= esc_html( $group_data['group_label'] ); 263 264 $items_count = count( (array) $group_data['items'] ); 265 if ( $items_count > 1 ) { 266 $group_html .= sprintf( ' <span class="count">(%d)</span>', $items_count ); 267 } 268 269 $group_html .= '</h2>'; 270 271 if ( ! empty( $group_data['group_description'] ) ) { 272 $group_html .= '<p>' . esc_html( $group_data['group_description'] ) . '</p>'; 273 } 274 275 $group_html .= '<div>'; 276 277 foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) { 278 $group_html .= '<table>'; 279 $group_html .= '<tbody>'; 280 281 foreach ( (array) $group_item_data as $group_item_datum ) { 282 $value = $group_item_datum['value']; 283 // If it looks like a link, make it a link. 284 if ( ! str_contains( $value, ' ' ) && ( str_starts_with( $value, 'http://' ) || str_starts_with( $value, 'https://' ) ) ) { 285 $value = '<a href="' . esc_url( $value ) . '">' . esc_html( $value ) . '</a>'; 286 } 287 288 $group_html .= '<tr>'; 289 $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>'; 290 $group_html .= '<td>' . wp_kses( $value, 'personal_data_export' ) . '</td>'; 291 $group_html .= '</tr>'; 292 } 293 294 $group_html .= '</tbody>'; 295 $group_html .= '</table>'; 296 } 297 298 if ( $groups_count > 1 ) { 299 $group_html .= '<div class="return-to-top">'; 300 $group_html .= '<a href="#top"><span aria-hidden="true">↑ </span> ' . esc_html__( 'Go to top' ) . '</a>'; 301 $group_html .= '</div>'; 302 } 303 304 $group_html .= '</div>'; 305 306 return $group_html; 307 } 308 309 /** 310 * Generate the personal data export file. 311 * 312 * @since 4.9.6 313 * 314 * @param int $request_id The export request ID. 315 */ 316 function wp_privacy_generate_personal_data_export_file( $request_id ) { 317 if ( ! class_exists( 'ZipArchive' ) ) { 318 wp_send_json_error( __( 'Unable to generate personal data export file. ZipArchive not available.' ) ); 319 } 320 321 // Get the request. 322 $request = wp_get_user_request( $request_id ); 323 324 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 325 wp_send_json_error( __( 'Invalid request ID when generating personal data export file.' ) ); 326 } 327 328 $email_address = $request->email; 329 330 if ( ! is_email( $email_address ) ) { 331 wp_send_json_error( __( 'Invalid email address when generating personal data export file.' ) ); 332 } 333 334 // Create the exports folder if needed. 335 $exports_dir = wp_privacy_exports_dir(); 336 $exports_url = wp_privacy_exports_url(); 337 338 if ( ! wp_mkdir_p( $exports_dir ) ) { 339 wp_send_json_error( __( 'Unable to create personal data export folder.' ) ); 340 } 341 342 // Protect export folder from browsing. 343 $index_pathname = $exports_dir . 'index.php'; 344 if ( ! file_exists( $index_pathname ) ) { 345 $file = fopen( $index_pathname, 'w' ); 346 if ( false === $file ) { 347 wp_send_json_error( __( 'Unable to protect personal data export folder from browsing.' ) ); 348 } 349 fwrite( $file, "<?php\n// Silence is golden.\n" ); 350 fclose( $file ); 351 } 352 353 $obscura = wp_generate_password( 32, false, false ); 354 $file_basename = 'wp-personal-data-file-' . $obscura; 355 $html_report_filename = wp_unique_filename( $exports_dir, $file_basename . '.html' ); 356 $html_report_pathname = wp_normalize_path( $exports_dir . $html_report_filename ); 357 $json_report_filename = $file_basename . '.json'; 358 $json_report_pathname = wp_normalize_path( $exports_dir . $json_report_filename ); 359 360 /* 361 * Gather general data needed. 362 */ 363 364 // Title. 365 $title = sprintf( 366 /* translators: %s: User's email address. */ 367 __( 'Personal Data Export for %s' ), 368 $email_address 369 ); 370 371 // First, build an "About" group on the fly for this report. 372 $about_group = array( 373 /* translators: Header for the About section in a personal data export. */ 374 'group_label' => _x( 'About', 'personal data group label' ), 375 /* translators: Description for the About section in a personal data export. */ 376 'group_description' => _x( 'Overview of export report.', 'personal data group description' ), 377 'items' => array( 378 'about-1' => array( 379 array( 380 'name' => _x( 'Report generated for', 'email address' ), 381 'value' => $email_address, 382 ), 383 array( 384 'name' => _x( 'For site', 'website name' ), 385 'value' => get_bloginfo( 'name' ), 386 ), 387 array( 388 'name' => _x( 'At URL', 'website URL' ), 389 'value' => get_bloginfo( 'url' ), 390 ), 391 array( 392 'name' => _x( 'On', 'date/time' ), 393 'value' => current_time( 'mysql' ), 394 ), 395 ), 396 ), 397 ); 398 399 // And now, all the Groups. 400 $groups = get_post_meta( $request_id, '_export_data_grouped', true ); 401 if ( is_array( $groups ) ) { 402 // Merge in the special "About" group. 403 $groups = array_merge( array( 'about' => $about_group ), $groups ); 404 $groups_count = count( $groups ); 405 } else { 406 if ( false !== $groups ) { 407 _doing_it_wrong( 408 __FUNCTION__, 409 /* translators: %s: Post meta key. */ 410 sprintf( __( 'The %s post meta must be an array.' ), '<code>_export_data_grouped</code>' ), 411 '5.8.0' 412 ); 413 } 414 415 $groups = null; 416 $groups_count = 0; 417 } 418 419 // Convert the groups to JSON format. 420 $groups_json = wp_json_encode( $groups ); 421 422 if ( false === $groups_json ) { 423 $error_message = sprintf( 424 /* translators: %s: Error message. */ 425 __( 'Unable to encode the personal data for export. Error: %s' ), 426 json_last_error_msg() 427 ); 428 429 wp_send_json_error( $error_message ); 430 } 431 432 /* 433 * Handle the JSON export. 434 */ 435 $file = fopen( $json_report_pathname, 'w' ); 436 437 if ( false === $file ) { 438 wp_send_json_error( __( 'Unable to open personal data export file (JSON report) for writing.' ) ); 439 } 440 441 fwrite( $file, '{' ); 442 fwrite( $file, '"' . $title . '":' ); 443 fwrite( $file, $groups_json ); 444 fwrite( $file, '}' ); 445 fclose( $file ); 446 447 /* 448 * Handle the HTML export. 449 */ 450 $file = fopen( $html_report_pathname, 'w' ); 451 452 if ( false === $file ) { 453 wp_send_json_error( __( 'Unable to open personal data export (HTML report) for writing.' ) ); 454 } 455 456 fwrite( $file, "<!DOCTYPE html>\n" ); 457 fwrite( $file, "<html>\n" ); 458 fwrite( $file, "<head>\n" ); 459 fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" ); 460 fwrite( $file, '<style>' ); 461 fwrite( $file, 'body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }' ); 462 fwrite( $file, 'table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }' ); 463 fwrite( $file, 'th { padding: 5px; text-align: left; width: 20%; }' ); 464 fwrite( $file, 'td { padding: 5px; }' ); 465 fwrite( $file, 'tr:nth-child(odd) { background-color: #fafafa; }' ); 466 fwrite( $file, '.return-to-top { text-align: right; }' ); 467 fwrite( $file, '</style>' ); 468 fwrite( $file, '<title>' ); 469 fwrite( $file, esc_html( $title ) ); 470 fwrite( $file, '</title>' ); 471 fwrite( $file, "</head>\n" ); 472 fwrite( $file, "<body>\n" ); 473 fwrite( $file, '<h1 id="top">' . esc_html__( 'Personal Data Export' ) . '</h1>' ); 474 475 // Create TOC. 476 if ( $groups_count > 1 ) { 477 fwrite( $file, '<div id="table_of_contents">' ); 478 fwrite( $file, '<h2>' . esc_html__( 'Table of Contents' ) . '</h2>' ); 479 fwrite( $file, '<ul>' ); 480 foreach ( (array) $groups as $group_id => $group_data ) { 481 $group_label = esc_html( $group_data['group_label'] ); 482 $group_id_attr = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id ); 483 $group_items_count = count( (array) $group_data['items'] ); 484 if ( $group_items_count > 1 ) { 485 $group_label .= sprintf( ' <span class="count">(%d)</span>', $group_items_count ); 486 } 487 fwrite( $file, '<li>' ); 488 fwrite( $file, '<a href="#' . esc_attr( $group_id_attr ) . '">' . $group_label . '</a>' ); 489 fwrite( $file, '</li>' ); 490 } 491 fwrite( $file, '</ul>' ); 492 fwrite( $file, '</div>' ); 493 } 494 495 // Now, iterate over every group in $groups and have the formatter render it in HTML. 496 foreach ( (array) $groups as $group_id => $group_data ) { 497 fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id, $groups_count ) ); 498 } 499 500 fwrite( $file, "</body>\n" ); 501 fwrite( $file, "</html>\n" ); 502 fclose( $file ); 503 504 /* 505 * Now, generate the ZIP. 506 * 507 * If an archive has already been generated, then remove it and reuse the filename, 508 * to avoid breaking any URLs that may have been previously sent via email. 509 */ 510 $error = false; 511 512 // This meta value is used from version 5.5. 513 $archive_filename = get_post_meta( $request_id, '_export_file_name', true ); 514 515 // This one stored an absolute path and is used for backward compatibility. 516 $archive_pathname = get_post_meta( $request_id, '_export_file_path', true ); 517 518 // If a filename meta exists, use it. 519 if ( ! empty( $archive_filename ) ) { 520 $archive_pathname = $exports_dir . $archive_filename; 521 } elseif ( ! empty( $archive_pathname ) ) { 522 // If a full path meta exists, use it and create the new meta value. 523 $archive_filename = basename( $archive_pathname ); 524 525 update_post_meta( $request_id, '_export_file_name', $archive_filename ); 526 527 // Remove the back-compat meta values. 528 delete_post_meta( $request_id, '_export_file_url' ); 529 delete_post_meta( $request_id, '_export_file_path' ); 530 } else { 531 // If there's no filename or full path stored, create a new file. 532 $archive_filename = $file_basename . '.zip'; 533 $archive_pathname = $exports_dir . $archive_filename; 534 535 update_post_meta( $request_id, '_export_file_name', $archive_filename ); 536 } 537 538 $archive_url = $exports_url . $archive_filename; 539 540 if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) { 541 wp_delete_file( $archive_pathname ); 542 } 543 544 $zip = new ZipArchive(); 545 if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) { 546 if ( ! $zip->addFile( $json_report_pathname, 'export.json' ) ) { 547 $error = __( 'Unable to archive the personal data export file (JSON format).' ); 548 } 549 550 if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) { 551 $error = __( 'Unable to archive the personal data export file (HTML format).' ); 552 } 553 554 $zip->close(); 555 556 if ( ! $error ) { 557 /** 558 * Fires right after all personal data has been written to the export file. 559 * 560 * @since 4.9.6 561 * @since 5.4.0 Added the `$json_report_pathname` parameter. 562 * 563 * @param string $archive_pathname The full path to the export file on the filesystem. 564 * @param string $archive_url The URL of the archive file. 565 * @param string $html_report_pathname The full path to the HTML personal data report on the filesystem. 566 * @param int $request_id The export request ID. 567 * @param string $json_report_pathname The full path to the JSON personal data report on the filesystem. 568 */ 569 do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id, $json_report_pathname ); 570 } 571 } else { 572 $error = __( 'Unable to open personal data export file (archive) for writing.' ); 573 } 574 575 // Remove the JSON file. 576 unlink( $json_report_pathname ); 577 578 // Remove the HTML file. 579 unlink( $html_report_pathname ); 580 581 if ( $error ) { 582 wp_send_json_error( $error ); 583 } 584 } 585 586 /** 587 * Send an email to the user with a link to the personal data export file 588 * 589 * @since 4.9.6 590 * 591 * @param int $request_id The request ID for this personal data export. 592 * @return true|WP_Error True on success or `WP_Error` on failure. 593 */ 594 function wp_privacy_send_personal_data_export_email( $request_id ) { 595 // Get the request. 596 $request = wp_get_user_request( $request_id ); 597 598 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 599 return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) ); 600 } 601 602 // Localize message content for user; fallback to site default for visitors. 603 if ( ! empty( $request->user_id ) ) { 604 $switched_locale = switch_to_user_locale( $request->user_id ); 605 } else { 606 $switched_locale = switch_to_locale( get_locale() ); 607 } 608 609 /** This filter is documented in wp-includes/functions.php */ 610 $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS ); 611 $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration ); 612 613 $exports_url = wp_privacy_exports_url(); 614 $export_file_name = get_post_meta( $request_id, '_export_file_name', true ); 615 $export_file_url = $exports_url . $export_file_name; 616 617 $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); 618 $site_url = home_url(); 619 620 /** 621 * Filters the recipient of the personal data export email notification. 622 * Should be used with great caution to avoid sending the data export link to the wrong email. 623 * 624 * @since 5.3.0 625 * 626 * @param string $request_email The email address of the notification recipient. 627 * @param WP_User_Request $request The request that is initiating the notification. 628 */ 629 $request_email = apply_filters( 'wp_privacy_personal_data_email_to', $request->email, $request ); 630 631 $email_data = array( 632 'request' => $request, 633 'expiration' => $expiration, 634 'expiration_date' => $expiration_date, 635 'message_recipient' => $request_email, 636 'export_file_url' => $export_file_url, 637 'sitename' => $site_name, 638 'siteurl' => $site_url, 639 ); 640 641 /* translators: Personal data export notification email subject. %s: Site title. */ 642 $subject = sprintf( __( '[%s] Personal Data Export' ), $site_name ); 643 644 /** 645 * Filters the subject of the email sent when an export request is completed. 646 * 647 * @since 5.3.0 648 * 649 * @param string $subject The email subject. 650 * @param string $sitename The name of the site. 651 * @param array $email_data { 652 * Data relating to the account action email. 653 * 654 * @type WP_User_Request $request User request object. 655 * @type int $expiration The time in seconds until the export file expires. 656 * @type string $expiration_date The localized date and time when the export file expires. 657 * @type string $message_recipient The address that the email will be sent to. Defaults 658 * to the value of `$request->email`, but can be changed 659 * by the `wp_privacy_personal_data_email_to` filter. 660 * @type string $export_file_url The export file URL. 661 * @type string $sitename The site name sending the mail. 662 * @type string $siteurl The site URL sending the mail. 663 * } 664 */ 665 $subject = apply_filters( 'wp_privacy_personal_data_email_subject', $subject, $site_name, $email_data ); 666 667 /* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */ 668 $email_text = __( 669 'Howdy, 670 671 Your request for an export of personal data has been completed. You may 672 download your personal data by clicking on the link below. For privacy 673 and security, we will automatically delete the file on ###EXPIRATION###, 674 so please download it before then. 675 676 ###LINK### 677 678 Regards, 679 All at ###SITENAME### 680 ###SITEURL###' 681 ); 682 683 /** 684 * Filters the text of the email sent with a personal data export file. 685 * 686 * The following strings have a special meaning and will get replaced dynamically: 687 * 688 * - `###EXPIRATION###` The date when the URL will be automatically deleted. 689 * - `###LINK###` URL of the personal data export file for the user. 690 * - `###SITENAME###` The name of the site. 691 * - `###SITEURL###` The URL to the site. 692 * 693 * @since 4.9.6 694 * @since 5.3.0 Introduced the `$email_data` array. 695 * 696 * @param string $email_text Text in the email. 697 * @param int $request_id The request ID for this personal data export. 698 * @param array $email_data { 699 * Data relating to the account action email. 700 * 701 * @type WP_User_Request $request User request object. 702 * @type int $expiration The time in seconds until the export file expires. 703 * @type string $expiration_date The localized date and time when the export file expires. 704 * @type string $message_recipient The address that the email will be sent to. Defaults 705 * to the value of `$request->email`, but can be changed 706 * by the `wp_privacy_personal_data_email_to` filter. 707 * @type string $export_file_url The export file URL. 708 * @type string $sitename The site name sending the mail. 709 * @type string $siteurl The site URL sending the mail. 710 */ 711 $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id, $email_data ); 712 713 $content = str_replace( '###EXPIRATION###', $expiration_date, $content ); 714 $content = str_replace( '###LINK###', sanitize_url( $export_file_url ), $content ); 715 $content = str_replace( '###EMAIL###', $request_email, $content ); 716 $content = str_replace( '###SITENAME###', $site_name, $content ); 717 $content = str_replace( '###SITEURL###', sanitize_url( $site_url ), $content ); 718 719 $headers = ''; 720 721 /** 722 * Filters the headers of the email sent with a personal data export file. 723 * 724 * @since 5.4.0 725 * 726 * @param string|array $headers The email headers. 727 * @param string $subject The email subject. 728 * @param string $content The email content. 729 * @param int $request_id The request ID. 730 * @param array $email_data { 731 * Data relating to the account action email. 732 * 733 * @type WP_User_Request $request User request object. 734 * @type int $expiration The time in seconds until the export file expires. 735 * @type string $expiration_date The localized date and time when the export file expires. 736 * @type string $message_recipient The address that the email will be sent to. Defaults 737 * to the value of `$request->email`, but can be changed 738 * by the `wp_privacy_personal_data_email_to` filter. 739 * @type string $export_file_url The export file URL. 740 * @type string $sitename The site name sending the mail. 741 * @type string $siteurl The site URL sending the mail. 742 * } 743 */ 744 $headers = apply_filters( 'wp_privacy_personal_data_email_headers', $headers, $subject, $content, $request_id, $email_data ); 745 746 $mail_success = wp_mail( $request_email, $subject, $content, $headers ); 747 748 if ( $switched_locale ) { 749 restore_previous_locale(); 750 } 751 752 if ( ! $mail_success ) { 753 return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) ); 754 } 755 756 return true; 757 } 758 759 /** 760 * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file. 761 * 762 * @since 4.9.6 763 * 764 * @see 'wp_privacy_personal_data_export_page' 765 * 766 * @param array $response The response from the personal data exporter for the given page. 767 * @param int $exporter_index The index of the personal data exporter. Begins at 1. 768 * @param string $email_address The email address of the user whose personal data this is. 769 * @param int $page The page of personal data for this exporter. Begins at 1. 770 * @param int $request_id The request ID for this personal data export. 771 * @param bool $send_as_email Whether the final results of the export should be emailed to the user. 772 * @param string $exporter_key The slug (key) of the exporter. 773 * @return array The filtered response. 774 */ 775 function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) { 776 /* Do some simple checks on the shape of the response from the exporter. 777 * If the exporter response is malformed, don't attempt to consume it - let it 778 * pass through to generate a warning to the user by default Ajax processing. 779 */ 780 if ( ! is_array( $response ) ) { 781 return $response; 782 } 783 784 if ( ! array_key_exists( 'done', $response ) ) { 785 return $response; 786 } 787 788 if ( ! array_key_exists( 'data', $response ) ) { 789 return $response; 790 } 791 792 if ( ! is_array( $response['data'] ) ) { 793 return $response; 794 } 795 796 // Get the request. 797 $request = wp_get_user_request( $request_id ); 798 799 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 800 wp_send_json_error( __( 'Invalid request ID when merging personal data to export.' ) ); 801 } 802 803 $export_data = array(); 804 805 // First exporter, first page? Reset the report data accumulation array. 806 if ( 1 === $exporter_index && 1 === $page ) { 807 update_post_meta( $request_id, '_export_data_raw', $export_data ); 808 } else { 809 $accumulated_data = get_post_meta( $request_id, '_export_data_raw', true ); 810 811 if ( $accumulated_data ) { 812 $export_data = $accumulated_data; 813 } 814 } 815 816 // Now, merge the data from the exporter response into the data we have accumulated already. 817 $export_data = array_merge( $export_data, $response['data'] ); 818 update_post_meta( $request_id, '_export_data_raw', $export_data ); 819 820 // If we are not yet on the last page of the last exporter, return now. 821 /** This filter is documented in wp-admin/includes/ajax-actions.php */ 822 $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); 823 $is_last_exporter = count( $exporters ) === $exporter_index; 824 $exporter_done = $response['done']; 825 if ( ! $is_last_exporter || ! $exporter_done ) { 826 return $response; 827 } 828 829 // Last exporter, last page - let's prepare the export file. 830 831 // First we need to re-organize the raw data hierarchically in groups and items. 832 $groups = array(); 833 foreach ( (array) $export_data as $export_datum ) { 834 $group_id = $export_datum['group_id']; 835 $group_label = $export_datum['group_label']; 836 837 $group_description = ''; 838 if ( ! empty( $export_datum['group_description'] ) ) { 839 $group_description = $export_datum['group_description']; 840 } 841 842 if ( ! array_key_exists( $group_id, $groups ) ) { 843 $groups[ $group_id ] = array( 844 'group_label' => $group_label, 845 'group_description' => $group_description, 846 'items' => array(), 847 ); 848 } 849 850 $item_id = $export_datum['item_id']; 851 if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) { 852 $groups[ $group_id ]['items'][ $item_id ] = array(); 853 } 854 855 $old_item_data = $groups[ $group_id ]['items'][ $item_id ]; 856 $merged_item_data = array_merge( $export_datum['data'], $old_item_data ); 857 $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data; 858 } 859 860 // Then save the grouped data into the request. 861 delete_post_meta( $request_id, '_export_data_raw' ); 862 update_post_meta( $request_id, '_export_data_grouped', $groups ); 863 864 /** 865 * Generate the export file from the collected, grouped personal data. 866 * 867 * @since 4.9.6 868 * 869 * @param int $request_id The export request ID. 870 */ 871 do_action( 'wp_privacy_personal_data_export_file', $request_id ); 872 873 // Clear the grouped data now that it is no longer needed. 874 delete_post_meta( $request_id, '_export_data_grouped' ); 875 876 // If the destination is email, send it now. 877 if ( $send_as_email ) { 878 $mail_success = wp_privacy_send_personal_data_export_email( $request_id ); 879 if ( is_wp_error( $mail_success ) ) { 880 wp_send_json_error( $mail_success->get_error_message() ); 881 } 882 883 // Update the request to completed state when the export email is sent. 884 _wp_privacy_completed_request( $request_id ); 885 } else { 886 // Modify the response to include the URL of the export file so the browser can fetch it. 887 $exports_url = wp_privacy_exports_url(); 888 $export_file_name = get_post_meta( $request_id, '_export_file_name', true ); 889 $export_file_url = $exports_url . $export_file_name; 890 891 if ( ! empty( $export_file_url ) ) { 892 $response['url'] = $export_file_url; 893 } 894 } 895 896 return $response; 897 } 898 899 /** 900 * Mark erasure requests as completed after processing is finished. 901 * 902 * This intercepts the Ajax responses to personal data eraser page requests, and 903 * monitors the status of a request. Once all of the processing has finished, the 904 * request is marked as completed. 905 * 906 * @since 4.9.6 907 * 908 * @see 'wp_privacy_personal_data_erasure_page' 909 * 910 * @param array $response The response from the personal data eraser for 911 * the given page. 912 * @param int $eraser_index The index of the personal data eraser. Begins 913 * at 1. 914 * @param string $email_address The email address of the user whose personal 915 * data this is. 916 * @param int $page The page of personal data for this eraser. 917 * Begins at 1. 918 * @param int $request_id The request ID for this personal data erasure. 919 * @return array The filtered response. 920 */ 921 function wp_privacy_process_personal_data_erasure_page( $response, $eraser_index, $email_address, $page, $request_id ) { 922 /* 923 * If the eraser response is malformed, don't attempt to consume it; let it 924 * pass through, so that the default Ajax processing will generate a warning 925 * to the user. 926 */ 927 if ( ! is_array( $response ) ) { 928 return $response; 929 } 930 931 if ( ! array_key_exists( 'done', $response ) ) { 932 return $response; 933 } 934 935 if ( ! array_key_exists( 'items_removed', $response ) ) { 936 return $response; 937 } 938 939 if ( ! array_key_exists( 'items_retained', $response ) ) { 940 return $response; 941 } 942 943 if ( ! array_key_exists( 'messages', $response ) ) { 944 return $response; 945 } 946 947 // Get the request. 948 $request = wp_get_user_request( $request_id ); 949 950 if ( ! $request || 'remove_personal_data' !== $request->action_name ) { 951 wp_send_json_error( __( 'Invalid request ID when processing personal data to erase.' ) ); 952 } 953 954 /** This filter is documented in wp-admin/includes/ajax-actions.php */ 955 $erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() ); 956 $is_last_eraser = count( $erasers ) === $eraser_index; 957 $eraser_done = $response['done']; 958 959 if ( ! $is_last_eraser || ! $eraser_done ) { 960 return $response; 961 } 962 963 _wp_privacy_completed_request( $request_id ); 964 965 /** 966 * Fires immediately after a personal data erasure request has been marked completed. 967 * 968 * @since 4.9.6 969 * 970 * @param int $request_id The privacy request post ID associated with this request. 971 */ 972 do_action( 'wp_privacy_personal_data_erased', $request_id ); 973 974 return $response; 975 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Jul 11 08:20:14 2026 | Cross-referenced by PHPXref |