From d0c1b77cda3f4ea0b502d2abd686e4ddd45dad7e Mon Sep 17 00:00:00 2001 From: Andrew Ozz Date: Mon, 10 Jun 2019 23:54:53 +0000 Subject: [PATCH] Privacy tools: - Move the (remaining) privacy tools related functions from `wp-admin/includes/file.php` to `wp-admin/includes/privacy-tools.php`. - Move the `WP_User_Request` class to a separate file. See #43895. Built from https://develop.svn.wordpress.org/trunk@45519 git-svn-id: http://core.svn.wordpress.org/trunk@45330 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/includes/file.php | 455 -------------------------- wp-admin/includes/privacy-tools.php | 455 ++++++++++++++++++++++++++ wp-includes/class-wp-user-request.php | 107 ++++++ wp-includes/user.php | 107 ------ wp-includes/version.php | 2 +- wp-settings.php | 1 + 6 files changed, 564 insertions(+), 563 deletions(-) create mode 100644 wp-includes/class-wp-user-request.php diff --git a/wp-admin/includes/file.php b/wp-admin/includes/file.php index fbc47ce7e2..51c0bf9e5d 100644 --- a/wp-admin/includes/file.php +++ b/wp-admin/includes/file.php @@ -2194,458 +2194,3 @@ function wp_print_request_filesystem_credentials_modal() { ' . esc_html( $group_data['group_label'] ) . ''; - $group_html .= '
'; - - foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) { - $group_html .= ''; - $group_html .= ''; - - foreach ( (array) $group_item_data as $group_item_datum ) { - $value = $group_item_datum['value']; - // If it looks like a link, make it a link. - if ( false === strpos( $value, ' ' ) && ( 0 === strpos( $value, 'http://' ) || 0 === strpos( $value, 'https://' ) ) ) { - $value = '' . esc_html( $value ) . ''; - } - - $group_html .= ''; - $group_html .= ''; - $group_html .= ''; - $group_html .= ''; - } - - $group_html .= ''; - $group_html .= '
' . esc_html( $group_item_datum['name'] ) . '' . wp_kses( $value, 'personal_data_export' ) . '
'; - } - - $group_html .= '
'; - - return $group_html; -} - -/** - * Generate the personal data export file. - * - * @since 4.9.6 - * - * @param int $request_id The export request ID. - */ -function wp_privacy_generate_personal_data_export_file( $request_id ) { - if ( ! class_exists( 'ZipArchive' ) ) { - wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) ); - } - - // Get the request data. - $request = wp_get_user_request_data( $request_id ); - - if ( ! $request || 'export_personal_data' !== $request->action_name ) { - wp_send_json_error( __( 'Invalid request ID when generating export file.' ) ); - } - - $email_address = $request->email; - - if ( ! is_email( $email_address ) ) { - wp_send_json_error( __( 'Invalid email address when generating export file.' ) ); - } - - // Create the exports folder if needed. - $exports_dir = wp_privacy_exports_dir(); - $exports_url = wp_privacy_exports_url(); - - if ( ! wp_mkdir_p( $exports_dir ) ) { - wp_send_json_error( __( 'Unable to create export folder.' ) ); - } - - // Protect export folder from browsing. - $index_pathname = $exports_dir . 'index.html'; - if ( ! file_exists( $index_pathname ) ) { - $file = fopen( $index_pathname, 'w' ); - if ( false === $file ) { - wp_send_json_error( __( 'Unable to protect export folder from browsing.' ) ); - } - fwrite( $file, '' ); - fclose( $file ); - } - - $stripped_email = str_replace( '@', '-at-', $email_address ); - $stripped_email = sanitize_title( $stripped_email ); // slugify the email address - $obscura = wp_generate_password( 32, false, false ); - $file_basename = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura; - $html_report_filename = $file_basename . '.html'; - $html_report_pathname = wp_normalize_path( $exports_dir . $html_report_filename ); - $file = fopen( $html_report_pathname, 'w' ); - if ( false === $file ) { - wp_send_json_error( __( 'Unable to open export file (HTML report) for writing.' ) ); - } - - $title = sprintf( - /* translators: %s: user's email address */ - __( 'Personal Data Export for %s' ), - $email_address - ); - - // Open HTML. - fwrite( $file, "\n" ); - fwrite( $file, "\n" ); - - // Head. - fwrite( $file, "\n" ); - fwrite( $file, "\n" ); - fwrite( $file, "' ); - fwrite( $file, '' ); - fwrite( $file, esc_html( $title ) ); - fwrite( $file, '' ); - fwrite( $file, "\n" ); - - // Body. - fwrite( $file, "\n" ); - - // Heading. - fwrite( $file, '

