WordPress.org

Make WordPress Core

Ticket #43546: 43546.5.diff

File 43546.5.diff, 14.4 KB (added by allendav, 2 years ago)

Updated to accomodate latest working copy revisions

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

     
    133133add_action( 'upgrader_process_complete', 'wp_update_plugins', 10, 0 );
    134134add_action( 'upgrader_process_complete', 'wp_update_themes', 10, 0 );
    135135
     136// Privacy hooks
     137add_filter( 'wp_privacy_personal_data_export_page', 'wp_privacy_process_personal_data_export_page', 10, 4 );
     138
    136139// Privacy policy text changes check.
    137140add_action( 'admin_init', array( 'WP_Privacy_Policy_Content', 'text_change_check' ), 20 );
    138141
  • src/wp-admin/includes/ajax-actions.php

     
    43284328}
    43294329
    43304330function wp_ajax_wp_privacy_export_personal_data() {
    4331         check_ajax_referer( 'wp-privacy-export-personal-data', 'security' );
     4331        // check_ajax_referer( 'wp-privacy-export-personal-data', 'security' );
     4332        // TODO nonce with request ID
    43324333
    43334334        if ( ! current_user_can( 'manage_options' ) ) {
    43344335                wp_send_json_error( __( 'Error: Invalid request.' ) );
    43354336        }
    43364337
    4337         $email_address  = sanitize_text_field( $_POST['email'] );
     4338        $request_id  = sanitize_text_field( $_POST['id'] );
     4339
     4340        // Find the request CPT
     4341        $request = get_post( $request_id );
     4342        if ( 'user_export_request' !== $request->post_type ) {
     4343                wp_send_json_error( 'internal error: invalid request id' );
     4344        }
     4345
     4346        $email_address = get_post_meta( $request_id, '_user_email', true );
     4347
     4348        // Surprisingly, email addresses can contain mutli-byte characters now
     4349        $email_address = trim( mb_strtolower( $email_address ) );
     4350
     4351        if ( ! is_email( $email_address ) ) {
     4352                wp_send_json_error( 'A valid email address must be given.' );
     4353        }
     4354
    43384355        $exporter_index = (int) $_POST['exporter'];
    43394356        $page           = (int) $_POST['page'];
    43404357
     
    43754392                        wp_send_json_error( 'Page index cannot be less than one.' );
    43764393                }
    43774394
    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 
    43854395                $exporter = $exporters[ $index ];
    43864396                if ( ! is_array( $exporter ) ) {
    43874397                        wp_send_json_error( "Expected an array describing the exporter at index {$exporter_index}." );
     
    44354445         * @param int    $exporter_index  The index of the exporter that provided this data.
    44364446         * @param string $email_address   The email address associated with this personal data.
    44374447         * @param int    $page            The zero-based page for this response.
     4448         * @param int    $request_id      The privacy request post ID associated with this request.
    44384449         */
    4439         $response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page );
     4450        $response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page, $request_id );
    44404451        if ( is_wp_error( $response ) ) {
    44414452                wp_send_json_error( $response );
    44424453        }
  • src/wp-admin/includes/file.php

     
    19341934        </div>
    19351935        <?php
    19361936}
     1937
     1938// TODO phpDocs
     1939function wp_privacy_generate_personal_data_export_group_html( $group_id, $group_data ) {
     1940        $group_html = '';
     1941
     1942        $group_html .= '<h2>' . esc_html( $group_data['group_label'] ) . '</h2>';
     1943        $group_html .= '<div>';
     1944
     1945        foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) {
     1946                $group_html .= '<table>';
     1947                $group_html .= '<tbody>';
     1948
     1949                foreach ( (array) $group_item_data as $group_item_datum ) {
     1950                        $group_html .= '<tr>';
     1951                        $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>';
     1952                        // TODO entities!
     1953                        $group_html .= '<td>' . esc_html( $group_item_datum['value'] ) . '</td>';
     1954                        // TODO support attachment links
     1955                        $group_html .= '</tr>';
     1956                }
     1957
     1958                $group_html .= '</tbody>';
     1959                $group_html .= '</table>';
     1960        }
     1961
     1962        $group_html .= '</div>';
     1963
     1964        return $group_html;
     1965}
     1966
     1967// TODO phpDocs
     1968function wp_privacy_generate_personal_data_export( $exports_dir, $email_address, $personal_data ) {
     1969
     1970        // TODO Instead of HTML, return a ZIP containing the HTML and the attachments
     1971        $timestamp = current_time( 'timestamp' );
     1972        $index_filename = 'wp-personal-data-export-' . md5( $email_address ) . '-' . md5( $timestamp ) . '.html';
     1973
     1974        $index_path = trailingslashit( $exports_dir ) . $index_filename;
     1975        $file = fopen( $index_path, 'w' );
     1976        // TODO catch fopen error
     1977
     1978        $title = sprintf(
     1979                __( 'Personal Data Export for %s' ),
     1980                $email_address
     1981        );
     1982
     1983        // Open HTML
     1984        fwrite( $file, "<!DOCTYPE html>\n" );
     1985        fwrite( $file, "<html>\n" );
     1986
     1987        // Head
     1988        fwrite( $file, "<head>\n" );
     1989        fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" );
     1990        fwrite( $file, "<style type='text/css'>" );
     1991        fwrite( $file, "body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }" );
     1992        fwrite( $file, "table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }" );
     1993        fwrite( $file, "th { padding: 5px; text-align: left; width: 20%; }" );
     1994        fwrite( $file, "td { padding: 5px; }" );
     1995        fwrite( $file, "tr:nth-child(odd) { background-color: #fafafa; }" );
     1996        fwrite( $file, "</style>" );
     1997        fwrite( $file, "<title>" );
     1998        fwrite( $file, esc_html( $title ) );
     1999        fwrite( $file, "</title>" );
     2000        fwrite( $file, "</head>\n" );
     2001
     2002        // Body
     2003        fwrite( $file, "<body>\n" );
     2004
     2005        // Heading
     2006        fwrite( $file, "<h1>" . esc_html__( 'Personal Data Export' ) . "</h1>" );
     2007
     2008        // And now, all the Groups
     2009        $groups = array();
     2010
     2011        // First, build a "About" group on the fly for this report
     2012        $groups['about'] = array(
     2013                'group_label' => __( 'About' ),
     2014                'items'       => array(
     2015                        'about-1' => array(
     2016                                array(
     2017                                        'name'  => __( 'Report generated for' ),
     2018                                        'value' => $email_address,
     2019                                ),
     2020                                array(
     2021                                        'name'  => __( 'For site' ),
     2022                                        'value' => get_bloginfo( 'name' ),
     2023                                ),
     2024                                array(
     2025                                        'name'  => __( 'At URL' ),
     2026                                        'value' => get_bloginfo( 'url' ),
     2027                                ),
     2028                                array(
     2029                                        'name'  => __( 'On' ),
     2030                                        'value' => current_time( 'mysql' ),
     2031                                ),
     2032                        ),
     2033                )
     2034        );
     2035
     2036        // Next, iterate over every item in $personal_data
     2037        // Extract all the unique group_ids, group_labels and item_ids
     2038        // Initialize/append the data for each item under its item_id
     2039        foreach ( (array) $personal_data as $personal_datum ) {
     2040                $group_id    = $personal_datum['group_id'];
     2041                $group_label = $personal_datum['group_label'];
     2042                if ( ! array_key_exists( $group_id, $groups ) ) {
     2043                        $groups[ $group_id ] = array(
     2044                                'group_label' => $group_label,
     2045                                'items'       => array(),
     2046                        );
     2047                }
     2048
     2049                $item_id = $personal_datum['item_id'];
     2050                if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) {
     2051                        $groups[ $group_id ]['items'][ $item_id ] = array();
     2052                }
     2053
     2054                $old_item_data = $groups[ $group_id ]['items'][ $item_id ];
     2055                $merged_item_data = array_merge( $personal_datum['data'], $old_item_data );
     2056                $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data;
     2057        }
     2058
     2059        // Now, iterate over every group in $groups and have the formatter render it in HTML
     2060        foreach ( (array) $groups as $group_id => $group_data ) {
     2061                fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_id, $group_data ) );
     2062        }
     2063
     2064        fwrite( $file, "</body>\n" );
     2065
     2066        // Close HTML
     2067        fwrite( $file, "</html>\n" );
     2068        fclose( $file );
     2069
     2070        // Now, generate the ZIP
     2071        $archive_filename = 'wp-personal-data-export-' . md5( $email_address ) . '-' . md5( $timestamp ) . '.zip';
     2072        $archive_path = trailingslashit( $exports_dir ) . $archive_filename;
     2073
     2074        $zip = new ZipArchive;
     2075        // TODO test for no ZipArchive to work with
     2076
     2077        if ( TRUE === $zip->open( $archive_path, ZipArchive::CREATE ) ) {
     2078                $zip->addFile( $index_path, 'index.html' );
     2079                // TODO - add things referenced in wp-content/uploads
     2080                $zip->close();
     2081        } else {
     2082                error_log( "unable to open zip for creation" );
     2083                // TODO handle error here
     2084        }
     2085
     2086        // And remove the HTML file
     2087        unlink( $index_path );
     2088
     2089        return(
     2090                array(
     2091                        'timestamp' => $timestamp,
     2092                        'filename' => $archive_filename,
     2093                )
     2094        );
     2095}
     2096
     2097// TODO phpDocs
     2098function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page ) {
     2099
     2100        // Housekeeping
     2101        $upload_dir  = wp_upload_dir();
     2102        $exports_dir = $upload_dir['basedir'] . '/exports';
     2103        $exports_url = $upload_dir['baseurl'] . '/exports';
     2104
     2105        // Create the exports folder if needed
     2106        $result = wp_mkdir_p( $exports_dir );
     2107        if ( is_wp_error( $result ) ) {
     2108                return $result;
     2109        }
     2110
     2111        // TODO Make sure the exports folder is protected (htaccess, index)
     2112
     2113        // Generate a export file option key from the email address
     2114        $export_file_option = '_wp_privacy_export_file_' . md5( $email_address );
     2115
     2116        // See if we have an export URL cached for this user already
     2117        $export_file_details = get_site_option( $export_file_option, '', false );
     2118
     2119        // And if we do, short circuit the export and send the path back instead
     2120        if ( ! empty( $export_file_details ) ) {
     2121                $pieces = explode( ':', $export_file_details ); // path:timestamp
     2122                $export_file_path = trailingslashit( $exports_dir ) . $pieces[0];
     2123
     2124                if ( file_exists( $export_file_path ) ) {
     2125                        return(
     2126                                array(
     2127                                        'data' => array(),
     2128                                        'done' => true,
     2129                                        'url'  => $pieces[0],
     2130                                )
     2131                        );
     2132                }
     2133
     2134                // No file? Delete the option and continue building the export
     2135                delete_site_option( $export_file_option );
     2136        }
     2137
     2138        // Generate a (temporary) data storage option key from the email address
     2139        $export_data_option = '_wp_privacy_export_data_' . md5( $email_address );
     2140
     2141        // First page of first exporter? Prepare/clear the option where we will assemble all the responses
     2142        if ( 1 === $exporter_index && 1 === $page ) {
     2143                update_site_option( $export_data_option, array() );
     2144        }
     2145
     2146        // Grab whatever data has already been collected on previous exporters and/or pages
     2147        $export_data = get_site_option( $export_data_option, array() );
     2148
     2149        // Does the response include data? If so, add this response's data to all the data we have received so far
     2150        // TODO - enforce the shape?
     2151        // TODO - enforce data as a numeric (not associative) array
     2152        $exporter_page_data = $response['data'];
     2153        if ( ! empty( $exporter_page_data ) && is_array( $exporter_page_data ) ) {
     2154                $export_data = array_merge( $export_data, $exporter_page_data );
     2155                update_site_option( $export_data_option, $export_data );
     2156        }
     2157
     2158        // Are we on the last exporter, and did that export say it was done? If not, return now
     2159        // so we can continue to collect data for export
     2160        $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );
     2161        $is_last_exporter = $exporter_index === count( $exporters );
     2162        $exporter_done = $response['done'];
     2163        if ( ! $is_last_exporter || ! $exporter_done ) {
     2164                return $response;
     2165        }
     2166
     2167        // TODO de-repetitive-ize the data
     2168
     2169        // Generate the export
     2170        $result = wp_privacy_generate_personal_data_export( $exports_dir, $email_address, $export_data );
     2171        if ( is_wp_error( $result ) ) {
     2172                return $result;
     2173        }
     2174
     2175        // Get the timestamp and filename from the export
     2176        $timestamp = $result['timestamp'];
     2177        $filename = $result['filename'];
     2178
     2179        // Build the URL for the file
     2180        $export_url = trailingslashit( $exports_url ) . $filename;
     2181
     2182        // Modify the response to include the URL of the export file
     2183        $response['url'] = $export_url;
     2184
     2185        // TODO Save the export file in an option for safekeeping
     2186
     2187        return $response;
     2188}
     2189
     2190function wp_privacy_delete_old_export_files() {
     2191
     2192        // TODO delete old export files and their options
     2193
     2194}
  • src/wp-admin/includes/user.php

     
    847847
    848848        _wp_personal_data_handle_actions();
    849849
     850        // "Borrow" xfn.js for now so we don't have to create new files.
     851        wp_enqueue_script( 'xfn' );
     852
    850853        $requests_table = new WP_Privacy_Data_Export_Requests_Table( array(
    851854                'plural'   => 'privacy_requests',
    852855                'singular' => 'privacy_request',
  • src/wp-admin/js/xfn.js

     
    5050                } );
    5151        }
    5252
     53        $( '.download_personal_data a' ).click( function( event ) {
     54                event.stopPropagation();
     55
     56                var $this          = $( this );
     57                var $action        = $this.parents( '.download_personal_data' );
     58                var requestID      = $action.data( 'request-id' );
     59                var nonce          = $action.data( 'nonce' );
     60                var exportersCount = $action.data( 'exporters-count' );
     61
     62                $action.blur();
     63
     64                function on_export_done_success( url ) {
     65                        set_action_state( $action, 'download_personal_data_idle' );
     66                        // TODO - simplify once 43551 has landed - we won't need to test for a url
     67                        // nor show the successMessage then - we can just kick off the ZIP download
     68                        if ( url ) {
     69                                window.location = url; // kick off ZIP download
     70                        } else {
     71                                alert( 'Success!' ); // TODO DO NOT COMMIT
     72                        }
     73                }
     74
     75                function on_export_failure( textStatus, error ) {
     76                        set_action_state( $action, 'download_personal_data_failed' );
     77                        // TODO show a div with the error message
     78                        alert( 'Failure!' ); // TODO DO NOT COMMIT
     79                }
     80
     81                function do_next_export( exporterIndex, pageIndex ) {
     82                        $.ajax( {
     83                                url: ajaxurl,
     84                                data: {
     85                                action: 'wp-privacy-export-personal-data',
     86                                exporter: exporterIndex,
     87                                id: requestID,
     88                                page: pageIndex,
     89                                security: nonce,
     90                        },
     91                        method: 'post'
     92                } ).done( function( response ) {
     93                        if ( ! response.success ) {
     94                                on_export_failure( 'error', responseData.data );
     95                                return;
     96                        }
     97                        var responseData = response.data;
     98                        if ( ! responseData.done ) {
     99                                setTimeout( do_next_export( exporterIndex, pageIndex + 1 ) );
     100                        } else {
     101                                if ( exporterIndex < exportersCount ) {
     102                                        setTimeout( do_next_export( exporterIndex + 1, 1 ) );
     103                                } else {
     104                                        console.log( responseData );
     105                                        on_export_done_success( responseData.url );
     106                                }
     107                        }
     108                } ).fail( function( jqxhr, textStatus, error ) {
     109                        on_export_failure( textStatus, error );
     110                } );
     111        }
     112
     113        // And now, let's begin
     114        set_action_state( $action, 'download_personal_data_processing' );
     115
     116        do_next_export( 1, 1 );
     117        } )
     118
    53119        $( '.remove_personal_data a' ).click( function( event ) {
    54120                event.preventDefault();
    55121                event.stopPropagation();