Changeset 45519 for trunk/src/wp-admin/includes/file.php
- Timestamp:
- 06/10/2019 11:53:32 PM (4 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-admin/includes/file.php
r45515 r45519 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 }
Note: See TracChangeset
for help on using the changeset viewer.