' . esc_html__( 'Personal Data Export' ) . '

' ); - - // And now, all the Groups. - $groups = get_post_meta( $request_id, '_export_data_grouped', true ); - - // First, build an "About" group on the fly for this report. - $about_group = array( - /* translators: Header for the About section in a personal data export. */ - 'group_label' => _x( 'About', 'personal data group label' ), - 'items' => array( - 'about-1' => array( - array( - 'name' => _x( 'Report generated for', 'email address' ), - 'value' => $email_address, - ), - array( - 'name' => _x( 'For site', 'website name' ), - 'value' => get_bloginfo( 'name' ), - ), - array( - 'name' => _x( 'At URL', 'website URL' ), - 'value' => get_bloginfo( 'url' ), - ), - array( - 'name' => _x( 'On', 'date/time' ), - 'value' => current_time( 'mysql' ), - ), - ), - ), - ); - - // Merge in the special about group. - $groups = array_merge( array( 'about' => $about_group ), $groups ); - - // Now, iterate over every group in $groups and have the formatter render it in HTML. - foreach ( (array) $groups as $group_id => $group_data ) { - fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data ) ); - } - - fwrite( $file, "\n" ); - - // Close HTML. - fwrite( $file, "\n" ); - fclose( $file ); - - /* - * Now, generate the ZIP. - * - * If an archive has already been generated, then remove it and reuse the - * filename, to avoid breaking any URLs that may have been previously sent - * via email. - */ - $error = false; - $archive_url = get_post_meta( $request_id, '_export_file_url', true ); - $archive_pathname = get_post_meta( $request_id, '_export_file_path', true ); - - if ( empty( $archive_pathname ) || empty( $archive_url ) ) { - $archive_filename = $file_basename . '.zip'; - $archive_pathname = $exports_dir . $archive_filename; - $archive_url = $exports_url . $archive_filename; - - update_post_meta( $request_id, '_export_file_url', $archive_url ); - update_post_meta( $request_id, '_export_file_path', wp_normalize_path( $archive_pathname ) ); - } - - if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) { - wp_delete_file( $archive_pathname ); - } - - $zip = new ZipArchive; - if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) { - if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) { - $error = __( 'Unable to add data to export file.' ); - } - - $zip->close(); - - if ( ! $error ) { - /** - * Fires right after all personal data has been written to the export file. - * - * @since 4.9.6 - * - * @param string $archive_pathname The full path to the export file on the filesystem. - * @param string $archive_url The URL of the archive file. - * @param string $html_report_pathname The full path to the personal data report on the filesystem. - * @param int $request_id The export request ID. - */ - do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id ); - } - } else { - $error = __( 'Unable to open export file (archive) for writing.' ); - } - - // And remove the HTML file. - unlink( $html_report_pathname ); - - if ( $error ) { - wp_send_json_error( $error ); - } -} - -/** - * Send an email to the user with a link to the personal data export file - * - * @since 4.9.6 - * - * @param int $request_id The request ID for this personal data export. - * @return true|WP_Error True on success or `WP_Error` on failure. - */ -function wp_privacy_send_personal_data_export_email( $request_id ) { - // Get the request data. - $request = wp_get_user_request_data( $request_id ); - - if ( ! $request || 'export_personal_data' !== $request->action_name ) { - return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) ); - } - - // Localize message content for user; fallback to site default for visitors. - if ( ! empty( $request->user_id ) ) { - $locale = get_user_locale( $request->user_id ); - } else { - $locale = get_locale(); - } - - $switched_locale = switch_to_locale( $locale ); - - /** This filter is documented in wp-includes/functions.php */ - $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS ); - $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration ); - - /* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */ - $email_text = __( - 'Howdy, - -Your request for an export of personal data has been completed. You may -download your personal data by clicking on the link below. For privacy -and security, we will automatically delete the file on ###EXPIRATION###, -so please download it before then. - -###LINK### - -Regards, -All at ###SITENAME### -###SITEURL###' - ); - - /** - * Filters the text of the email sent with a personal data export file. - * - * The following strings have a special meaning and will get replaced dynamically: - * ###EXPIRATION### The date when the URL will be automatically deleted. - * ###LINK### URL of the personal data export file for the user. - * ###SITENAME### The name of the site. - * ###SITEURL### The URL to the site. - * - * @since 4.9.6 - * - * @param string $email_text Text in the email. - * @param int $request_id The request ID for this personal data export. - */ - $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id ); - - $email_address = $request->email; - $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); - $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); - $site_url = home_url(); - - $content = str_replace( '###EXPIRATION###', $expiration_date, $content ); - $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content ); - $content = str_replace( '###EMAIL###', $email_address, $content ); - $content = str_replace( '###SITENAME###', $site_name, $content ); - $content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content ); - - $mail_success = wp_mail( - $email_address, - sprintf( - /* translators: Personal data export notification email subject. %s: Site title */ - __( '[%s] Personal Data Export' ), - $site_name - ), - $content - ); - - if ( $switched_locale ) { - restore_previous_locale(); - } - - if ( ! $mail_success ) { - return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) ); - } - - return true; -} - -/** - * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file. - * @see wp_privacy_personal_data_export_page - * @since 4.9.6 - * - * @param array $response The response from the personal data exporter for the given page. - * @param int $exporter_index The index of the personal data exporter. Begins at 1. - * @param string $email_address The email address of the user whose personal data this is. - * @param int $page The page of personal data for this exporter. Begins at 1. - * @param int $request_id The request ID for this personal data export. - * @param bool $send_as_email Whether the final results of the export should be emailed to the user. - * @param string $exporter_key The slug (key) of the exporter. - * @return array The filtered response. - */ -function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) { - /* Do some simple checks on the shape of the response from the exporter. - * If the exporter response is malformed, don't attempt to consume it - let it - * pass through to generate a warning to the user by default Ajax processing. - */ - if ( ! is_array( $response ) ) { - return $response; - } - - if ( ! array_key_exists( 'done', $response ) ) { - return $response; - } - - if ( ! array_key_exists( 'data', $response ) ) { - return $response; - } - - if ( ! is_array( $response['data'] ) ) { - return $response; - } - - // Get the request data. - $request = wp_get_user_request_data( $request_id ); - - if ( ! $request || 'export_personal_data' !== $request->action_name ) { - wp_send_json_error( __( 'Invalid request ID when merging exporter data.' ) ); - } - - $export_data = array(); - - // First exporter, first page? Reset the report data accumulation array. - if ( 1 === $exporter_index && 1 === $page ) { - update_post_meta( $request_id, '_export_data_raw', $export_data ); - } else { - $export_data = get_post_meta( $request_id, '_export_data_raw', true ); - } - - // Now, merge the data from the exporter response into the data we have accumulated already. - $export_data = array_merge( $export_data, $response['data'] ); - update_post_meta( $request_id, '_export_data_raw', $export_data ); - - // If we are not yet on the last page of the last exporter, return now. - /** This filter is documented in wp-admin/includes/ajax-actions.php */ - $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); - $is_last_exporter = $exporter_index === count( $exporters ); - $exporter_done = $response['done']; - if ( ! $is_last_exporter || ! $exporter_done ) { - return $response; - } - - // Last exporter, last page - let's prepare the export file. - - // First we need to re-organize the raw data hierarchically in groups and items. - $groups = array(); - foreach ( (array) $export_data as $export_datum ) { - $group_id = $export_datum['group_id']; - $group_label = $export_datum['group_label']; - if ( ! array_key_exists( $group_id, $groups ) ) { - $groups[ $group_id ] = array( - 'group_label' => $group_label, - 'items' => array(), - ); - } - - $item_id = $export_datum['item_id']; - if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) { - $groups[ $group_id ]['items'][ $item_id ] = array(); - } - - $old_item_data = $groups[ $group_id ]['items'][ $item_id ]; - $merged_item_data = array_merge( $export_datum['data'], $old_item_data ); - $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data; - } - - // Then save the grouped data into the request. - delete_post_meta( $request_id, '_export_data_raw' ); - update_post_meta( $request_id, '_export_data_grouped', $groups ); - - /** - * Generate the export file from the collected, grouped personal data. - * - * @since 4.9.6 - * - * @param int $request_id The export request ID. - */ - do_action( 'wp_privacy_personal_data_export_file', $request_id ); - - // Clear the grouped data now that it is no longer needed. - delete_post_meta( $request_id, '_export_data_grouped' ); - - // If the destination is email, send it now. - if ( $send_as_email ) { - $mail_success = wp_privacy_send_personal_data_export_email( $request_id ); - if ( is_wp_error( $mail_success ) ) { - wp_send_json_error( $mail_success->get_error_message() ); - } - - // Update the request to completed state when the export email is sent. - _wp_privacy_completed_request( $request_id ); - } else { - // Modify the response to include the URL of the export file so the browser can fetch it. - $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); - if ( ! empty( $export_file_url ) ) { - $response['url'] = $export_file_url; - } - } - - return $response; -} diff --git a/wp-admin/includes/privacy-tools.php b/wp-admin/includes/privacy-tools.php index 67d8e43611..7e2badcb4e 100644 --- a/wp-admin/includes/privacy-tools.php +++ b/wp-admin/includes/privacy-tools.php @@ -211,6 +211,461 @@ function _wp_personal_data_cleanup_requests() { } } +/** + * Generate a single group for the personal data export report. + * + * @since 4.9.6 + * + * @param array $group_data { + * The group data to render. + * + * @type string $group_label The user-facing heading for the group, e.g. 'Comments'. + * @type array $items { + * An array of group items. + * + * @type array $group_item_data { + * An array of name-value pairs for the item. + * + * @type string $name The user-facing name of an item name-value pair, e.g. 'IP Address'. + * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'. + * } + * } + * } + * @return string The HTML for this group and its items. + */ +function wp_privacy_generate_personal_data_export_group_html( $group_data ) { + $group_html = '

