Changeset 45519 for trunk/src/wp-admin/includes/privacy-tools.php
- Timestamp:
- 06/10/2019 11:53:32 PM (5 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-admin/includes/privacy-tools.php
r45448 r45519 213 213 214 214 /** 215 * Generate a single group for the personal data export report. 216 * 217 * @since 4.9.6 218 * 219 * @param array $group_data { 220 * The group data to render. 221 * 222 * @type string $group_label The user-facing heading for the group, e.g. 'Comments'. 223 * @type array $items { 224 * An array of group items. 225 * 226 * @type array $group_item_data { 227 * An array of name-value pairs for the item. 228 * 229 * @type string $name The user-facing name of an item name-value pair, e.g. 'IP Address'. 230 * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'. 231 * } 232 * } 233 * } 234 * @return string The HTML for this group and its items. 235 */ 236 function wp_privacy_generate_personal_data_export_group_html( $group_data ) { 237 $group_html = '<h2>' . esc_html( $group_data['group_label'] ) . '</h2>'; 238 $group_html .= '<div>'; 239 240 foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) { 241 $group_html .= '<table>'; 242 $group_html .= '<tbody>'; 243 244 foreach ( (array) $group_item_data as $group_item_datum ) { 245 $value = $group_item_datum['value']; 246 // If it looks like a link, make it a link. 247 if ( false === strpos( $value, ' ' ) && ( 0 === strpos( $value, 'http://' ) || 0 === strpos( $value, 'https://' ) ) ) { 248 $value = '<a href="' . esc_url( $value ) . '">' . esc_html( $value ) . '</a>'; 249 } 250 251 $group_html .= '<tr>'; 252 $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>'; 253 $group_html .= '<td>' . wp_kses( $value, 'personal_data_export' ) . '</td>'; 254 $group_html .= '</tr>'; 255 } 256 257 $group_html .= '</tbody>'; 258 $group_html .= '</table>'; 259 } 260 261 $group_html .= '</div>'; 262 263 return $group_html; 264 } 265 266 /** 267 * Generate the personal data export file. 268 * 269 * @since 4.9.6 270 * 271 * @param int $request_id The export request ID. 272 */ 273 function wp_privacy_generate_personal_data_export_file( $request_id ) { 274 if ( ! class_exists( 'ZipArchive' ) ) { 275 wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) ); 276 } 277 278 // Get the request data. 279 $request = wp_get_user_request_data( $request_id ); 280 281 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 282 wp_send_json_error( __( 'Invalid request ID when generating export file.' ) ); 283 } 284 285 $email_address = $request->email; 286 287 if ( ! is_email( $email_address ) ) { 288 wp_send_json_error( __( 'Invalid email address when generating export file.' ) ); 289 } 290 291 // Create the exports folder if needed. 292 $exports_dir = wp_privacy_exports_dir(); 293 $exports_url = wp_privacy_exports_url(); 294 295 if ( ! wp_mkdir_p( $exports_dir ) ) { 296 wp_send_json_error( __( 'Unable to create export folder.' ) ); 297 } 298 299 // Protect export folder from browsing. 300 $index_pathname = $exports_dir . 'index.html'; 301 if ( ! file_exists( $index_pathname ) ) { 302 $file = fopen( $index_pathname, 'w' ); 303 if ( false === $file ) { 304 wp_send_json_error( __( 'Unable to protect export folder from browsing.' ) ); 305 } 306 fwrite( $file, '<!-- Silence is golden. -->' ); 307 fclose( $file ); 308 } 309 310 $stripped_email = str_replace( '@', '-at-', $email_address ); 311 $stripped_email = sanitize_title( $stripped_email ); // slugify the email address 312 $obscura = wp_generate_password( 32, false, false ); 313 $file_basename = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura; 314 $html_report_filename = $file_basename . '.html'; 315 $html_report_pathname = wp_normalize_path( $exports_dir . $html_report_filename ); 316 $file = fopen( $html_report_pathname, 'w' ); 317 if ( false === $file ) { 318 wp_send_json_error( __( 'Unable to open export file (HTML report) for writing.' ) ); 319 } 320 321 $title = sprintf( 322 /* translators: %s: user's email address */ 323 __( 'Personal Data Export for %s' ), 324 $email_address 325 ); 326 327 // Open HTML. 328 fwrite( $file, "<!DOCTYPE html>\n" ); 329 fwrite( $file, "<html>\n" ); 330 331 // Head. 332 fwrite( $file, "<head>\n" ); 333 fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" ); 334 fwrite( $file, "<style type='text/css'>" ); 335 fwrite( $file, 'body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }' ); 336 fwrite( $file, 'table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }' ); 337 fwrite( $file, 'th { padding: 5px; text-align: left; width: 20%; }' ); 338 fwrite( $file, 'td { padding: 5px; }' ); 339 fwrite( $file, 'tr:nth-child(odd) { background-color: #fafafa; }' ); 340 fwrite( $file, '</style>' ); 341 fwrite( $file, '<title>' ); 342 fwrite( $file, esc_html( $title ) ); 343 fwrite( $file, '</title>' ); 344 fwrite( $file, "</head>\n" ); 345 346 // Body. 347 fwrite( $file, "<body>\n" ); 348 349 // Heading. 350 fwrite( $file, '<h1>' . esc_html__( 'Personal Data Export' ) . '</h1>' ); 351 352 // And now, all the Groups. 353 $groups = get_post_meta( $request_id, '_export_data_grouped', true ); 354 355 // First, build an "About" group on the fly for this report. 356 $about_group = array( 357 /* translators: Header for the About section in a personal data export. */ 358 'group_label' => _x( 'About', 'personal data group label' ), 359 'items' => array( 360 'about-1' => array( 361 array( 362 'name' => _x( 'Report generated for', 'email address' ), 363 'value' => $email_address, 364 ), 365 array( 366 'name' => _x( 'For site', 'website name' ), 367 'value' => get_bloginfo( 'name' ), 368 ), 369 array( 370 'name' => _x( 'At URL', 'website URL' ), 371 'value' => get_bloginfo( 'url' ), 372 ), 373 array( 374 'name' => _x( 'On', 'date/time' ), 375 'value' => current_time( 'mysql' ), 376 ), 377 ), 378 ), 379 ); 380 381 // Merge in the special about group. 382 $groups = array_merge( array( 'about' => $about_group ), $groups ); 383 384 // Now, iterate over every group in $groups and have the formatter render it in HTML. 385 foreach ( (array) $groups as $group_id => $group_data ) { 386 fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data ) ); 387 } 388 389 fwrite( $file, "</body>\n" ); 390 391 // Close HTML. 392 fwrite( $file, "</html>\n" ); 393 fclose( $file ); 394 395 /* 396 * Now, generate the ZIP. 397 * 398 * If an archive has already been generated, then remove it and reuse the 399 * filename, to avoid breaking any URLs that may have been previously sent 400 * via email. 401 */ 402 $error = false; 403 $archive_url = get_post_meta( $request_id, '_export_file_url', true ); 404 $archive_pathname = get_post_meta( $request_id, '_export_file_path', true ); 405 406 if ( empty( $archive_pathname ) || empty( $archive_url ) ) { 407 $archive_filename = $file_basename . '.zip'; 408 $archive_pathname = $exports_dir . $archive_filename; 409 $archive_url = $exports_url . $archive_filename; 410 411 update_post_meta( $request_id, '_export_file_url', $archive_url ); 412 update_post_meta( $request_id, '_export_file_path', wp_normalize_path( $archive_pathname ) ); 413 } 414 415 if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) { 416 wp_delete_file( $archive_pathname ); 417 } 418 419 $zip = new ZipArchive; 420 if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) { 421 if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) { 422 $error = __( 'Unable to add data to export file.' ); 423 } 424 425 $zip->close(); 426 427 if ( ! $error ) { 428 /** 429 * Fires right after all personal data has been written to the export file. 430 * 431 * @since 4.9.6 432 * 433 * @param string $archive_pathname The full path to the export file on the filesystem. 434 * @param string $archive_url The URL of the archive file. 435 * @param string $html_report_pathname The full path to the personal data report on the filesystem. 436 * @param int $request_id The export request ID. 437 */ 438 do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id ); 439 } 440 } else { 441 $error = __( 'Unable to open export file (archive) for writing.' ); 442 } 443 444 // And remove the HTML file. 445 unlink( $html_report_pathname ); 446 447 if ( $error ) { 448 wp_send_json_error( $error ); 449 } 450 } 451 452 /** 453 * Send an email to the user with a link to the personal data export file 454 * 455 * @since 4.9.6 456 * 457 * @param int $request_id The request ID for this personal data export. 458 * @return true|WP_Error True on success or `WP_Error` on failure. 459 */ 460 function wp_privacy_send_personal_data_export_email( $request_id ) { 461 // Get the request data. 462 $request = wp_get_user_request_data( $request_id ); 463 464 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 465 return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) ); 466 } 467 468 // Localize message content for user; fallback to site default for visitors. 469 if ( ! empty( $request->user_id ) ) { 470 $locale = get_user_locale( $request->user_id ); 471 } else { 472 $locale = get_locale(); 473 } 474 475 $switched_locale = switch_to_locale( $locale ); 476 477 /** This filter is documented in wp-includes/functions.php */ 478 $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS ); 479 $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration ); 480 481 /* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */ 482 $email_text = __( 483 'Howdy, 484 485 Your request for an export of personal data has been completed. You may 486 download your personal data by clicking on the link below. For privacy 487 and security, we will automatically delete the file on ###EXPIRATION###, 488 so please download it before then. 489 490 ###LINK### 491 492 Regards, 493 All at ###SITENAME### 494 ###SITEURL###' 495 ); 496 497 /** 498 * Filters the text of the email sent with a personal data export file. 499 * 500 * The following strings have a special meaning and will get replaced dynamically: 501 * ###EXPIRATION### The date when the URL will be automatically deleted. 502 * ###LINK### URL of the personal data export file for the user. 503 * ###SITENAME### The name of the site. 504 * ###SITEURL### The URL to the site. 505 * 506 * @since 4.9.6 507 * 508 * @param string $email_text Text in the email. 509 * @param int $request_id The request ID for this personal data export. 510 */ 511 $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id ); 512 513 $email_address = $request->email; 514 $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); 515 $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); 516 $site_url = home_url(); 517 518 $content = str_replace( '###EXPIRATION###', $expiration_date, $content ); 519 $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content ); 520 $content = str_replace( '###EMAIL###', $email_address, $content ); 521 $content = str_replace( '###SITENAME###', $site_name, $content ); 522 $content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content ); 523 524 $mail_success = wp_mail( 525 $email_address, 526 sprintf( 527 /* translators: Personal data export notification email subject. %s: Site title */ 528 __( '[%s] Personal Data Export' ), 529 $site_name 530 ), 531 $content 532 ); 533 534 if ( $switched_locale ) { 535 restore_previous_locale(); 536 } 537 538 if ( ! $mail_success ) { 539 return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) ); 540 } 541 542 return true; 543 } 544 545 /** 546 * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file. 547 * @see wp_privacy_personal_data_export_page 548 * @since 4.9.6 549 * 550 * @param array $response The response from the personal data exporter for the given page. 551 * @param int $exporter_index The index of the personal data exporter. Begins at 1. 552 * @param string $email_address The email address of the user whose personal data this is. 553 * @param int $page The page of personal data for this exporter. Begins at 1. 554 * @param int $request_id The request ID for this personal data export. 555 * @param bool $send_as_email Whether the final results of the export should be emailed to the user. 556 * @param string $exporter_key The slug (key) of the exporter. 557 * @return array The filtered response. 558 */ 559 function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) { 560 /* Do some simple checks on the shape of the response from the exporter. 561 * If the exporter response is malformed, don't attempt to consume it - let it 562 * pass through to generate a warning to the user by default Ajax processing. 563 */ 564 if ( ! is_array( $response ) ) { 565 return $response; 566 } 567 568 if ( ! array_key_exists( 'done', $response ) ) { 569 return $response; 570 } 571 572 if ( ! array_key_exists( 'data', $response ) ) { 573 return $response; 574 } 575 576 if ( ! is_array( $response['data'] ) ) { 577 return $response; 578 } 579 580 // Get the request data. 581 $request = wp_get_user_request_data( $request_id ); 582 583 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 584 wp_send_json_error( __( 'Invalid request ID when merging exporter data.' ) ); 585 } 586 587 $export_data = array(); 588 589 // First exporter, first page? Reset the report data accumulation array. 590 if ( 1 === $exporter_index && 1 === $page ) { 591 update_post_meta( $request_id, '_export_data_raw', $export_data ); 592 } else { 593 $export_data = get_post_meta( $request_id, '_export_data_raw', true ); 594 } 595 596 // Now, merge the data from the exporter response into the data we have accumulated already. 597 $export_data = array_merge( $export_data, $response['data'] ); 598 update_post_meta( $request_id, '_export_data_raw', $export_data ); 599 600 // If we are not yet on the last page of the last exporter, return now. 601 /** This filter is documented in wp-admin/includes/ajax-actions.php */ 602 $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); 603 $is_last_exporter = $exporter_index === count( $exporters ); 604 $exporter_done = $response['done']; 605 if ( ! $is_last_exporter || ! $exporter_done ) { 606 return $response; 607 } 608 609 // Last exporter, last page - let's prepare the export file. 610 611 // First we need to re-organize the raw data hierarchically in groups and items. 612 $groups = array(); 613 foreach ( (array) $export_data as $export_datum ) { 614 $group_id = $export_datum['group_id']; 615 $group_label = $export_datum['group_label']; 616 if ( ! array_key_exists( $group_id, $groups ) ) { 617 $groups[ $group_id ] = array( 618 'group_label' => $group_label, 619 'items' => array(), 620 ); 621 } 622 623 $item_id = $export_datum['item_id']; 624 if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) { 625 $groups[ $group_id ]['items'][ $item_id ] = array(); 626 } 627 628 $old_item_data = $groups[ $group_id ]['items'][ $item_id ]; 629 $merged_item_data = array_merge( $export_datum['data'], $old_item_data ); 630 $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data; 631 } 632 633 // Then save the grouped data into the request. 634 delete_post_meta( $request_id, '_export_data_raw' ); 635 update_post_meta( $request_id, '_export_data_grouped', $groups ); 636 637 /** 638 * Generate the export file from the collected, grouped personal data. 639 * 640 * @since 4.9.6 641 * 642 * @param int $request_id The export request ID. 643 */ 644 do_action( 'wp_privacy_personal_data_export_file', $request_id ); 645 646 // Clear the grouped data now that it is no longer needed. 647 delete_post_meta( $request_id, '_export_data_grouped' ); 648 649 // If the destination is email, send it now. 650 if ( $send_as_email ) { 651 $mail_success = wp_privacy_send_personal_data_export_email( $request_id ); 652 if ( is_wp_error( $mail_success ) ) { 653 wp_send_json_error( $mail_success->get_error_message() ); 654 } 655 656 // Update the request to completed state when the export email is sent. 657 _wp_privacy_completed_request( $request_id ); 658 } else { 659 // Modify the response to include the URL of the export file so the browser can fetch it. 660 $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); 661 if ( ! empty( $export_file_url ) ) { 662 $response['url'] = $export_file_url; 663 } 664 } 665 666 return $response; 667 } 668 669 /** 215 670 * Mark erasure requests as completed after processing is finished. 216 671 *
Note: See TracChangeset
for help on using the changeset viewer.