diff --git src/wp-admin/includes/admin-filters.php src/wp-admin/includes/admin-filters.php
index 30da2d0a56..3d3a9f1214 100644
|
|
add_action( 'admin_head', '_ipad_meta' ); |
47 | 47 | |
48 | 48 | // Privacy tools |
49 | 49 | add_action( 'admin_menu', '_wp_privacy_hook_requests_page' ); |
| 50 | add_action( 'admin_init', 'wp_schedule_delete_old_privacy_export_files' ); |
| 51 | add_action( 'wp_privacy_delete_old_export_files', 'wp_privacy_delete_old_export_files' ); |
| 52 | // but will this only run when there's a visit to wp-admin? probalby. can't rely on that |
| 53 | // need to move to default-filters.php and 'init' hook, but where's a good place for `wp_schedule_delete_old_privacy_export_files` to live? |
50 | 54 | |
51 | 55 | // Prerendering. |
52 | 56 | if ( ! is_customize_preview() ) { |
diff --git src/wp-admin/includes/file.php src/wp-admin/includes/file.php
index 8c8d6bc8d2..3e5fa9f8df 100644
|
|
function wp_privacy_generate_personal_data_export_group_html( $group_data ) { |
1999 | 1999 | * @param int $request_id The export request ID. |
2000 | 2000 | */ |
2001 | 2001 | function wp_privacy_generate_personal_data_export_file( $request_id ) { |
2002 | | // Maybe make this a cron job instead. |
2003 | | wp_privacy_delete_old_export_files(); |
2004 | | |
2005 | 2002 | if ( ! class_exists( 'ZipArchive' ) ) { |
2006 | 2003 | wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) ); |
2007 | 2004 | } |
… |
… |
function wp_privacy_generate_personal_data_export_file( $request_id ) { |
2042 | 2039 | |
2043 | 2040 | $stripped_email = str_replace( '@', '-at-', $email_address ); |
2044 | 2041 | $stripped_email = sanitize_title( $stripped_email ); // slugify the email address |
2045 | | $obscura = md5( rand() ); |
| 2042 | $obscura = wp_generate_password( 32, false, false ); |
2046 | 2043 | $file_basename = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura; |
2047 | 2044 | $html_report_filename = $file_basename . '.html'; |
2048 | 2045 | $html_report_pathname = $exports_dir . $html_report_filename; |
… |
… |
function wp_privacy_send_personal_data_export_email( $request_id ) { |
2162 | 2159 | return new WP_Error( 'invalid', __( 'Invalid request ID when sending personal data export email.' ) ); |
2163 | 2160 | } |
2164 | 2161 | |
2165 | | /* translators: Do not translate LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */ |
2166 | | $email_text = __( |
2167 | | 'Howdy, |
| 2162 | /** This filter is documented in wp-admin/includes/file.php */ |
| 2163 | $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS ); |
| 2164 | $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration ); |
2168 | 2165 | |
2169 | | Your request for an export of personal data has been completed. You may |
2170 | | download your personal data by clicking on the link below. This link is |
2171 | | good for the next 3 days. |
| 2166 | // need to write any new tests for stuff you're adding? |
| 2167 | // were tests added for all existing stuff? if not, add some, or at least mark ticket as needs-unit-tests |
2172 | 2168 | |
2173 | | ###LINK### |
| 2169 | /* translators: Do not translate EXPIRATION, LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */ |
| 2170 | $email_text = str_replace( "\t", '', __( |
| 2171 | 'Howdy, |
2174 | 2172 | |
2175 | | This email has been sent to ###EMAIL###. |
| 2173 | Your request for an export of personal data has been completed. You may |
| 2174 | download your personal data by clicking on the link below. For privacy |
| 2175 | and security, we will automatically delete the file on ###EXPIRATION###, |
| 2176 | so please download it before then. |
2176 | 2177 | |
2177 | | Regards, |
2178 | | All at ###SITENAME### |
2179 | | ###SITEURL###' |
2180 | | ); |
| 2178 | ###LINK### |
| 2179 | |
| 2180 | This email has been sent to ###EMAIL###. |
| 2181 | |
| 2182 | Regards, |
| 2183 | All at ###SITENAME### |
| 2184 | ###SITEURL###' |
| 2185 | ) ); |
2181 | 2186 | |
2182 | 2187 | /** |
2183 | 2188 | * Filters the text of the email sent with a personal data export file. |
… |
… |
All at ###SITENAME### |
2200 | 2205 | $site_name = is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' ); |
2201 | 2206 | $site_url = network_home_url(); |
2202 | 2207 | |
| 2208 | $content = str_replace( '###EXPIRATION###', $expiration_date, $content ); |
2203 | 2209 | $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content ); |
2204 | 2210 | $content = str_replace( '###EMAIL###', $email_address, $content ); |
2205 | 2211 | $content = str_replace( '###SITENAME###', wp_specialchars_decode( $site_name, ENT_QUOTES ), $content ); |
… |
… |
function wp_privacy_process_personal_data_export_page( $response, $exporter_inde |
2345 | 2351 | return $response; |
2346 | 2352 | } |
2347 | 2353 | |
| 2354 | /** |
| 2355 | * Schedule a `WP_Cron` job to delete expired export files. |
| 2356 | * |
| 2357 | * @since 4.9.6 |
| 2358 | */ |
| 2359 | function wp_schedule_delete_old_privacy_export_files() { |
| 2360 | if ( ! wp_next_scheduled( 'wp_privacy_delete_old_export_files' ) ) { |
| 2361 | wp_schedule_event( time(), 'hourly', 'wp_privacy_delete_old_export_files' ); |
| 2362 | } |
| 2363 | } |
| 2364 | |
2348 | 2365 | /** |
2349 | 2366 | * Cleans up export files older than three days old. |
2350 | 2367 | * |
| 2368 | * The export files are stored in `wp-content/uploads`, and are therefore publicly |
| 2369 | * accessible. A CSPRN is appended to the filename to mitigate the risk of an |
| 2370 | * unauthorized person downloading the file, but it is still possible. Deleting |
| 2371 | * the file after the data subject has had a chance to delete it adds an additional |
| 2372 | * layer of protection. |
| 2373 | * |
2351 | 2374 | * @since 4.9.6 |
2352 | 2375 | */ |
2353 | 2376 | function wp_privacy_delete_old_export_files() { |
2354 | 2377 | $upload_dir = wp_upload_dir(); |
2355 | 2378 | $exports_dir = trailingslashit( $upload_dir['basedir'] . '/exports' ); |
2356 | | $export_files = list_files( $exports_dir ); |
| 2379 | $export_files = list_files( $exports_dir, 100, array( 'index.html' ) ); |
| 2380 | |
| 2381 | /** |
| 2382 | * Filters the lifetime, in seconds, of a personal data export file. |
| 2383 | * |
| 2384 | * By default, the lifetime is 3 days. Once the file reaches that age, it will automatically |
| 2385 | * be deleted by a cron job. |
| 2386 | * |
| 2387 | * @since 4.9.6 |
| 2388 | * |
| 2389 | * @param int $expiration The expiration age of the export, in seconds. |
| 2390 | */ |
| 2391 | $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS ); |
2357 | 2392 | |
2358 | 2393 | foreach( (array) $export_files as $export_file ) { |
2359 | 2394 | $file_age_in_seconds = time() - filemtime( $export_file ); |
2360 | 2395 | |
2361 | | if ( 3 * DAY_IN_SECONDS < $file_age_in_seconds ) { |
2362 | | @unlink( $export_file ); |
| 2396 | if ( $expiration < $file_age_in_seconds ) { |
| 2397 | unlink( $export_file ); |
2363 | 2398 | } |
2364 | 2399 | } |
2365 | 2400 | } |
diff --git tests/phpunit/tests/comment.php tests/phpunit/tests/comment.php
index 29453179e4..bb7ed60d39 100644
|
|
class Tests_Comment extends WP_UnitTestCase { |
1116 | 1116 | /** |
1117 | 1117 | * Testing the `wp_comments_personal_data_exporter()` function. |
1118 | 1118 | * |
| 1119 | * @group privacy |
1119 | 1120 | * @ticket 43440 |
1120 | 1121 | */ |
1121 | 1122 | public function test_wp_comments_personal_data_exporter() { |
… |
… |
class Tests_Comment extends WP_UnitTestCase { |
1130 | 1131 | 'comment_content' => 'Comment', |
1131 | 1132 | ); |
1132 | 1133 | |
1133 | | $c = self::factory()->comment->create( $args ); |
| 1134 | $comment_id = self::factory()->comment->create( $args ); |
1134 | 1135 | |
1135 | 1136 | $actual = wp_comments_personal_data_exporter( $args['comment_author_email'] ); |
1136 | 1137 | $expected = $args; |
… |
… |
class Tests_Comment extends WP_UnitTestCase { |
1155 | 1156 | $this->assertSame( $expected['comment_agent'], $actual['data'][0]['data'][4]['value'] ); |
1156 | 1157 | $this->assertSame( $expected['comment_date'], $actual['data'][0]['data'][5]['value'] ); |
1157 | 1158 | $this->assertSame( $expected['comment_content'], $actual['data'][0]['data'][6]['value'] ); |
1158 | | $this->assertSame( get_comment_link( $c ), $actual['data'][0]['data'][7]['value'] ); |
| 1159 | $this->assertSame( get_comment_link( $comment_id ), strip_tags( $actual['data'][0]['data'][7]['value'] ) ); |
1159 | 1160 | } |
1160 | 1161 | |
1161 | 1162 | /** |
1162 | 1163 | * Testing the `wp_comments_personal_data_exporter()` function for no comments found. |
1163 | 1164 | * |
| 1165 | * @group privacy |
1164 | 1166 | * @ticket 43440 |
1165 | 1167 | */ |
1166 | 1168 | public function test_wp_comments_personal_data_exporter_no_comments_found() { |
… |
… |
class Tests_Comment extends WP_UnitTestCase { |
1178 | 1180 | /** |
1179 | 1181 | * Testing the `wp_comments_personal_data_exporter()` function for an empty comment property. |
1180 | 1182 | * |
| 1183 | * @group privacy |
1181 | 1184 | * @ticket 43440 |
1182 | 1185 | */ |
1183 | 1186 | public function test_wp_comments_personal_data_exporter_empty_comment_prop() { |
… |
… |
class Tests_Comment extends WP_UnitTestCase { |
1208 | 1211 | /** |
1209 | 1212 | * Testing the `wp_comments_personal_data_exporter()` function with an empty second page. |
1210 | 1213 | * |
| 1214 | * @group privacy |
1211 | 1215 | * @ticket 43440 |
1212 | 1216 | */ |
1213 | 1217 | public function test_wp_comments_personal_data_exporter_empty_second_page() { |