Ticket #43895: 43895.3.diff
File 43895.3.diff, 37.9 KB (added by , 6 years ago) |
---|
-
src/wp-admin/includes/file.php
2194 2194 </div> 2195 2195 <?php 2196 2196 } 2197 2198 /**2199 * Generate a single group for the personal data export report.2200 *2201 * @since 4.9.62202 *2203 * @param array $group_data {2204 * The group data to render.2205 *2206 * @type string $group_label The user-facing heading for the group, e.g. 'Comments'.2207 * @type array $items {2208 * An array of group items.2209 *2210 * @type array $group_item_data {2211 * An array of name-value pairs for the item.2212 *2213 * @type string $name The user-facing name of an item name-value pair, e.g. 'IP Address'.2214 * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'.2215 * }2216 * }2217 * }2218 * @return string The HTML for this group and its items.2219 */2220 function wp_privacy_generate_personal_data_export_group_html( $group_data ) {2221 $group_html = '<h2>' . esc_html( $group_data['group_label'] ) . '</h2>';2222 $group_html .= '<div>';2223 2224 foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) {2225 $group_html .= '<table>';2226 $group_html .= '<tbody>';2227 2228 foreach ( (array) $group_item_data as $group_item_datum ) {2229 $value = $group_item_datum['value'];2230 // If it looks like a link, make it a link.2231 if ( false === strpos( $value, ' ' ) && ( 0 === strpos( $value, 'http://' ) || 0 === strpos( $value, 'https://' ) ) ) {2232 $value = '<a href="' . esc_url( $value ) . '">' . esc_html( $value ) . '</a>';2233 }2234 2235 $group_html .= '<tr>';2236 $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>';2237 $group_html .= '<td>' . wp_kses( $value, 'personal_data_export' ) . '</td>';2238 $group_html .= '</tr>';2239 }2240 2241 $group_html .= '</tbody>';2242 $group_html .= '</table>';2243 }2244 2245 $group_html .= '</div>';2246 2247 return $group_html;2248 }2249 2250 /**2251 * Generate the personal data export file.2252 *2253 * @since 4.9.62254 *2255 * @param int $request_id The export request ID.2256 */2257 function wp_privacy_generate_personal_data_export_file( $request_id ) {2258 if ( ! class_exists( 'ZipArchive' ) ) {2259 wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) );2260 }2261 2262 // Get the request data.2263 $request = wp_get_user_request_data( $request_id );2264 2265 if ( ! $request || 'export_personal_data' !== $request->action_name ) {2266 wp_send_json_error( __( 'Invalid request ID when generating export file.' ) );2267 }2268 2269 $email_address = $request->email;2270 2271 if ( ! is_email( $email_address ) ) {2272 wp_send_json_error( __( 'Invalid email address when generating export file.' ) );2273 }2274 2275 // Create the exports folder if needed.2276 $exports_dir = wp_privacy_exports_dir();2277 $exports_url = wp_privacy_exports_url();2278 2279 if ( ! wp_mkdir_p( $exports_dir ) ) {2280 wp_send_json_error( __( 'Unable to create export folder.' ) );2281 }2282 2283 // Protect export folder from browsing.2284 $index_pathname = $exports_dir . 'index.html';2285 if ( ! file_exists( $index_pathname ) ) {2286 $file = fopen( $index_pathname, 'w' );2287 if ( false === $file ) {2288 wp_send_json_error( __( 'Unable to protect export folder from browsing.' ) );2289 }2290 fwrite( $file, '<!-- Silence is golden. -->' );2291 fclose( $file );2292 }2293 2294 $stripped_email = str_replace( '@', '-at-', $email_address );2295 $stripped_email = sanitize_title( $stripped_email ); // slugify the email address2296 $obscura = wp_generate_password( 32, false, false );2297 $file_basename = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura;2298 $html_report_filename = $file_basename . '.html';2299 $html_report_pathname = wp_normalize_path( $exports_dir . $html_report_filename );2300 $file = fopen( $html_report_pathname, 'w' );2301 if ( false === $file ) {2302 wp_send_json_error( __( 'Unable to open export file (HTML report) for writing.' ) );2303 }2304 2305 $title = sprintf(2306 /* translators: %s: user's email address */2307 __( 'Personal Data Export for %s' ),2308 $email_address2309 );2310 2311 // Open HTML.2312 fwrite( $file, "<!DOCTYPE html>\n" );2313 fwrite( $file, "<html>\n" );2314 2315 // Head.2316 fwrite( $file, "<head>\n" );2317 fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" );2318 fwrite( $file, "<style type='text/css'>" );2319 fwrite( $file, 'body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }' );2320 fwrite( $file, 'table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }' );2321 fwrite( $file, 'th { padding: 5px; text-align: left; width: 20%; }' );2322 fwrite( $file, 'td { padding: 5px; }' );2323 fwrite( $file, 'tr:nth-child(odd) { background-color: #fafafa; }' );2324 fwrite( $file, '</style>' );2325 fwrite( $file, '<title>' );2326 fwrite( $file, esc_html( $title ) );2327 fwrite( $file, '</title>' );2328 fwrite( $file, "</head>\n" );2329 2330 // Body.2331 fwrite( $file, "<body>\n" );2332 2333 // Heading.2334 fwrite( $file, '<h1>' . esc_html__( 'Personal Data Export' ) . '</h1>' );2335 2336 // And now, all the Groups.2337 $groups = get_post_meta( $request_id, '_export_data_grouped', true );2338 2339 // First, build an "About" group on the fly for this report.2340 $about_group = array(2341 /* translators: Header for the About section in a personal data export. */2342 'group_label' => _x( 'About', 'personal data group label' ),2343 'items' => array(2344 'about-1' => array(2345 array(2346 'name' => _x( 'Report generated for', 'email address' ),2347 'value' => $email_address,2348 ),2349 array(2350 'name' => _x( 'For site', 'website name' ),2351 'value' => get_bloginfo( 'name' ),2352 ),2353 array(2354 'name' => _x( 'At URL', 'website URL' ),2355 'value' => get_bloginfo( 'url' ),2356 ),2357 array(2358 'name' => _x( 'On', 'date/time' ),2359 'value' => current_time( 'mysql' ),2360 ),2361 ),2362 ),2363 );2364 2365 // Merge in the special about group.2366 $groups = array_merge( array( 'about' => $about_group ), $groups );2367 2368 // Now, iterate over every group in $groups and have the formatter render it in HTML.2369 foreach ( (array) $groups as $group_id => $group_data ) {2370 fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data ) );2371 }2372 2373 fwrite( $file, "</body>\n" );2374 2375 // Close HTML.2376 fwrite( $file, "</html>\n" );2377 fclose( $file );2378 2379 /*2380 * Now, generate the ZIP.2381 *2382 * If an archive has already been generated, then remove it and reuse the2383 * filename, to avoid breaking any URLs that may have been previously sent2384 * via email.2385 */2386 $error = false;2387 $archive_url = get_post_meta( $request_id, '_export_file_url', true );2388 $archive_pathname = get_post_meta( $request_id, '_export_file_path', true );2389 2390 if ( empty( $archive_pathname ) || empty( $archive_url ) ) {2391 $archive_filename = $file_basename . '.zip';2392 $archive_pathname = $exports_dir . $archive_filename;2393 $archive_url = $exports_url . $archive_filename;2394 2395 update_post_meta( $request_id, '_export_file_url', $archive_url );2396 update_post_meta( $request_id, '_export_file_path', wp_normalize_path( $archive_pathname ) );2397 }2398 2399 if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) {2400 wp_delete_file( $archive_pathname );2401 }2402 2403 $zip = new ZipArchive;2404 if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) {2405 if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) {2406 $error = __( 'Unable to add data to export file.' );2407 }2408 2409 $zip->close();2410 2411 if ( ! $error ) {2412 /**2413 * Fires right after all personal data has been written to the export file.2414 *2415 * @since 4.9.62416 *2417 * @param string $archive_pathname The full path to the export file on the filesystem.2418 * @param string $archive_url The URL of the archive file.2419 * @param string $html_report_pathname The full path to the personal data report on the filesystem.2420 * @param int $request_id The export request ID.2421 */2422 do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id );2423 }2424 } else {2425 $error = __( 'Unable to open export file (archive) for writing.' );2426 }2427 2428 // And remove the HTML file.2429 unlink( $html_report_pathname );2430 2431 if ( $error ) {2432 wp_send_json_error( $error );2433 }2434 }2435 2436 /**2437 * Send an email to the user with a link to the personal data export file2438 *2439 * @since 4.9.62440 *2441 * @param int $request_id The request ID for this personal data export.2442 * @return true|WP_Error True on success or `WP_Error` on failure.2443 */2444 function wp_privacy_send_personal_data_export_email( $request_id ) {2445 // Get the request data.2446 $request = wp_get_user_request_data( $request_id );2447 2448 if ( ! $request || 'export_personal_data' !== $request->action_name ) {2449 return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) );2450 }2451 2452 // Localize message content for user; fallback to site default for visitors.2453 if ( ! empty( $request->user_id ) ) {2454 $locale = get_user_locale( $request->user_id );2455 } else {2456 $locale = get_locale();2457 }2458 2459 $switched_locale = switch_to_locale( $locale );2460 2461 /** This filter is documented in wp-includes/functions.php */2462 $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );2463 $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration );2464 2465 /* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */2466 $email_text = __(2467 'Howdy,2468 2469 Your request for an export of personal data has been completed. You may2470 download your personal data by clicking on the link below. For privacy2471 and security, we will automatically delete the file on ###EXPIRATION###,2472 so please download it before then.2473 2474 ###LINK###2475 2476 Regards,2477 All at ###SITENAME###2478 ###SITEURL###'2479 );2480 2481 /**2482 * Filters the text of the email sent with a personal data export file.2483 *2484 * The following strings have a special meaning and will get replaced dynamically:2485 * ###EXPIRATION### The date when the URL will be automatically deleted.2486 * ###LINK### URL of the personal data export file for the user.2487 * ###SITENAME### The name of the site.2488 * ###SITEURL### The URL to the site.2489 *2490 * @since 4.9.62491 *2492 * @param string $email_text Text in the email.2493 * @param int $request_id The request ID for this personal data export.2494 */2495 $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id );2496 2497 $email_address = $request->email;2498 $export_file_url = get_post_meta( $request_id, '_export_file_url', true );2499 $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );2500 $site_url = home_url();2501 2502 $content = str_replace( '###EXPIRATION###', $expiration_date, $content );2503 $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content );2504 $content = str_replace( '###EMAIL###', $email_address, $content );2505 $content = str_replace( '###SITENAME###', $site_name, $content );2506 $content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content );2507 2508 $mail_success = wp_mail(2509 $email_address,2510 sprintf(2511 /* translators: Personal data export notification email subject. %s: Site title */2512 __( '[%s] Personal Data Export' ),2513 $site_name2514 ),2515 $content2516 );2517 2518 if ( $switched_locale ) {2519 restore_previous_locale();2520 }2521 2522 if ( ! $mail_success ) {2523 return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) );2524 }2525 2526 return true;2527 }2528 2529 /**2530 * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file.2531 * @see wp_privacy_personal_data_export_page2532 * @since 4.9.62533 *2534 * @param array $response The response from the personal data exporter for the given page.2535 * @param int $exporter_index The index of the personal data exporter. Begins at 1.2536 * @param string $email_address The email address of the user whose personal data this is.2537 * @param int $page The page of personal data for this exporter. Begins at 1.2538 * @param int $request_id The request ID for this personal data export.2539 * @param bool $send_as_email Whether the final results of the export should be emailed to the user.2540 * @param string $exporter_key The slug (key) of the exporter.2541 * @return array The filtered response.2542 */2543 function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) {2544 /* Do some simple checks on the shape of the response from the exporter.2545 * If the exporter response is malformed, don't attempt to consume it - let it2546 * pass through to generate a warning to the user by default Ajax processing.2547 */2548 if ( ! is_array( $response ) ) {2549 return $response;2550 }2551 2552 if ( ! array_key_exists( 'done', $response ) ) {2553 return $response;2554 }2555 2556 if ( ! array_key_exists( 'data', $response ) ) {2557 return $response;2558 }2559 2560 if ( ! is_array( $response['data'] ) ) {2561 return $response;2562 }2563 2564 // Get the request data.2565 $request = wp_get_user_request_data( $request_id );2566 2567 if ( ! $request || 'export_personal_data' !== $request->action_name ) {2568 wp_send_json_error( __( 'Invalid request ID when merging exporter data.' ) );2569 }2570 2571 $export_data = array();2572 2573 // First exporter, first page? Reset the report data accumulation array.2574 if ( 1 === $exporter_index && 1 === $page ) {2575 update_post_meta( $request_id, '_export_data_raw', $export_data );2576 } else {2577 $export_data = get_post_meta( $request_id, '_export_data_raw', true );2578 }2579 2580 // Now, merge the data from the exporter response into the data we have accumulated already.2581 $export_data = array_merge( $export_data, $response['data'] );2582 update_post_meta( $request_id, '_export_data_raw', $export_data );2583 2584 // If we are not yet on the last page of the last exporter, return now.2585 /** This filter is documented in wp-admin/includes/ajax-actions.php */2586 $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );2587 $is_last_exporter = $exporter_index === count( $exporters );2588 $exporter_done = $response['done'];2589 if ( ! $is_last_exporter || ! $exporter_done ) {2590 return $response;2591 }2592 2593 // Last exporter, last page - let's prepare the export file.2594 2595 // First we need to re-organize the raw data hierarchically in groups and items.2596 $groups = array();2597 foreach ( (array) $export_data as $export_datum ) {2598 $group_id = $export_datum['group_id'];2599 $group_label = $export_datum['group_label'];2600 if ( ! array_key_exists( $group_id, $groups ) ) {2601 $groups[ $group_id ] = array(2602 'group_label' => $group_label,2603 'items' => array(),2604 );2605 }2606 2607 $item_id = $export_datum['item_id'];2608 if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) {2609 $groups[ $group_id ]['items'][ $item_id ] = array();2610 }2611 2612 $old_item_data = $groups[ $group_id ]['items'][ $item_id ];2613 $merged_item_data = array_merge( $export_datum['data'], $old_item_data );2614 $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data;2615 }2616 2617 // Then save the grouped data into the request.2618 delete_post_meta( $request_id, '_export_data_raw' );2619 update_post_meta( $request_id, '_export_data_grouped', $groups );2620 2621 /**2622 * Generate the export file from the collected, grouped personal data.2623 *2624 * @since 4.9.62625 *2626 * @param int $request_id The export request ID.2627 */2628 do_action( 'wp_privacy_personal_data_export_file', $request_id );2629 2630 // Clear the grouped data now that it is no longer needed.2631 delete_post_meta( $request_id, '_export_data_grouped' );2632 2633 // If the destination is email, send it now.2634 if ( $send_as_email ) {2635 $mail_success = wp_privacy_send_personal_data_export_email( $request_id );2636 if ( is_wp_error( $mail_success ) ) {2637 wp_send_json_error( $mail_success->get_error_message() );2638 }2639 2640 // Update the request to completed state when the export email is sent.2641 _wp_privacy_completed_request( $request_id );2642 } else {2643 // Modify the response to include the URL of the export file so the browser can fetch it.2644 $export_file_url = get_post_meta( $request_id, '_export_file_url', true );2645 if ( ! empty( $export_file_url ) ) {2646 $response['url'] = $export_file_url;2647 }2648 }2649 2650 return $response;2651 } -
src/wp-admin/includes/privacy-tools.php
212 212 } 213 213 214 214 /** 215 * Generate a single group for the personal data export report. 216 * 217 * @since 4.9.6 218 * 219 * @param array $group_data { 220 * The group data to render. 221 * 222 * @type string $group_label The user-facing heading for the group, e.g. 'Comments'. 223 * @type array $items { 224 * An array of group items. 225 * 226 * @type array $group_item_data { 227 * An array of name-value pairs for the item. 228 * 229 * @type string $name The user-facing name of an item name-value pair, e.g. 'IP Address'. 230 * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'. 231 * } 232 * } 233 * } 234 * @return string The HTML for this group and its items. 235 */ 236 function wp_privacy_generate_personal_data_export_group_html( $group_data ) { 237 $group_html = '<h2>' . esc_html( $group_data['group_label'] ) . '</h2>'; 238 $group_html .= '<div>'; 239 240 foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) { 241 $group_html .= '<table>'; 242 $group_html .= '<tbody>'; 243 244 foreach ( (array) $group_item_data as $group_item_datum ) { 245 $value = $group_item_datum['value']; 246 // If it looks like a link, make it a link. 247 if ( false === strpos( $value, ' ' ) && ( 0 === strpos( $value, 'http://' ) || 0 === strpos( $value, 'https://' ) ) ) { 248 $value = '<a href="' . esc_url( $value ) . '">' . esc_html( $value ) . '</a>'; 249 } 250 251 $group_html .= '<tr>'; 252 $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>'; 253 $group_html .= '<td>' . wp_kses( $value, 'personal_data_export' ) . '</td>'; 254 $group_html .= '</tr>'; 255 } 256 257 $group_html .= '</tbody>'; 258 $group_html .= '</table>'; 259 } 260 261 $group_html .= '</div>'; 262 263 return $group_html; 264 } 265 266 /** 267 * Generate the personal data export file. 268 * 269 * @since 4.9.6 270 * 271 * @param int $request_id The export request ID. 272 */ 273 function wp_privacy_generate_personal_data_export_file( $request_id ) { 274 if ( ! class_exists( 'ZipArchive' ) ) { 275 wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) ); 276 } 277 278 // Get the request data. 279 $request = wp_get_user_request_data( $request_id ); 280 281 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 282 wp_send_json_error( __( 'Invalid request ID when generating export file.' ) ); 283 } 284 285 $email_address = $request->email; 286 287 if ( ! is_email( $email_address ) ) { 288 wp_send_json_error( __( 'Invalid email address when generating export file.' ) ); 289 } 290 291 // Create the exports folder if needed. 292 $exports_dir = wp_privacy_exports_dir(); 293 $exports_url = wp_privacy_exports_url(); 294 295 if ( ! wp_mkdir_p( $exports_dir ) ) { 296 wp_send_json_error( __( 'Unable to create export folder.' ) ); 297 } 298 299 // Protect export folder from browsing. 300 $index_pathname = $exports_dir . 'index.html'; 301 if ( ! file_exists( $index_pathname ) ) { 302 $file = fopen( $index_pathname, 'w' ); 303 if ( false === $file ) { 304 wp_send_json_error( __( 'Unable to protect export folder from browsing.' ) ); 305 } 306 fwrite( $file, '<!-- Silence is golden. -->' ); 307 fclose( $file ); 308 } 309 310 $stripped_email = str_replace( '@', '-at-', $email_address ); 311 $stripped_email = sanitize_title( $stripped_email ); // slugify the email address 312 $obscura = wp_generate_password( 32, false, false ); 313 $file_basename = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura; 314 $html_report_filename = $file_basename . '.html'; 315 $html_report_pathname = wp_normalize_path( $exports_dir . $html_report_filename ); 316 $file = fopen( $html_report_pathname, 'w' ); 317 if ( false === $file ) { 318 wp_send_json_error( __( 'Unable to open export file (HTML report) for writing.' ) ); 319 } 320 321 $title = sprintf( 322 /* translators: %s: user's email address */ 323 __( 'Personal Data Export for %s' ), 324 $email_address 325 ); 326 327 // Open HTML. 328 fwrite( $file, "<!DOCTYPE html>\n" ); 329 fwrite( $file, "<html>\n" ); 330 331 // Head. 332 fwrite( $file, "<head>\n" ); 333 fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" ); 334 fwrite( $file, "<style type='text/css'>" ); 335 fwrite( $file, 'body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }' ); 336 fwrite( $file, 'table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }' ); 337 fwrite( $file, 'th { padding: 5px; text-align: left; width: 20%; }' ); 338 fwrite( $file, 'td { padding: 5px; }' ); 339 fwrite( $file, 'tr:nth-child(odd) { background-color: #fafafa; }' ); 340 fwrite( $file, '</style>' ); 341 fwrite( $file, '<title>' ); 342 fwrite( $file, esc_html( $title ) ); 343 fwrite( $file, '</title>' ); 344 fwrite( $file, "</head>\n" ); 345 346 // Body. 347 fwrite( $file, "<body>\n" ); 348 349 // Heading. 350 fwrite( $file, '<h1>' . esc_html__( 'Personal Data Export' ) . '</h1>' ); 351 352 // And now, all the Groups. 353 $groups = get_post_meta( $request_id, '_export_data_grouped', true ); 354 355 // First, build an "About" group on the fly for this report. 356 $about_group = array( 357 /* translators: Header for the About section in a personal data export. */ 358 'group_label' => _x( 'About', 'personal data group label' ), 359 'items' => array( 360 'about-1' => array( 361 array( 362 'name' => _x( 'Report generated for', 'email address' ), 363 'value' => $email_address, 364 ), 365 array( 366 'name' => _x( 'For site', 'website name' ), 367 'value' => get_bloginfo( 'name' ), 368 ), 369 array( 370 'name' => _x( 'At URL', 'website URL' ), 371 'value' => get_bloginfo( 'url' ), 372 ), 373 array( 374 'name' => _x( 'On', 'date/time' ), 375 'value' => current_time( 'mysql' ), 376 ), 377 ), 378 ), 379 ); 380 381 // Merge in the special about group. 382 $groups = array_merge( array( 'about' => $about_group ), $groups ); 383 384 // Now, iterate over every group in $groups and have the formatter render it in HTML. 385 foreach ( (array) $groups as $group_id => $group_data ) { 386 fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data ) ); 387 } 388 389 fwrite( $file, "</body>\n" ); 390 391 // Close HTML. 392 fwrite( $file, "</html>\n" ); 393 fclose( $file ); 394 395 /* 396 * Now, generate the ZIP. 397 * 398 * If an archive has already been generated, then remove it and reuse the 399 * filename, to avoid breaking any URLs that may have been previously sent 400 * via email. 401 */ 402 $error = false; 403 $archive_url = get_post_meta( $request_id, '_export_file_url', true ); 404 $archive_pathname = get_post_meta( $request_id, '_export_file_path', true ); 405 406 if ( empty( $archive_pathname ) || empty( $archive_url ) ) { 407 $archive_filename = $file_basename . '.zip'; 408 $archive_pathname = $exports_dir . $archive_filename; 409 $archive_url = $exports_url . $archive_filename; 410 411 update_post_meta( $request_id, '_export_file_url', $archive_url ); 412 update_post_meta( $request_id, '_export_file_path', wp_normalize_path( $archive_pathname ) ); 413 } 414 415 if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) { 416 wp_delete_file( $archive_pathname ); 417 } 418 419 $zip = new ZipArchive; 420 if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) { 421 if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) { 422 $error = __( 'Unable to add data to export file.' ); 423 } 424 425 $zip->close(); 426 427 if ( ! $error ) { 428 /** 429 * Fires right after all personal data has been written to the export file. 430 * 431 * @since 4.9.6 432 * 433 * @param string $archive_pathname The full path to the export file on the filesystem. 434 * @param string $archive_url The URL of the archive file. 435 * @param string $html_report_pathname The full path to the personal data report on the filesystem. 436 * @param int $request_id The export request ID. 437 */ 438 do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id ); 439 } 440 } else { 441 $error = __( 'Unable to open export file (archive) for writing.' ); 442 } 443 444 // And remove the HTML file. 445 unlink( $html_report_pathname ); 446 447 if ( $error ) { 448 wp_send_json_error( $error ); 449 } 450 } 451 452 /** 453 * Send an email to the user with a link to the personal data export file 454 * 455 * @since 4.9.6 456 * 457 * @param int $request_id The request ID for this personal data export. 458 * @return true|WP_Error True on success or `WP_Error` on failure. 459 */ 460 function wp_privacy_send_personal_data_export_email( $request_id ) { 461 // Get the request data. 462 $request = wp_get_user_request_data( $request_id ); 463 464 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 465 return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) ); 466 } 467 468 // Localize message content for user; fallback to site default for visitors. 469 if ( ! empty( $request->user_id ) ) { 470 $locale = get_user_locale( $request->user_id ); 471 } else { 472 $locale = get_locale(); 473 } 474 475 $switched_locale = switch_to_locale( $locale ); 476 477 /** This filter is documented in wp-includes/functions.php */ 478 $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS ); 479 $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration ); 480 481 /* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */ 482 $email_text = __( 483 'Howdy, 484 485 Your request for an export of personal data has been completed. You may 486 download your personal data by clicking on the link below. For privacy 487 and security, we will automatically delete the file on ###EXPIRATION###, 488 so please download it before then. 489 490 ###LINK### 491 492 Regards, 493 All at ###SITENAME### 494 ###SITEURL###' 495 ); 496 497 /** 498 * Filters the text of the email sent with a personal data export file. 499 * 500 * The following strings have a special meaning and will get replaced dynamically: 501 * ###EXPIRATION### The date when the URL will be automatically deleted. 502 * ###LINK### URL of the personal data export file for the user. 503 * ###SITENAME### The name of the site. 504 * ###SITEURL### The URL to the site. 505 * 506 * @since 4.9.6 507 * 508 * @param string $email_text Text in the email. 509 * @param int $request_id The request ID for this personal data export. 510 */ 511 $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id ); 512 513 $email_address = $request->email; 514 $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); 515 $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); 516 $site_url = home_url(); 517 518 $content = str_replace( '###EXPIRATION###', $expiration_date, $content ); 519 $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content ); 520 $content = str_replace( '###EMAIL###', $email_address, $content ); 521 $content = str_replace( '###SITENAME###', $site_name, $content ); 522 $content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content ); 523 524 $mail_success = wp_mail( 525 $email_address, 526 sprintf( 527 /* translators: Personal data export notification email subject. %s: Site title */ 528 __( '[%s] Personal Data Export' ), 529 $site_name 530 ), 531 $content 532 ); 533 534 if ( $switched_locale ) { 535 restore_previous_locale(); 536 } 537 538 if ( ! $mail_success ) { 539 return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) ); 540 } 541 542 return true; 543 } 544 545 /** 546 * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file. 547 * @see wp_privacy_personal_data_export_page 548 * @since 4.9.6 549 * 550 * @param array $response The response from the personal data exporter for the given page. 551 * @param int $exporter_index The index of the personal data exporter. Begins at 1. 552 * @param string $email_address The email address of the user whose personal data this is. 553 * @param int $page The page of personal data for this exporter. Begins at 1. 554 * @param int $request_id The request ID for this personal data export. 555 * @param bool $send_as_email Whether the final results of the export should be emailed to the user. 556 * @param string $exporter_key The slug (key) of the exporter. 557 * @return array The filtered response. 558 */ 559 function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) { 560 /* Do some simple checks on the shape of the response from the exporter. 561 * If the exporter response is malformed, don't attempt to consume it - let it 562 * pass through to generate a warning to the user by default Ajax processing. 563 */ 564 if ( ! is_array( $response ) ) { 565 return $response; 566 } 567 568 if ( ! array_key_exists( 'done', $response ) ) { 569 return $response; 570 } 571 572 if ( ! array_key_exists( 'data', $response ) ) { 573 return $response; 574 } 575 576 if ( ! is_array( $response['data'] ) ) { 577 return $response; 578 } 579 580 // Get the request data. 581 $request = wp_get_user_request_data( $request_id ); 582 583 if ( ! $request || 'export_personal_data' !== $request->action_name ) { 584 wp_send_json_error( __( 'Invalid request ID when merging exporter data.' ) ); 585 } 586 587 $export_data = array(); 588 589 // First exporter, first page? Reset the report data accumulation array. 590 if ( 1 === $exporter_index && 1 === $page ) { 591 update_post_meta( $request_id, '_export_data_raw', $export_data ); 592 } else { 593 $export_data = get_post_meta( $request_id, '_export_data_raw', true ); 594 } 595 596 // Now, merge the data from the exporter response into the data we have accumulated already. 597 $export_data = array_merge( $export_data, $response['data'] ); 598 update_post_meta( $request_id, '_export_data_raw', $export_data ); 599 600 // If we are not yet on the last page of the last exporter, return now. 601 /** This filter is documented in wp-admin/includes/ajax-actions.php */ 602 $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); 603 $is_last_exporter = $exporter_index === count( $exporters ); 604 $exporter_done = $response['done']; 605 if ( ! $is_last_exporter || ! $exporter_done ) { 606 return $response; 607 } 608 609 // Last exporter, last page - let's prepare the export file. 610 611 // First we need to re-organize the raw data hierarchically in groups and items. 612 $groups = array(); 613 foreach ( (array) $export_data as $export_datum ) { 614 $group_id = $export_datum['group_id']; 615 $group_label = $export_datum['group_label']; 616 if ( ! array_key_exists( $group_id, $groups ) ) { 617 $groups[ $group_id ] = array( 618 'group_label' => $group_label, 619 'items' => array(), 620 ); 621 } 622 623 $item_id = $export_datum['item_id']; 624 if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) { 625 $groups[ $group_id ]['items'][ $item_id ] = array(); 626 } 627 628 $old_item_data = $groups[ $group_id ]['items'][ $item_id ]; 629 $merged_item_data = array_merge( $export_datum['data'], $old_item_data ); 630 $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data; 631 } 632 633 // Then save the grouped data into the request. 634 delete_post_meta( $request_id, '_export_data_raw' ); 635 update_post_meta( $request_id, '_export_data_grouped', $groups ); 636 637 /** 638 * Generate the export file from the collected, grouped personal data. 639 * 640 * @since 4.9.6 641 * 642 * @param int $request_id The export request ID. 643 */ 644 do_action( 'wp_privacy_personal_data_export_file', $request_id ); 645 646 // Clear the grouped data now that it is no longer needed. 647 delete_post_meta( $request_id, '_export_data_grouped' ); 648 649 // If the destination is email, send it now. 650 if ( $send_as_email ) { 651 $mail_success = wp_privacy_send_personal_data_export_email( $request_id ); 652 if ( is_wp_error( $mail_success ) ) { 653 wp_send_json_error( $mail_success->get_error_message() ); 654 } 655 656 // Update the request to completed state when the export email is sent. 657 _wp_privacy_completed_request( $request_id ); 658 } else { 659 // Modify the response to include the URL of the export file so the browser can fetch it. 660 $export_file_url = get_post_meta( $request_id, '_export_file_url', true ); 661 if ( ! empty( $export_file_url ) ) { 662 $response['url'] = $export_file_url; 663 } 664 } 665 666 return $response; 667 } 668 669 /** 215 670 * Mark erasure requests as completed after processing is finished. 216 671 * 217 672 * This intercepts the Ajax responses to personal data eraser page requests, and -
src/wp-includes/class-wp-user-request.php
1 <?php 2 /** 3 * WP_User_Request class. 4 * 5 * Represents user request data loaded from a WP_Post object. 6 * 7 * @since 4.9.6 8 */ 9 final class WP_User_Request { 10 /** 11 * Request ID. 12 * 13 * @var int 14 */ 15 public $ID = 0; 16 17 /** 18 * User ID. 19 * 20 * @var int 21 */ 22 public $user_id = 0; 23 24 /** 25 * User email. 26 * 27 * @var int 28 */ 29 public $email = ''; 30 31 /** 32 * Action name. 33 * 34 * @var string 35 */ 36 public $action_name = ''; 37 38 /** 39 * Current status. 40 * 41 * @var string 42 */ 43 public $status = ''; 44 45 /** 46 * Timestamp this request was created. 47 * 48 * @var int|null 49 */ 50 public $created_timestamp = null; 51 52 /** 53 * Timestamp this request was last modified. 54 * 55 * @var int|null 56 */ 57 public $modified_timestamp = null; 58 59 /** 60 * Timestamp this request was confirmed. 61 * 62 * @var int 63 */ 64 public $confirmed_timestamp = null; 65 66 /** 67 * Timestamp this request was completed. 68 * 69 * @var int 70 */ 71 public $completed_timestamp = null; 72 73 /** 74 * Misc data assigned to this request. 75 * 76 * @var array 77 */ 78 public $request_data = array(); 79 80 /** 81 * Key used to confirm this request. 82 * 83 * @var string 84 */ 85 public $confirm_key = ''; 86 87 /** 88 * Constructor. 89 * 90 * @since 4.9.6 91 * 92 * @param WP_Post|object $post Post object. 93 */ 94 public function __construct( $post ) { 95 $this->ID = $post->ID; 96 $this->user_id = $post->post_author; 97 $this->email = $post->post_title; 98 $this->action_name = $post->post_name; 99 $this->status = $post->post_status; 100 $this->created_timestamp = strtotime( $post->post_date_gmt ); 101 $this->modified_timestamp = strtotime( $post->post_modified_gmt ); 102 $this->confirmed_timestamp = (int) get_post_meta( $post->ID, '_wp_user_request_confirmed_timestamp', true ); 103 $this->completed_timestamp = (int) get_post_meta( $post->ID, '_wp_user_request_completed_timestamp', true ); 104 $this->request_data = json_decode( $post->post_content, true ); 105 $this->confirm_key = $post->post_password; 106 } 107 } -
src/wp-includes/user.php
3647 3647 3648 3648 return new WP_User_Request( $post ); 3649 3649 } 3650 3651 /**3652 * WP_User_Request class.3653 *3654 * Represents user request data loaded from a WP_Post object.3655 *3656 * @since 4.9.63657 */3658 final class WP_User_Request {3659 /**3660 * Request ID.3661 *3662 * @var int3663 */3664 public $ID = 0;3665 3666 /**3667 * User ID.3668 *3669 * @var int3670 */3671 public $user_id = 0;3672 3673 /**3674 * User email.3675 *3676 * @var int3677 */3678 public $email = '';3679 3680 /**3681 * Action name.3682 *3683 * @var string3684 */3685 public $action_name = '';3686 3687 /**3688 * Current status.3689 *3690 * @var string3691 */3692 public $status = '';3693 3694 /**3695 * Timestamp this request was created.3696 *3697 * @var int|null3698 */3699 public $created_timestamp = null;3700 3701 /**3702 * Timestamp this request was last modified.3703 *3704 * @var int|null3705 */3706 public $modified_timestamp = null;3707 3708 /**3709 * Timestamp this request was confirmed.3710 *3711 * @var int3712 */3713 public $confirmed_timestamp = null;3714 3715 /**3716 * Timestamp this request was completed.3717 *3718 * @var int3719 */3720 public $completed_timestamp = null;3721 3722 /**3723 * Misc data assigned to this request.3724 *3725 * @var array3726 */3727 public $request_data = array();3728 3729 /**3730 * Key used to confirm this request.3731 *3732 * @var string3733 */3734 public $confirm_key = '';3735 3736 /**3737 * Constructor.3738 *3739 * @since 4.9.63740 *3741 * @param WP_Post|object $post Post object.3742 */3743 public function __construct( $post ) {3744 $this->ID = $post->ID;3745 $this->user_id = $post->post_author;3746 $this->email = $post->post_title;3747 $this->action_name = $post->post_name;3748 $this->status = $post->post_status;3749 $this->created_timestamp = strtotime( $post->post_date_gmt );3750 $this->modified_timestamp = strtotime( $post->post_modified_gmt );3751 $this->confirmed_timestamp = (int) get_post_meta( $post->ID, '_wp_user_request_confirmed_timestamp', true );3752 $this->completed_timestamp = (int) get_post_meta( $post->ID, '_wp_user_request_completed_timestamp', true );3753 $this->request_data = json_decode( $post->post_content, true );3754 $this->confirm_key = $post->post_password;3755 }3756 } -
src/wp-settings.php
167 167 require( ABSPATH . WPINC . '/theme.php' ); 168 168 require( ABSPATH . WPINC . '/class-wp-theme.php' ); 169 169 require( ABSPATH . WPINC . '/template.php' ); 170 require( ABSPATH . WPINC . '/class-wp-user-request.php' ); 170 171 require( ABSPATH . WPINC . '/user.php' ); 171 172 require( ABSPATH . WPINC . '/class-wp-user-query.php' ); 172 173 require( ABSPATH . WPINC . '/class-wp-session-tokens.php' );