WordPress Version: 5.8
/**
* 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 personal data export file. ZipArchive not available.'));
}
// Get the request.
$request = wp_get_user_request($request_id);
if (!$request || 'export_personal_data' !== $request->action_name) {
wp_send_json_error(__('Invalid request ID when generating personal data export file.'));
}
$email_address = $request->email;
if (!is_email($email_address)) {
wp_send_json_error(__('Invalid email address when generating personal data 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 personal data export folder.'));
}
// Protect export folder from browsing.
$index_pathname = $exports_dir . 'index.php';
if (!file_exists($index_pathname)) {
$file = fopen($index_pathname, 'w');
if (false === $file) {
wp_send_json_error(__('Unable to protect personal data export folder from browsing.'));
}
fwrite($file, "<?php\n// Silence is golden.\n");
fclose($file);
}
$obscura = wp_generate_password(32, false, false);
$file_basename = 'wp-personal-data-file-' . $obscura;
$html_report_filename = wp_unique_filename($exports_dir, $file_basename . '.html');
$html_report_pathname = wp_normalize_path($exports_dir . $html_report_filename);
$json_report_filename = $file_basename . '.json';
$json_report_pathname = wp_normalize_path($exports_dir . $json_report_filename);
/*
* Gather general data needed.
*/
// Title.
$title = sprintf(
/* translators: %s: User's email address. */
__('Personal Data Export for %s'),
$email_address
);
// 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'),
/* translators: Description for the About section in a personal data export. */
'group_description' => _x('Overview of export report.', 'personal data group description'),
'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')))),
);
// And now, all the Groups.
$groups = get_post_meta($request_id, '_export_data_grouped', true);
if (is_array($groups)) {
// Merge in the special "About" group.
$groups = array_merge(array('about' => $about_group), $groups);
$groups_count = count($groups);
} else {
if (false !== $groups) {
_doing_it_wrong(
__FUNCTION__,
/* translators: %s: Post meta key. */
sprintf(__('The %s post meta must be an array.'), '<code>_export_data_grouped</code>'),
'5.8.0'
);
}
$groups = null;
$groups_count = 0;
}
// Convert the groups to JSON format.
$groups_json = wp_json_encode($groups);
if (false === $groups_json) {
$error_message = sprintf(
/* translators: %s: Error message. */
__('Unable to encode the personal data for export. Error: %s'),
json_last_error_msg()
);
wp_send_json_error($error_message);
}
/*
* Handle the JSON export.
*/
$file = fopen($json_report_pathname, 'w');
if (false === $file) {
wp_send_json_error(__('Unable to open personal data export file (JSON report) for writing.'));
}
fwrite($file, '{');
fwrite($file, '"' . $title . '":');
fwrite($file, $groups_json);
fwrite($file, '}');
fclose($file);
/*
* Handle the HTML export.
*/
$file = fopen($html_report_pathname, 'w');
if (false === $file) {
wp_send_json_error(__('Unable to open personal data export (HTML report) for writing.'));
}
fwrite($file, "<!DOCTYPE html>\n");
fwrite($file, "<html>\n");
fwrite($file, "<head>\n");
fwrite($file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n");
fwrite($file, "<style type='text/css'>");
fwrite($file, 'body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }');
fwrite($file, 'table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }');
fwrite($file, 'th { padding: 5px; text-align: left; width: 20%; }');
fwrite($file, 'td { padding: 5px; }');
fwrite($file, 'tr:nth-child(odd) { background-color: #fafafa; }');
fwrite($file, '.return-to-top { text-align: right; }');
fwrite($file, '</style>');
fwrite($file, '<title>');
fwrite($file, esc_html($title));
fwrite($file, '</title>');
fwrite($file, "</head>\n");
fwrite($file, "<body>\n");
fwrite($file, '<h1 id="top">' . esc_html__('Personal Data Export') . '</h1>');
// Create TOC.
if ($groups_count > 1) {
fwrite($file, '<div id="table_of_contents">');
fwrite($file, '<h2>' . esc_html__('Table of Contents') . '</h2>');
fwrite($file, '<ul>');
foreach ((array) $groups as $group_id => $group_data) {
$group_label = esc_html($group_data['group_label']);
$group_id_attr = sanitize_title_with_dashes($group_data['group_label'] . '-' . $group_id);
$group_items_count = count((array) $group_data['items']);
if ($group_items_count > 1) {
$group_label .= sprintf(' <span class="count">(%d)</span>', $group_items_count);
}
fwrite($file, '<li>');
fwrite($file, '<a href="#' . esc_attr($group_id_attr) . '">' . $group_label . '</a>');
fwrite($file, '</li>');
}
fwrite($file, '</ul>');
fwrite($file, '</div>');
}
// 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, $group_id, $groups_count));
}
fwrite($file, "</body>\n");
fwrite($file, "</html>\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;
// This meta value is used from version 5.5.
$archive_filename = get_post_meta($request_id, '_export_file_name', true);
// This one stored an absolute path and is used for backward compatibility.
$archive_pathname = get_post_meta($request_id, '_export_file_path', true);
// If a filename meta exists, use it.
if (!empty($archive_filename)) {
$archive_pathname = $exports_dir . $archive_filename;
} elseif (!empty($archive_pathname)) {
// If a full path meta exists, use it and create the new meta value.
$archive_filename = basename($archive_pathname);
update_post_meta($request_id, '_export_file_name', $archive_filename);
// Remove the back-compat meta values.
delete_post_meta($request_id, '_export_file_url');
delete_post_meta($request_id, '_export_file_path');
} else {
// If there's no filename or full path stored, create a new file.
$archive_filename = $file_basename . '.zip';
$archive_pathname = $exports_dir . $archive_filename;
update_post_meta($request_id, '_export_file_name', $archive_filename);
}
$archive_url = $exports_url . $archive_filename;
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($json_report_pathname, 'export.json')) {
$error = __('Unable to archive the personal data export file (JSON format).');
}
if (!$zip->addFile($html_report_pathname, 'index.html')) {
$error = __('Unable to archive the personal data export file (HTML format).');
}
$zip->close();
if (!$error) {
/**
* Fires right after all personal data has been written to the export file.
*
* @since 4.9.6
* @since 5.4.0 Added the `$json_report_pathname` parameter.
*
* @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 HTML personal data report on the filesystem.
* @param int $request_id The export request ID.
* @param string $json_report_pathname The full path to the JSON personal data report on the filesystem.
*/
do_action('wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id, $json_report_pathname);
}
} else {
$error = __('Unable to open personal data export file (archive) for writing.');
}
// Remove the JSON file.
unlink($json_report_pathname);
// Remove the HTML file.
unlink($html_report_pathname);
if ($error) {
wp_send_json_error($error);
}
}