WordPress.org

Make WordPress Core

Changeset 43012


Ignore:
Timestamp:
04/27/2018 07:53:37 PM (19 months ago)
Author:
azaozz
Message:

Privacy: add means to export personal data by username or email address. Generate a zipped export file containing all data. First run.

Props allendav.
See #43546.

Location:
trunk/src
Files:
8 edited

Legend:

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

    r43008 r43012  
    133133add_action( 'upgrader_process_complete', 'wp_update_themes', 10, 0 );
    134134
     135// Privacy hooks
     136add_filter( 'wp_privacy_personal_data_export_page', 'wp_privacy_process_personal_data_export_page', 10, 6 );
     137add_action( 'wp_privacy_personal_data_export_file', 'wp_privacy_generate_personal_data_export_file', 10 );
     138
    135139// Privacy policy text changes check.
    136140add_action( 'admin_init', array( 'WP_Privacy_Policy_Content', 'text_change_check' ), 20 );
     
    144148// Stop checking for text changes after the policy page is updated.
    145149add_action( 'post_updated', array( 'WP_Privacy_Policy_Content', '_policy_page_updated' ) );
    146 
  • trunk/src/wp-admin/includes/ajax-actions.php

    r43011 r43012  
    43284328}
    43294329
     4330/**
     4331 * Ajax handler for exporting a user's personal data.
     4332 *
     4333 * @since 4.9.6
     4334 */
    43304335function wp_ajax_wp_privacy_export_personal_data() {
    4331     check_ajax_referer( 'wp-privacy-export-personal-data', 'security' );
     4336    $request_id  = (int) $_POST['id'];
     4337
     4338    if ( empty( $request_id ) ) {
     4339        wp_send_json_error( __( 'Error: Invalid request ID.' ) );
     4340    }
    43324341
    43334342    if ( ! current_user_can( 'manage_options' ) ) {
     
    43354344    }
    43364345
    4337     $email_address  = sanitize_text_field( $_POST['email'] );
     4346    check_ajax_referer( 'wp-privacy-export-personal-data-' . $request_id, 'security' );
     4347
     4348    // Get the request data.
     4349    $request = wp_get_user_request_data( $request_id );
     4350
     4351    if ( ! $request || 'export_personal_data' !== $request->action_name ) {
     4352        wp_send_json_error( __( 'Error: Invalid request type.' ) );
     4353    }
     4354
     4355    $email_address = $request->email;
     4356    if ( ! is_email( $email_address ) ) {
     4357        wp_send_json_error( __( 'Error: A valid email address must be given.' ) );
     4358    }
     4359
    43384360    $exporter_index = (int) $_POST['exporter'];
    43394361    $page           = (int) $_POST['page'];
     4362    $send_as_email  = isset( $_POST['sendAsEmail'] ) ? ( "true" === $_POST['sendAsEmail'] ) : false;
    43404363
    43414364    /**
     
    43494372     *         callback               string  Callable exporter that accepts an email address and
    43504373     *                                        a page and returns an array of name => value
    4351      *                                        pairs of personal data
    4352      *         exporter_friendly_name string  Translated user facing friendly name for the exporter
     4374     *                                        pairs of personal data.
     4375     *         exporter_friendly_name string  Translated user facing friendly name for the exporter.
    43534376     *     ]
    43544377     * }
     
    43764399        }
    43774400
    4378         // Surprisingly, email addresses can contain mutli-byte characters now
    4379         $email_address = trim( mb_strtolower( $email_address ) );
    4380 
    4381         if ( ! is_email( $email_address ) ) {
    4382             wp_send_json_error( 'A valid email address must be given.' );
    4383         }
    4384 
    43854401        $exporter = $exporters[ $index ];
     4402
    43864403        if ( ! is_array( $exporter ) ) {
    43874404            wp_send_json_error( "Expected an array describing the exporter at index {$exporter_index}." );
    43884405        }
    4389         if ( ! array_key_exists( 'callback', $exporter ) ) {
    4390             wp_send_json_error( "Exporter array at index {$exporter_index} does not include a callback." );
    4391         }
    4392         if ( ! is_callable( $exporter['callback'] ) ) {
    4393             wp_send_json_error( "Exporter callback at index {$exporter_index} is not a valid callback." );
    4394         }
    43954406        if ( ! array_key_exists( 'exporter_friendly_name', $exporter ) ) {
    43964407            wp_send_json_error( "Exporter array at index {$exporter_index} does not include a friendly name." );
     4408        }
     4409        if ( ! array_key_exists( 'callback', $exporter ) ) {
     4410            wp_send_json_error( "Exporter does not include a callback: {$exporter['exporter_friendly_name']}." );
     4411        }
     4412        if ( ! is_callable( $exporter['callback'] ) ) {
     4413            wp_send_json_error( "Exporter callback is not a valid callback: {$exporter['exporter_friendly_name']}." );
    43974414        }
    43984415
     
    44184435        }
    44194436    } else {
    4420         // No exporters, so we're done
     4437        // No exporters, so we're done.
    44214438        $response = array(
    44224439            'data' => array(),
     
    44364453     * @param string $email_address   The email address associated with this personal data.
    44374454     * @param int    $page            The zero-based page for this response.
     4455     * @param int    $request_id      The privacy request post ID associated with this request.
     4456     * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
    44384457     */
    4439     $response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page );
     4458    $response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page, $request_id, $send_as_email );
     4459
    44404460    if ( is_wp_error( $response ) ) {
    44414461        wp_send_json_error( $response );
     
    44634483    check_ajax_referer( 'wp-privacy-erase-personal-data-' . $request_id, 'security' );
    44644484
    4465     // Find the request CPT
     4485    // Get the request data.
    44664486    $request = wp_get_user_request_data( $request_id );
    44674487
  • trunk/src/wp-admin/includes/file.php

    r42983 r43012  
    19351935    <?php
    19361936}
     1937
     1938/**
     1939 * Generate a single group for the personal data export report.
     1940 *
     1941 * @since 4.9.6
     1942 *
     1943 * @param array  $group_data {
     1944 *     The group data to render.
     1945 *
     1946 *     @type string $group_label  The user-facing heading for the group, e.g. 'Comments'.
     1947 *     @type array  $items        {
     1948 *         An array of group items.
     1949 *
     1950 *         @type array  $group_item_data  {
     1951 *             An array of name-value pairs for the item.
     1952 *
     1953 *             @type string $name   The user-facing name of an item name-value pair, e.g. 'IP Address'.
     1954 *             @type string $value  The user-facing value of an item data pair, e.g. '50.60.70.0'.
     1955 *         }
     1956 *     }
     1957 * }
     1958 * @return string The HTML for this group and its items.
     1959 */
     1960function wp_privacy_generate_personal_data_export_group_html( $group_data ) {
     1961    $allowed_tags      = array(
     1962        'a' => array(
     1963            'href'   => array(),
     1964            'target' => array()
     1965        ),
     1966        'br' => array()
     1967    );
     1968    $allowed_protocols = array( 'http', 'https' );
     1969    $group_html        = '';
     1970
     1971    $group_html .= '<h2>' . esc_html( $group_data['group_label'] ) . '</h2>';
     1972    $group_html .= '<div>';
     1973
     1974    foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) {
     1975        $group_html .= '<table>';
     1976        $group_html .= '<tbody>';
     1977
     1978        foreach ( (array) $group_item_data as $group_item_datum ) {
     1979            $group_html .= '<tr>';
     1980            $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>';
     1981            $group_html .= '<td>' . wp_kses( $group_item_datum['value'], $allowed_tags, $allowed_protocols ) . '</td>';
     1982            $group_html .= '</tr>';
     1983        }
     1984
     1985        $group_html .= '</tbody>';
     1986        $group_html .= '</table>';
     1987    }
     1988
     1989    $group_html .= '</div>';
     1990
     1991    return $group_html;
     1992}
     1993
     1994/**
     1995 * Generate the personal data export file.
     1996 *
     1997 * @since 4.9.6
     1998 *
     1999 * @param int  $request_id  The export request ID.
     2000 */
     2001function wp_privacy_generate_personal_data_export_file( $request_id ) {
     2002    // Maybe make this a cron job instead.
     2003    wp_privacy_delete_old_export_files();
     2004
     2005    if ( ! class_exists( 'ZipArchive' ) ) {
     2006        wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) );
     2007    }
     2008
     2009    // Get the request data.
     2010    $request = wp_get_user_request_data( $request_id );
     2011
     2012    if ( ! $request || 'export_personal_data' !== $request->action_name ) {
     2013        wp_send_json_error( __( 'Invalid request ID when generating export file' ) );
     2014    }
     2015
     2016    $email_address = $request->email;
     2017
     2018    if ( ! is_email( $email_address ) ) {
     2019        wp_send_json_error( __( 'Invalid email address when generating export file' ) );
     2020    }
     2021
     2022    // Create the exports folder if needed.
     2023    $upload_dir  = wp_upload_dir();
     2024    $exports_dir = trailingslashit( $upload_dir['basedir'] . '/exports' );
     2025    $exports_url = trailingslashit( $upload_dir['baseurl'] . '/exports' );
     2026
     2027    $result = wp_mkdir_p( $exports_dir );
     2028    if ( is_wp_error( $result ) ) {
     2029        wp_send_json_error( $result->get_error_message() );
     2030    }
     2031
     2032    // Protect export folder from browsing.
     2033    $index_pathname = $exports_dir . 'index.html';
     2034    if ( ! file_exists( $index_pathname ) ) {
     2035        $file = fopen( $index_pathname, 'w' );
     2036        if ( false === $file ) {
     2037            wp_send_json_error( __( 'Unable to protect export folder from browsing' ) );
     2038        }
     2039        fwrite( $file, 'Silence is golden.' );
     2040        fclose( $file );
     2041    }
     2042
     2043    $stripped_email       = str_replace( '@', '-at-', $email_address );
     2044    $stripped_email       = sanitize_title( $stripped_email ); // slugify the email address
     2045    $obscura              = md5( rand() );
     2046    $file_basename        = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura;
     2047    $html_report_filename = $file_basename . '.html';
     2048    $html_report_pathname = $exports_dir . $html_report_filename;
     2049    $file = fopen( $html_report_pathname, 'w' );
     2050    if ( false === $file ) {
     2051        wp_send_json_error( __( 'Unable to open export file (HTML report) for writing' ) );
     2052    }
     2053
     2054    $title = sprintf(
     2055        // translators: %s Users e-mail address.
     2056        __( 'Personal Data Export for %s' ),
     2057        $email_address
     2058    );
     2059
     2060    // Open HTML.
     2061    fwrite( $file, "<!DOCTYPE html>\n" );
     2062    fwrite( $file, "<html>\n" );
     2063
     2064    // Head.
     2065    fwrite( $file, "<head>\n" );
     2066    fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" );
     2067    fwrite( $file, "<style type='text/css'>" );
     2068    fwrite( $file, "body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }" );
     2069    fwrite( $file, "table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }" );
     2070    fwrite( $file, "th { padding: 5px; text-align: left; width: 20%; }" );
     2071    fwrite( $file, "td { padding: 5px; }" );
     2072    fwrite( $file, "tr:nth-child(odd) { background-color: #fafafa; }" );
     2073    fwrite( $file, "</style>" );
     2074    fwrite( $file, "<title>" );
     2075    fwrite( $file, esc_html( $title ) );
     2076    fwrite( $file, "</title>" );
     2077    fwrite( $file, "</head>\n" );
     2078
     2079    // Body.
     2080    fwrite( $file, "<body>\n" );
     2081
     2082    // Heading.
     2083    fwrite( $file, "<h1>" . esc_html__( 'Personal Data Export' ) . "</h1>" );
     2084
     2085    // And now, all the Groups.
     2086    $groups = get_post_meta( $request_id, '_export_data_grouped', true );
     2087
     2088    // First, build an "About" group on the fly for this report.
     2089    $about_group = array(
     2090        'group_label' => __( 'About' ),
     2091        'items'       => array(
     2092            'about-1' => array(
     2093                array(
     2094                    'name'  => __( 'Report generated for' ),
     2095                    'value' => $email_address,
     2096                ),
     2097                array(
     2098                    'name'  => __( 'For site' ),
     2099                    'value' => get_bloginfo( 'name' ),
     2100                ),
     2101                array(
     2102                    'name'  => __( 'At URL' ),
     2103                    'value' => get_bloginfo( 'url' ),
     2104                ),
     2105                array(
     2106                    'name'  => __( 'On' ),
     2107                    'value' => current_time( 'mysql' ),
     2108                ),
     2109            ),
     2110        ),
     2111    );
     2112
     2113    // Merge in the special about group.
     2114    $groups = array_merge( array( 'about' => $about_group ), $groups );
     2115
     2116    // Now, iterate over every group in $groups and have the formatter render it in HTML.
     2117    foreach ( (array) $groups as $group_id => $group_data ) {
     2118        fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data ) );
     2119    }
     2120
     2121    fwrite( $file, "</body>\n" );
     2122
     2123    // Close HTML.
     2124    fwrite( $file, "</html>\n" );
     2125    fclose( $file );
     2126
     2127    // Now, generate the ZIP.
     2128    $archive_filename = $file_basename . '.zip';
     2129    $archive_pathname = $exports_dir . $archive_filename;
     2130    $archive_url      = $exports_url . $archive_filename;
     2131
     2132    $zip = new ZipArchive;
     2133
     2134    if ( TRUE === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) {
     2135        $zip->addFile( $html_report_pathname, 'index.html' );
     2136        $zip->close();
     2137    } else {
     2138        wp_send_json_error( __( 'Unable to open export file (archive) for writing' ) );
     2139    }
     2140
     2141    // And remove the HTML file.
     2142    unlink( $html_report_pathname );
     2143
     2144    // Save the export file in the request.
     2145    update_post_meta( $request_id, '_export_file_url', $archive_url );
     2146    update_post_meta( $request_id, '_export_file_path', $archive_pathname );
     2147}
     2148
     2149/**
     2150 * Send an email to the user with a link to the personal data export file
     2151 *
     2152 * @since 4.9.6
     2153 *
     2154 * @param int  $request_id  The request ID for this personal data export.
     2155 * @return true|WP_Error    True on success or `WP_Error` on failure.
     2156 */
     2157function wp_privacy_send_personal_data_export_email( $request_id ) {
     2158    // Get the request data.
     2159    $request = wp_get_user_request_data( $request_id );
     2160
     2161    if ( ! $request || 'export_personal_data' !== $request->action_name ) {
     2162        return new WP_Error( 'invalid', __( 'Invalid request ID when sending personal data export email.' ) );
     2163    }
     2164
     2165/* translators: Do not translate LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */
     2166$email_text = __(
     2167'Howdy,
     2168
     2169Your request for an export of personal data has been completed. You may
     2170download your personal data by clicking on the link below. This link is
     2171good for the next 3 days.
     2172
     2173###LINK###
     2174
     2175This email has been sent to ###EMAIL###.
     2176
     2177Regards,
     2178All at ###SITENAME###
     2179###SITEURL###'
     2180);
     2181
     2182    /**
     2183     * Filters the text of the email sent with a personal data export file.
     2184     *
     2185     * The following strings have a special meaning and will get replaced dynamically:
     2186     * ###LINK###               URL of the personal data export file for the user.
     2187     * ###EMAIL###              The email we are sending to.
     2188     * ###SITENAME###           The name of the site.
     2189     * ###SITEURL###            The URL to the site.
     2190     *
     2191     * @since 4.9.6
     2192     *
     2193     * @param string $email_text     Text in the email.
     2194     * @param int    $request_id     The request ID for this personal data export.
     2195     */
     2196    $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id );
     2197
     2198    $email_address = $request->email;
     2199    $export_file_url = get_post_meta( $request_id, '_export_file_url', true );
     2200    $site_name = is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' );
     2201    $site_url = network_home_url();
     2202
     2203    $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content );
     2204    $content = str_replace( '###EMAIL###', $email_address, $content );
     2205    $content = str_replace( '###SITENAME###', wp_specialchars_decode( $site_name, ENT_QUOTES ), $content );
     2206    $content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content );
     2207
     2208    $mail_success = wp_mail(
     2209        $email_address,
     2210        sprintf(
     2211            __( '[%s] Personal Data Export' ),
     2212            wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES )
     2213        ),
     2214        $content
     2215    );
     2216
     2217    if ( ! $mail_success ) {
     2218        return new WP_Error( 'error', __( 'Unable to send personal data export email.' ) );
     2219    }
     2220
     2221    return true;
     2222}
     2223
     2224/**
     2225 * Intercept personal data exporter page ajax responses in order to assemble the personal data export file.
     2226 * @see wp_privacy_personal_data_export_page
     2227 * @since 4.9.6
     2228 *
     2229 * @param array  $response        The response from the personal data exporter for the given page.
     2230 * @param int    $exporter_index  The index of the personal data exporter. Begins at 1.
     2231 * @param string $email_address   The email address of the user whose personal data this is.
     2232 * @param int    $page            The page of personal data for this exporter. Begins at 1.
     2233 * @param int    $request_id      The request ID for this personal data export.
     2234 * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
     2235 * @return array The filtered response.
     2236 */
     2237function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email ) {
     2238    /* Do some simple checks on the shape of the response from the exporter.
     2239     * If the exporter response is malformed, don't attempt to consume it - let it
     2240     * pass through to generate a warning to the user by default ajax processing.
     2241     */
     2242    if ( ! is_array( $response ) ) {
     2243        return $response;
     2244    }
     2245
     2246    if ( ! array_key_exists( 'done', $response ) ) {
     2247        return $response;
     2248    }
     2249
     2250    if ( ! array_key_exists( 'data', $response ) ) {
     2251        return $response;
     2252    }
     2253
     2254    if ( ! is_array( $response['data'] ) ) {
     2255        return $response;
     2256    }
     2257
     2258    // Get the request data.
     2259    $request = wp_get_user_request_data( $request_id );
     2260
     2261    if ( ! $request || 'export_personal_data' !== $request->action_name ) {
     2262        wp_send_json_error( __( 'Invalid request ID when merging exporter data' ) );
     2263    }
     2264
     2265    $export_data = array();
     2266
     2267    // First exporter, first page? Reset the report data accumulation array.
     2268    if ( 1 === $exporter_index && 1 === $page ) {
     2269        update_post_meta( $request_id, '_export_data_raw', $export_data );
     2270    } else {
     2271        $export_data = get_post_meta( $request_id, '_export_data_raw', true );
     2272    }
     2273
     2274    // Now, merge the data from the exporter response into the data we have accumulated already.
     2275    $export_data = array_merge( $export_data, $response['data'] );
     2276    update_post_meta( $request_id, '_export_data_raw', $export_data );
     2277
     2278    // If we are not yet on the last page of the last exporter, return now.
     2279    $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );
     2280    $is_last_exporter = $exporter_index === count( $exporters );
     2281    $exporter_done = $response['done'];
     2282    if ( ! $is_last_exporter || ! $exporter_done ) {
     2283        return $response;
     2284    }
     2285
     2286    // Last exporter, last page - let's prepare the export file.
     2287
     2288    // First we need to re-organize the raw data hierarchically in groups and items.
     2289    $groups = array();
     2290    foreach ( (array) $export_data as $export_datum ) {
     2291        $group_id    = $export_datum['group_id'];
     2292        $group_label = $export_datum['group_label'];
     2293        if ( ! array_key_exists( $group_id, $groups ) ) {
     2294            $groups[ $group_id ] = array(
     2295                'group_label' => $group_label,
     2296                'items'       => array(),
     2297            );
     2298        }
     2299
     2300        $item_id = $export_datum['item_id'];
     2301        if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) {
     2302            $groups[ $group_id ]['items'][ $item_id ] = array();
     2303        }
     2304
     2305        $old_item_data = $groups[ $group_id ]['items'][ $item_id ];
     2306        $merged_item_data = array_merge( $export_datum['data'], $old_item_data );
     2307        $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data;
     2308    }
     2309
     2310    // Then save the grouped data into the request.
     2311    delete_post_meta( $request_id, '_export_data_raw' );
     2312    update_post_meta( $request_id, '_export_data_grouped', $groups );
     2313
     2314    // And now, generate the export file, cleaning up any previous file
     2315    $export_path = get_post_meta( $request_id, '_export_file_path', true );
     2316    if ( ! empty( $export_path ) ) {
     2317        delete_post_meta( $request_id, '_export_file_path' );
     2318        @unlink( $export_path );
     2319    }
     2320    delete_post_meta( $request_id, '_export_file_url' );
     2321
     2322    // Generate the export file from the collected, grouped personal data.
     2323    do_action( 'wp_privacy_personal_data_export_file', $request_id );
     2324
     2325    // Clear the grouped data now that it is no longer needed.
     2326    delete_post_meta( $request_id, '_export_data_grouped' );
     2327
     2328    // If the destination is email, send it now.
     2329    if ( $send_as_email ) {
     2330        $mail_success = wp_privacy_send_personal_data_export_email( $request_id );
     2331        if ( is_wp_error( $mail_success ) ) {
     2332            wp_send_json_error( $mail_success->get_error_message() );
     2333        }
     2334    } else {
     2335        // Modify the response to include the URL of the export file so the browser can fetch it.
     2336        $export_file_url = get_post_meta( $request_id, '_export_file_url', true );
     2337        if ( ! empty( $export_file_url ) ) {
     2338            $response['url'] = $export_file_url;
     2339        }
     2340    }
     2341
     2342    // Update the request to completed state.
     2343    _wp_privacy_completed_request( $request_id );
     2344
     2345    return $response;
     2346}
     2347
     2348/**
     2349 * Cleans up export files older than three days old.
     2350 *
     2351 * @since 4.9.6
     2352 */
     2353function wp_privacy_delete_old_export_files() {
     2354    $upload_dir   = wp_upload_dir();
     2355    $exports_dir  = trailingslashit( $upload_dir['basedir'] . '/exports' );
     2356    $export_files = list_files( $exports_dir );
     2357
     2358    foreach( (array) $export_files as $export_file ) {
     2359        $file_age_in_seconds = time() - filemtime( $export_file );
     2360
     2361        if ( 3 * DAY_IN_SECONDS < $file_age_in_seconds ) {
     2362            @unlink( $export_file );
     2363        }
     2364    }
     2365}
  • trunk/src/wp-admin/includes/user.php

    r43011 r43012  
    661661                'privacy_action_email_retry',
    662662                __( 'Confirmation request re-resent successfully.' ),
    663                 'updated'
    664             );
    665         }
    666 
    667     } elseif ( isset( $_POST['export_personal_data_email_send'] ) ) { // WPCS: input var ok.
    668         check_admin_referer( 'bulk-privacy_requests' );
    669 
    670         $request_id = absint( current( array_keys( (array) wp_unslash( $_POST['export_personal_data_email_send'] ) ) ) ); // WPCS: input var ok, sanitization ok.
    671         $result     = false;
    672 
    673         /**
    674          * TODO: Email the data to the user here.
    675          */
    676 
    677         if ( is_wp_error( $result ) ) {
    678             add_settings_error(
    679                 'export_personal_data_email_send',
    680                 'export_personal_data_email_send',
    681                 $result->get_error_message(),
    682                 'error'
    683             );
    684         } else {
    685             _wp_privacy_completed_request( $request_id );
    686             add_settings_error(
    687                 'export_personal_data_email_send',
    688                 'export_personal_data_email_send',
    689                 __( 'Personal data was sent to the user successfully.' ),
    690663                'updated'
    691664            );
     
    820793    _wp_personal_data_cleanup_requests();
    821794
     795    // "Borrow" xfn.js for now so we don't have to create new files.
     796    wp_enqueue_script( 'xfn' );
     797
    822798    $requests_table = new WP_Privacy_Data_Export_Requests_Table( array(
    823799        'plural'   => 'privacy_requests',
     
    13621338        $nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
    13631339
    1364         $download_data_markup = '<div class="download_personal_data" ' .
     1340        $download_data_markup = '<div class="export_personal_data" ' .
    13651341            'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
    13661342            'data-request-id="' . esc_attr( $request_id ) . '" ' .
     
    13681344            '">';
    13691345
    1370         $download_data_markup .= '<span class="download_personal_data_idle"><a href="#" >' . __( 'Download Personal Data' ) . '</a></span>' .
    1371             '<span style="display:none" class="download_personal_data_processing" >' . __( 'Downloading Data...' ) . '</span>' .
    1372             '<span style="display:none" class="download_personal_data_failed">' . __( 'Download Failed!' ) . ' <a href="#" >' . __( 'Retry' ) . '</a></span>';
     1346        $download_data_markup .= '<span class="export_personal_data_idle"><a href="#" >' . __( 'Download Personal Data' ) . '</a></span>' .
     1347            '<span style="display:none" class="export_personal_data_processing" >' . __( 'Downloading Data...' ) . '</span>' .
     1348            '<span style="display:none" class="export_personal_data_success"><a href="#" >' . __( 'Download Personal Data Again' ) . '</a></span>' .
     1349            '<span style="display:none" class="export_personal_data_failed">' . __( 'Download Failed!' ) . ' <a href="#" >' . __( 'Retry' ) . '</a></span>';
     1350
     1351        $download_data_markup .= '</div>';
    13731352
    13741353        $row_actions = array(
     
    13941373                break;
    13951374            case 'request-confirmed':
    1396                 // TODO Complete in follow on patch.
     1375                $exporters       = apply_filters( 'wp_privacy_personal_data_exporters', array() );
     1376                $exporters_count = count( $exporters );
     1377                $request_id      = $item->ID;
     1378                $nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
     1379
     1380                echo '<div class="export_personal_data" ' .
     1381                    'data-send-as-email="1" ' .
     1382                    'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
     1383                    'data-request-id="' . esc_attr( $request_id ) . '" ' .
     1384                    'data-nonce="' . esc_attr( $nonce ) .
     1385                    '">';
     1386
     1387                ?>
     1388                <span class="export_personal_data_idle"><a class="button" href="#" ><?php _e( 'Email Data' ); ?></a></span>
     1389                <span style="display:none" class="export_personal_data_processing button updating-message" ><?php _e( 'Sending Email...' ); ?></span>
     1390                <span style="display:none" class="export_personal_data_success success-message" ><?php _e( 'Email Sent!' ); ?></span>
     1391                <span style="display:none" class="export_personal_data_failed"><?php _e( 'Email Failed!' ); ?> <a class="button" href="#" ><?php _e( 'Retry' ); ?></a></span>
     1392                <?php
     1393
     1394                echo '</div>';
    13971395                break;
    13981396            case 'request-failed':
     
    14621460                '<span style="display:none" class="remove_personal_data_failed">' . __( 'Force Remove Failed!' ) . ' <a href="#" >' . __( 'Retry' ) . '</a></span>';
    14631461
     1462            $remove_data_markup .= '</div>';
     1463
    14641464            $row_actions = array(
    14651465                'remove_data' => $remove_data_markup,
     
    15021502                <span style="display:none" class="remove_personal_data_failed"><?php _e( 'Removing Data Failed!' ); ?> <a class="button" href="#" ><?php _e( 'Retry' ); ?></a></span>
    15031503                <?php
     1504
     1505                echo '</div>';
    15041506
    15051507                break;
  • trunk/src/wp-admin/js/xfn.js

    r42994 r43012  
    2323
    2424// Privacy request action handling
    25 
    2625jQuery( document ).ready( function( $ ) {
    2726    var strings = window.privacyToolsL10n || {};
     
    4039    function appendResultsAfterRow( $requestRow, classes, summaryMessage, additionalMessages ) {
    4140        clearResultsAfterRow( $requestRow );
     41
     42        var itemList = '';
    4243        if ( additionalMessages.length ) {
    43             // TODO - render additionalMessages after the summaryMessage
     44            $.each( additionalMessages, function( index, value ) {
     45                itemList = itemList + '<li>' + value + '</li>';
     46            } );
     47            itemList = '<ul>' + itemList + '</ul>';
    4448        }
    4549
    4650        $requestRow.after( function() {
    47             return '<tr class="request-results"><td colspan="5"><div class="notice inline notice-alt ' + classes + '"><p>' +
    48                 summaryMessage +
    49                 '</p></div></td></tr>';
     51            return '<tr class="request-results"><td colspan="5">' +
     52                '<div class="notice inline notice-alt ' + classes + '">' +
     53                '<p>' + summaryMessage + '</p>' +
     54                itemList +
     55                '</div>' +
     56                '</td>' +
     57                '</tr>';
    5058        } );
    5159    }
     60
     61    $( '.export_personal_data a' ).click( function( event ) {
     62        event.preventDefault();
     63        event.stopPropagation();
     64
     65        var $this          = $( this );
     66        var $action        = $this.parents( '.export_personal_data' );
     67        var $requestRow    = $this.parents( 'tr' );
     68        var requestID      = $action.data( 'request-id' );
     69        var nonce          = $action.data( 'nonce' );
     70        var exportersCount = $action.data( 'exporters-count' );
     71        var sendAsEmail    = $action.data( 'send-as-email' ) ? true : false;
     72
     73        $action.blur();
     74        clearResultsAfterRow( $requestRow );
     75
     76        function on_export_done_success( zipUrl ) {
     77            set_action_state( $action, 'export_personal_data_success' );
     78            if ( 'undefined' !== typeof zipUrl ) {
     79                window.location = zipUrl;
     80            } else if ( ! sendAsEmail ) {
     81                on_export_failure( strings.noExportFile );
     82            }
     83        }
     84
     85        function on_export_failure( errorMessage ) {
     86            set_action_state( $action, 'export_personal_data_failed' );
     87            if ( errorMessage ) {
     88                appendResultsAfterRow( $requestRow, 'notice-error', strings.exportError, [ errorMessage ] );
     89            }
     90        }
     91
     92        function do_next_export( exporterIndex, pageIndex ) {
     93            $.ajax(
     94                {
     95                    url: window.ajaxurl,
     96                    data: {
     97                        action: 'wp-privacy-export-personal-data',
     98                        exporter: exporterIndex,
     99                        id: requestID,
     100                        page: pageIndex,
     101                        security: nonce,
     102                        sendAsEmail: sendAsEmail
     103                    },
     104                    method: 'post'
     105                }
     106            ).done( function( response ) {
     107                if ( ! response.success ) {
     108                    // e.g. invalid request ID
     109                    on_export_failure( response.data );
     110                    return;
     111                }
     112                var responseData = response.data;
     113                if ( ! responseData.done ) {
     114                    setTimeout( do_next_export( exporterIndex, pageIndex + 1 ) );
     115                } else {
     116                    if ( exporterIndex < exportersCount ) {
     117                        setTimeout( do_next_export( exporterIndex + 1, 1 ) );
     118                    } else {
     119                        on_export_done_success( responseData.url );
     120                    }
     121                }
     122            } ).fail( function( jqxhr, textStatus, error ) {
     123                // e.g. Nonce failure
     124                on_export_failure( error );
     125            } );
     126        }
     127
     128        // And now, let's begin
     129        set_action_state( $action, 'export_personal_data_processing' );
     130        do_next_export( 1, 1 );
     131    } );
    52132
    53133    $( '.remove_personal_data a' ).click( function( event ) {
     
    93173        function on_erasure_failure() {
    94174            set_action_state( $action, 'remove_personal_data_failed' );
    95             appendResultsAfterRow( $requestRow, 'notice-error', strings.anErrorOccurred, [] );
     175            appendResultsAfterRow( $requestRow, 'notice-error', strings.removalError, [] );
    96176        }
    97177
  • trunk/src/wp-includes/comment.php

    r42994 r43012  
    33533353                case 'comment_link':
    33543354                    $value = get_comment_link( $comment->comment_ID );
     3355                    $value = '<a href="' . $value . '" target="_blank" rel="noreferrer noopener">' . $value . '</a>';
    33553356                    break;
    33563357            }
  • trunk/src/wp-includes/script-loader.php

    r42986 r43012  
    716716                'noneRemoved'     => __( 'Personal data was found for this user but was not removed.' ),
    717717                'someNotRemoved'  => __( 'Personal data was found for this user but some of the personal data found was not removed.' ),
    718                 'anErrorOccurred' => __( 'An error occurred while attempting to find and remove personal data.' ),
     718                'removalError'    => __( 'An error occurred while attempting to find and remove personal data.' ),
     719                'noExportFile'    => __( 'No personal data export file was generated.' ),
     720                'exportError'     => __( 'An error occurred while attempting to export personal data.' ),
    719721            )
    720722        );
  • trunk/src/wp-includes/user.php

    r43011 r43012  
    31463146 *
    31473147 * @param int $request_id Request ID to get data about.
    3148  * @return array|false
     3148 * @return WP_User_Request|false
    31493149 */
    31503150function wp_get_user_request_data( $request_id ) {
Note: See TracChangeset for help on using the changeset viewer.