Make WordPress Core

source: tags/5.9/src/wp-admin/includes/privacy-tools.php

Last change on this file was 51662, checked in by hellofromTonya, 10 months ago

Coding Standards: Fix indentation and remove ignore annotation in wp_privacy_send_personal_data_export_email().

Follow-up to [51129], [51410].

Props jrf, hellofromTonya.
See #53359.

  • Property svn:eol-style set to native
File size: 32.7 KB
Line 
1<?php
2/**
3 * WordPress Administration Privacy Tools API.
4 *
5 * @package WordPress
6 * @subpackage Administration
7 */
8
9/**
10 * Resend an existing request and return the result.
11 *
12 * @since 4.9.6
13 * @access private
14 *
15 * @param int $request_id Request ID.
16 * @return true|WP_Error Returns true if sending the email was successful, or a WP_Error object.
17 */
18function _wp_privacy_resend_request( $request_id ) {
19        $request_id = absint( $request_id );
20        $request    = get_post( $request_id );
21
22        if ( ! $request || 'user_request' !== $request->post_type ) {
23                return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) );
24        }
25
26        $result = wp_send_user_request( $request_id );
27
28        if ( is_wp_error( $result ) ) {
29                return $result;
30        } elseif ( ! $result ) {
31                return new WP_Error( 'privacy_request_error', __( 'Unable to initiate confirmation for personal data request.' ) );
32        }
33
34        return true;
35}
36
37/**
38 * Marks a request as completed by the admin and logs the current timestamp.
39 *
40 * @since 4.9.6
41 * @access private
42 *
43 * @param int $request_id Request ID.
44 * @return int|WP_Error Request ID on success, or a WP_Error on failure.
45 */
46function _wp_privacy_completed_request( $request_id ) {
47        // Get the request.
48        $request_id = absint( $request_id );
49        $request    = wp_get_user_request( $request_id );
50
51        if ( ! $request ) {
52                return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) );
53        }
54
55        update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() );
56
57        $result = wp_update_post(
58                array(
59                        'ID'          => $request_id,
60                        'post_status' => 'request-completed',
61                )
62        );
63
64        return $result;
65}
66
67/**
68 * Handle list table actions.
69 *
70 * @since 4.9.6
71 * @access private
72 */
73function _wp_personal_data_handle_actions() {
74        if ( isset( $_POST['privacy_action_email_retry'] ) ) {
75                check_admin_referer( 'bulk-privacy_requests' );
76
77                $request_id = absint( current( array_keys( (array) wp_unslash( $_POST['privacy_action_email_retry'] ) ) ) );
78                $result     = _wp_privacy_resend_request( $request_id );
79
80                if ( is_wp_error( $result ) ) {
81                        add_settings_error(
82                                'privacy_action_email_retry',
83                                'privacy_action_email_retry',
84                                $result->get_error_message(),
85                                'error'
86                        );
87                } else {
88                        add_settings_error(
89                                'privacy_action_email_retry',
90                                'privacy_action_email_retry',
91                                __( 'Confirmation request sent again successfully.' ),
92                                'success'
93                        );
94                }
95        } elseif ( isset( $_POST['action'] ) ) {
96                $action = ! empty( $_POST['action'] ) ? sanitize_key( wp_unslash( $_POST['action'] ) ) : '';
97
98                switch ( $action ) {
99                        case 'add_export_personal_data_request':
100                        case 'add_remove_personal_data_request':
101                                check_admin_referer( 'personal-data-request' );
102
103                                if ( ! isset( $_POST['type_of_action'], $_POST['username_or_email_for_privacy_request'] ) ) {
104                                        add_settings_error(
105                                                'action_type',
106                                                'action_type',
107                                                __( 'Invalid personal data action.' ),
108                                                'error'
109                                        );
110                                }
111                                $action_type               = sanitize_text_field( wp_unslash( $_POST['type_of_action'] ) );
112                                $username_or_email_address = sanitize_text_field( wp_unslash( $_POST['username_or_email_for_privacy_request'] ) );
113                                $email_address             = '';
114                                $status                    = 'pending';
115
116                                if ( ! isset( $_POST['send_confirmation_email'] ) ) {
117                                        $status = 'confirmed';
118                                }
119
120                                if ( ! in_array( $action_type, _wp_privacy_action_request_types(), true ) ) {
121                                        add_settings_error(
122                                                'action_type',
123                                                'action_type',
124                                                __( 'Invalid personal data action.' ),
125                                                'error'
126                                        );
127                                }
128
129                                if ( ! is_email( $username_or_email_address ) ) {
130                                        $user = get_user_by( 'login', $username_or_email_address );
131                                        if ( ! $user instanceof WP_User ) {
132                                                add_settings_error(
133                                                        'username_or_email_for_privacy_request',
134                                                        'username_or_email_for_privacy_request',
135                                                        __( 'Unable to add this request. A valid email address or username must be supplied.' ),
136                                                        'error'
137                                                );
138                                        } else {
139                                                $email_address = $user->user_email;
140                                        }
141                                } else {
142                                        $email_address = $username_or_email_address;
143                                }
144
145                                if ( empty( $email_address ) ) {
146                                        break;
147                                }
148
149                                $request_id = wp_create_user_request( $email_address, $action_type, array(), $status );
150                                $message    = '';
151
152                                if ( is_wp_error( $request_id ) ) {
153                                        $message = $request_id->get_error_message();
154                                } elseif ( ! $request_id ) {
155                                        $message = __( 'Unable to initiate confirmation request.' );
156                                }
157
158                                if ( $message ) {
159                                        add_settings_error(
160                                                'username_or_email_for_privacy_request',
161                                                'username_or_email_for_privacy_request',
162                                                $message,
163                                                'error'
164                                        );
165                                        break;
166                                }
167
168                                if ( 'pending' === $status ) {
169                                        wp_send_user_request( $request_id );
170
171                                        $message = __( 'Confirmation request initiated successfully.' );
172                                } elseif ( 'confirmed' === $status ) {
173                                        $message = __( 'Request added successfully.' );
174                                }
175
176                                if ( $message ) {
177                                        add_settings_error(
178                                                'username_or_email_for_privacy_request',
179                                                'username_or_email_for_privacy_request',
180                                                $message,
181                                                'success'
182                                        );
183                                        break;
184                                }
185                }
186        }
187}
188
189/**
190 * Cleans up failed and expired requests before displaying the list table.
191 *
192 * @since 4.9.6
193 * @access private
194 */
195function _wp_personal_data_cleanup_requests() {
196        /** This filter is documented in wp-includes/user.php */
197        $expires = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS );
198
199        $requests_query = new WP_Query(
200                array(
201                        'post_type'      => 'user_request',
202                        'posts_per_page' => -1,
203                        'post_status'    => 'request-pending',
204                        'fields'         => 'ids',
205                        'date_query'     => array(
206                                array(
207                                        'column' => 'post_modified_gmt',
208                                        'before' => $expires . ' seconds ago',
209                                ),
210                        ),
211                )
212        );
213
214        $request_ids = $requests_query->posts;
215
216        foreach ( $request_ids as $request_id ) {
217                wp_update_post(
218                        array(
219                                'ID'            => $request_id,
220                                'post_status'   => 'request-failed',
221                                'post_password' => '',
222                        )
223                );
224        }
225}
226
227/**
228 * Generate a single group for the personal data export report.
229 *
230 * @since 4.9.6
231 * @since 5.4.0 Added the `$group_id` and `$groups_count` parameters.
232 *
233 * @param array  $group_data {
234 *     The group data to render.
235 *
236 *     @type string $group_label  The user-facing heading for the group, e.g. 'Comments'.
237 *     @type array  $items        {
238 *         An array of group items.
239 *
240 *         @type array  $group_item_data  {
241 *             An array of name-value pairs for the item.
242 *
243 *             @type string $name   The user-facing name of an item name-value pair, e.g. 'IP Address'.
244 *             @type string $value  The user-facing value of an item data pair, e.g. '50.60.70.0'.
245 *         }
246 *     }
247 * }
248 * @param string $group_id     The group identifier.
249 * @param int    $groups_count The number of all groups
250 * @return string The HTML for this group and its items.
251 */
252function wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id = '', $groups_count = 1 ) {
253        $group_id_attr = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id );
254
255        $group_html  = '<h2 id="' . esc_attr( $group_id_attr ) . '">';
256        $group_html .= esc_html( $group_data['group_label'] );
257
258        $items_count = count( (array) $group_data['items'] );
259        if ( $items_count > 1 ) {
260                $group_html .= sprintf( ' <span class="count">(%d)</span>', $items_count );
261        }
262
263        $group_html .= '</h2>';
264
265        if ( ! empty( $group_data['group_description'] ) ) {
266                $group_html .= '<p>' . esc_html( $group_data['group_description'] ) . '</p>';
267        }
268
269        $group_html .= '<div>';
270
271        foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) {
272                $group_html .= '<table>';
273                $group_html .= '<tbody>';
274
275                foreach ( (array) $group_item_data as $group_item_datum ) {
276                        $value = $group_item_datum['value'];
277                        // If it looks like a link, make it a link.
278                        if ( false === strpos( $value, ' ' ) && ( 0 === strpos( $value, 'http://' ) || 0 === strpos( $value, 'https://' ) ) ) {
279                                $value = '<a href="' . esc_url( $value ) . '">' . esc_html( $value ) . '</a>';
280                        }
281
282                        $group_html .= '<tr>';
283                        $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>';
284                        $group_html .= '<td>' . wp_kses( $value, 'personal_data_export' ) . '</td>';
285                        $group_html .= '</tr>';
286                }
287
288                $group_html .= '</tbody>';
289                $group_html .= '</table>';
290        }
291
292        if ( $groups_count > 1 ) {
293                $group_html .= '<div class="return-to-top">';
294                $group_html .= '<a href="#top"><span aria-hidden="true">&uarr; </span> ' . esc_html__( 'Go to top' ) . '</a>';
295                $group_html .= '</div>';
296        }
297
298        $group_html .= '</div>';
299
300        return $group_html;
301}
302
303/**
304 * Generate the personal data export file.
305 *
306 * @since 4.9.6
307 *
308 * @param int $request_id The export request ID.
309 */
310function wp_privacy_generate_personal_data_export_file( $request_id ) {
311        if ( ! class_exists( 'ZipArchive' ) ) {
312                wp_send_json_error( __( 'Unable to generate personal data export file. ZipArchive not available.' ) );
313        }
314
315        // Get the request.
316        $request = wp_get_user_request( $request_id );
317
318        if ( ! $request || 'export_personal_data' !== $request->action_name ) {
319                wp_send_json_error( __( 'Invalid request ID when generating personal data export file.' ) );
320        }
321
322        $email_address = $request->email;
323
324        if ( ! is_email( $email_address ) ) {
325                wp_send_json_error( __( 'Invalid email address when generating personal data export file.' ) );
326        }
327
328        // Create the exports folder if needed.
329        $exports_dir = wp_privacy_exports_dir();
330        $exports_url = wp_privacy_exports_url();
331
332        if ( ! wp_mkdir_p( $exports_dir ) ) {
333                wp_send_json_error( __( 'Unable to create personal data export folder.' ) );
334        }
335
336        // Protect export folder from browsing.
337        $index_pathname = $exports_dir . 'index.php';
338        if ( ! file_exists( $index_pathname ) ) {
339                $file = fopen( $index_pathname, 'w' );
340                if ( false === $file ) {
341                        wp_send_json_error( __( 'Unable to protect personal data export folder from browsing.' ) );
342                }
343                fwrite( $file, "<?php\n// Silence is golden.\n" );
344                fclose( $file );
345        }
346
347        $obscura              = wp_generate_password( 32, false, false );
348        $file_basename        = 'wp-personal-data-file-' . $obscura;
349        $html_report_filename = wp_unique_filename( $exports_dir, $file_basename . '.html' );
350        $html_report_pathname = wp_normalize_path( $exports_dir . $html_report_filename );
351        $json_report_filename = $file_basename . '.json';
352        $json_report_pathname = wp_normalize_path( $exports_dir . $json_report_filename );
353
354        /*
355         * Gather general data needed.
356         */
357
358        // Title.
359        $title = sprintf(
360                /* translators: %s: User's email address. */
361                __( 'Personal Data Export for %s' ),
362                $email_address
363        );
364
365        // First, build an "About" group on the fly for this report.
366        $about_group = array(
367                /* translators: Header for the About section in a personal data export. */
368                'group_label'       => _x( 'About', 'personal data group label' ),
369                /* translators: Description for the About section in a personal data export. */
370                'group_description' => _x( 'Overview of export report.', 'personal data group description' ),
371                'items'             => array(
372                        'about-1' => array(
373                                array(
374                                        'name'  => _x( 'Report generated for', 'email address' ),
375                                        'value' => $email_address,
376                                ),
377                                array(
378                                        'name'  => _x( 'For site', 'website name' ),
379                                        'value' => get_bloginfo( 'name' ),
380                                ),
381                                array(
382                                        'name'  => _x( 'At URL', 'website URL' ),
383                                        'value' => get_bloginfo( 'url' ),
384                                ),
385                                array(
386                                        'name'  => _x( 'On', 'date/time' ),
387                                        'value' => current_time( 'mysql' ),
388                                ),
389                        ),
390                ),
391        );
392
393        // And now, all the Groups.
394        $groups = get_post_meta( $request_id, '_export_data_grouped', true );
395        if ( is_array( $groups ) ) {
396                // Merge in the special "About" group.
397                $groups       = array_merge( array( 'about' => $about_group ), $groups );
398                $groups_count = count( $groups );
399        } else {
400                if ( false !== $groups ) {
401                        _doing_it_wrong(
402                                __FUNCTION__,
403                                /* translators: %s: Post meta key. */
404                                sprintf( __( 'The %s post meta must be an array.' ), '<code>_export_data_grouped</code>' ),
405                                '5.8.0'
406                        );
407                }
408
409                $groups       = null;
410                $groups_count = 0;
411        }
412
413        // Convert the groups to JSON format.
414        $groups_json = wp_json_encode( $groups );
415
416        if ( false === $groups_json ) {
417                $error_message = sprintf(
418                        /* translators: %s: Error message. */
419                        __( 'Unable to encode the personal data for export. Error: %s' ),
420                        json_last_error_msg()
421                );
422
423                wp_send_json_error( $error_message );
424        }
425
426        /*
427         * Handle the JSON export.
428         */
429        $file = fopen( $json_report_pathname, 'w' );
430
431        if ( false === $file ) {
432                wp_send_json_error( __( 'Unable to open personal data export file (JSON report) for writing.' ) );
433        }
434
435        fwrite( $file, '{' );
436        fwrite( $file, '"' . $title . '":' );
437        fwrite( $file, $groups_json );
438        fwrite( $file, '}' );
439        fclose( $file );
440
441        /*
442         * Handle the HTML export.
443         */
444        $file = fopen( $html_report_pathname, 'w' );
445
446        if ( false === $file ) {
447                wp_send_json_error( __( 'Unable to open personal data export (HTML report) for writing.' ) );
448        }
449
450        fwrite( $file, "<!DOCTYPE html>\n" );
451        fwrite( $file, "<html>\n" );
452        fwrite( $file, "<head>\n" );
453        fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" );
454        fwrite( $file, "<style type='text/css'>" );
455        fwrite( $file, 'body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }' );
456        fwrite( $file, 'table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }' );
457        fwrite( $file, 'th { padding: 5px; text-align: left; width: 20%; }' );
458        fwrite( $file, 'td { padding: 5px; }' );
459        fwrite( $file, 'tr:nth-child(odd) { background-color: #fafafa; }' );
460        fwrite( $file, '.return-to-top { text-align: right; }' );
461        fwrite( $file, '</style>' );
462        fwrite( $file, '<title>' );
463        fwrite( $file, esc_html( $title ) );
464        fwrite( $file, '</title>' );
465        fwrite( $file, "</head>\n" );
466        fwrite( $file, "<body>\n" );
467        fwrite( $file, '<h1 id="top">' . esc_html__( 'Personal Data Export' ) . '</h1>' );
468
469        // Create TOC.
470        if ( $groups_count > 1 ) {
471                fwrite( $file, '<div id="table_of_contents">' );
472                fwrite( $file, '<h2>' . esc_html__( 'Table of Contents' ) . '</h2>' );
473                fwrite( $file, '<ul>' );
474                foreach ( (array) $groups as $group_id => $group_data ) {
475                        $group_label       = esc_html( $group_data['group_label'] );
476                        $group_id_attr     = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id );
477                        $group_items_count = count( (array) $group_data['items'] );
478                        if ( $group_items_count > 1 ) {
479                                $group_label .= sprintf( ' <span class="count">(%d)</span>', $group_items_count );
480                        }
481                        fwrite( $file, '<li>' );
482                        fwrite( $file, '<a href="#' . esc_attr( $group_id_attr ) . '">' . $group_label . '</a>' );
483                        fwrite( $file, '</li>' );
484                }
485                fwrite( $file, '</ul>' );
486                fwrite( $file, '</div>' );
487        }
488
489        // Now, iterate over every group in $groups and have the formatter render it in HTML.
490        foreach ( (array) $groups as $group_id => $group_data ) {
491                fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id, $groups_count ) );
492        }
493
494        fwrite( $file, "</body>\n" );
495        fwrite( $file, "</html>\n" );
496        fclose( $file );
497
498        /*
499         * Now, generate the ZIP.
500         *
501         * If an archive has already been generated, then remove it and reuse the filename,
502         * to avoid breaking any URLs that may have been previously sent via email.
503         */
504        $error = false;
505
506        // This meta value is used from version 5.5.
507        $archive_filename = get_post_meta( $request_id, '_export_file_name', true );
508
509        // This one stored an absolute path and is used for backward compatibility.
510        $archive_pathname = get_post_meta( $request_id, '_export_file_path', true );
511
512        // If a filename meta exists, use it.
513        if ( ! empty( $archive_filename ) ) {
514                $archive_pathname = $exports_dir . $archive_filename;
515        } elseif ( ! empty( $archive_pathname ) ) {
516                // If a full path meta exists, use it and create the new meta value.
517                $archive_filename = basename( $archive_pathname );
518
519                update_post_meta( $request_id, '_export_file_name', $archive_filename );
520
521                // Remove the back-compat meta values.
522                delete_post_meta( $request_id, '_export_file_url' );
523                delete_post_meta( $request_id, '_export_file_path' );
524        } else {
525                // If there's no filename or full path stored, create a new file.
526                $archive_filename = $file_basename . '.zip';
527                $archive_pathname = $exports_dir . $archive_filename;
528
529                update_post_meta( $request_id, '_export_file_name', $archive_filename );
530        }
531
532        $archive_url = $exports_url . $archive_filename;
533
534        if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) {
535                wp_delete_file( $archive_pathname );
536        }
537
538        $zip = new ZipArchive;
539        if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) {
540                if ( ! $zip->addFile( $json_report_pathname, 'export.json' ) ) {
541                        $error = __( 'Unable to archive the personal data export file (JSON format).' );
542                }
543
544                if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) {
545                        $error = __( 'Unable to archive the personal data export file (HTML format).' );
546                }
547
548                $zip->close();
549
550                if ( ! $error ) {
551                        /**
552                         * Fires right after all personal data has been written to the export file.
553                         *
554                         * @since 4.9.6
555                         * @since 5.4.0 Added the `$json_report_pathname` parameter.
556                         *
557                         * @param string $archive_pathname     The full path to the export file on the filesystem.
558                         * @param string $archive_url          The URL of the archive file.
559                         * @param string $html_report_pathname The full path to the HTML personal data report on the filesystem.
560                         * @param int    $request_id           The export request ID.
561                         * @param string $json_report_pathname The full path to the JSON personal data report on the filesystem.
562                         */
563                        do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id, $json_report_pathname );
564                }
565        } else {
566                $error = __( 'Unable to open personal data export file (archive) for writing.' );
567        }
568
569        // Remove the JSON file.
570        unlink( $json_report_pathname );
571
572        // Remove the HTML file.
573        unlink( $html_report_pathname );
574
575        if ( $error ) {
576                wp_send_json_error( $error );
577        }
578}
579
580/**
581 * Send an email to the user with a link to the personal data export file
582 *
583 * @since 4.9.6
584 *
585 * @param int $request_id The request ID for this personal data export.
586 * @return true|WP_Error True on success or `WP_Error` on failure.
587 */
588function wp_privacy_send_personal_data_export_email( $request_id ) {
589        // Get the request.
590        $request = wp_get_user_request( $request_id );
591
592        if ( ! $request || 'export_personal_data' !== $request->action_name ) {
593                return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) );
594        }
595
596        // Localize message content for user; fallback to site default for visitors.
597        if ( ! empty( $request->user_id ) ) {
598                $locale = get_user_locale( $request->user_id );
599        } else {
600                $locale = get_locale();
601        }
602
603        $switched_locale = switch_to_locale( $locale );
604
605        /** This filter is documented in wp-includes/functions.php */
606        $expiration      = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );
607        $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration );
608
609        $exports_url      = wp_privacy_exports_url();
610        $export_file_name = get_post_meta( $request_id, '_export_file_name', true );
611        $export_file_url  = $exports_url . $export_file_name;
612
613        $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
614        $site_url  = home_url();
615
616        /**
617         * Filters the recipient of the personal data export email notification.
618         * Should be used with great caution to avoid sending the data export link to wrong emails.
619         *
620         * @since 5.3.0
621         *
622         * @param string          $request_email The email address of the notification recipient.
623         * @param WP_User_Request $request       The request that is initiating the notification.
624         */
625        $request_email = apply_filters( 'wp_privacy_personal_data_email_to', $request->email, $request );
626
627        $email_data = array(
628                'request'           => $request,
629                'expiration'        => $expiration,
630                'expiration_date'   => $expiration_date,
631                'message_recipient' => $request_email,
632                'export_file_url'   => $export_file_url,
633                'sitename'          => $site_name,
634                'siteurl'           => $site_url,
635        );
636
637        /* translators: Personal data export notification email subject. %s: Site title. */
638        $subject = sprintf( __( '[%s] Personal Data Export' ), $site_name );
639
640        /**
641         * Filters the subject of the email sent when an export request is completed.
642         *
643         * @since 5.3.0
644         *
645         * @param string $subject    The email subject.
646         * @param string $sitename   The name of the site.
647         * @param array  $email_data {
648         *     Data relating to the account action email.
649         *
650         *     @type WP_User_Request $request           User request object.
651         *     @type int             $expiration        The time in seconds until the export file expires.
652         *     @type string          $expiration_date   The localized date and time when the export file expires.
653         *     @type string          $message_recipient The address that the email will be sent to. Defaults
654         *                                              to the value of `$request->email`, but can be changed
655         *                                              by the `wp_privacy_personal_data_email_to` filter.
656         *     @type string          $export_file_url   The export file URL.
657         *     @type string          $sitename          The site name sending the mail.
658         *     @type string          $siteurl           The site URL sending the mail.
659         * }
660         */
661        $subject = apply_filters( 'wp_privacy_personal_data_email_subject', $subject, $site_name, $email_data );
662
663        /* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */
664        $email_text = __(
665                'Howdy,
666
667Your request for an export of personal data has been completed. You may
668download your personal data by clicking on the link below. For privacy
669and security, we will automatically delete the file on ###EXPIRATION###,
670so please download it before then.
671
672###LINK###
673
674Regards,
675All at ###SITENAME###
676###SITEURL###'
677        );
678
679        /**
680         * Filters the text of the email sent with a personal data export file.
681         *
682         * The following strings have a special meaning and will get replaced dynamically:
683         * ###EXPIRATION###         The date when the URL will be automatically deleted.
684         * ###LINK###               URL of the personal data export file for the user.
685         * ###SITENAME###           The name of the site.
686         * ###SITEURL###            The URL to the site.
687         *
688         * @since 4.9.6
689         * @since 5.3.0 Introduced the `$email_data` array.
690         *
691         * @param string $email_text Text in the email.
692         * @param int    $request_id The request ID for this personal data export.
693         * @param array  $email_data {
694         *     Data relating to the account action email.
695         *
696         *     @type WP_User_Request $request           User request object.
697         *     @type int             $expiration        The time in seconds until the export file expires.
698         *     @type string          $expiration_date   The localized date and time when the export file expires.
699         *     @type string          $message_recipient The address that the email will be sent to. Defaults
700         *                                              to the value of `$request->email`, but can be changed
701         *                                              by the `wp_privacy_personal_data_email_to` filter.
702         *     @type string          $export_file_url   The export file URL.
703         *     @type string          $sitename          The site name sending the mail.
704         *     @type string          $siteurl           The site URL sending the mail.
705         */
706        $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id, $email_data );
707
708        $content = str_replace( '###EXPIRATION###', $expiration_date, $content );
709        $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content );
710        $content = str_replace( '###EMAIL###', $request_email, $content );
711        $content = str_replace( '###SITENAME###', $site_name, $content );
712        $content = str_replace( '###SITEURL###', esc_url_raw( $site_url ), $content );
713
714        $headers = '';
715
716        /**
717         * Filters the headers of the email sent with a personal data export file.
718         *
719         * @since 5.4.0
720         *
721         * @param string|array $headers    The email headers.
722         * @param string       $subject    The email subject.
723         * @param string       $content    The email content.
724         * @param int          $request_id The request ID.
725         * @param array        $email_data {
726         *     Data relating to the account action email.
727         *
728         *     @type WP_User_Request $request           User request object.
729         *     @type int             $expiration        The time in seconds until the export file expires.
730         *     @type string          $expiration_date   The localized date and time when the export file expires.
731         *     @type string          $message_recipient The address that the email will be sent to. Defaults
732         *                                              to the value of `$request->email`, but can be changed
733         *                                              by the `wp_privacy_personal_data_email_to` filter.
734         *     @type string          $export_file_url   The export file URL.
735         *     @type string          $sitename          The site name sending the mail.
736         *     @type string          $siteurl           The site URL sending the mail.
737         * }
738         */
739        $headers = apply_filters( 'wp_privacy_personal_data_email_headers', $headers, $subject, $content, $request_id, $email_data );
740
741        $mail_success = wp_mail( $request_email, $subject, $content, $headers );
742
743        if ( $switched_locale ) {
744                restore_previous_locale();
745        }
746
747        if ( ! $mail_success ) {
748                return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) );
749        }
750
751        return true;
752}
753
754/**
755 * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file.
756 *
757 * @since 4.9.6
758 *
759 * @see 'wp_privacy_personal_data_export_page'
760 *
761 * @param array  $response        The response from the personal data exporter for the given page.
762 * @param int    $exporter_index  The index of the personal data exporter. Begins at 1.
763 * @param string $email_address   The email address of the user whose personal data this is.
764 * @param int    $page            The page of personal data for this exporter. Begins at 1.
765 * @param int    $request_id      The request ID for this personal data export.
766 * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
767 * @param string $exporter_key    The slug (key) of the exporter.
768 * @return array The filtered response.
769 */
770function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) {
771        /* Do some simple checks on the shape of the response from the exporter.
772         * If the exporter response is malformed, don't attempt to consume it - let it
773         * pass through to generate a warning to the user by default Ajax processing.
774         */
775        if ( ! is_array( $response ) ) {
776                return $response;
777        }
778
779        if ( ! array_key_exists( 'done', $response ) ) {
780                return $response;
781        }
782
783        if ( ! array_key_exists( 'data', $response ) ) {
784                return $response;
785        }
786
787        if ( ! is_array( $response['data'] ) ) {
788                return $response;
789        }
790
791        // Get the request.
792        $request = wp_get_user_request( $request_id );
793
794        if ( ! $request || 'export_personal_data' !== $request->action_name ) {
795                wp_send_json_error( __( 'Invalid request ID when merging personal data to export.' ) );
796        }
797
798        $export_data = array();
799
800        // First exporter, first page? Reset the report data accumulation array.
801        if ( 1 === $exporter_index && 1 === $page ) {
802                update_post_meta( $request_id, '_export_data_raw', $export_data );
803        } else {
804                $accumulated_data = get_post_meta( $request_id, '_export_data_raw', true );
805
806                if ( $accumulated_data ) {
807                        $export_data = $accumulated_data;
808                }
809        }
810
811        // Now, merge the data from the exporter response into the data we have accumulated already.
812        $export_data = array_merge( $export_data, $response['data'] );
813        update_post_meta( $request_id, '_export_data_raw', $export_data );
814
815        // If we are not yet on the last page of the last exporter, return now.
816        /** This filter is documented in wp-admin/includes/ajax-actions.php */
817        $exporters        = apply_filters( 'wp_privacy_personal_data_exporters', array() );
818        $is_last_exporter = count( $exporters ) === $exporter_index;
819        $exporter_done    = $response['done'];
820        if ( ! $is_last_exporter || ! $exporter_done ) {
821                return $response;
822        }
823
824        // Last exporter, last page - let's prepare the export file.
825
826        // First we need to re-organize the raw data hierarchically in groups and items.
827        $groups = array();
828        foreach ( (array) $export_data as $export_datum ) {
829                $group_id    = $export_datum['group_id'];
830                $group_label = $export_datum['group_label'];
831
832                $group_description = '';
833                if ( ! empty( $export_datum['group_description'] ) ) {
834                        $group_description = $export_datum['group_description'];
835                }
836
837                if ( ! array_key_exists( $group_id, $groups ) ) {
838                        $groups[ $group_id ] = array(
839                                'group_label'       => $group_label,
840                                'group_description' => $group_description,
841                                'items'             => array(),
842                        );
843                }
844
845                $item_id = $export_datum['item_id'];
846                if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) {
847                        $groups[ $group_id ]['items'][ $item_id ] = array();
848                }
849
850                $old_item_data                            = $groups[ $group_id ]['items'][ $item_id ];
851                $merged_item_data                         = array_merge( $export_datum['data'], $old_item_data );
852                $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data;
853        }
854
855        // Then save the grouped data into the request.
856        delete_post_meta( $request_id, '_export_data_raw' );
857        update_post_meta( $request_id, '_export_data_grouped', $groups );
858
859        /**
860         * Generate the export file from the collected, grouped personal data.
861         *
862         * @since 4.9.6
863         *
864         * @param int $request_id The export request ID.
865         */
866        do_action( 'wp_privacy_personal_data_export_file', $request_id );
867
868        // Clear the grouped data now that it is no longer needed.
869        delete_post_meta( $request_id, '_export_data_grouped' );
870
871        // If the destination is email, send it now.
872        if ( $send_as_email ) {
873                $mail_success = wp_privacy_send_personal_data_export_email( $request_id );
874                if ( is_wp_error( $mail_success ) ) {
875                        wp_send_json_error( $mail_success->get_error_message() );
876                }
877
878                // Update the request to completed state when the export email is sent.
879                _wp_privacy_completed_request( $request_id );
880        } else {
881                // Modify the response to include the URL of the export file so the browser can fetch it.
882                $exports_url      = wp_privacy_exports_url();
883                $export_file_name = get_post_meta( $request_id, '_export_file_name', true );
884                $export_file_url  = $exports_url . $export_file_name;
885
886                if ( ! empty( $export_file_url ) ) {
887                        $response['url'] = $export_file_url;
888                }
889        }
890
891        return $response;
892}
893
894/**
895 * Mark erasure requests as completed after processing is finished.
896 *
897 * This intercepts the Ajax responses to personal data eraser page requests, and
898 * monitors the status of a request. Once all of the processing has finished, the
899 * request is marked as completed.
900 *
901 * @since 4.9.6
902 *
903 * @see 'wp_privacy_personal_data_erasure_page'
904 *
905 * @param array  $response      The response from the personal data eraser for
906 *                              the given page.
907 * @param int    $eraser_index  The index of the personal data eraser. Begins
908 *                              at 1.
909 * @param string $email_address The email address of the user whose personal
910 *                              data this is.
911 * @param int    $page          The page of personal data for this eraser.
912 *                              Begins at 1.
913 * @param int    $request_id    The request ID for this personal data erasure.
914 * @return array The filtered response.
915 */
916function wp_privacy_process_personal_data_erasure_page( $response, $eraser_index, $email_address, $page, $request_id ) {
917        /*
918         * If the eraser response is malformed, don't attempt to consume it; let it
919         * pass through, so that the default Ajax processing will generate a warning
920         * to the user.
921         */
922        if ( ! is_array( $response ) ) {
923                return $response;
924        }
925
926        if ( ! array_key_exists( 'done', $response ) ) {
927                return $response;
928        }
929
930        if ( ! array_key_exists( 'items_removed', $response ) ) {
931                return $response;
932        }
933
934        if ( ! array_key_exists( 'items_retained', $response ) ) {
935                return $response;
936        }
937
938        if ( ! array_key_exists( 'messages', $response ) ) {
939                return $response;
940        }
941
942        // Get the request.
943        $request = wp_get_user_request( $request_id );
944
945        if ( ! $request || 'remove_personal_data' !== $request->action_name ) {
946                wp_send_json_error( __( 'Invalid request ID when processing personal data to erase.' ) );
947        }
948
949        /** This filter is documented in wp-admin/includes/ajax-actions.php */
950        $erasers        = apply_filters( 'wp_privacy_personal_data_erasers', array() );
951        $is_last_eraser = count( $erasers ) === $eraser_index;
952        $eraser_done    = $response['done'];
953
954        if ( ! $is_last_eraser || ! $eraser_done ) {
955                return $response;
956        }
957
958        _wp_privacy_completed_request( $request_id );
959
960        /**
961         * Fires immediately after a personal data erasure request has been marked completed.
962         *
963         * @since 4.9.6
964         *
965         * @param int $request_id The privacy request post ID associated with this request.
966         */
967        do_action( 'wp_privacy_personal_data_erased', $request_id );
968
969        return $response;
970}
Note: See TracBrowser for help on using the repository browser.