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


Generated: Tue Dec 24 01:00:02 2024 Cross-referenced by PHPXref 0.7.1