WordPress.org

Make WordPress Core

Ticket #43546: 43546.12.diff

File 43546.12.diff, 15.8 KB (added by iandunn, 2 years ago)

Move cron to front end; improve error handling; add file_create action; apply coding standards; stub unit tests

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

    diff --git src/wp-admin/includes/file.php src/wp-admin/includes/file.php
    index 8c8d6bc8d2..64cacd03df 100644
    function wp_print_request_filesystem_credentials_modal() { 
    19591959 */
    19601960function wp_privacy_generate_personal_data_export_group_html( $group_data ) {
    19611961        $allowed_tags      = array(
    1962                 'a' => array(
     1962                'a'  => array(
    19631963                        'href'   => array(),
    1964                         'target' => array()
     1964                        'target' => array(),
    19651965                ),
    1966                 'br' => array()
     1966                'br' => array(),
    19671967        );
    19681968        $allowed_protocols = array( 'http', 'https' );
    19691969        $group_html        = '';
    function wp_privacy_generate_personal_data_export_group_html( $group_data ) { 
    19991999 * @param int  $request_id  The export request ID.
    20002000 */
    20012001function 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 
    20052002        if ( ! class_exists( 'ZipArchive' ) ) {
    20062003                wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) );
    20072004        }
    function wp_privacy_generate_personal_data_export_file( $request_id ) { 
    20422039
    20432040        $stripped_email       = str_replace( '@', '-at-', $email_address );
    20442041        $stripped_email       = sanitize_title( $stripped_email ); // slugify the email address
    2045         $obscura              = md5( rand() );
     2042        $obscura              = wp_generate_password( 32, false, false );
    20462043        $file_basename        = 'wp-personal-data-file-' . $stripped_email . '-' . $obscura;
    20472044        $html_report_filename = $file_basename . '.html';
    20482045        $html_report_pathname = $exports_dir . $html_report_filename;
    2049         $file = fopen( $html_report_pathname, 'w' );
     2046        $file                 = fopen( $html_report_pathname, 'w' );
     2047
    20502048        if ( false === $file ) {
    20512049                wp_send_json_error( __( 'Unable to open export file (HTML report) for writing' ) );
    20522050        }
    function wp_privacy_generate_personal_data_export_file( $request_id ) { 
    21252123        fclose( $file );
    21262124
    21272125        // Now, generate the ZIP.
     2126        $error            = false;
    21282127        $archive_filename = $file_basename . '.zip';
    21292128        $archive_pathname = $exports_dir . $archive_filename;
    21302129        $archive_url      = $exports_url . $archive_filename;
     2130        $zip              = new ZipArchive;
    21312131
    2132         $zip = new ZipArchive;
     2132        if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) {
     2133                if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) {
     2134                        $error = __( 'Unable to add data to export file.' );
     2135                }
    21332136
    2134         if ( TRUE === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) {
    2135                 $zip->addFile( $html_report_pathname, 'index.html' );
    21362137                $zip->close();
     2138
     2139                if ( ! $error ) {
     2140                        /**
     2141                         * Fires right after all personal data has been written to the export file.
     2142                         *
     2143                         * @since 4.9.6
     2144                         *
     2145                         * @param string $archive_pathname     The full path to the export file on the filesystem.
     2146                         * @param string $archive_url          The URL of the archive file.
     2147                         * @param string $html_report_pathname The full path to the personal data report on the filesystem.
     2148                         */
     2149                        do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname );
     2150                }
    21372151        } else {
    2138                 wp_send_json_error( __( 'Unable to open export file (archive) for writing' ) );
     2152                $error = __( 'Unable to open export file (archive) for writing.' );
    21392153        }
    21402154
    21412155        // And remove the HTML file.
    21422156        unlink( $html_report_pathname );
    21432157
     2158        if ( $error ) {
     2159                wp_send_json_error( $error );
     2160        }
     2161
    21442162        // Save the export file in the request.
    21452163        update_post_meta( $request_id, '_export_file_url', $archive_url );
    21462164        update_post_meta( $request_id, '_export_file_path', $archive_pathname );
    function wp_privacy_send_personal_data_export_email( $request_id ) { 
    21622180                return new WP_Error( 'invalid', __( 'Invalid request ID when sending personal data export email.' ) );
    21632181        }
    21642182
    2165 /* translators: Do not translate LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */
    2166 $email_text = __(
    2167 'Howdy,
     2183        /** This filter is documented in wp-admin/includes/file.php */
     2184        $expiration      = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );
     2185        $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration );
    21682186
    2169 Your request for an export of personal data has been completed. You may
    2170 download your personal data by clicking on the link below. This link is
    2171 good for the next 3 days.
     2187        /* translators: Do not translate EXPIRATION, LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */
     2188        $email_text = str_replace( "\t", '', __(
     2189                'Howdy,
    21722190
    2173 ###LINK###
     2191                Your request for an export of personal data has been completed. You may
     2192                download your personal data by clicking on the link below. For privacy
     2193                and security, we will automatically delete the file on ###EXPIRATION###,
     2194                so please download it before then.
    21742195
    2175 This email has been sent to ###EMAIL###.
     2196                ###LINK###
    21762197
    2177 Regards,
    2178 All at ###SITENAME###
    2179 ###SITEURL###'
    2180 );
     2198                This email has been sent to ###EMAIL###.
     2199
     2200                Regards,
     2201                All at ###SITENAME###
     2202                ###SITEURL###'
     2203        ) );
    21812204
    21822205        /**
    21832206         * Filters the text of the email sent with a personal data export file.
    All at ###SITENAME### 
    21932216         * @param string $email_text     Text in the email.
    21942217         * @param int    $request_id     The request ID for this personal data export.
    21952218         */
    2196         $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id );
    2197 
    2198         $email_address = $request->email;
     2219        $content         = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id );
     2220        $email_address   = $request->email;
    21992221        $export_file_url = get_post_meta( $request_id, '_export_file_url', true );
    2200         $site_name = is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' );
    2201         $site_url = network_home_url();
     2222        $site_name       = is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' );
     2223        $site_url        = network_home_url();
    22022224
     2225        $content = str_replace( '###EXPIRATION###', $expiration_date, $content );
    22032226        $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content );
    22042227        $content = str_replace( '###EMAIL###', $email_address, $content );
    22052228        $content = str_replace( '###SITENAME###', wp_specialchars_decode( $site_name, ENT_QUOTES ), $content );
    All at ###SITENAME### 
    22082231        $mail_success = wp_mail(
    22092232                $email_address,
    22102233                sprintf(
     2234                        // translators: %s is the name of the site.
    22112235                        __( '[%s] Personal Data Export' ),
    22122236                        wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES )
    22132237                ),
    All at ###SITENAME### 
    22232247
    22242248/**
    22252249 * Intercept personal data exporter page ajax responses in order to assemble the personal data export file.
    2226  * @see wp_privacy_personal_data_export_page
     2250 *
    22272251 * @since 4.9.6
    22282252 *
     2253 * @see wp_privacy_personal_data_export_page
     2254 *
    22292255 * @param array  $response        The response from the personal data exporter for the given page.
    22302256 * @param int    $exporter_index  The index of the personal data exporter. Begins at 1.
    22312257 * @param string $email_address   The email address of the user whose personal data this is.
    function wp_privacy_process_personal_data_export_page( $response, $exporter_inde 
    22762302        update_post_meta( $request_id, '_export_data_raw', $export_data );
    22772303
    22782304        // If we are not yet on the last page of the last exporter, return now.
    2279         $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );
    2280         $is_last_exporter = $exporter_index === count( $exporters );
    2281         $exporter_done = $response['done'];
     2305        $exporters        = apply_filters( 'wp_privacy_personal_data_exporters', array() );
     2306        $is_last_exporter = count( $exporters ) === $exporter_index;
     2307        $exporter_done    = $response['done'];
     2308
    22822309        if ( ! $is_last_exporter || ! $exporter_done ) {
    22832310                return $response;
    22842311        }
    function wp_privacy_process_personal_data_export_page( $response, $exporter_inde 
    23022329                        $groups[ $group_id ]['items'][ $item_id ] = array();
    23032330                }
    23042331
    2305                 $old_item_data = $groups[ $group_id ]['items'][ $item_id ];
    2306                 $merged_item_data = array_merge( $export_datum['data'], $old_item_data );
     2332                $old_item_data                            = $groups[ $group_id ]['items'][ $item_id ];
     2333                $merged_item_data                         = array_merge( $export_datum['data'], $old_item_data );
    23072334                $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data;
    23082335        }
    23092336
    function wp_privacy_process_personal_data_export_page( $response, $exporter_inde 
    23442371
    23452372        return $response;
    23462373}
    2347 
    2348 /**
    2349  * Cleans up export files older than three days old.
    2350  *
    2351  * @since 4.9.6
    2352  */
    2353 function wp_privacy_delete_old_export_files() {
    2354         $upload_dir   = wp_upload_dir();
    2355         $exports_dir  = trailingslashit( $upload_dir['basedir'] . '/exports' );
    2356         $export_files = list_files( $exports_dir );
    2357 
    2358         foreach( (array) $export_files as $export_file ) {
    2359                 $file_age_in_seconds = time() - filemtime( $export_file );
    2360 
    2361                 if ( 3 * DAY_IN_SECONDS < $file_age_in_seconds ) {
    2362                         @unlink( $export_file );
    2363                 }
    2364         }
    2365 }
  • src/wp-includes/default-filters.php

    diff --git src/wp-includes/default-filters.php src/wp-includes/default-filters.php
    index dcdebacb1d..a3c68eed86 100644
    add_action( 'user_request_action_confirmed', '_wp_privacy_account_request_confir 
    352352add_filter( 'user_request_action_confirmed_message', '_wp_privacy_account_request_confirmed_message', 10, 2 );
    353353add_filter( 'wp_privacy_personal_data_exporters', 'wp_register_comment_personal_data_exporter' );
    354354add_filter( 'wp_privacy_personal_data_erasers', 'wp_register_comment_personal_data_eraser' );
     355add_action( 'init', 'wp_schedule_delete_old_privacy_export_files' );
     356add_action( 'wp_privacy_delete_old_export_files', 'wp_privacy_delete_old_export_files' );
    355357
    356358// Cron tasks
    357359add_action( 'wp_scheduled_delete', 'wp_scheduled_delete' );
  • src/wp-includes/functions.php

    diff --git src/wp-includes/functions.php src/wp-includes/functions.php
    index de978b3b5d..647e0db2d0 100644
    function wp_privacy_anonymize_data( $type, $data = '' ) { 
    62576257function _wp_privacy_active_plugins_change() {
    62586258        update_option( '_wp_privacy_text_change_check', 'check' );
    62596259}
     6260
     6261/**
     6262 * Schedule a `WP_Cron` job to delete expired export files.
     6263 *
     6264 * @since 4.9.6
     6265 */
     6266function wp_schedule_delete_old_privacy_export_files() {
     6267        if ( ! wp_next_scheduled( 'wp_privacy_delete_old_export_files' ) ) {
     6268                wp_schedule_event( time(), 'hourly', 'wp_privacy_delete_old_export_files' );
     6269        }
     6270}
     6271
     6272/**
     6273 * Cleans up export files older than three days old.
     6274 *
     6275 * The export files are stored in `wp-content/uploads`, and are therefore publicly
     6276 * accessible. A CSPRN is appended to the filename to mitigate the risk of an
     6277 * unauthorized person downloading the file, but it is still possible. Deleting
     6278 * the file after the data subject has had a chance to delete it adds an additional
     6279 * layer of protection.
     6280 *
     6281 * @since 4.9.6
     6282 */
     6283function wp_privacy_delete_old_export_files() {
     6284        $upload_dir   = wp_upload_dir();
     6285        $exports_dir  = trailingslashit( $upload_dir['basedir'] . '/exports' );
     6286        $export_files = list_files( $exports_dir, 100, array( 'index.html' ) );
     6287
     6288        /**
     6289         * Filters the lifetime, in seconds, of a personal data export file.
     6290         *
     6291         * By default, the lifetime is 3 days. Once the file reaches that age, it will automatically
     6292         * be deleted by a cron job.
     6293         *
     6294         * @since 4.9.6
     6295         *
     6296         * @param int $expiration The expiration age of the export, in seconds.
     6297         */
     6298        $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );
     6299
     6300        foreach ( (array) $export_files as $export_file ) {
     6301                $file_age_in_seconds = time() - filemtime( $export_file );
     6302
     6303                if ( $expiration < $file_age_in_seconds ) {
     6304                        unlink( $export_file );
     6305                }
     6306        }
     6307}
  • new file tests/phpunit/tests/privacy/Tests_Privacy_wpPrivacyDeleteOldExportFiles.php

    diff --git tests/phpunit/tests/privacy/Tests_Privacy_wpPrivacyDeleteOldExportFiles.php tests/phpunit/tests/privacy/Tests_Privacy_wpPrivacyDeleteOldExportFiles.php
    new file mode 100644
    index 0000000000..8f8cb23a6d
    - +  
     1<?php
     2/**
     3 * Define a class to test `wp_privacy_delete_old_export_files()`.
     4 *
     5 * @package WordPress
     6 * @subpackage UnitTests
     7 * @since 4.9.6
     8 */
     9
     10/**
     11 * Test cases for `wp_privacy_delete_old_export_files()`.
     12 *
     13 * @group privacy
     14 * @covers wp_privacy_delete_old_export_files
     15 *
     16 * @since 4.9.6
     17 */
     18class Tests_Privacy_wpPrivacyDeleteOldExportFiles extends WP_UnitTestCase {
     19        /**
     20         * Create fixtures that are shared by multiple test cases.
     21         *
     22         * @param WP_UnitTest_Factory $factory The base factory object.
     23         */
     24        public static function wpSetUpBeforeClass( $factory ) {
     25
     26        }
     27
     28        /*
     29        todo cases:
     30        ?
     31        */
     32}
  • new file tests/phpunit/tests/privacy/wpPrivacyGeneratePersonalDataExportFile.php

    diff --git tests/phpunit/tests/privacy/wpPrivacyGeneratePersonalDataExportFile.php tests/phpunit/tests/privacy/wpPrivacyGeneratePersonalDataExportFile.php
    new file mode 100644
    index 0000000000..0de255e7a5
    - +  
     1<?php
     2/**
     3 * Define a class to test `wp_privacy_generate_personal_data_export_file()`.
     4 *
     5 * @package WordPress
     6 * @subpackage UnitTests
     7 * @since 4.9.6
     8 */
     9
     10/**
     11 * Test cases for `wp_privacy_generate_personal_data_export_file()`.
     12 *
     13 * @group privacy
     14 * @covers wp_privacy_generate_personal_data_export_file
     15 *
     16 * @since 4.9.6
     17 */
     18class Tests_Privacy_wpPrivacyGeneratePersonalDataExportFile extends WP_UnitTestCase {
     19        /**
     20         * Create fixtures that are shared by multiple test cases.
     21         *
     22         * @param WP_UnitTest_Factory $factory The base factory object.
     23         */
     24        public static function wpSetUpBeforeClass( $factory ) {
     25
     26        }
     27
     28        /*
     29        todo cases:
     30        ziparchive not available
     31        invalid request id
     32        invalid email addr
     33        mkdir_p fails
     34        index.html created in export folder - silence is golden
     35        unable to protect error if can't create index.html file
     36        unable to open export file error if can't write
     37        report file exists, has starts with <html> and ends with </html>
     38        unable to add data error if can't add file to export
     39        error if can't open archive file
     40        report file removed, even if errors
     41        */
     42}
  • new file tests/phpunit/tests/privacy/wpPrivacyProcessPersonalDataExportPage.php

    diff --git tests/phpunit/tests/privacy/wpPrivacyProcessPersonalDataExportPage.php tests/phpunit/tests/privacy/wpPrivacyProcessPersonalDataExportPage.php
    new file mode 100644
    index 0000000000..7031b6f48d
    - +  
     1<?php
     2/**
     3 * Define a class to test `wp_privacy_process_personal_data_export_page()`.
     4 *
     5 * @package WordPress
     6 * @subpackage UnitTests
     7 * @since 4.9.6
     8 */
     9
     10/**
     11 * Test cases for `wp_privacy_process_personal_data_export_page()`.
     12 *
     13 * @group privacy
     14 * @covers wp_privacy_process_personal_data_export_page
     15 *
     16 * @since 4.9.6
     17 */
     18class Tests_Privacy_wpPrivacyProcessPersonalDataExportPage extends WP_UnitTestCase {
     19        /**
     20         * Create fixtures that are shared by multiple test cases.
     21         *
     22         * @param WP_UnitTest_Factory $factory The base factory object.
     23         */
     24        public static function wpSetUpBeforeClass( $factory ) {
     25
     26        }
     27
     28        /*
     29        todo cases:
     30        ?
     31        */
     32}
  • new file tests/phpunit/tests/privacy/wpPrivacySendPersonalDataExportEmail.php

    diff --git tests/phpunit/tests/privacy/wpPrivacySendPersonalDataExportEmail.php tests/phpunit/tests/privacy/wpPrivacySendPersonalDataExportEmail.php
    new file mode 100644
    index 0000000000..0a2dccb6c0
    - +  
     1<?php
     2/**
     3 * Define a class to test `wp_privacy_send_personal_data_export_email()`.
     4 *
     5 * @package WordPress
     6 * @subpackage UnitTests
     7 * @since 4.9.6
     8 */
     9
     10/**
     11 * Test cases for `wp_privacy_send_personal_data_export_email()`.
     12 *
     13 * @group privacy
     14 * @covers wp_privacy_send_personal_data_export_email
     15 *
     16 * @since 4.9.6
     17 */
     18class Tests_Privacy_wpPrivacySendPersonalDataExportEmail extends WP_UnitTestCase {
     19        /**
     20         * Create fixtures that are shared by multiple test cases.
     21         *
     22         * @param WP_UnitTest_Factory $factory The base factory object.
     23         */
     24        public static function wpSetUpBeforeClass( $factory ) {
     25
     26        }
     27
     28        /*
     29        todo cases:
     30        ?
     31        */
     32}