Make WordPress Core

Ticket #43895: 43895.3.diff

File 43895.3.diff, 37.9 KB (added by azaozz, 6 years ago)
  • src/wp-admin/includes/file.php

     
    21942194        </div>
    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 }
  • src/wp-admin/includes/privacy-tools.php

     
    212212}
    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 *
    217672 * 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 */
     9final 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

     
    36473647
    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 }
  • src/wp-settings.php

     
    167167require( ABSPATH . WPINC . '/theme.php' );
    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' );
    172173require( ABSPATH . WPINC . '/class-wp-session-tokens.php' );