Make WordPress Core

Changeset 45519


Ignore:
Timestamp:
06/10/2019 11:53:32 PM (5 years ago)
Author:
azaozz
Message:

Privacy tools:

  • Move the (remaining) privacy tools related functions from wp-admin/includes/file.php to wp-admin/includes/privacy-tools.php.
  • Move the WP_User_Request class to a separate file.

See #43895.

Location:
trunk/src
Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/file.php

    r45515 r45519  
    21952195    <?php
    21962196}
    2197 
    2198 /**
    2199  * Generate a single group for the personal data export report.
    2200  *
    2201  * @since 4.9.6
    2202  *
    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.6
    2254  *
    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 address
    2296     $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_address
    2309     );
    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 the
    2383      * filename, to avoid breaking any URLs that may have been previously sent
    2384      * 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.6
    2416              *
    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 file
    2438  *
    2439  * @since 4.9.6
    2440  *
    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 may
    2470 download your personal data by clicking on the link below. For privacy
    2471 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.6
    2491      *
    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_name
    2514         ),
    2515         $content
    2516     );
    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_page
    2532  * @since 4.9.6
    2533  *
    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 it
    2546      * 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.6
    2625      *
    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 }
  • trunk/src/wp-admin/includes/privacy-tools.php

    r45448 r45519  
    213213
    214214/**
     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 */
     236function 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 */
     273function 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 */
     460function 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
     485Your request for an export of personal data has been completed. You may
     486download your personal data by clicking on the link below. For privacy
     487and security, we will automatically delete the file on ###EXPIRATION###,
     488so please download it before then.
     489
     490###LINK###
     491
     492Regards,
     493All 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 */
     559function 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/**
    215670 * Mark erasure requests as completed after processing is finished.
    216671 *
  • trunk/src/wp-includes/user.php

    r45479 r45519  
    36483648    return new WP_User_Request( $post );
    36493649}
    3650 
    3651 /**
    3652  * WP_User_Request class.
    3653  *
    3654  * Represents user request data loaded from a WP_Post object.
    3655  *
    3656  * @since 4.9.6
    3657  */
    3658 final class WP_User_Request {
    3659     /**
    3660      * Request ID.
    3661      *
    3662      * @var int
    3663      */
    3664     public $ID = 0;
    3665 
    3666     /**
    3667      * User ID.
    3668      *
    3669      * @var int
    3670      */
    3671     public $user_id = 0;
    3672 
    3673     /**
    3674      * User email.
    3675      *
    3676      * @var int
    3677      */
    3678     public $email = '';
    3679 
    3680     /**
    3681      * Action name.
    3682      *
    3683      * @var string
    3684      */
    3685     public $action_name = '';
    3686 
    3687     /**
    3688      * Current status.
    3689      *
    3690      * @var string
    3691      */
    3692     public $status = '';
    3693 
    3694     /**
    3695      * Timestamp this request was created.
    3696      *
    3697      * @var int|null
    3698      */
    3699     public $created_timestamp = null;
    3700 
    3701     /**
    3702      * Timestamp this request was last modified.
    3703      *
    3704      * @var int|null
    3705      */
    3706     public $modified_timestamp = null;
    3707 
    3708     /**
    3709      * Timestamp this request was confirmed.
    3710      *
    3711      * @var int
    3712      */
    3713     public $confirmed_timestamp = null;
    3714 
    3715     /**
    3716      * Timestamp this request was completed.
    3717      *
    3718      * @var int
    3719      */
    3720     public $completed_timestamp = null;
    3721 
    3722     /**
    3723      * Misc data assigned to this request.
    3724      *
    3725      * @var array
    3726      */
    3727     public $request_data = array();
    3728 
    3729     /**
    3730      * Key used to confirm this request.
    3731      *
    3732      * @var string
    3733      */
    3734     public $confirm_key = '';
    3735 
    3736     /**
    3737      * Constructor.
    3738      *
    3739      * @since 4.9.6
    3740      *
    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 }
  • trunk/src/wp-settings.php

    r45056 r45519  
    168168require( ABSPATH . WPINC . '/class-wp-theme.php' );
    169169require( ABSPATH . WPINC . '/template.php' );
     170require( ABSPATH . WPINC . '/class-wp-user-request.php' );
    170171require( ABSPATH . WPINC . '/user.php' );
    171172require( ABSPATH . WPINC . '/class-wp-user-query.php' );
Note: See TracChangeset for help on using the changeset viewer.