Make WordPress Core

Changeset 43092


Ignore:
Timestamp:
05/02/2018 02:15:05 AM (7 years ago)
Author:
SergeyBiryukov
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.
Merges [43012] and [43089] to the 4.9 branch.
See #43546.

Location:
branches/4.9
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • branches/4.9

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

    r43083 r43092  
    132132add_action( 'upgrader_process_complete', 'wp_update_plugins', 10, 0 );
    133133add_action( 'upgrader_process_complete', 'wp_update_themes', 10, 0 );
     134
     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 );
  • branches/4.9/src/wp-admin/includes/ajax-actions.php

    r43084 r43092  
    40194019}
    40204020
     4021/**
     4022 * Ajax handler for exporting a user's personal data.
     4023 *
     4024 * @since 4.9.6
     4025 */
    40214026function wp_ajax_wp_privacy_export_personal_data() {
    4022     check_ajax_referer( 'wp-privacy-export-personal-data', 'security' );
     4027    $request_id  = (int) $_POST['id'];
     4028
     4029    if ( empty( $request_id ) ) {
     4030        wp_send_json_error( __( 'Error: Invalid request ID.' ) );
     4031    }
    40234032
    40244033    if ( ! current_user_can( 'manage_options' ) ) {
     
    40264035    }
    40274036
    4028     $email_address  = sanitize_text_field( $_POST['email'] );
     4037    check_ajax_referer( 'wp-privacy-export-personal-data-' . $request_id, 'security' );
     4038
     4039    // Get the request data.
     4040    $request = wp_get_user_request_data( $request_id );
     4041
     4042    if ( ! $request || 'export_personal_data' !== $request->action_name ) {
     4043        wp_send_json_error( __( 'Error: Invalid request type.' ) );
     4044    }
     4045
     4046    $email_address = $request->email;
     4047    if ( ! is_email( $email_address ) ) {
     4048        wp_send_json_error( __( 'Error: A valid email address must be given.' ) );
     4049    }
     4050
    40294051    $exporter_index = (int) $_POST['exporter'];
    40304052    $page           = (int) $_POST['page'];
     4053    $send_as_email  = isset( $_POST['sendAsEmail'] ) ? ( "true" === $_POST['sendAsEmail'] ) : false;
    40314054
    40324055    /**
     
    40404063     *         callback               string  Callable exporter that accepts an email address and
    40414064     *                                        a page and returns an array of name => value
    4042      *                                        pairs of personal data
    4043      *         exporter_friendly_name string  Translated user facing friendly name for the exporter
     4065     *                                        pairs of personal data.
     4066     *         exporter_friendly_name string  Translated user facing friendly name for the exporter.
    40444067     *     ]
    40454068     * }
     
    40674090        }
    40684091
    4069         // Surprisingly, email addresses can contain mutli-byte characters now
    4070         $email_address = trim( mb_strtolower( $email_address ) );
    4071 
    4072         if ( ! is_email( $email_address ) ) {
    4073             wp_send_json_error( 'A valid email address must be given.' );
    4074         }
    4075 
    40764092        $exporter = $exporters[ $index ];
     4093
    40774094        if ( ! is_array( $exporter ) ) {
    40784095            wp_send_json_error( "Expected an array describing the exporter at index {$exporter_index}." );
    40794096        }
    4080         if ( ! array_key_exists( 'callback', $exporter ) ) {
    4081             wp_send_json_error( "Exporter array at index {$exporter_index} does not include a callback." );
    4082         }
    4083         if ( ! is_callable( $exporter['callback'] ) ) {
    4084             wp_send_json_error( "Exporter callback at index {$exporter_index} is not a valid callback." );
    4085         }
    40864097        if ( ! array_key_exists( 'exporter_friendly_name', $exporter ) ) {
    40874098            wp_send_json_error( "Exporter array at index {$exporter_index} does not include a friendly name." );
     4099        }
     4100        if ( ! array_key_exists( 'callback', $exporter ) ) {
     4101            wp_send_json_error( "Exporter does not include a callback: {$exporter['exporter_friendly_name']}." );
     4102        }
     4103        if ( ! is_callable( $exporter['callback'] ) ) {
     4104            wp_send_json_error( "Exporter callback is not a valid callback: {$exporter['exporter_friendly_name']}." );
    40884105        }
    40894106
     
    41094126        }
    41104127    } else {
    4111         // No exporters, so we're done
     4128        // No exporters, so we're done.
    41124129        $response = array(
    41134130            'data' => array(),
     
    41274144     * @param string $email_address   The email address associated with this personal data.
    41284145     * @param int    $page            The zero-based page for this response.
     4146     * @param int    $request_id      The privacy request post ID associated with this request.
     4147     * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
    41294148     */
    4130     $response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page );
     4149    $response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page, $request_id, $send_as_email );
     4150
    41314151    if ( is_wp_error( $response ) ) {
    41324152        wp_send_json_error( $response );
     
    41544174    check_ajax_referer( 'wp-privacy-erase-personal-data-' . $request_id, 'security' );
    41554175
    4156     // Find the request CPT
     4176    // Get the request data.
    41574177    $request = wp_get_user_request_data( $request_id );
    41584178
  • branches/4.9/src/wp-admin/includes/file.php

    r42811 r43092  
    17981798    <?php
    17991799}
     1800
     1801/**
     1802 * Generate a single group for the personal data export report.
     1803 *
     1804 * @since 4.9.6
     1805 *
     1806 * @param array  $group_data {
     1807 *     The group data to render.
     1808 *
     1809 *     @type string $group_label  The user-facing heading for the group, e.g. 'Comments'.
     1810 *     @type array  $items        {
     1811 *         An array of group items.
     1812 *
     1813 *         @type array  $group_item_data  {
     1814 *             An array of name-value pairs for the item.
     1815 *
     1816 *             @type string $name   The user-facing name of an item name-value pair, e.g. 'IP Address'.
     1817 *             @type string $value  The user-facing value of an item data pair, e.g. '50.60.70.0'.
     1818 *         }
     1819 *     }
     1820 * }
     1821 * @return string The HTML for this group and its items.
     1822 */
     1823function wp_privacy_generate_personal_data_export_group_html( $group_data ) {
     1824    $allowed_tags      = array(
     1825        'a' => array(
     1826            'href'   => array(),
     1827            'target' => array()
     1828        ),
     1829        'br' => array()
     1830    );
     1831    $allowed_protocols = array( 'http', 'https' );
     1832    $group_html        = '';
     1833
     1834    $group_html .= '<h2>' . esc_html( $group_data['group_label'] ) . '</h2>';
     1835    $group_html .= '<div>';
     1836
     1837    foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) {
     1838        $group_html .= '<table>';
     1839        $group_html .= '<tbody>';
     1840
     1841        foreach ( (array) $group_item_data as $group_item_datum ) {
     1842            $group_html .= '<tr>';
     1843            $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>';
     1844            $group_html .= '<td>' . wp_kses( $group_item_datum['value'], $allowed_tags, $allowed_protocols ) . '</td>';
     1845            $group_html .= '</tr>';
     1846        }
     1847
     1848        $group_html .= '</tbody>';
     1849        $group_html .= '</table>';
     1850    }
     1851
     1852    $group_html .= '</div>';
     1853
     1854    return $group_html;
     1855}
     1856
     1857/**
     1858 * Generate the personal data export file.
     1859 *
     1860 * @since 4.9.6
     1861 *
     1862 * @param int  $request_id  The export request ID.
     1863 */
     1864function wp_privacy_generate_personal_data_export_file( $request_id ) {
     1865    // Maybe make this a cron job instead.
     1866    wp_privacy_delete_old_export_files();
     1867
     1868    if ( ! class_exists( 'ZipArchive' ) ) {
     1869        wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) );
     1870    }
     1871
     1872    // Get the request data.
     1873    $request = wp_get_user_request_data( $request_id );
     1874
     1875    if ( ! $request || 'export_personal_data' !== $request->action_name ) {
     1876        wp_send_json_error( __( 'Invalid request ID when generating export file' ) );
     1877    }
     1878
     1879    $email_address = $request->email;
     1880
     1881    if ( ! is_email( $email_address ) ) {
     1882        wp_send_json_error( __( 'Invalid email address when generating export file' ) );
     1883    }
     1884
     1885    // Create the exports folder if needed.
     1886    $upload_dir  = wp_upload_dir();
     1887    $exports_dir = trailingslashit( $upload_dir['basedir'] . '/exports' );
     1888    $exports_url = trailingslashit( $upload_dir['baseurl'] . '/exports' );
     1889
     1890    $result = wp_mkdir_p( $exports_dir );
     1891    if ( is_wp_error( $result ) ) {
     1892        wp_send_json_error( $result->get_error_message() );
     1893    }
     1894
     1895    // Protect export folder from browsing.
     1896    $index_pathname = $exports_dir . 'index.html';
     1897    if ( ! file_exists( $index_pathname ) ) {
     1898        $file = fopen( $index_pathname, 'w' );
     1899        if ( false === $file ) {
     1900            wp_send_json_error( __( 'Unable to protect export folder from browsing' ) );
     1901        }
     1902        fwrite( $file, 'Silence is golden.' );
     1903        fclose( $file );
     1904    }
     1905
     1906    $stripped_email       = str_replace( '@', '-at-', $email_address );
     1907    $stripped_email       = sanitize_title( $stripped_email ); // slugify the email address
     1908    $obscura              = md5( rand() );
     1909    $file_basename        = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura;
     1910    $html_report_filename = $file_basename . '.html';
     1911    $html_report_pathname = $exports_dir . $html_report_filename;
     1912    $file = fopen( $html_report_pathname, 'w' );
     1913    if ( false === $file ) {
     1914        wp_send_json_error( __( 'Unable to open export file (HTML report) for writing' ) );
     1915    }
     1916
     1917    $title = sprintf(
     1918        /* translators: %s: user's e-mail address */
     1919        __( 'Personal Data Export for %s' ),
     1920        $email_address
     1921    );
     1922
     1923    // Open HTML.
     1924    fwrite( $file, "<!DOCTYPE html>\n" );
     1925    fwrite( $file, "<html>\n" );
     1926
     1927    // Head.
     1928    fwrite( $file, "<head>\n" );
     1929    fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" );
     1930    fwrite( $file, "<style type='text/css'>" );
     1931    fwrite( $file, "body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }" );
     1932    fwrite( $file, "table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }" );
     1933    fwrite( $file, "th { padding: 5px; text-align: left; width: 20%; }" );
     1934    fwrite( $file, "td { padding: 5px; }" );
     1935    fwrite( $file, "tr:nth-child(odd) { background-color: #fafafa; }" );
     1936    fwrite( $file, "</style>" );
     1937    fwrite( $file, "<title>" );
     1938    fwrite( $file, esc_html( $title ) );
     1939    fwrite( $file, "</title>" );
     1940    fwrite( $file, "</head>\n" );
     1941
     1942    // Body.
     1943    fwrite( $file, "<body>\n" );
     1944
     1945    // Heading.
     1946    fwrite( $file, "<h1>" . esc_html__( 'Personal Data Export' ) . "</h1>" );
     1947
     1948    // And now, all the Groups.
     1949    $groups = get_post_meta( $request_id, '_export_data_grouped', true );
     1950
     1951    // First, build an "About" group on the fly for this report.
     1952    $about_group = array(
     1953        'group_label' => __( 'About' ),
     1954        'items'       => array(
     1955            'about-1' => array(
     1956                array(
     1957                    'name'  => __( 'Report generated for' ),
     1958                    'value' => $email_address,
     1959                ),
     1960                array(
     1961                    'name'  => __( 'For site' ),
     1962                    'value' => get_bloginfo( 'name' ),
     1963                ),
     1964                array(
     1965                    'name'  => __( 'At URL' ),
     1966                    'value' => get_bloginfo( 'url' ),
     1967                ),
     1968                array(
     1969                    'name'  => __( 'On' ),
     1970                    'value' => current_time( 'mysql' ),
     1971                ),
     1972            ),
     1973        ),
     1974    );
     1975
     1976    // Merge in the special about group.
     1977    $groups = array_merge( array( 'about' => $about_group ), $groups );
     1978
     1979    // Now, iterate over every group in $groups and have the formatter render it in HTML.
     1980    foreach ( (array) $groups as $group_id => $group_data ) {
     1981        fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data ) );
     1982    }
     1983
     1984    fwrite( $file, "</body>\n" );
     1985
     1986    // Close HTML.
     1987    fwrite( $file, "</html>\n" );
     1988    fclose( $file );
     1989
     1990    // Now, generate the ZIP.
     1991    $archive_filename = $file_basename . '.zip';
     1992    $archive_pathname = $exports_dir . $archive_filename;
     1993    $archive_url      = $exports_url . $archive_filename;
     1994
     1995    $zip = new ZipArchive;
     1996
     1997    if ( TRUE === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) {
     1998        $zip->addFile( $html_report_pathname, 'index.html' );
     1999        $zip->close();
     2000    } else {
     2001        wp_send_json_error( __( 'Unable to open export file (archive) for writing' ) );
     2002    }
     2003
     2004    // And remove the HTML file.
     2005    unlink( $html_report_pathname );
     2006
     2007    // Save the export file in the request.
     2008    update_post_meta( $request_id, '_export_file_url', $archive_url );
     2009    update_post_meta( $request_id, '_export_file_path', $archive_pathname );
     2010}
     2011
     2012/**
     2013 * Send an email to the user with a link to the personal data export file
     2014 *
     2015 * @since 4.9.6
     2016 *
     2017 * @param int  $request_id  The request ID for this personal data export.
     2018 * @return true|WP_Error    True on success or `WP_Error` on failure.
     2019 */
     2020function wp_privacy_send_personal_data_export_email( $request_id ) {
     2021    // Get the request data.
     2022    $request = wp_get_user_request_data( $request_id );
     2023
     2024    if ( ! $request || 'export_personal_data' !== $request->action_name ) {
     2025        return new WP_Error( 'invalid', __( 'Invalid request ID when sending personal data export email.' ) );
     2026    }
     2027
     2028/* translators: Do not translate LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */
     2029$email_text = __(
     2030'Howdy,
     2031
     2032Your request for an export of personal data has been completed. You may
     2033download your personal data by clicking on the link below. This link is
     2034good for the next 3 days.
     2035
     2036###LINK###
     2037
     2038This email has been sent to ###EMAIL###.
     2039
     2040Regards,
     2041All at ###SITENAME###
     2042###SITEURL###'
     2043);
     2044
     2045    /**
     2046     * Filters the text of the email sent with a personal data export file.
     2047     *
     2048     * The following strings have a special meaning and will get replaced dynamically:
     2049     * ###LINK###               URL of the personal data export file for the user.
     2050     * ###EMAIL###              The email we are sending to.
     2051     * ###SITENAME###           The name of the site.
     2052     * ###SITEURL###            The URL to the site.
     2053     *
     2054     * @since 4.9.6
     2055     *
     2056     * @param string $email_text     Text in the email.
     2057     * @param int    $request_id     The request ID for this personal data export.
     2058     */
     2059    $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id );
     2060
     2061    $email_address = $request->email;
     2062    $export_file_url = get_post_meta( $request_id, '_export_file_url', true );
     2063    $site_name = is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' );
     2064    $site_url = network_home_url();
     2065
     2066    $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content );
     2067    $content = str_replace( '###EMAIL###', $email_address, $content );
     2068    $content = str_replace( '###SITENAME###', wp_specialchars_decode( $site_name, ENT_QUOTES ), $content );
     2069    $content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content );
     2070
     2071    $mail_success = wp_mail(
     2072        $email_address,
     2073        sprintf(
     2074            __( '[%s] Personal Data Export' ),
     2075            wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES )
     2076        ),
     2077        $content
     2078    );
     2079
     2080    if ( ! $mail_success ) {
     2081        return new WP_Error( 'error', __( 'Unable to send personal data export email.' ) );
     2082    }
     2083
     2084    return true;
     2085}
     2086
     2087/**
     2088 * Intercept personal data exporter page ajax responses in order to assemble the personal data export file.
     2089 * @see wp_privacy_personal_data_export_page
     2090 * @since 4.9.6
     2091 *
     2092 * @param array  $response        The response from the personal data exporter for the given page.
     2093 * @param int    $exporter_index  The index of the personal data exporter. Begins at 1.
     2094 * @param string $email_address   The email address of the user whose personal data this is.
     2095 * @param int    $page            The page of personal data for this exporter. Begins at 1.
     2096 * @param int    $request_id      The request ID for this personal data export.
     2097 * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
     2098 * @return array The filtered response.
     2099 */
     2100function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email ) {
     2101    /* Do some simple checks on the shape of the response from the exporter.
     2102     * If the exporter response is malformed, don't attempt to consume it - let it
     2103     * pass through to generate a warning to the user by default ajax processing.
     2104     */
     2105    if ( ! is_array( $response ) ) {
     2106        return $response;
     2107    }
     2108
     2109    if ( ! array_key_exists( 'done', $response ) ) {
     2110        return $response;
     2111    }
     2112
     2113    if ( ! array_key_exists( 'data', $response ) ) {
     2114        return $response;
     2115    }
     2116
     2117    if ( ! is_array( $response['data'] ) ) {
     2118        return $response;
     2119    }
     2120
     2121    // Get the request data.
     2122    $request = wp_get_user_request_data( $request_id );
     2123
     2124    if ( ! $request || 'export_personal_data' !== $request->action_name ) {
     2125        wp_send_json_error( __( 'Invalid request ID when merging exporter data' ) );
     2126    }
     2127
     2128    $export_data = array();
     2129
     2130    // First exporter, first page? Reset the report data accumulation array.
     2131    if ( 1 === $exporter_index && 1 === $page ) {
     2132        update_post_meta( $request_id, '_export_data_raw', $export_data );
     2133    } else {
     2134        $export_data = get_post_meta( $request_id, '_export_data_raw', true );
     2135    }
     2136
     2137    // Now, merge the data from the exporter response into the data we have accumulated already.
     2138    $export_data = array_merge( $export_data, $response['data'] );
     2139    update_post_meta( $request_id, '_export_data_raw', $export_data );
     2140
     2141    // If we are not yet on the last page of the last exporter, return now.
     2142    $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );
     2143    $is_last_exporter = $exporter_index === count( $exporters );
     2144    $exporter_done = $response['done'];
     2145    if ( ! $is_last_exporter || ! $exporter_done ) {
     2146        return $response;
     2147    }
     2148
     2149    // Last exporter, last page - let's prepare the export file.
     2150
     2151    // First we need to re-organize the raw data hierarchically in groups and items.
     2152    $groups = array();
     2153    foreach ( (array) $export_data as $export_datum ) {
     2154        $group_id    = $export_datum['group_id'];
     2155        $group_label = $export_datum['group_label'];
     2156        if ( ! array_key_exists( $group_id, $groups ) ) {
     2157            $groups[ $group_id ] = array(
     2158                'group_label' => $group_label,
     2159                'items'       => array(),
     2160            );
     2161        }
     2162
     2163        $item_id = $export_datum['item_id'];
     2164        if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) {
     2165            $groups[ $group_id ]['items'][ $item_id ] = array();
     2166        }
     2167
     2168        $old_item_data = $groups[ $group_id ]['items'][ $item_id ];
     2169        $merged_item_data = array_merge( $export_datum['data'], $old_item_data );
     2170        $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data;
     2171    }
     2172
     2173    // Then save the grouped data into the request.
     2174    delete_post_meta( $request_id, '_export_data_raw' );
     2175    update_post_meta( $request_id, '_export_data_grouped', $groups );
     2176
     2177    // And now, generate the export file, cleaning up any previous file
     2178    $export_path = get_post_meta( $request_id, '_export_file_path', true );
     2179    if ( ! empty( $export_path ) ) {
     2180        delete_post_meta( $request_id, '_export_file_path' );
     2181        @unlink( $export_path );
     2182    }
     2183    delete_post_meta( $request_id, '_export_file_url' );
     2184
     2185    // Generate the export file from the collected, grouped personal data.
     2186    do_action( 'wp_privacy_personal_data_export_file', $request_id );
     2187
     2188    // Clear the grouped data now that it is no longer needed.
     2189    delete_post_meta( $request_id, '_export_data_grouped' );
     2190
     2191    // If the destination is email, send it now.
     2192    if ( $send_as_email ) {
     2193        $mail_success = wp_privacy_send_personal_data_export_email( $request_id );
     2194        if ( is_wp_error( $mail_success ) ) {
     2195            wp_send_json_error( $mail_success->get_error_message() );
     2196        }
     2197    } else {
     2198        // Modify the response to include the URL of the export file so the browser can fetch it.
     2199        $export_file_url = get_post_meta( $request_id, '_export_file_url', true );
     2200        if ( ! empty( $export_file_url ) ) {
     2201            $response['url'] = $export_file_url;
     2202        }
     2203    }
     2204
     2205    // Update the request to completed state.
     2206    _wp_privacy_completed_request( $request_id );
     2207
     2208    return $response;
     2209}
     2210
     2211/**
     2212 * Cleans up export files older than three days old.
     2213 *
     2214 * @since 4.9.6
     2215 */
     2216function wp_privacy_delete_old_export_files() {
     2217    $upload_dir   = wp_upload_dir();
     2218    $exports_dir  = trailingslashit( $upload_dir['basedir'] . '/exports' );
     2219    $export_files = list_files( $exports_dir );
     2220
     2221    foreach( (array) $export_files as $export_file ) {
     2222        $file_age_in_seconds = time() - filemtime( $export_file );
     2223
     2224        if ( 3 * DAY_IN_SECONDS < $file_age_in_seconds ) {
     2225            @unlink( $export_file );
     2226        }
     2227    }
     2228}
  • branches/4.9/src/wp-admin/includes/user.php

    r43086 r43092  
    620620                'privacy_action_email_retry',
    621621                __( 'Confirmation request re-resent successfully.' ),
    622                 'updated'
    623             );
    624         }
    625 
    626     } elseif ( isset( $_POST['export_personal_data_email_send'] ) ) { // WPCS: input var ok.
    627         check_admin_referer( 'bulk-privacy_requests' );
    628 
    629         $request_id = absint( current( array_keys( (array) wp_unslash( $_POST['export_personal_data_email_send'] ) ) ) ); // WPCS: input var ok, sanitization ok.
    630         $result     = false;
    631 
    632         /**
    633          * TODO: Email the data to the user here.
    634          */
    635 
    636         if ( is_wp_error( $result ) ) {
    637             add_settings_error(
    638                 'export_personal_data_email_send',
    639                 'export_personal_data_email_send',
    640                 $result->get_error_message(),
    641                 'error'
    642             );
    643         } else {
    644             _wp_privacy_completed_request( $request_id );
    645             add_settings_error(
    646                 'export_personal_data_email_send',
    647                 'export_personal_data_email_send',
    648                 __( 'Personal data was sent to the user successfully.' ),
    649622                'updated'
    650623            );
     
    779752    _wp_personal_data_cleanup_requests();
    780753
     754    // "Borrow" xfn.js for now so we don't have to create new files.
     755    wp_enqueue_script( 'xfn' );
     756
    781757    $requests_table = new WP_Privacy_Data_Export_Requests_Table( array(
    782758        'plural'   => 'privacy_requests',
     
    13211297        $nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
    13221298
    1323         $download_data_markup = '<div class="download_personal_data" ' .
     1299        $download_data_markup = '<div class="export_personal_data" ' .
    13241300            'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
    13251301            'data-request-id="' . esc_attr( $request_id ) . '" ' .
     
    13271303            '">';
    13281304
    1329         $download_data_markup .= '<span class="download_personal_data_idle"><a href="#" >' . __( 'Download Personal Data' ) . '</a></span>' .
    1330             '<span style="display:none" class="download_personal_data_processing" >' . __( 'Downloading Data...' ) . '</span>' .
    1331             '<span style="display:none" class="download_personal_data_failed">' . __( 'Download Failed!' ) . ' <a href="#" >' . __( 'Retry' ) . '</a></span>';
     1305        $download_data_markup .= '<span class="export_personal_data_idle"><a href="#" >' . __( 'Download Personal Data' ) . '</a></span>' .
     1306            '<span style="display:none" class="export_personal_data_processing" >' . __( 'Downloading Data...' ) . '</span>' .
     1307            '<span style="display:none" class="export_personal_data_success"><a href="#" >' . __( 'Download Personal Data Again' ) . '</a></span>' .
     1308            '<span style="display:none" class="export_personal_data_failed">' . __( 'Download Failed!' ) . ' <a href="#" >' . __( 'Retry' ) . '</a></span>';
     1309
     1310        $download_data_markup .= '</div>';
    13321311
    13331312        $row_actions = array(
     
    13531332                break;
    13541333            case 'request-confirmed':
    1355                 // TODO Complete in follow on patch.
     1334                $exporters       = apply_filters( 'wp_privacy_personal_data_exporters', array() );
     1335                $exporters_count = count( $exporters );
     1336                $request_id      = $item->ID;
     1337                $nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
     1338
     1339                echo '<div class="export_personal_data" ' .
     1340                    'data-send-as-email="1" ' .
     1341                    'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
     1342                    'data-request-id="' . esc_attr( $request_id ) . '" ' .
     1343                    'data-nonce="' . esc_attr( $nonce ) .
     1344                    '">';
     1345
     1346                ?>
     1347                <span class="export_personal_data_idle"><a class="button" href="#" ><?php _e( 'Email Data' ); ?></a></span>
     1348                <span style="display:none" class="export_personal_data_processing button updating-message" ><?php _e( 'Sending Email...' ); ?></span>
     1349                <span style="display:none" class="export_personal_data_success success-message" ><?php _e( 'Email Sent!' ); ?></span>
     1350                <span style="display:none" class="export_personal_data_failed"><?php _e( 'Email Failed!' ); ?> <a class="button" href="#" ><?php _e( 'Retry' ); ?></a></span>
     1351                <?php
     1352
     1353                echo '</div>';
    13561354                break;
    13571355            case 'request-failed':
     
    14211419                '<span style="display:none" class="remove_personal_data_failed">' . __( 'Force Remove Failed!' ) . ' <a href="#" >' . __( 'Retry' ) . '</a></span>';
    14221420
     1421            $remove_data_markup .= '</div>';
     1422
    14231423            $row_actions = array(
    14241424                'remove_data' => $remove_data_markup,
     
    14611461                <span style="display:none" class="remove_personal_data_failed"><?php _e( 'Removing Data Failed!' ); ?> <a class="button" href="#" ><?php _e( 'Retry' ); ?></a></span>
    14621462                <?php
     1463
     1464                echo '</div>';
    14631465
    14641466                break;
  • branches/4.9/src/wp-admin/js/xfn.js

    r43080 r43092  
    1818
    1919// Privacy request action handling
    20 
    2120jQuery( document ).ready( function( $ ) {
    2221    var strings = window.privacyToolsL10n || {};
     
    3534    function appendResultsAfterRow( $requestRow, classes, summaryMessage, additionalMessages ) {
    3635        clearResultsAfterRow( $requestRow );
     36
     37        var itemList = '';
    3738        if ( additionalMessages.length ) {
    38             // TODO - render additionalMessages after the summaryMessage
     39            $.each( additionalMessages, function( index, value ) {
     40                itemList = itemList + '<li>' + value + '</li>';
     41            } );
     42            itemList = '<ul>' + itemList + '</ul>';
    3943        }
    4044
    4145        $requestRow.after( function() {
    42             return '<tr class="request-results"><td colspan="5"><div class="notice inline notice-alt ' + classes + '"><p>' +
    43                 summaryMessage +
    44                 '</p></div></td></tr>';
     46            return '<tr class="request-results"><td colspan="5">' +
     47                '<div class="notice inline notice-alt ' + classes + '">' +
     48                '<p>' + summaryMessage + '</p>' +
     49                itemList +
     50                '</div>' +
     51                '</td>' +
     52                '</tr>';
    4553        } );
    4654    }
     55
     56    $( '.export_personal_data a' ).click( function( event ) {
     57        event.preventDefault();
     58        event.stopPropagation();
     59
     60        var $this          = $( this );
     61        var $action        = $this.parents( '.export_personal_data' );
     62        var $requestRow    = $this.parents( 'tr' );
     63        var requestID      = $action.data( 'request-id' );
     64        var nonce          = $action.data( 'nonce' );
     65        var exportersCount = $action.data( 'exporters-count' );
     66        var sendAsEmail    = $action.data( 'send-as-email' ) ? true : false;
     67
     68        $action.blur();
     69        clearResultsAfterRow( $requestRow );
     70
     71        function on_export_done_success( zipUrl ) {
     72            set_action_state( $action, 'export_personal_data_success' );
     73            if ( 'undefined' !== typeof zipUrl ) {
     74                window.location = zipUrl;
     75            } else if ( ! sendAsEmail ) {
     76                on_export_failure( strings.noExportFile );
     77            }
     78        }
     79
     80        function on_export_failure( errorMessage ) {
     81            set_action_state( $action, 'export_personal_data_failed' );
     82            if ( errorMessage ) {
     83                appendResultsAfterRow( $requestRow, 'notice-error', strings.exportError, [ errorMessage ] );
     84            }
     85        }
     86
     87        function do_next_export( exporterIndex, pageIndex ) {
     88            $.ajax(
     89                {
     90                    url: window.ajaxurl,
     91                    data: {
     92                        action: 'wp-privacy-export-personal-data',
     93                        exporter: exporterIndex,
     94                        id: requestID,
     95                        page: pageIndex,
     96                        security: nonce,
     97                        sendAsEmail: sendAsEmail
     98                    },
     99                    method: 'post'
     100                }
     101            ).done( function( response ) {
     102                if ( ! response.success ) {
     103                    // e.g. invalid request ID
     104                    on_export_failure( response.data );
     105                    return;
     106                }
     107                var responseData = response.data;
     108                if ( ! responseData.done ) {
     109                    setTimeout( do_next_export( exporterIndex, pageIndex + 1 ) );
     110                } else {
     111                    if ( exporterIndex < exportersCount ) {
     112                        setTimeout( do_next_export( exporterIndex + 1, 1 ) );
     113                    } else {
     114                        on_export_done_success( responseData.url );
     115                    }
     116                }
     117            } ).fail( function( jqxhr, textStatus, error ) {
     118                // e.g. Nonce failure
     119                on_export_failure( error );
     120            } );
     121        }
     122
     123        // And now, let's begin
     124        set_action_state( $action, 'export_personal_data_processing' );
     125        do_next_export( 1, 1 );
     126    } );
    47127
    48128    $( '.remove_personal_data a' ).click( function( event ) {
     
    88168        function on_erasure_failure() {
    89169            set_action_state( $action, 'remove_personal_data_failed' );
    90             appendResultsAfterRow( $requestRow, 'notice-error', strings.anErrorOccurred, [] );
     170            appendResultsAfterRow( $requestRow, 'notice-error', strings.removalError, [] );
    91171        }
    92172
  • branches/4.9/src/wp-includes/comment.php

    r43080 r43092  
    32243224                case 'comment_link':
    32253225                    $value = get_comment_link( $comment->comment_ID );
     3226                    $value = '<a href="' . $value . '" target="_blank" rel="noreferrer noopener">' . $value . '</a>';
    32263227                    break;
    32273228            }
  • branches/4.9/src/wp-includes/script-loader.php

    r43075 r43092  
    666666                'noneRemoved'     => __( 'Personal data was found for this user but was not removed.' ),
    667667                'someNotRemoved'  => __( 'Personal data was found for this user but some of the personal data found was not removed.' ),
    668                 'anErrorOccurred' => __( 'An error occurred while attempting to find and remove personal data.' ),
     668                'removalError'    => __( 'An error occurred while attempting to find and remove personal data.' ),
     669                'noExportFile'    => __( 'No personal data export file was generated.' ),
     670                'exportError'     => __( 'An error occurred while attempting to export personal data.' ),
    669671            )
    670672        );
  • branches/4.9/src/wp-includes/user.php

    r43084 r43092  
    30703070 *
    30713071 * @param int $request_id Request ID to get data about.
    3072  * @return array|false
     3072 * @return WP_User_Request|false
    30733073 */
    30743074function wp_get_user_request_data( $request_id ) {
Note: See TracChangeset for help on using the changeset viewer.