angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

privacy-tools.php (33560B)


      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 // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect, PEAR.Functions.FunctionCallSignature.Indent
    666 'Howdy,
    667 
    668 Your request for an export of personal data has been completed. You may
    669 download your personal data by clicking on the link below. For privacy
    670 and security, we will automatically delete the file on ###EXPIRATION###,
    671 so please download it before then.
    672 
    673 ###LINK###
    674 
    675 Regards,
    676 All at ###SITENAME###
    677 ###SITEURL###'
    678 	);
    679 
    680 	/**
    681 	 * Filters the text of the email sent with a personal data export file.
    682 	 *
    683 	 * The following strings have a special meaning and will get replaced dynamically:
    684 	 * ###EXPIRATION###         The date when the URL will be automatically deleted.
    685 	 * ###LINK###               URL of the personal data export file for the user.
    686 	 * ###SITENAME###           The name of the site.
    687 	 * ###SITEURL###            The URL to the site.
    688 	 *
    689 	 * @since 4.9.6
    690 	 * @since 5.3.0 Introduced the `$email_data` array.
    691 	 *
    692 	 * @param string $email_text Text in the email.
    693 	 * @param int    $request_id The request ID for this personal data export.
    694 	 * @param array  $email_data {
    695 	 *     Data relating to the account action email.
    696 	 *
    697 	 *     @type WP_User_Request $request           User request object.
    698 	 *     @type int             $expiration        The time in seconds until the export file expires.
    699 	 *     @type string          $expiration_date   The localized date and time when the export file expires.
    700 	 *     @type string          $message_recipient The address that the email will be sent to. Defaults
    701 	 *                                              to the value of `$request->email`, but can be changed
    702 	 *                                              by the `wp_privacy_personal_data_email_to` filter.
    703 	 *     @type string          $export_file_url   The export file URL.
    704 	 *     @type string          $sitename          The site name sending the mail.
    705 	 *     @type string          $siteurl           The site URL sending the mail.
    706 	 */
    707 	$content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id, $email_data );
    708 
    709 	$content = str_replace( '###EXPIRATION###', $expiration_date, $content );
    710 	$content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content );
    711 	$content = str_replace( '###EMAIL###', $request_email, $content );
    712 	$content = str_replace( '###SITENAME###', $site_name, $content );
    713 	$content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content );
    714 
    715 	$headers = '';
    716 
    717 	/**
    718 	 * Filters the headers of the email sent with a personal data export file.
    719 	 *
    720 	 * @since 5.4.0
    721 	 *
    722 	 * @param string|array $headers    The email headers.
    723 	 * @param string       $subject    The email subject.
    724 	 * @param string       $content    The email content.
    725 	 * @param int          $request_id The request ID.
    726 	 * @param array        $email_data {
    727 	 *     Data relating to the account action email.
    728 	 *
    729 	 *     @type WP_User_Request $request           User request object.
    730 	 *     @type int             $expiration        The time in seconds until the export file expires.
    731 	 *     @type string          $expiration_date   The localized date and time when the export file expires.
    732 	 *     @type string          $message_recipient The address that the email will be sent to. Defaults
    733 	 *                                              to the value of `$request->email`, but can be changed
    734 	 *                                              by the `wp_privacy_personal_data_email_to` filter.
    735 	 *     @type string          $export_file_url   The export file URL.
    736 	 *     @type string          $sitename          The site name sending the mail.
    737 	 *     @type string          $siteurl           The site URL sending the mail.
    738 	 * }
    739 	 */
    740 	$headers = apply_filters( 'wp_privacy_personal_data_email_headers', $headers, $subject, $content, $request_id, $email_data );
    741 
    742 	$mail_success = wp_mail( $request_email, $subject, $content, $headers );
    743 
    744 	if ( $switched_locale ) {
    745 		restore_previous_locale();
    746 	}
    747 
    748 	if ( ! $mail_success ) {
    749 		return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) );
    750 	}
    751 
    752 	return true;
    753 }
    754 
    755 /**
    756  * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file.
    757  *
    758  * @since 4.9.6
    759  *
    760  * @see 'wp_privacy_personal_data_export_page'
    761  *
    762  * @param array  $response        The response from the personal data exporter for the given page.
    763  * @param int    $exporter_index  The index of the personal data exporter. Begins at 1.
    764  * @param string $email_address   The email address of the user whose personal data this is.
    765  * @param int    $page            The page of personal data for this exporter. Begins at 1.
    766  * @param int    $request_id      The request ID for this personal data export.
    767  * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
    768  * @param string $exporter_key    The slug (key) of the exporter.
    769  * @return array The filtered response.
    770  */
    771 function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) {
    772 	/* Do some simple checks on the shape of the response from the exporter.
    773 	 * If the exporter response is malformed, don't attempt to consume it - let it
    774 	 * pass through to generate a warning to the user by default Ajax processing.
    775 	 */
    776 	if ( ! is_array( $response ) ) {
    777 		return $response;
    778 	}
    779 
    780 	if ( ! array_key_exists( 'done', $response ) ) {
    781 		return $response;
    782 	}
    783 
    784 	if ( ! array_key_exists( 'data', $response ) ) {
    785 		return $response;
    786 	}
    787 
    788 	if ( ! is_array( $response['data'] ) ) {
    789 		return $response;
    790 	}
    791 
    792 	// Get the request.
    793 	$request = wp_get_user_request( $request_id );
    794 
    795 	if ( ! $request || 'export_personal_data' !== $request->action_name ) {
    796 		wp_send_json_error( __( 'Invalid request ID when merging personal data to export.' ) );
    797 	}
    798 
    799 	$export_data = array();
    800 
    801 	// First exporter, first page? Reset the report data accumulation array.
    802 	if ( 1 === $exporter_index && 1 === $page ) {
    803 		update_post_meta( $request_id, '_export_data_raw', $export_data );
    804 	} else {
    805 		$accumulated_data = get_post_meta( $request_id, '_export_data_raw', true );
    806 
    807 		if ( $accumulated_data ) {
    808 			$export_data = $accumulated_data;
    809 		}
    810 	}
    811 
    812 	// Now, merge the data from the exporter response into the data we have accumulated already.
    813 	$export_data = array_merge( $export_data, $response['data'] );
    814 	update_post_meta( $request_id, '_export_data_raw', $export_data );
    815 
    816 	// If we are not yet on the last page of the last exporter, return now.
    817 	/** This filter is documented in wp-admin/includes/ajax-actions.php */
    818 	$exporters        = apply_filters( 'wp_privacy_personal_data_exporters', array() );
    819 	$is_last_exporter = count( $exporters ) === $exporter_index;
    820 	$exporter_done    = $response['done'];
    821 	if ( ! $is_last_exporter || ! $exporter_done ) {
    822 		return $response;
    823 	}
    824 
    825 	// Last exporter, last page - let's prepare the export file.
    826 
    827 	// First we need to re-organize the raw data hierarchically in groups and items.
    828 	$groups = array();
    829 	foreach ( (array) $export_data as $export_datum ) {
    830 		$group_id    = $export_datum['group_id'];
    831 		$group_label = $export_datum['group_label'];
    832 
    833 		$group_description = '';
    834 		if ( ! empty( $export_datum['group_description'] ) ) {
    835 			$group_description = $export_datum['group_description'];
    836 		}
    837 
    838 		if ( ! array_key_exists( $group_id, $groups ) ) {
    839 			$groups[ $group_id ] = array(
    840 				'group_label'       => $group_label,
    841 				'group_description' => $group_description,
    842 				'items'             => array(),
    843 			);
    844 		}
    845 
    846 		$item_id = $export_datum['item_id'];
    847 		if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) {
    848 			$groups[ $group_id ]['items'][ $item_id ] = array();
    849 		}
    850 
    851 		$old_item_data                            = $groups[ $group_id ]['items'][ $item_id ];
    852 		$merged_item_data                         = array_merge( $export_datum['data'], $old_item_data );
    853 		$groups[ $group_id ]['items'][ $item_id ] = $merged_item_data;
    854 	}
    855 
    856 	// Then save the grouped data into the request.
    857 	delete_post_meta( $request_id, '_export_data_raw' );
    858 	update_post_meta( $request_id, '_export_data_grouped', $groups );
    859 
    860 	/**
    861 	 * Generate the export file from the collected, grouped personal data.
    862 	 *
    863 	 * @since 4.9.6
    864 	 *
    865 	 * @param int $request_id The export request ID.
    866 	 */
    867 	do_action( 'wp_privacy_personal_data_export_file', $request_id );
    868 
    869 	// Clear the grouped data now that it is no longer needed.
    870 	delete_post_meta( $request_id, '_export_data_grouped' );
    871 
    872 	// If the destination is email, send it now.
    873 	if ( $send_as_email ) {
    874 		$mail_success = wp_privacy_send_personal_data_export_email( $request_id );
    875 		if ( is_wp_error( $mail_success ) ) {
    876 			wp_send_json_error( $mail_success->get_error_message() );
    877 		}
    878 
    879 		// Update the request to completed state when the export email is sent.
    880 		_wp_privacy_completed_request( $request_id );
    881 	} else {
    882 		// Modify the response to include the URL of the export file so the browser can fetch it.
    883 		$exports_url      = wp_privacy_exports_url();
    884 		$export_file_name = get_post_meta( $request_id, '_export_file_name', true );
    885 		$export_file_url  = $exports_url . $export_file_name;
    886 
    887 		if ( ! empty( $export_file_url ) ) {
    888 			$response['url'] = $export_file_url;
    889 		}
    890 	}
    891 
    892 	return $response;
    893 }
    894 
    895 /**
    896  * Mark erasure requests as completed after processing is finished.
    897  *
    898  * This intercepts the Ajax responses to personal data eraser page requests, and
    899  * monitors the status of a request. Once all of the processing has finished, the
    900  * request is marked as completed.
    901  *
    902  * @since 4.9.6
    903  *
    904  * @see 'wp_privacy_personal_data_erasure_page'
    905  *
    906  * @param array  $response      The response from the personal data eraser for
    907  *                              the given page.
    908  * @param int    $eraser_index  The index of the personal data eraser. Begins
    909  *                              at 1.
    910  * @param string $email_address The email address of the user whose personal
    911  *                              data this is.
    912  * @param int    $page          The page of personal data for this eraser.
    913  *                              Begins at 1.
    914  * @param int    $request_id    The request ID for this personal data erasure.
    915  * @return array The filtered response.
    916  */
    917 function wp_privacy_process_personal_data_erasure_page( $response, $eraser_index, $email_address, $page, $request_id ) {
    918 	/*
    919 	 * If the eraser response is malformed, don't attempt to consume it; let it
    920 	 * pass through, so that the default Ajax processing will generate a warning
    921 	 * to the user.
    922 	 */
    923 	if ( ! is_array( $response ) ) {
    924 		return $response;
    925 	}
    926 
    927 	if ( ! array_key_exists( 'done', $response ) ) {
    928 		return $response;
    929 	}
    930 
    931 	if ( ! array_key_exists( 'items_removed', $response ) ) {
    932 		return $response;
    933 	}
    934 
    935 	if ( ! array_key_exists( 'items_retained', $response ) ) {
    936 		return $response;
    937 	}
    938 
    939 	if ( ! array_key_exists( 'messages', $response ) ) {
    940 		return $response;
    941 	}
    942 
    943 	// Get the request.
    944 	$request = wp_get_user_request( $request_id );
    945 
    946 	if ( ! $request || 'remove_personal_data' !== $request->action_name ) {
    947 		wp_send_json_error( __( 'Invalid request ID when processing personal data to erase.' ) );
    948 	}
    949 
    950 	/** This filter is documented in wp-admin/includes/ajax-actions.php */
    951 	$erasers        = apply_filters( 'wp_privacy_personal_data_erasers', array() );
    952 	$is_last_eraser = count( $erasers ) === $eraser_index;
    953 	$eraser_done    = $response['done'];
    954 
    955 	if ( ! $is_last_eraser || ! $eraser_done ) {
    956 		return $response;
    957 	}
    958 
    959 	_wp_privacy_completed_request( $request_id );
    960 
    961 	/**
    962 	 * Fires immediately after a personal data erasure request has been marked completed.
    963 	 *
    964 	 * @since 4.9.6
    965 	 *
    966 	 * @param int $request_id The privacy request post ID associated with this request.
    967 	 */
    968 	do_action( 'wp_privacy_personal_data_erased', $request_id );
    969 
    970 	return $response;
    971 }