[ Index ]

PHP Cross Reference of WordPress

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


Generated: Sat Jul 4 01:00:03 2020 Cross-referenced by PHPXref 0.7.1