' . esc_html( $group_data['group_label'] ) . '

'; + $group_html .= '
'; + + foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) { + $group_html .= ''; + $group_html .= ''; + + foreach ( (array) $group_item_data as $group_item_datum ) { + $value = $group_item_datum['value']; + // If it looks like a link, make it a link. + if ( false === strpos( $value, ' ' ) && ( 0 === strpos( $value, 'http://' ) || 0 === strpos( $value, 'https://' ) ) ) { + $value = '' . esc_html( $value ) . ''; + } + + $group_html .= ''; + $group_html .= ''; + $group_html .= ''; + $group_html .= ''; + } + + $group_html .= ''; + $group_html .= '
' . esc_html( $group_item_datum['name'] ) . '' . wp_kses( $value, 'personal_data_export' ) . '
'; + } + + $group_html .= '
'; + + return $group_html; +} + +/** + * Generate the personal data export file. + * + * @since 4.9.6 + * + * @param int $request_id The export request ID. + */ +function wp_privacy_generate_personal_data_export_file( $request_id ) { + if ( ! class_exists( 'ZipArchive' ) ) { + wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) ); + } + + // Get the request data. + $request = wp_get_user_request_data( $request_id ); + + if ( ! $request || 'export_personal_data' !== $request->action_name ) { + wp_send_json_error( __( 'Invalid request ID when generating export file.' ) ); + } + + $email_address = $request->email; + + if ( ! is_email( $email_address ) ) { + wp_send_json_error( __( 'Invalid email address when generating export file.' ) ); + } + + // Create the exports folder if needed. + $exports_dir = wp_privacy_exports_dir(); + $exports_url = wp_privacy_exports_url(); + + if ( ! wp_mkdir_p( $exports_dir ) ) { + wp_send_json_error( __( 'Unable to create export folder.' ) ); + } + + // Protect export folder from browsing. + $index_pathname = $exports_dir . 'index.html'; + if ( ! file_exists( $index_pathname ) ) { + $file = fopen( $index_pathname, 'w' ); + if ( false === $file ) { + wp_send_json_error( __( 'Unable to protect export folder from browsing.' ) ); + } + fwrite( $file, '' ); + fclose( $file ); + } + + $stripped_email = str_replace( '@', '-at-', $email_address ); + $stripped_email = sanitize_title( $stripped_email ); // slugify the email address + $obscura = wp_generate_password( 32, false, false ); + $file_basename = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura; + $html_report_filename = $file_basename . '.html'; + $html_report_pathname = wp_normalize_path( $exports_dir . $html_report_filename ); + $file = fopen( $html_report_pathname, 'w' ); + if ( false === $file ) { + wp_send_json_error( __( 'Unable to open export file (HTML report) for writing.' ) ); + } + + $title = sprintf( + /* translators: %s: user's email address */ + __( 'Personal Data Export for %s' ), + $email_address + ); + + // Open HTML. + fwrite( $file, "\n" ); + fwrite( $file, "\n" ); + + // Head. + fwrite( $file, "\n" ); + fwrite( $file, "\n" ); + fwrite( $file, "' ); + fwrite( $file, '' ); + fwrite( $file, esc_html( $title ) ); + fwrite( $file, '' ); + fwrite( $file, "\n" ); + + // Body. + fwrite( $file, "\n" ); + + // Heading. + fwrite( $file, '

