Index: src/wp-admin/includes/admin-filters.php
===================================================================
--- src/wp-admin/includes/admin-filters.php	(revision 42856)
+++ src/wp-admin/includes/admin-filters.php	(working copy)
@@ -128,3 +128,7 @@
 add_action( 'upgrader_process_complete', 'wp_version_check', 10, 0 );
 add_action( 'upgrader_process_complete', 'wp_update_plugins', 10, 0 );
 add_action( 'upgrader_process_complete', 'wp_update_themes', 10, 0 );
+
+// Privacy hooks
+add_action( 'admin_init', 'wp_privacy_delete_old_export_files' );
+add_filter( 'wp_privacy_personal_data_export_page', 'wp_privacy_process_personal_data_export_page', 10, 4 );
Index: src/wp-admin/includes/file.php
===================================================================
--- src/wp-admin/includes/file.php	(revision 42856)
+++ src/wp-admin/includes/file.php	(working copy)
@@ -1934,3 +1934,174 @@
 	</div>
 	<?php
 }
+
+// TODO phpDocs
+function wp_privacy_generate_personal_data_export( $exports_dir, $email_address, $personal_data ) {
+
+	// TODO Instead of HTML, return a ZIP containing the HTML and the attachments
+	$timestamp = current_time( 'timestamp' );
+	$index_filename = 'wp-personal-data-export-' . md5( $email_address ) . '-' . md5( $timestamp ) . '.html';
+
+	$index_path = trailingslashit( $exports_dir ) . $index_filename;
+	$file = fopen( $index_path, 'w' );
+	// TODO catch fopen error
+
+	fwrite( $file, "<!DOCTYPE html>\n" );
+	fwrite( $file, "<html>\n" );
+	fwrite( $file, "<head>\n" );
+
+	// TODO title using email address
+
+	fwrite( $file, "</head>\n" );
+	fwrite( $file, "<body>\n" );
+
+	// TODO heading using email address and possibly gravatar
+	// TODO heading should also include date/time generated and for what site
+
+	fwrite( $file, "<table>\n" );
+	fwrite( $file, "<tbody>\n" );
+
+	foreach( (array) $personal_data as $personal_datum ) {
+		fwrite( $file, "<tr>\n" );
+		fwrite( $file, "<td>" );
+		fwrite( $file, esc_html( $personal_datum['name'] ) );
+		fwrite( $file, "</td>\n" );
+		fwrite( $file, "<td>" );
+		// TODO kses the value
+		// TODO linkify filepaths from wp-content/uploads
+		fwrite( $file, nl2br( $personal_datum['value'] ) );
+		fwrite( $file, "</td>" );
+		fwrite( $file, "</tr>\n" );
+	}
+
+	fwrite( $file, "</tbody>\n" );
+	fwrite( $file, "</table>\n" );
+	fwrite( $file, "</body>\n" );
+	fwrite( $file, "</html>\n" );
+	fclose( $file );
+
+	// Now, generate the ZIP
+	$archive_filename = 'wp-personal-data-export-' . md5( $email_address ) . '-' . md5( $timestamp ) . '.zip';
+	$archive_path = trailingslashit( $exports_dir ) . $archive_filename;
+
+	$zip = new ZipArchive;
+	// TODO test for no ZipArchive to work with
+
+	if ( TRUE === $zip->open( $archive_path, ZipArchive::CREATE ) ) {
+		$zip->addFile( $index_path, 'index.html' );
+		// TODO - add things referenced in wp-content/uploads
+		$zip->close();
+	} else {
+		error_log( "unable to open zip for creation" );
+		// TODO handle error here
+	}
+
+	// And remove the HTML file
+	unlink( $index_path );
+
+	return(
+		array(
+			'timestamp' => $timestamp,
+			'filename' => $archive_filename,
+		)
+	);
+}
+
+// TODO phpDocs
+function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page ) {
+
+	// Housekeeping
+	$upload_dir  = wp_upload_dir();
+	$exports_dir = $upload_dir['basedir'] . '/exports';
+	$exports_url = $upload_dir['baseurl'] . '/exports';
+
+	// Create the exports folder if needed
+	$result = wp_mkdir_p( $exports_dir );
+	if ( is_wp_error( $result ) ) {
+		return $result;
+	}
+
+	// TODO Make sure the exports folder is protected (htaccess, index)
+
+	// Generate a export file option key from the email address
+	$export_file_option = '_wp_privacy_export_file_' . md5( $email_address );
+
+	// See if we have an export URL cached for this user already
+	$export_file_details = get_site_option( $export_file_option, '', false );
+
+	// And if we do, short circuit the export and send the path back instead
+	if ( ! empty( $export_file_details ) ) {
+		$pieces = explode( ':', $export_file_details ); // path:timestamp
+		$export_file_path = trailingslashit( $exports_dir ) . $pieces[0];
+
+		if ( file_exists( $export_file_path ) ) {
+			return(
+				array(
+					'data' => array(),
+					'done' => true,
+					'url'  => $pieces[0],
+				)
+			);
+		}
+
+		// No file? Delete the option and continue building the export
+		delete_site_option( $export_file_option );
+	}
+
+	// Generate a (temporary) data storage option key from the email address
+	$export_data_option = '_wp_privacy_export_data_' . md5( $email_address );
+
+	// First page of first exporter? Prepare/clear the option where we will assemble all the responses
+	if ( 0 === $exporter_index && 0 === $page ) {
+		update_site_option( $export_data_option, array() );
+	}
+
+	// Grab whatever data has already been collected on previous exporters and/or pages
+	$export_data = get_site_option( $export_data_option, array() );
+
+	// Does the response include data? If so, add this response's data to all the data we have received so far
+	// TODO - make 43438 enforce the data/done/url shape?
+	// TODO - enforce data as a numeric (not associative) array
+	$exporter_page_data = $response['data'];
+	if ( ! empty( $exporter_page_data ) && is_array( $exporter_page_data ) ) {
+		$export_data = array_merge( $export_data, $exporter_page_data );
+		update_site_option( $export_data_option, $export_data );
+	}
+
+	// Are we on the last exporter, and did that export say it was done? If not, return now
+	// so we can continue to collect data for export
+	$exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );
+	$is_last_exporter = $exporter_index === count( $exporters ) - 1;
+	$exporter_done = $response['done'];
+	if ( ! $is_last_exporter || ! $exporter_done ) {
+		return $response;
+	}
+
+	// TODO de-repetitive-ize the data
+
+	// Generate the export
+	$result = wp_privacy_generate_personal_data_export( $exports_dir, $email_address, $export_data );
+	if ( is_wp_error( $result ) ) {
+		return $result;
+	}
+
+	// Get the timestamp and filename from the export
+	$timestamp = $result['timestamp'];
+	$filename = $result['filename'];
+
+	// Build the URL for the file
+	$export_url = trailingslashit( $exports_url ) . $filename;
+
+	// Modify the response to include the URL of the export file
+	$response['url'] = $export_url;
+
+	// TODO Save the export file in an option for safekeeping
+
+	return $response;
+}
+
+function wp_privacy_delete_old_export_files() {
+
+	// TODO delete old export files and their options
+
+}
