[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/includes/ -> privacy-tools.php (source)

   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">&uarr; </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  }


Generated : Sat Jul 11 08:20:14 2026 Cross-referenced by PHPXref