Make WordPress Core


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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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 *
Note: See TracChangeset for help on using the changeset viewer.