Make WordPress Core

Ticket #43546: 43546.9.diff

File 43546.9.diff, 30.4 KB (added by allendav, 5 years ago)

Incorporates desrosj patch; fixes old export cleanup logic; fixes email address meta key for requests

  • src/wp-admin/includes/admin-filters.php

     
    132132add_action( 'upgrader_process_complete', 'wp_update_plugins', 10, 0 );
    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 );
    137141
     
    143147
    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 
  • src/wp-admin/includes/ajax-actions.php

     
    43274327        }
    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'];
    43324337
     4338        if ( empty( $request_id ) ) {
     4339                wp_send_json_error( __( 'Error: Invalid request ID.' ) );
     4340        }
     4341
    43334342        if ( ! current_user_can( 'manage_options' ) ) {
    43344343                wp_send_json_error( __( 'Error: Invalid request.' ) );
    43354344        }
    43364345
    4337         $email_address  = sanitize_text_field( $_POST['email'] );
     4346        check_ajax_referer( 'wp-privacy-export-personal-data-' . $request_id, 'security' );
     4347
     4348        // Find the request CPT.
     4349        $request = get_post( $request_id );
     4350        if ( 'user_request' !== $request->post_type ) {
     4351                wp_send_json_error( __( 'Error: Invalid request type' ) );
     4352        }
     4353
     4354        $email_address = get_post_meta( $request_id, '_wp_user_request_user_email', true );
     4355        if ( ! is_email( $email_address ) ) {
     4356                wp_send_json_error( __( 'Error: A valid email address must be given.' ) );
     4357        }
     4358
    43384359        $exporter_index = (int) $_POST['exporter'];
    43394360        $page           = (int) $_POST['page'];
     4361        $send_as_email  = isset( $_POST['sendAsEmail'] ) ? ( "true" === $_POST['sendAsEmail'] ) : false;
    43404362
    43414363        /**
    43424364         * Filters the array of exporter callbacks.
     
    43484370         *     [
    43494371         *         callback               string  Callable exporter that accepts an email address and
    43504372         *                                        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
     4373         *                                        pairs of personal data.
     4374         *         exporter_friendly_name string  Translated user facing friendly name for the exporter.
    43534375         *     ]
    43544376         * }
    43554377         */
     
    43754397                        wp_send_json_error( 'Page index cannot be less than one.' );
    43764398                }
    43774399
    4378                 // Surprisingly, email addresses can contain mutli-byte characters now
    4379                 $email_address = trim( mb_strtolower( $email_address ) );
     4400                $exporter = $exporters[ $index ];
    43804401
    4381                 if ( ! is_email( $email_address ) ) {
    4382                         wp_send_json_error( 'A valid email address must be given.' );
    4383                 }
    4384 
    4385                 $exporter = $exporters[ $index ];
    43864402                if ( ! is_array( $exporter ) ) {
    43874403                        wp_send_json_error( "Expected an array describing the exporter at index {$exporter_index}." );
    43884404                }
     4405                if ( ! array_key_exists( 'exporter_friendly_name', $exporter ) ) {
     4406                        wp_send_json_error( "Exporter array at index {$exporter_index} does not include a friendly name." );
     4407                }
    43894408                if ( ! array_key_exists( 'callback', $exporter ) ) {
    4390                         wp_send_json_error( "Exporter array at index {$exporter_index} does not include a callback." );
     4409                        wp_send_json_error( "Exporter does not include a callback: {$exporter['exporter_friendly_name']}." );
    43914410                }
    43924411                if ( ! is_callable( $exporter['callback'] ) ) {
    4393                         wp_send_json_error( "Exporter callback at index {$exporter_index} is not a valid callback." );
     4412                        wp_send_json_error( "Exporter callback is not a valid callback: {$exporter['exporter_friendly_name']}." );
    43944413                }
    4395                 if ( ! array_key_exists( 'exporter_friendly_name', $exporter ) ) {
    4396                         wp_send_json_error( "Exporter array at index {$exporter_index} does not include a friendly name." );
    4397                 }
    43984414
    43994415                $callback = $exporters[ $index ]['callback'];
    44004416                $exporter_friendly_name = $exporters[ $index ]['exporter_friendly_name'];
     
    44174433                        wp_send_json_error( "Expected done (boolean) in response array from exporter: {$exporter_friendly_name}." );
    44184434                }
    44194435        } else {
    4420                 // No exporters, so we're done
     4436                // No exporters, so we're done.
    44214437                $response = array(
    44224438                        'data' => array(),
    44234439                        'done' => true,
     
    44354451         * @param int    $exporter_index  The index of the exporter that provided this data.
    44364452         * @param string $email_address   The email address associated with this personal data.
    44374453         * @param int    $page            The zero-based page for this response.
     4454         * @param int    $request_id      The privacy request post ID associated with this request.
     4455         * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
    44384456         */
    4439         $response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page );
     4457        $response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page, $request_id, $send_as_email );
     4458
    44404459        if ( is_wp_error( $response ) ) {
    44414460                wp_send_json_error( $response );
    44424461        }
  • src/wp-admin/includes/file.php

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

     
    662662                        );
    663663                }
    664664
    665         } elseif ( isset( $_POST['export_personal_data_email_send'] ) ) { // WPCS: input var ok.
    666                 check_admin_referer( 'bulk-privacy_requests' );
    667 
    668                 $request_id = absint( current( array_keys( (array) wp_unslash( $_POST['export_personal_data_email_send'] ) ) ) ); // WPCS: input var ok, sanitization ok.
    669                 $result     = false;
    670 
    671                 /**
    672                  * TODO: Email the data to the user here.
    673                  */
    674 
    675                 if ( is_wp_error( $result ) ) {
    676                         add_settings_error(
    677                                 'export_personal_data_email_send',
    678                                 'export_personal_data_email_send',
    679                                 $result->get_error_message(),
    680                                 'error'
    681                         );
    682                 } else {
    683                         _wp_privacy_completed_request( $request_id );
    684                         add_settings_error(
    685                                 'export_personal_data_email_send',
    686                                 'export_personal_data_email_send',
    687                                 __( 'Personal data was sent to the user successfully.' ),
    688                                 'updated'
    689                         );
    690                 }
    691 
    692665        } elseif ( isset( $_POST['action'] ) ) {
    693666                $action = isset( $_POST['action'] ) ? sanitize_key( wp_unslash( $_POST['action'] ) ) : ''; // WPCS: input var ok, CSRF ok.
    694667
     
    784757
    785758        _wp_personal_data_handle_actions();
    786759
     760        // "Borrow" xfn.js for now so we don't have to create new files.
     761        wp_enqueue_script( 'xfn' );
     762
    787763        $requests_table = new WP_Privacy_Data_Export_Requests_Table( array(
    788764                'plural'   => 'privacy_requests',
    789765                'singular' => 'privacy_request',
     
    13341310                $request_id      = $item['request_id'];
    13351311                $nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
    13361312
    1337                 $download_data_markup = '<div class="download_personal_data" ' .
     1313                $download_data_markup = '<div class="export_personal_data" ' .
    13381314                        'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
    13391315                        'data-request-id="' . esc_attr( $request_id ) . '" ' .
    13401316                        'data-nonce="' . esc_attr( $nonce ) .
    13411317                        '">';
    13421318
    1343                 $download_data_markup .= '<span class="download_personal_data_idle"><a href="#" >' . __( 'Download Personal Data' ) . '</a></span>' .
    1344                         '<span style="display:none" class="download_personal_data_processing" >' . __( 'Downloading Data...' ) . '</span>' .
    1345                         '<span style="display:none" class="download_personal_data_failed">' . __( 'Download Failed!' ) . ' <a href="#" >' . __( 'Retry' ) . '</a></span>';
     1319                $download_data_markup .= '<span class="export_personal_data_idle"><a href="#" >' . __( 'Download Personal Data' ) . '</a></span>' .
     1320                        '<span style="display:none" class="export_personal_data_processing" >' . __( 'Downloading Data...' ) . '</span>' .
     1321                        '<span style="display:none" class="export_personal_data_success"><a href="#" >' . __( 'Download Personal Data Again' ) . '</a></span>' .
     1322                        '<span style="display:none" class="export_personal_data_failed">' . __( 'Download Failed!' ) . ' <a href="#" >' . __( 'Retry' ) . '</a></span>';
    13461323
     1324                $download_data_markup .= '</div>';
     1325
    13471326                $row_actions = array(
    13481327                        'download_data' => $download_data_markup,
    13491328                );
     
    13661345                                esc_html_e( 'Waiting for confirmation' );
    13671346                                break;
    13681347                        case 'request-confirmed':
    1369                                 // TODO Complete in follow on patch.
     1348                                $exporters       = apply_filters( 'wp_privacy_personal_data_exporters', array() );
     1349                                $exporters_count = count( $exporters );
     1350                                $request_id      = $item['request_id'];
     1351                                $nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
     1352
     1353                                echo '<div class="export_personal_data" ' .
     1354                                        'data-send-as-email="1" ' .
     1355                                        'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
     1356                                        'data-request-id="' . esc_attr( $request_id ) . '" ' .
     1357                                        'data-nonce="' . esc_attr( $nonce ) .
     1358                                        '">';
     1359
     1360                                ?>
     1361                                <span class="export_personal_data_idle"><a class="button" href="#" ><?php _e( 'Email Data' ); ?></a></span>
     1362                                <span style="display:none" class="export_personal_data_processing button updating-message" ><?php _e( 'Sending Email...' ); ?></span>
     1363                                <span style="display:none" class="export_personal_data_success success-message" ><?php _e( 'Email Sent!' ); ?></span>
     1364                                <span style="display:none" class="export_personal_data_failed"><?php _e( 'Email Failed!' ); ?> <a class="button" href="#" ><?php _e( 'Retry' ); ?></a></span>
     1365                                <?php
     1366
     1367                                echo '</div>';
    13701368                                break;
    13711369                        case 'request-failed':
    13721370                                submit_button( __( 'Retry' ), 'secondary', 'privacy_action_email_retry[' . $item['request_id'] . ']', false );
     
    14341432                                '<span style="display:none" class="remove_personal_data_processing" >' . __( 'Removing Data...' ) . '</span>' .
    14351433                                '<span style="display:none" class="remove_personal_data_failed">' . __( 'Force Remove Failed!' ) . ' <a href="#" >' . __( 'Retry' ) . '</a></span>';
    14361434
     1435                        $remove_data_markup .= '</div>';
     1436
    14371437                        $row_actions = array(
    14381438                                'remove_data' => $remove_data_markup,
    14391439                        );
     
    14751475                                <span style="display:none" class="remove_personal_data_failed"><?php _e( 'Removing Data Failed!' ); ?> <a class="button" href="#" ><?php _e( 'Retry' ); ?></a></span>
    14761476                                <?php
    14771477
     1478                                echo '</div>';
     1479
    14781480                                break;
    14791481                        case 'request-failed':
    14801482                                submit_button( __( 'Retry' ), 'secondary', 'privacy_action_email_retry[' . $item['request_id'] . ']', false );
  • src/wp-admin/js/xfn.js

     
    3939
    4040        function appendResultsAfterRow( $requestRow, classes, summaryMessage, additionalMessages ) {
    4141                clearResultsAfterRow( $requestRow );
     42
     43                var itemList = '';
    4244                if ( additionalMessages.length ) {
    43                         // TODO - render additionalMessages after the summaryMessage
     45                        $.each( additionalMessages, function( index, value ) {
     46                                itemList = itemList + '<li>' + value + '</li>';
     47                        } );
     48                        itemList = '<ul>' + itemList + '</ul>';
    4449                }
    4550
    4651                $requestRow.after( function() {
    47                         return '<tr class="request-results"><td colspan="5"><div class="notice inline notice-alt ' + classes + '"><p>' +
     52                        return '<tr class="request-results"><td colspan="5">' +
     53                                '<div class="notice inline notice-alt ' + classes + '">' +
     54                                '<p>' +
    4855                                summaryMessage +
    49                                 '</p></div></td></tr>';
     56                                '</p>' +
     57                                itemList +
     58                                '</div>' +
     59                                '</td>' +
     60                                '</tr>';
    5061                } );
    5162        }
    5263
     64        $( '.export_personal_data a' ).click( function( event ) {
     65                event.preventDefault();
     66                event.stopPropagation();
     67
     68                var $this          = $( this );
     69                var $action        = $this.parents( '.export_personal_data' );
     70                var $requestRow    = $this.parents( 'tr' );
     71                var requestID      = $action.data( 'request-id' );
     72                var nonce          = $action.data( 'nonce' );
     73                var exportersCount = $action.data( 'exporters-count' );
     74                var sendAsEmail    = $action.data( 'send-as-email' ) ? true : false;
     75
     76                $action.blur();
     77                clearResultsAfterRow( $requestRow );
     78
     79                function on_export_done_success( zipUrl ) {
     80                        set_action_state( $action, 'export_personal_data_success' );
     81                        if ( 'undefined' !== typeof zipUrl ) {
     82                                window.location = zipUrl;
     83                        } else if ( ! sendAsEmail ) {
     84                                on_export_failure( strings.noExportFile );
     85                        }
     86                }
     87
     88                function on_export_failure( errorMessage ) {
     89                        set_action_state( $action, 'export_personal_data_failed' );
     90                        if ( errorMessage ) {
     91                                appendResultsAfterRow( $requestRow, 'notice-error', strings.exportError, [ errorMessage ] );
     92                        }
     93                }
     94
     95                function do_next_export( exporterIndex, pageIndex ) {
     96                        $.ajax(
     97                                {
     98                                        url: ajaxurl,
     99                                        data: {
     100                                                action: 'wp-privacy-export-personal-data',
     101                                                exporter: exporterIndex,
     102                                                id: requestID,
     103                                                page: pageIndex,
     104                                                security: nonce,
     105                                                sendAsEmail: sendAsEmail,
     106                                        },
     107                                        method: 'post'
     108                                }
     109                        ).done( function( response ) {
     110                                if ( ! response.success ) {
     111                                        // e.g. invalid request ID
     112                                        on_export_failure( response.data );
     113                                        return;
     114                                }
     115                                var responseData = response.data;
     116                                if ( ! responseData.done ) {
     117                                        setTimeout( do_next_export( exporterIndex, pageIndex + 1 ) );
     118                                } else {
     119                                        if ( exporterIndex < exportersCount ) {
     120                                                setTimeout( do_next_export( exporterIndex + 1, 1 ) );
     121                                        } else {
     122                                                on_export_done_success( responseData.url );
     123                                        }
     124                                }
     125                        } ).fail( function( jqxhr, textStatus, error ) {
     126                                // e.g. Nonce failure
     127                                on_export_failure( error );
     128                        } );
     129                }
     130
     131                // And now, let's begin
     132                set_action_state( $action, 'export_personal_data_processing' );
     133                do_next_export( 1, 1 );
     134        } )
     135
    53136        $( '.remove_personal_data a' ).click( function( event ) {
    54137                event.preventDefault();
    55138                event.stopPropagation();
     
    92175
    93176                function on_erasure_failure() {
    94177                        set_action_state( $action, 'remove_personal_data_failed' );
    95                         appendResultsAfterRow( $requestRow, 'notice-error', strings.anErrorOccurred, [] );
     178                        appendResultsAfterRow( $requestRow, 'notice-error', strings.removalError, [] );
    96179                }
    97180
    98181                function do_next_erasure( eraserIndex, pageIndex ) {
  • src/wp-includes/comment.php

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

     
    715715                                'foundAndRemoved' => __( 'All of the personal data found for this user was removed.' ),
    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                );
    721723