wp/wp-admin/includes/privacy-tools.php
changeset 16 a86126ab1dd4
child 18 be944660c56a
equal deleted inserted replaced
15:3d4e9c994f10 16:a86126ab1dd4
       
     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 ( $groups_count > 1 ) {
       
   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 ( $groups_count > 1 ) {
       
   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 filename,
       
   468 	 * to avoid breaking any URLs that may have been previously sent via email.
       
   469 	 */
       
   470 	$error = false;
       
   471 
       
   472 	// This meta value is used from version 5.5.
       
   473 	$archive_filename = get_post_meta( $request_id, '_export_file_name', true );
       
   474 
       
   475 	// This one stored an absolute path and is used for backward compatibility.
       
   476 	$archive_pathname = get_post_meta( $request_id, '_export_file_path', true );
       
   477 
       
   478 	// If a filename meta exists, use it.
       
   479 	if ( ! empty( $archive_filename ) ) {
       
   480 		$archive_pathname = $exports_dir . $archive_filename;
       
   481 	} elseif ( ! empty( $archive_pathname ) ) {
       
   482 		// If a full path meta exists, use it and create the new meta value.
       
   483 		$archive_filename = basename( $archive_pathname );
       
   484 
       
   485 		update_post_meta( $request_id, '_export_file_name', $archive_filename );
       
   486 
       
   487 		// Remove the back-compat meta values.
       
   488 		delete_post_meta( $request_id, '_export_file_url' );
       
   489 		delete_post_meta( $request_id, '_export_file_path' );
       
   490 	} else {
       
   491 		// If there's no filename or full path stored, create a new file.
       
   492 		$archive_filename = $file_basename . '.zip';
       
   493 		$archive_pathname = $exports_dir . $archive_filename;
       
   494 
       
   495 		update_post_meta( $request_id, '_export_file_name', $archive_filename );
       
   496 	}
       
   497 
       
   498 	$archive_url = $exports_url . $archive_filename;
       
   499 
       
   500 	if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) {
       
   501 		wp_delete_file( $archive_pathname );
       
   502 	}
       
   503 
       
   504 	$zip = new ZipArchive;
       
   505 	if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) {
       
   506 		if ( ! $zip->addFile( $json_report_pathname, 'export.json' ) ) {
       
   507 			$error = __( 'Unable to add data to JSON file.' );
       
   508 		}
       
   509 
       
   510 		if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) {
       
   511 			$error = __( 'Unable to add data to HTML file.' );
       
   512 		}
       
   513 
       
   514 		$zip->close();
       
   515 
       
   516 		if ( ! $error ) {
       
   517 			/**
       
   518 			 * Fires right after all personal data has been written to the export file.
       
   519 			 *
       
   520 			 * @since 4.9.6
       
   521 			 * @since 5.4.0 Added the `$json_report_pathname` parameter.
       
   522 			 *
       
   523 			 * @param string $archive_pathname     The full path to the export file on the filesystem.
       
   524 			 * @param string $archive_url          The URL of the archive file.
       
   525 			 * @param string $html_report_pathname The full path to the HTML personal data report on the filesystem.
       
   526 			 * @param int    $request_id           The export request ID.
       
   527 			 * @param string $json_report_pathname The full path to the JSON personal data report on the filesystem.
       
   528 			 */
       
   529 			do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id, $json_report_pathname );
       
   530 		}
       
   531 	} else {
       
   532 		$error = __( 'Unable to open export file (archive) for writing.' );
       
   533 	}
       
   534 
       
   535 	// Remove the JSON file.
       
   536 	unlink( $json_report_pathname );
       
   537 
       
   538 	// Remove the HTML file.
       
   539 	unlink( $html_report_pathname );
       
   540 
       
   541 	if ( $error ) {
       
   542 		wp_send_json_error( $error );
       
   543 	}
       
   544 }
       
   545 
       
   546 /**
       
   547  * Send an email to the user with a link to the personal data export file
       
   548  *
       
   549  * @since 4.9.6
       
   550  *
       
   551  * @param int $request_id The request ID for this personal data export.
       
   552  * @return true|WP_Error True on success or `WP_Error` on failure.
       
   553  */
       
   554 function wp_privacy_send_personal_data_export_email( $request_id ) {
       
   555 	// Get the request.
       
   556 	$request = wp_get_user_request( $request_id );
       
   557 
       
   558 	if ( ! $request || 'export_personal_data' !== $request->action_name ) {
       
   559 		return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) );
       
   560 	}
       
   561 
       
   562 	// Localize message content for user; fallback to site default for visitors.
       
   563 	if ( ! empty( $request->user_id ) ) {
       
   564 		$locale = get_user_locale( $request->user_id );
       
   565 	} else {
       
   566 		$locale = get_locale();
       
   567 	}
       
   568 
       
   569 	$switched_locale = switch_to_locale( $locale );
       
   570 
       
   571 	/** This filter is documented in wp-includes/functions.php */
       
   572 	$expiration      = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );
       
   573 	$expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration );
       
   574 
       
   575 	$exports_url      = wp_privacy_exports_url();
       
   576 	$export_file_name = get_post_meta( $request_id, '_export_file_name', true );
       
   577 	$export_file_url  = $exports_url . $export_file_name;
       
   578 
       
   579 	$site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
       
   580 	$site_url  = home_url();
       
   581 
       
   582 	/**
       
   583 	 * Filters the recipient of the personal data export email notification.
       
   584 	 * Should be used with great caution to avoid sending the data export link to wrong emails.
       
   585 	 *
       
   586 	 * @since 5.3.0
       
   587 	 *
       
   588 	 * @param string          $request_email The email address of the notification recipient.
       
   589 	 * @param WP_User_Request $request       The request that is initiating the notification.
       
   590 	 */
       
   591 	$request_email = apply_filters( 'wp_privacy_personal_data_email_to', $request->email, $request );
       
   592 
       
   593 	$email_data = array(
       
   594 		'request'           => $request,
       
   595 		'expiration'        => $expiration,
       
   596 		'expiration_date'   => $expiration_date,
       
   597 		'message_recipient' => $request_email,
       
   598 		'export_file_url'   => $export_file_url,
       
   599 		'sitename'          => $site_name,
       
   600 		'siteurl'           => $site_url,
       
   601 	);
       
   602 
       
   603 	/* translators: Personal data export notification email subject. %s: Site title. */
       
   604 	$subject = sprintf( __( '[%s] Personal Data Export' ), $site_name );
       
   605 
       
   606 	/**
       
   607 	 * Filters the subject of the email sent when an export request is completed.
       
   608 	 *
       
   609 	 * @since 5.3.0
       
   610 	 *
       
   611 	 * @param string $subject    The email subject.
       
   612 	 * @param string $sitename   The name of the site.
       
   613 	 * @param array  $email_data {
       
   614 	 *     Data relating to the account action email.
       
   615 	 *
       
   616 	 *     @type WP_User_Request $request           User request object.
       
   617 	 *     @type int             $expiration        The time in seconds until the export file expires.
       
   618 	 *     @type string          $expiration_date   The localized date and time when the export file expires.
       
   619 	 *     @type string          $message_recipient The address that the email will be sent to. Defaults
       
   620 	 *                                              to the value of `$request->email`, but can be changed
       
   621 	 *                                              by the `wp_privacy_personal_data_email_to` filter.
       
   622 	 *     @type string          $export_file_url   The export file URL.
       
   623 	 *     @type string          $sitename          The site name sending the mail.
       
   624 	 *     @type string          $siteurl           The site URL sending the mail.
       
   625 	 * }
       
   626 	 */
       
   627 	$subject = apply_filters( 'wp_privacy_personal_data_email_subject', $subject, $site_name, $email_data );
       
   628 
       
   629 	/* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */
       
   630 	$email_text = __(
       
   631 		'Howdy,
       
   632 
       
   633 Your request for an export of personal data has been completed. You may
       
   634 download your personal data by clicking on the link below. For privacy
       
   635 and security, we will automatically delete the file on ###EXPIRATION###,
       
   636 so please download it before then.
       
   637 
       
   638 ###LINK###
       
   639 
       
   640 Regards,
       
   641 All at ###SITENAME###
       
   642 ###SITEURL###'
       
   643 	);
       
   644 
       
   645 	/**
       
   646 	 * Filters the text of the email sent with a personal data export file.
       
   647 	 *
       
   648 	 * The following strings have a special meaning and will get replaced dynamically:
       
   649 	 * ###EXPIRATION###         The date when the URL will be automatically deleted.
       
   650 	 * ###LINK###               URL of the personal data export file for the user.
       
   651 	 * ###SITENAME###           The name of the site.
       
   652 	 * ###SITEURL###            The URL to the site.
       
   653 	 *
       
   654 	 * @since 4.9.6
       
   655 	 * @since 5.3.0 Introduced the `$email_data` array.
       
   656 	 *
       
   657 	 * @param string $email_text Text in the email.
       
   658 	 * @param int    $request_id The request ID for this personal data export.
       
   659 	 * @param array  $email_data {
       
   660 	 *     Data relating to the account action email.
       
   661 	 *
       
   662 	 *     @type WP_User_Request $request           User request object.
       
   663 	 *     @type int             $expiration        The time in seconds until the export file expires.
       
   664 	 *     @type string          $expiration_date   The localized date and time when the export file expires.
       
   665 	 *     @type string          $message_recipient The address that the email will be sent to. Defaults
       
   666 	 *                                              to the value of `$request->email`, but can be changed
       
   667 	 *                                              by the `wp_privacy_personal_data_email_to` filter.
       
   668 	 *     @type string          $export_file_url   The export file URL.
       
   669 	 *     @type string          $sitename          The site name sending the mail.
       
   670 	 *     @type string          $siteurl           The site URL sending the mail.
       
   671 	 */
       
   672 	$content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id, $email_data );
       
   673 
       
   674 	$content = str_replace( '###EXPIRATION###', $expiration_date, $content );
       
   675 	$content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content );
       
   676 	$content = str_replace( '###EMAIL###', $request_email, $content );
       
   677 	$content = str_replace( '###SITENAME###', $site_name, $content );
       
   678 	$content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content );
       
   679 
       
   680 	$headers = '';
       
   681 
       
   682 	/**
       
   683 	 * Filters the headers of the email sent with a personal data export file.
       
   684 	 *
       
   685 	 * @since 5.4.0
       
   686 	 *
       
   687 	 * @param string|array $headers    The email headers.
       
   688 	 * @param string       $subject    The email subject.
       
   689 	 * @param string       $content    The email content.
       
   690 	 * @param int          $request_id The request ID.
       
   691 	 * @param array        $email_data {
       
   692 	 *     Data relating to the account action email.
       
   693 	 *
       
   694 	 *     @type WP_User_Request $request           User request object.
       
   695 	 *     @type int             $expiration        The time in seconds until the export file expires.
       
   696 	 *     @type string          $expiration_date   The localized date and time when the export file expires.
       
   697 	 *     @type string          $message_recipient The address that the email will be sent to. Defaults
       
   698 	 *                                              to the value of `$request->email`, but can be changed
       
   699 	 *                                              by the `wp_privacy_personal_data_email_to` filter.
       
   700 	 *     @type string          $export_file_url   The export file URL.
       
   701 	 *     @type string          $sitename          The site name sending the mail.
       
   702 	 *     @type string          $siteurl           The site URL sending the mail.
       
   703 	 * }
       
   704 	 */
       
   705 	$headers = apply_filters( 'wp_privacy_personal_data_email_headers', $headers, $subject, $content, $request_id, $email_data );
       
   706 
       
   707 	$mail_success = wp_mail( $request_email, $subject, $content, $headers );
       
   708 
       
   709 	if ( $switched_locale ) {
       
   710 		restore_previous_locale();
       
   711 	}
       
   712 
       
   713 	if ( ! $mail_success ) {
       
   714 		return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) );
       
   715 	}
       
   716 
       
   717 	return true;
       
   718 }
       
   719 
       
   720 /**
       
   721  * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file.
       
   722  *
       
   723  * @since 4.9.6
       
   724  *
       
   725  * @see 'wp_privacy_personal_data_export_page'
       
   726  *
       
   727  * @param array  $response        The response from the personal data exporter for the given page.
       
   728  * @param int    $exporter_index  The index of the personal data exporter. Begins at 1.
       
   729  * @param string $email_address   The email address of the user whose personal data this is.
       
   730  * @param int    $page            The page of personal data for this exporter. Begins at 1.
       
   731  * @param int    $request_id      The request ID for this personal data export.
       
   732  * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
       
   733  * @param string $exporter_key    The slug (key) of the exporter.
       
   734  * @return array The filtered response.
       
   735  */
       
   736 function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) {
       
   737 	/* Do some simple checks on the shape of the response from the exporter.
       
   738 	 * If the exporter response is malformed, don't attempt to consume it - let it
       
   739 	 * pass through to generate a warning to the user by default Ajax processing.
       
   740 	 */
       
   741 	if ( ! is_array( $response ) ) {
       
   742 		return $response;
       
   743 	}
       
   744 
       
   745 	if ( ! array_key_exists( 'done', $response ) ) {
       
   746 		return $response;
       
   747 	}
       
   748 
       
   749 	if ( ! array_key_exists( 'data', $response ) ) {
       
   750 		return $response;
       
   751 	}
       
   752 
       
   753 	if ( ! is_array( $response['data'] ) ) {
       
   754 		return $response;
       
   755 	}
       
   756 
       
   757 	// Get the request.
       
   758 	$request = wp_get_user_request( $request_id );
       
   759 
       
   760 	if ( ! $request || 'export_personal_data' !== $request->action_name ) {
       
   761 		wp_send_json_error( __( 'Invalid request ID when merging exporter data.' ) );
       
   762 	}
       
   763 
       
   764 	$export_data = array();
       
   765 
       
   766 	// First exporter, first page? Reset the report data accumulation array.
       
   767 	if ( 1 === $exporter_index && 1 === $page ) {
       
   768 		update_post_meta( $request_id, '_export_data_raw', $export_data );
       
   769 	} else {
       
   770 		$export_data = get_post_meta( $request_id, '_export_data_raw', true );
       
   771 	}
       
   772 
       
   773 	// Now, merge the data from the exporter response into the data we have accumulated already.
       
   774 	$export_data = array_merge( $export_data, $response['data'] );
       
   775 	update_post_meta( $request_id, '_export_data_raw', $export_data );
       
   776 
       
   777 	// If we are not yet on the last page of the last exporter, return now.
       
   778 	/** This filter is documented in wp-admin/includes/ajax-actions.php */
       
   779 	$exporters        = apply_filters( 'wp_privacy_personal_data_exporters', array() );
       
   780 	$is_last_exporter = count( $exporters ) === $exporter_index;
       
   781 	$exporter_done    = $response['done'];
       
   782 	if ( ! $is_last_exporter || ! $exporter_done ) {
       
   783 		return $response;
       
   784 	}
       
   785 
       
   786 	// Last exporter, last page - let's prepare the export file.
       
   787 
       
   788 	// First we need to re-organize the raw data hierarchically in groups and items.
       
   789 	$groups = array();
       
   790 	foreach ( (array) $export_data as $export_datum ) {
       
   791 		$group_id    = $export_datum['group_id'];
       
   792 		$group_label = $export_datum['group_label'];
       
   793 
       
   794 		$group_description = '';
       
   795 		if ( ! empty( $export_datum['group_description'] ) ) {
       
   796 			$group_description = $export_datum['group_description'];
       
   797 		}
       
   798 
       
   799 		if ( ! array_key_exists( $group_id, $groups ) ) {
       
   800 			$groups[ $group_id ] = array(
       
   801 				'group_label'       => $group_label,
       
   802 				'group_description' => $group_description,
       
   803 				'items'             => array(),
       
   804 			);
       
   805 		}
       
   806 
       
   807 		$item_id = $export_datum['item_id'];
       
   808 		if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) {
       
   809 			$groups[ $group_id ]['items'][ $item_id ] = array();
       
   810 		}
       
   811 
       
   812 		$old_item_data                            = $groups[ $group_id ]['items'][ $item_id ];
       
   813 		$merged_item_data                         = array_merge( $export_datum['data'], $old_item_data );
       
   814 		$groups[ $group_id ]['items'][ $item_id ] = $merged_item_data;
       
   815 	}
       
   816 
       
   817 	// Then save the grouped data into the request.
       
   818 	delete_post_meta( $request_id, '_export_data_raw' );
       
   819 	update_post_meta( $request_id, '_export_data_grouped', $groups );
       
   820 
       
   821 	/**
       
   822 	 * Generate the export file from the collected, grouped personal data.
       
   823 	 *
       
   824 	 * @since 4.9.6
       
   825 	 *
       
   826 	 * @param int $request_id The export request ID.
       
   827 	 */
       
   828 	do_action( 'wp_privacy_personal_data_export_file', $request_id );
       
   829 
       
   830 	// Clear the grouped data now that it is no longer needed.
       
   831 	delete_post_meta( $request_id, '_export_data_grouped' );
       
   832 
       
   833 	// If the destination is email, send it now.
       
   834 	if ( $send_as_email ) {
       
   835 		$mail_success = wp_privacy_send_personal_data_export_email( $request_id );
       
   836 		if ( is_wp_error( $mail_success ) ) {
       
   837 			wp_send_json_error( $mail_success->get_error_message() );
       
   838 		}
       
   839 
       
   840 		// Update the request to completed state when the export email is sent.
       
   841 		_wp_privacy_completed_request( $request_id );
       
   842 	} else {
       
   843 		// Modify the response to include the URL of the export file so the browser can fetch it.
       
   844 		$exports_url      = wp_privacy_exports_url();
       
   845 		$export_file_name = get_post_meta( $request_id, '_export_file_name', true );
       
   846 		$export_file_url  = $exports_url . $export_file_name;
       
   847 
       
   848 		if ( ! empty( $export_file_url ) ) {
       
   849 			$response['url'] = $export_file_url;
       
   850 		}
       
   851 	}
       
   852 
       
   853 	return $response;
       
   854 }
       
   855 
       
   856 /**
       
   857  * Mark erasure requests as completed after processing is finished.
       
   858  *
       
   859  * This intercepts the Ajax responses to personal data eraser page requests, and
       
   860  * monitors the status of a request. Once all of the processing has finished, the
       
   861  * request is marked as completed.
       
   862  *
       
   863  * @since 4.9.6
       
   864  *
       
   865  * @see 'wp_privacy_personal_data_erasure_page'
       
   866  *
       
   867  * @param array  $response      The response from the personal data eraser for
       
   868  *                              the given page.
       
   869  * @param int    $eraser_index  The index of the personal data eraser. Begins
       
   870  *                              at 1.
       
   871  * @param string $email_address The email address of the user whose personal
       
   872  *                              data this is.
       
   873  * @param int    $page          The page of personal data for this eraser.
       
   874  *                              Begins at 1.
       
   875  * @param int    $request_id    The request ID for this personal data erasure.
       
   876  * @return array The filtered response.
       
   877  */
       
   878 function wp_privacy_process_personal_data_erasure_page( $response, $eraser_index, $email_address, $page, $request_id ) {
       
   879 	/*
       
   880 	 * If the eraser response is malformed, don't attempt to consume it; let it
       
   881 	 * pass through, so that the default Ajax processing will generate a warning
       
   882 	 * to the user.
       
   883 	 */
       
   884 	if ( ! is_array( $response ) ) {
       
   885 		return $response;
       
   886 	}
       
   887 
       
   888 	if ( ! array_key_exists( 'done', $response ) ) {
       
   889 		return $response;
       
   890 	}
       
   891 
       
   892 	if ( ! array_key_exists( 'items_removed', $response ) ) {
       
   893 		return $response;
       
   894 	}
       
   895 
       
   896 	if ( ! array_key_exists( 'items_retained', $response ) ) {
       
   897 		return $response;
       
   898 	}
       
   899 
       
   900 	if ( ! array_key_exists( 'messages', $response ) ) {
       
   901 		return $response;
       
   902 	}
       
   903 
       
   904 	// Get the request.
       
   905 	$request = wp_get_user_request( $request_id );
       
   906 
       
   907 	if ( ! $request || 'remove_personal_data' !== $request->action_name ) {
       
   908 		wp_send_json_error( __( 'Invalid request ID when processing eraser data.' ) );
       
   909 	}
       
   910 
       
   911 	/** This filter is documented in wp-admin/includes/ajax-actions.php */
       
   912 	$erasers        = apply_filters( 'wp_privacy_personal_data_erasers', array() );
       
   913 	$is_last_eraser = count( $erasers ) === $eraser_index;
       
   914 	$eraser_done    = $response['done'];
       
   915 
       
   916 	if ( ! $is_last_eraser || ! $eraser_done ) {
       
   917 		return $response;
       
   918 	}
       
   919 
       
   920 	_wp_privacy_completed_request( $request_id );
       
   921 
       
   922 	/**
       
   923 	 * Fires immediately after a personal data erasure request has been marked completed.
       
   924 	 *
       
   925 	 * @since 4.9.6
       
   926 	 *
       
   927 	 * @param int $request_id The privacy request post ID associated with this request.
       
   928 	 */
       
   929 	do_action( 'wp_privacy_personal_data_erased', $request_id );
       
   930 
       
   931 	return $response;
       
   932 }