' . esc_html__( 'Personal Data Export' ) . '

' ); + + // And now, all the Groups. + $groups = get_post_meta( $request_id, '_export_data_grouped', true ); + + // First, build an "About" group on the fly for this report. + $about_group = array( + /* translators: Header for the About section in a personal data export. */ + 'group_label' => _x( 'About', 'personal data group label' ), + 'items' => array( + 'about-1' => array( + array( + 'name' => _x( 'Report generated for', 'email address' ), + 'value' => $email_address, + ), + array( + 'name' => _x( 'For site', 'website name' ), + 'value' => get_bloginfo( 'name' ), + ), + array( + 'name' => _x( 'At URL', 'website URL' ), + 'value' => get_bloginfo( 'url' ), + ), + array( + 'name' => _x( 'On', 'date/time' ), + 'value' => current_time( 'mysql' ), + ), + ), + ), + ); + + // Merge in the special about group. + $groups = array_merge( array( 'about' => $about_group ), $groups ); + + // Now, iterate over every group in $groups and have the formatter render it in HTML. + foreach ( (array) $groups as $group_id => $group_data ) { + fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data ) ); + } + + fwrite( $file, "\n" ); + + // Close HTML. + fwrite( $file, "\n" ); + fclose( $file ); + + /* + * Now, generate the ZIP. + * + * If an archive has already been generated, then remove it and reuse the + * filename, to avoid breaking any URLs that may have been previously sent + * via email. + */ + $error = false; + $archive_url = get_post_meta( $request_id, '_export_file_url', true ); + $archive_pathname = get_post_meta( $request_id, '_export_file_path', true ); + + if ( empty( $archive_pathname ) || empty( $archive_url ) ) { + $archive_filename = $file_basename . '.zip'; + $archive_pathname = $exports_dir . $archive_filename; + $archive_url = $exports_url . $archive_filename; + + update_post_meta( $request_id, '_export_file_url', $archive_url ); + update_post_meta( $request_id, '_export_file_path', wp_normalize_path( $archive_pathname ) ); + } + + if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) { + wp_delete_file( $archive_pathname ); + } + + $zip = new ZipArchive; + if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) { + if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) { + $error = __( 'Unable to add data to export file.' ); + } + + $zip->close(); + + if ( ! $error ) { + /** + * Fires right after all personal data has been written to the export file. + * + * @since 4.9.6 + * + * @param string $archive_pathname The full path to the export file on the filesystem. + * @param string $archive_url The URL of the archive file. + * @param string $html_report_pathname The full path to the personal data report on the filesystem. + * @param int $request_id The export request ID. + */ + do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id ); + } + } else { + $error = __( 'Unable to open export file (archive) for writing.' ); + } + + // And remove the HTML file. + unlink( $html_report_pathname ); + + if ( $error ) { + wp_send_json_error( $error ); + } +} + +/** + * Send an email to the user with a link to the personal data export file + * + * @since 4.9.6 + * + * @param int $request_id The request ID for this personal data export. + * @return true|WP_Error True on success or `WP_Error` on failure. + */ +function wp_privacy_send_personal_data_export_email( $request_id ) { + // Get the request data. + $request = wp_get_user_request_data( $request_id ); + + if ( ! $request || 'export_personal_data' !== $request->action_name ) { + return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) ); + } + + // Localize message content for user; fallback to site default for visitors. + if ( ! empty( $request->user_id ) ) { + $locale = get_user_locale( $request->user_id ); + } else { + $locale = get_locale(); + } + + $switched_locale = switch_to_locale( $locale ); + + /** This filter is documented in wp-includes/functions.php */ + $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS ); + $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration ); + + /* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */ + $email_text = __( + 'Howdy, + +Your request for an export of personal data has been completed. You may +download your personal data by clicking on the link below. For privacy +and security, we will automatically delete the file on ###EXPIRATION###, +so please download it before then. + +###LINK### + +Regards, +All at ###SITENAME### +###SITEURL###' + ); + + /** + * Filters the text of the email sent with a personal data export file. + * + * The following strings have a special meaning and will get replaced dynamically: + * ###EXPIRATION### The date when the URL will be automatically deleted. + * ###LINK### URL of the personal data export file for the user. + * ###SITENAME### The name of the site. + * ###SITEURL### The URL to the site. + * + * @since 4.9.6 + * + * @param string $email_text Text in the email. + * @param int $request_id The request ID for this personal data export. + */ + $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id ); + + $email_address = $request->email; + $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); + $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); + $site_url = home_url(); + + $content = str_replace( '###EXPIRATION###', $expiration_date, $content ); + $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content ); + $content = str_replace( '###EMAIL###', $email_address, $content ); + $content = str_replace( '###SITENAME###', $site_name, $content ); + $content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content ); + + $mail_success = wp_mail( + $email_address, + sprintf( + /* translators: Personal data export notification email subject. %s: Site title */ + __( '[%s] Personal Data Export' ), + $site_name + ), + $content + ); + + if ( $switched_locale ) { + restore_previous_locale(); + } + + if ( ! $mail_success ) { + return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) ); + } + + return true; +} + +/** + * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file. + * @see wp_privacy_personal_data_export_page + * @since 4.9.6 + * + * @param array $response The response from the personal data exporter for the given page. + * @param int $exporter_index The index of the personal data exporter. Begins at 1. + * @param string $email_address The email address of the user whose personal data this is. + * @param int $page The page of personal data for this exporter. Begins at 1. + * @param int $request_id The request ID for this personal data export. + * @param bool $send_as_email Whether the final results of the export should be emailed to the user. + * @param string $exporter_key The slug (key) of the exporter. + * @return array The filtered response. + */ +function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) { + /* Do some simple checks on the shape of the response from the exporter. + * If the exporter response is malformed, don't attempt to consume it - let it + * pass through to generate a warning to the user by default Ajax processing. + */ + if ( ! is_array( $response ) ) { + return $response; + } + + if ( ! array_key_exists( 'done', $response ) ) { + return $response; + } + + if ( ! array_key_exists( 'data', $response ) ) { + return $response; + } + + if ( ! is_array( $response['data'] ) ) { + return $response; + } + + // Get the request data. + $request = wp_get_user_request_data( $request_id ); + + if ( ! $request || 'export_personal_data' !== $request->action_name ) { + wp_send_json_error( __( 'Invalid request ID when merging exporter data.' ) ); + } + + $export_data = array(); + + // First exporter, first page? Reset the report data accumulation array. + if ( 1 === $exporter_index && 1 === $page ) { + update_post_meta( $request_id, '_export_data_raw', $export_data ); + } else { + $export_data = get_post_meta( $request_id, '_export_data_raw', true ); + } + + // Now, merge the data from the exporter response into the data we have accumulated already. + $export_data = array_merge( $export_data, $response['data'] ); + update_post_meta( $request_id, '_export_data_raw', $export_data ); + + // If we are not yet on the last page of the last exporter, return now. + /** This filter is documented in wp-admin/includes/ajax-actions.php */ + $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); + $is_last_exporter = $exporter_index === count( $exporters ); + $exporter_done = $response['done']; + if ( ! $is_last_exporter || ! $exporter_done ) { + return $response; + } + + // Last exporter, last page - let's prepare the export file. + + // First we need to re-organize the raw data hierarchically in groups and items. + $groups = array(); + foreach ( (array) $export_data as $export_datum ) { + $group_id = $export_datum['group_id']; + $group_label = $export_datum['group_label']; + if ( ! array_key_exists( $group_id, $groups ) ) { + $groups[ $group_id ] = array( + 'group_label' => $group_label, + 'items' => array(), + ); + } + + $item_id = $export_datum['item_id']; + if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) { + $groups[ $group_id ]['items'][ $item_id ] = array(); + } + + $old_item_data = $groups[ $group_id ]['items'][ $item_id ]; + $merged_item_data = array_merge( $export_datum['data'], $old_item_data ); + $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data; + } + + // Then save the grouped data into the request. + delete_post_meta( $request_id, '_export_data_raw' ); + update_post_meta( $request_id, '_export_data_grouped', $groups ); + + /** + * Generate the export file from the collected, grouped personal data. + * + * @since 4.9.6 + * + * @param int $request_id The export request ID. + */ + do_action( 'wp_privacy_personal_data_export_file', $request_id ); + + // Clear the grouped data now that it is no longer needed. + delete_post_meta( $request_id, '_export_data_grouped' ); + + // If the destination is email, send it now. + if ( $send_as_email ) { + $mail_success = wp_privacy_send_personal_data_export_email( $request_id ); + if ( is_wp_error( $mail_success ) ) { + wp_send_json_error( $mail_success->get_error_message() ); + } + + // Update the request to completed state when the export email is sent. + _wp_privacy_completed_request( $request_id ); + } else { + // Modify the response to include the URL of the export file so the browser can fetch it. + $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); + if ( ! empty( $export_file_url ) ) { + $response['url'] = $export_file_url; + } + } + + return $response; +} + /** * Mark erasure requests as completed after processing is finished. * diff --git a/wp-includes/class-wp-user-request.php b/wp-includes/class-wp-user-request.php new file mode 100644 index 0000000000..a59c7ffb8d --- /dev/null +++ b/wp-includes/class-wp-user-request.php @@ -0,0 +1,107 @@ +ID = $post->ID; + $this->user_id = $post->post_author; + $this->email = $post->post_title; + $this->action_name = $post->post_name; + $this->status = $post->post_status; + $this->created_timestamp = strtotime( $post->post_date_gmt ); + $this->modified_timestamp = strtotime( $post->post_modified_gmt ); + $this->confirmed_timestamp = (int) get_post_meta( $post->ID, '_wp_user_request_confirmed_timestamp', true ); + $this->completed_timestamp = (int) get_post_meta( $post->ID, '_wp_user_request_completed_timestamp', true ); + $this->request_data = json_decode( $post->post_content, true ); + $this->confirm_key = $post->post_password; + } +} diff --git a/wp-includes/user.php b/wp-includes/user.php index 2abbd7a7da..c3b911f0dd 100644 --- a/wp-includes/user.php +++ b/wp-includes/user.php @@ -3647,110 +3647,3 @@ function wp_get_user_request_data( $request_id ) { return new WP_User_Request( $post ); } - -/** - * WP_User_Request class. - * - * Represents user request data loaded from a WP_Post object. - * - * @since 4.9.6 - */ -final class WP_User_Request { - /** - * Request ID. - * - * @var int - */ - public $ID = 0; - - /** - * User ID. - * - * @var int - */ - public $user_id = 0; - - /** - * User email. - * - * @var int - */ - public $email = ''; - - /** - * Action name. - * - * @var string - */ - public $action_name = ''; - - /** - * Current status. - * - * @var string - */ - public $status = ''; - - /** - * Timestamp this request was created. - * - * @var int|null - */ - public $created_timestamp = null; - - /** - * Timestamp this request was last modified. - * - * @var int|null - */ - public $modified_timestamp = null; - - /** - * Timestamp this request was confirmed. - * - * @var int - */ - public $confirmed_timestamp = null; - - /** - * Timestamp this request was completed. - * - * @var int - */ - public $completed_timestamp = null; - - /** - * Misc data assigned to this request. - * - * @var array - */ - public $request_data = array(); - - /** - * Key used to confirm this request. - * - * @var string - */ - public $confirm_key = ''; - - /** - * Constructor. - * - * @since 4.9.6 - * - * @param WP_Post|object $post Post object. - */ - public function __construct( $post ) { - $this->ID = $post->ID; - $this->user_id = $post->post_author; - $this->email = $post->post_title; - $this->action_name = $post->post_name; - $this->status = $post->post_status; - $this->created_timestamp = strtotime( $post->post_date_gmt ); - $this->modified_timestamp = strtotime( $post->post_modified_gmt ); - $this->confirmed_timestamp = (int) get_post_meta( $post->ID, '_wp_user_request_confirmed_timestamp', true ); - $this->completed_timestamp = (int) get_post_meta( $post->ID, '_wp_user_request_completed_timestamp', true ); - $this->request_data = json_decode( $post->post_content, true ); - $this->confirm_key = $post->post_password; - } -} diff --git a/wp-includes/version.php b/wp-includes/version.php index dc6f5a7b8f..801eca776b 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -13,7 +13,7 @@ * * @global string $wp_version */ -$wp_version = '5.3-alpha-45515'; +$wp_version = '5.3-alpha-45519'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. diff --git a/wp-settings.php b/wp-settings.php index 518db55963..66a76c665b 100644 --- a/wp-settings.php +++ b/wp-settings.php @@ -167,6 +167,7 @@ require( ABSPATH . WPINC . '/date.php' ); require( ABSPATH . WPINC . '/theme.php' ); require( ABSPATH . WPINC . '/class-wp-theme.php' ); require( ABSPATH . WPINC . '/template.php' ); +require( ABSPATH . WPINC . '/class-wp-user-request.php' ); require( ABSPATH . WPINC . '/user.php' ); require( ABSPATH . WPINC . '/class-wp-user-query.php' ); require( ABSPATH . WPINC . '/class-wp-session-tokens.php' );