WordPress.org

Make WordPress Core

Ticket #43895: 43895.diff

File 43895.diff, 92.5 KB (added by xkon, 7 months ago)

Organize privacy code

  • Gruntfile.js

    diff --git a/Gruntfile.js b/Gruntfile.js
    index 333b5252f6..7fc89313ca 100644
    a b module.exports = function(grunt) { 
    264264                                        [ WORKING_DIR + 'wp-admin/js/tags-suggest.js' ]: [ './src/js/_enqueues/admin/tags-suggest.js' ],
    265265                                        [ WORKING_DIR + 'wp-admin/js/tags.js' ]: [ './src/js/_enqueues/admin/tags.js' ],
    266266                                        [ WORKING_DIR + 'wp-admin/js/site-health.js' ]: [ './src/js/_enqueues/admin/site-health.js' ],
     267                                        [ WORKING_DIR + 'wp-admin/js/privacy.js' ]: [ './src/js/_enqueues/admin/privacy.js' ],
    267268                                        [ WORKING_DIR + 'wp-admin/js/theme-plugin-editor.js' ]: [ './src/js/_enqueues/wp/theme-plugin-editor.js' ],
    268269                                        [ WORKING_DIR + 'wp-admin/js/theme.js' ]: [ './src/js/_enqueues/wp/theme.js' ],
    269270                                        [ WORKING_DIR + 'wp-admin/js/updates.js' ]: [ './src/js/_enqueues/wp/updates.js' ],
  • new file src/js/_enqueues/admin/privacy.js

    diff --git a/src/js/_enqueues/admin/privacy.js b/src/js/_enqueues/admin/privacy.js
    new file mode 100644
    index 0000000000..1d5cf3b1f2
    - +  
     1/**
     2 * Interactions used by the Site Health modules in WordPress.
     3 *
     4 * @output wp-admin/js/privacy.js
     5 */
     6
     7( function( $ ) {
     8        // Privacy request action handling
     9        $( document ).ready( function() {
     10                var strings = window.privacyToolsL10n || {};
     11
     12                function setActionState( $action, state ) {
     13                        $action.children().hide();
     14                        $action.children( '.' + state ).show();
     15                }
     16
     17                function clearResultsAfterRow( $requestRow ) {
     18                        $requestRow.removeClass( 'has-request-results' );
     19
     20                        if ( $requestRow.next().hasClass( 'request-results' ) ) {
     21                                $requestRow.next().remove();
     22                        }
     23                }
     24
     25                function appendResultsAfterRow( $requestRow, classes, summaryMessage, additionalMessages ) {
     26                        var itemList = '',
     27                                resultRowClasses = 'request-results';
     28
     29                        clearResultsAfterRow( $requestRow );
     30
     31                        if ( additionalMessages.length ) {
     32                                $.each( additionalMessages, function( index, value ) {
     33                                        itemList = itemList + '<li>' + value + '</li>';
     34                                });
     35                                itemList = '<ul>' + itemList + '</ul>';
     36                        }
     37
     38                        $requestRow.addClass( 'has-request-results' );
     39
     40                        if ( $requestRow.hasClass( 'status-request-confirmed' ) ) {
     41                                resultRowClasses = resultRowClasses + ' status-request-confirmed';
     42                        }
     43
     44                        if ( $requestRow.hasClass( 'status-request-failed' ) ) {
     45                                resultRowClasses = resultRowClasses + ' status-request-failed';
     46                        }
     47
     48                        $requestRow.after( function() {
     49                                return '<tr class="' + resultRowClasses + '"><th colspan="5">' +
     50                                        '<div class="notice inline notice-alt ' + classes + '">' +
     51                                        '<p>' + summaryMessage + '</p>' +
     52                                        itemList +
     53                                        '</div>' +
     54                                        '</td>' +
     55                                        '</tr>';
     56                        });
     57                }
     58
     59                $( '.export-personal-data-handle' ).click( function( event ) {
     60
     61                        var $this          = $( this ),
     62                                $action        = $this.parents( '.export-personal-data' ),
     63                                $requestRow    = $this.parents( 'tr' ),
     64                                requestID      = $action.data( 'request-id' ),
     65                                nonce          = $action.data( 'nonce' ),
     66                                exportersCount = $action.data( 'exporters-count' ),
     67                                sendAsEmail    = $action.data( 'send-as-email' ) ? true : false;
     68
     69                        event.preventDefault();
     70                        event.stopPropagation();
     71
     72                        $action.blur();
     73                        clearResultsAfterRow( $requestRow );
     74
     75                        function onExportDoneSuccess( zipUrl ) {
     76                                setActionState( $action, 'export-personal-data-success' );
     77                                if ( 'undefined' !== typeof zipUrl ) {
     78                                        window.location = zipUrl;
     79                                } else if ( ! sendAsEmail ) {
     80                                        onExportFailure( strings.noExportFile );
     81                                }
     82                        }
     83
     84                        function onExportFailure( errorMessage ) {
     85                                setActionState( $action, 'export-personal-data-failed' );
     86                                if ( errorMessage ) {
     87                                        appendResultsAfterRow( $requestRow, 'notice-error', strings.exportError, [ errorMessage ] );
     88                                }
     89                        }
     90
     91                        function doNextExport( exporterIndex, pageIndex ) {
     92                                $.ajax(
     93                                        {
     94                                                url: window.ajaxurl,
     95                                                data: {
     96                                                        action: 'wp-privacy-export-personal-data',
     97                                                        exporter: exporterIndex,
     98                                                        id: requestID,
     99                                                        page: pageIndex,
     100                                                        security: nonce,
     101                                                        sendAsEmail: sendAsEmail
     102                                                },
     103                                                method: 'post'
     104                                        }
     105                                ).done( function( response ) {
     106                                        var responseData = response.data;
     107
     108                                        if ( ! response.success ) {
     109
     110                                                // e.g. invalid request ID
     111                                                onExportFailure( response.data );
     112                                                return;
     113                                        }
     114
     115                                        if ( ! responseData.done ) {
     116                                                setTimeout( doNextExport( exporterIndex, pageIndex + 1 ) );
     117                                        } else {
     118                                                if ( exporterIndex < exportersCount ) {
     119                                                        setTimeout( doNextExport( exporterIndex + 1, 1 ) );
     120                                                } else {
     121                                                        onExportDoneSuccess( responseData.url );
     122                                                }
     123                                        }
     124                                }).fail( function( jqxhr, textStatus, error ) {
     125
     126                                        // e.g. Nonce failure
     127                                        onExportFailure( error );
     128                                });
     129                        }
     130
     131                        // And now, let's begin
     132                        setActionState( $action, 'export-personal-data-processing' );
     133                        doNextExport( 1, 1 );
     134                });
     135
     136                $( '.remove-personal-data-handle' ).click( function( event ) {
     137
     138                        var $this         = $( this ),
     139                                $action       = $this.parents( '.remove-personal-data' ),
     140                                $requestRow   = $this.parents( 'tr' ),
     141                                requestID     = $action.data( 'request-id' ),
     142                                nonce         = $action.data( 'nonce' ),
     143                                erasersCount  = $action.data( 'erasers-count' ),
     144                                hasRemoved    = false,
     145                                hasRetained   = false,
     146                                messages      = [];
     147
     148                        event.stopPropagation();
     149
     150                        $action.blur();
     151                        clearResultsAfterRow( $requestRow );
     152
     153                        function onErasureDoneSuccess() {
     154                                var summaryMessage = strings.noDataFound;
     155                                var classes = 'notice-success';
     156
     157                                setActionState( $action, 'remove-personal-data-idle' );
     158
     159                                if ( false === hasRemoved ) {
     160                                        if ( false === hasRetained ) {
     161                                                summaryMessage = strings.noDataFound;
     162                                        } else {
     163                                                summaryMessage = strings.noneRemoved;
     164                                                classes = 'notice-warning';
     165                                        }
     166                                } else {
     167                                        if ( false === hasRetained ) {
     168                                                summaryMessage = strings.foundAndRemoved;
     169                                        } else {
     170                                                summaryMessage = strings.someNotRemoved;
     171                                                classes = 'notice-warning';
     172                                        }
     173                                }
     174                                appendResultsAfterRow( $requestRow, 'notice-success', summaryMessage, messages );
     175                        }
     176
     177                        function onErasureFailure() {
     178                                setActionState( $action, 'remove-personal-data-failed' );
     179                                appendResultsAfterRow( $requestRow, 'notice-error', strings.removalError, [] );
     180                        }
     181
     182                        function doNextErasure( eraserIndex, pageIndex ) {
     183                                $.ajax({
     184                                        url: window.ajaxurl,
     185                                        data: {
     186                                                action: 'wp-privacy-erase-personal-data',
     187                                                eraser: eraserIndex,
     188                                                id: requestID,
     189                                                page: pageIndex,
     190                                                security: nonce
     191                                        },
     192                                        method: 'post'
     193                                }).done( function( response ) {
     194                                        var responseData = response.data;
     195
     196                                        if ( ! response.success ) {
     197                                                onErasureFailure();
     198                                                return;
     199                                        }
     200                                        if ( responseData.items_removed ) {
     201                                                hasRemoved = hasRemoved || responseData.items_removed;
     202                                        }
     203                                        if ( responseData.items_retained ) {
     204                                                hasRetained = hasRetained || responseData.items_retained;
     205                                        }
     206                                        if ( responseData.messages ) {
     207                                                messages = messages.concat( responseData.messages );
     208                                        }
     209                                        if ( ! responseData.done ) {
     210                                                setTimeout( doNextErasure( eraserIndex, pageIndex + 1 ) );
     211                                        } else {
     212                                                if ( eraserIndex < erasersCount ) {
     213                                                        setTimeout( doNextErasure( eraserIndex + 1, 1 ) );
     214                                                } else {
     215                                                        onErasureDoneSuccess();
     216                                                }
     217                                        }
     218                                }).fail( function() {
     219                                        onErasureFailure();
     220                                });
     221                        }
     222
     223                        // And now, let's begin
     224                        setActionState( $action, 'remove-personal-data-processing' );
     225
     226                        doNextErasure( 1, 1 );
     227                });
     228        });
     229
     230        // Privacy policy page, copy button.
     231        $( document ).on( 'click', function( event ) {
     232                var $target = $( event.target );
     233                var $parent, $container, range;
     234
     235                if ( $target.is( 'button.privacy-text-copy' ) ) {
     236                        $parent = $target.parent().parent();
     237                        $container = $parent.find( 'div.wp-suggested-text' );
     238
     239                        if ( ! $container.length ) {
     240                                $container = $parent.find( 'div.policy-text' );
     241                        }
     242
     243                        if ( $container.length ) {
     244                                try {
     245                                        window.getSelection().removeAllRanges();
     246                                        range = document.createRange();
     247                                        $container.addClass( 'hide-privacy-policy-tutorial' );
     248
     249                                        range.selectNodeContents( $container[0] );
     250                                        window.getSelection().addRange( range );
     251                                        document.execCommand( 'copy' );
     252
     253                                        $container.removeClass( 'hide-privacy-policy-tutorial' );
     254                                        window.getSelection().removeAllRanges();
     255                                } catch ( er ) {}
     256                        }
     257                }
     258        });
     259
     260} ( jQuery ) );
     261 No newline at end of file
  • src/js/_enqueues/admin/xfn.js

    diff --git a/src/js/_enqueues/admin/xfn.js b/src/js/_enqueues/admin/xfn.js
    index 61605a329b..b72540d335 100644
    a b jQuery( document ).ready(function( $ ) { 
    2121                $( '#link_rel' ).val( ( isMe ) ? 'me' : inputs.substr( 0,inputs.length - 1 ) );
    2222        });
    2323});
    24 
    25 // Privacy request action handling
    26 jQuery( document ).ready( function( $ ) {
    27         var strings = window.privacyToolsL10n || {};
    28 
    29         function setActionState( $action, state ) {
    30                 $action.children().hide();
    31                 $action.children( '.' + state ).show();
    32         }
    33 
    34         function clearResultsAfterRow( $requestRow ) {
    35                 $requestRow.removeClass( 'has-request-results' );
    36 
    37                 if ( $requestRow.next().hasClass( 'request-results' ) ) {
    38                         $requestRow.next().remove();
    39                 }
    40         }
    41 
    42         function appendResultsAfterRow( $requestRow, classes, summaryMessage, additionalMessages ) {
    43                 var itemList = '',
    44                         resultRowClasses = 'request-results';
    45 
    46                 clearResultsAfterRow( $requestRow );
    47 
    48                 if ( additionalMessages.length ) {
    49                         $.each( additionalMessages, function( index, value ) {
    50                                 itemList = itemList + '<li>' + value + '</li>';
    51                         });
    52                         itemList = '<ul>' + itemList + '</ul>';
    53                 }
    54 
    55                 $requestRow.addClass( 'has-request-results' );
    56 
    57                 if ( $requestRow.hasClass( 'status-request-confirmed' ) ) {
    58                         resultRowClasses = resultRowClasses + ' status-request-confirmed';
    59                 }
    60 
    61                 if ( $requestRow.hasClass( 'status-request-failed' ) ) {
    62                         resultRowClasses = resultRowClasses + ' status-request-failed';
    63                 }
    64 
    65                 $requestRow.after( function() {
    66                         return '<tr class="' + resultRowClasses + '"><th colspan="5">' +
    67                                 '<div class="notice inline notice-alt ' + classes + '">' +
    68                                 '<p>' + summaryMessage + '</p>' +
    69                                 itemList +
    70                                 '</div>' +
    71                                 '</td>' +
    72                                 '</tr>';
    73                 });
    74         }
    75 
    76         $( '.export-personal-data-handle' ).click( function( event ) {
    77 
    78                 var $this          = $( this ),
    79                         $action        = $this.parents( '.export-personal-data' ),
    80                         $requestRow    = $this.parents( 'tr' ),
    81                         requestID      = $action.data( 'request-id' ),
    82                         nonce          = $action.data( 'nonce' ),
    83                         exportersCount = $action.data( 'exporters-count' ),
    84                         sendAsEmail    = $action.data( 'send-as-email' ) ? true : false;
    85 
    86                 event.preventDefault();
    87                 event.stopPropagation();
    88 
    89                 $action.blur();
    90                 clearResultsAfterRow( $requestRow );
    91 
    92                 function onExportDoneSuccess( zipUrl ) {
    93                         setActionState( $action, 'export-personal-data-success' );
    94                         if ( 'undefined' !== typeof zipUrl ) {
    95                                 window.location = zipUrl;
    96                         } else if ( ! sendAsEmail ) {
    97                                 onExportFailure( strings.noExportFile );
    98                         }
    99                 }
    100 
    101                 function onExportFailure( errorMessage ) {
    102                         setActionState( $action, 'export-personal-data-failed' );
    103                         if ( errorMessage ) {
    104                                 appendResultsAfterRow( $requestRow, 'notice-error', strings.exportError, [ errorMessage ] );
    105                         }
    106                 }
    107 
    108                 function doNextExport( exporterIndex, pageIndex ) {
    109                         $.ajax(
    110                                 {
    111                                         url: window.ajaxurl,
    112                                         data: {
    113                                                 action: 'wp-privacy-export-personal-data',
    114                                                 exporter: exporterIndex,
    115                                                 id: requestID,
    116                                                 page: pageIndex,
    117                                                 security: nonce,
    118                                                 sendAsEmail: sendAsEmail
    119                                         },
    120                                         method: 'post'
    121                                 }
    122                         ).done( function( response ) {
    123                                 var responseData = response.data;
    124 
    125                                 if ( ! response.success ) {
    126 
    127                                         // e.g. invalid request ID
    128                                         onExportFailure( response.data );
    129                                         return;
    130                                 }
    131 
    132                                 if ( ! responseData.done ) {
    133                                         setTimeout( doNextExport( exporterIndex, pageIndex + 1 ) );
    134                                 } else {
    135                                         if ( exporterIndex < exportersCount ) {
    136                                                 setTimeout( doNextExport( exporterIndex + 1, 1 ) );
    137                                         } else {
    138                                                 onExportDoneSuccess( responseData.url );
    139                                         }
    140                                 }
    141                         }).fail( function( jqxhr, textStatus, error ) {
    142 
    143                                 // e.g. Nonce failure
    144                                 onExportFailure( error );
    145                         });
    146                 }
    147 
    148                 // And now, let's begin
    149                 setActionState( $action, 'export-personal-data-processing' );
    150                 doNextExport( 1, 1 );
    151         });
    152 
    153         $( '.remove-personal-data-handle' ).click( function( event ) {
    154 
    155                 var $this         = $( this ),
    156                         $action       = $this.parents( '.remove-personal-data' ),
    157                         $requestRow   = $this.parents( 'tr' ),
    158                         requestID     = $action.data( 'request-id' ),
    159                         nonce         = $action.data( 'nonce' ),
    160                         erasersCount  = $action.data( 'erasers-count' ),
    161                         hasRemoved    = false,
    162                         hasRetained   = false,
    163                         messages      = [];
    164 
    165                 event.stopPropagation();
    166 
    167                 $action.blur();
    168                 clearResultsAfterRow( $requestRow );
    169 
    170                 function onErasureDoneSuccess() {
    171                         var summaryMessage = strings.noDataFound;
    172                         var classes = 'notice-success';
    173 
    174                         setActionState( $action, 'remove-personal-data-idle' );
    175 
    176                         if ( false === hasRemoved ) {
    177                                 if ( false === hasRetained ) {
    178                                         summaryMessage = strings.noDataFound;
    179                                 } else {
    180                                         summaryMessage = strings.noneRemoved;
    181                                         classes = 'notice-warning';
    182                                 }
    183                         } else {
    184                                 if ( false === hasRetained ) {
    185                                         summaryMessage = strings.foundAndRemoved;
    186                                 } else {
    187                                         summaryMessage = strings.someNotRemoved;
    188                                         classes = 'notice-warning';
    189                                 }
    190                         }
    191                         appendResultsAfterRow( $requestRow, 'notice-success', summaryMessage, messages );
    192                 }
    193 
    194                 function onErasureFailure() {
    195                         setActionState( $action, 'remove-personal-data-failed' );
    196                         appendResultsAfterRow( $requestRow, 'notice-error', strings.removalError, [] );
    197                 }
    198 
    199                 function doNextErasure( eraserIndex, pageIndex ) {
    200                         $.ajax({
    201                                 url: window.ajaxurl,
    202                                 data: {
    203                                         action: 'wp-privacy-erase-personal-data',
    204                                         eraser: eraserIndex,
    205                                         id: requestID,
    206                                         page: pageIndex,
    207                                         security: nonce
    208                                 },
    209                                 method: 'post'
    210                         }).done( function( response ) {
    211                                 var responseData = response.data;
    212 
    213                                 if ( ! response.success ) {
    214                                         onErasureFailure();
    215                                         return;
    216                                 }
    217                                 if ( responseData.items_removed ) {
    218                                         hasRemoved = hasRemoved || responseData.items_removed;
    219                                 }
    220                                 if ( responseData.items_retained ) {
    221                                         hasRetained = hasRetained || responseData.items_retained;
    222                                 }
    223                                 if ( responseData.messages ) {
    224                                         messages = messages.concat( responseData.messages );
    225                                 }
    226                                 if ( ! responseData.done ) {
    227                                         setTimeout( doNextErasure( eraserIndex, pageIndex + 1 ) );
    228                                 } else {
    229                                         if ( eraserIndex < erasersCount ) {
    230                                                 setTimeout( doNextErasure( eraserIndex + 1, 1 ) );
    231                                         } else {
    232                                                 onErasureDoneSuccess();
    233                                         }
    234                                 }
    235                         }).fail( function() {
    236                                 onErasureFailure();
    237                         });
    238                 }
    239 
    240                 // And now, let's begin
    241                 setActionState( $action, 'remove-personal-data-processing' );
    242 
    243                 doNextErasure( 1, 1 );
    244         });
    245 });
    246 
    247 ( function( $ ) {
    248 
    249         // Privacy policy page, copy button.
    250         $( document ).on( 'click', function( event ) {
    251                 var $target = $( event.target );
    252                 var $parent, $container, range;
    253 
    254                 if ( $target.is( 'button.privacy-text-copy' ) ) {
    255                         $parent = $target.parent().parent();
    256                         $container = $parent.find( 'div.wp-suggested-text' );
    257 
    258                         if ( ! $container.length ) {
    259                                 $container = $parent.find( 'div.policy-text' );
    260                         }
    261 
    262                         if ( $container.length ) {
    263                                 try {
    264                                         window.getSelection().removeAllRanges();
    265                                         range = document.createRange();
    266                                         $container.addClass( 'hide-privacy-policy-tutorial' );
    267 
    268                                         range.selectNodeContents( $container[0] );
    269                                         window.getSelection().addRange( range );
    270                                         document.execCommand( 'copy' );
    271 
    272                                         $container.removeClass( 'hide-privacy-policy-tutorial' );
    273                                         window.getSelection().removeAllRanges();
    274                                 } catch ( er ) {}
    275                         }
    276                 }
    277         });
    278 
    279 } ( jQuery ) );
  • new file src/wp-admin/erase-personal-data.php

    diff --git a/src/wp-admin/erase-personal-data.php b/src/wp-admin/erase-personal-data.php
    new file mode 100644
    index 0000000000..674513b4d4
    - +  
     1<?php
     2
     3/**
     4 * Personal data anonymization.
     5 *
     6 * @since 4.9.6
     7 * @access private
     8 */
     9
     10/** WordPress Administration Bootstrap */
     11require_once( dirname( __FILE__ ) . '/admin.php' );
     12
     13if ( ! current_user_can( 'erase_others_personal_data' ) || ! current_user_can( 'delete_users' ) ) {
     14        wp_die( __( 'Sorry, you are not allowed to erase data on this site.' ) );
     15}
     16
     17if ( ! class_exists( 'WP_Privacy_Data_Removal_Requests_Table' ) ) {
     18        require_once( ABSPATH . 'wp-admin/includes/class-wp-privacy-data-removal-requests-table.php' );
     19}
     20
     21// Handle list table actions.
     22_wp_personal_data_handle_actions();
     23
     24// Cleans up failed and expired requests before displaying the list table.
     25_wp_personal_data_cleanup_requests();
     26
     27// Enqueue Privacy scripts.
     28wp_enqueue_script( 'privacy' );
     29
     30$requests_table = new WP_Privacy_Data_Removal_Requests_Table(
     31        array(
     32                'plural'   => 'privacy_requests',
     33                'singular' => 'privacy_request',
     34                'screen'   => 'remove_personal_data',
     35        )
     36);
     37
     38$requests_table->screen->set_screen_reader_content(
     39        array(
     40                'heading_views'      => __( 'Filter erase personal data list' ),
     41                'heading_pagination' => __( 'Erase personal data list navigation' ),
     42                'heading_list'       => __( 'Erase personal data list' ),
     43        )
     44);
     45
     46$requests_table->process_bulk_action();
     47$requests_table->prepare_items();
     48
     49require_once( ABSPATH . 'wp-admin/admin-header.php' );
     50?>
     51
     52<div class="wrap nosubsub">
     53        <h1><?php esc_html_e( 'Erase Personal Data' ); ?></h1>
     54        <hr class="wp-header-end" />
     55
     56        <?php settings_errors(); ?>
     57
     58        <form action="<?php echo esc_url( admin_url( 'erase-personal-data.php' ) ); ?>" method="post" class="wp-privacy-request-form">
     59                <h2><?php esc_html_e( 'Add Data Erasure Request' ); ?></h2>
     60                <p><?php esc_html_e( 'An email will be sent to the user at this email address asking them to verify the request.' ); ?></p>
     61
     62                <div class="wp-privacy-request-form-field">
     63                        <label for="username_or_email_for_privacy_request"><?php esc_html_e( 'Username or email address' ); ?></label>
     64                        <input type="text" required class="regular-text" id="username_or_email_for_privacy_request" name="username_or_email_for_privacy_request" />
     65                        <?php submit_button( __( 'Send Request' ), 'secondary', 'submit', false ); ?>
     66                </div>
     67                <?php wp_nonce_field( 'personal-data-request' ); ?>
     68                <input type="hidden" name="action" value="add_remove_personal_data_request" />
     69                <input type="hidden" name="type_of_action" value="remove_personal_data" />
     70        </form>
     71        <hr />
     72
     73        <?php $requests_table->views(); ?>
     74
     75        <form class="search-form wp-clearfix">
     76                <?php $requests_table->search_box( __( 'Search Requests' ), 'requests' ); ?>
     77                <input type="hidden" name="page" value="remove_personal_data" />
     78                <input type="hidden" name="filter-status" value="<?php echo isset( $_REQUEST['filter-status'] ) ? esc_attr( sanitize_text_field( $_REQUEST['filter-status'] ) ) : ''; ?>" />
     79                <input type="hidden" name="orderby" value="<?php echo isset( $_REQUEST['orderby'] ) ? esc_attr( sanitize_text_field( $_REQUEST['orderby'] ) ) : ''; ?>" />
     80                <input type="hidden" name="order" value="<?php echo isset( $_REQUEST['order'] ) ? esc_attr( sanitize_text_field( $_REQUEST['order'] ) ) : ''; ?>" />
     81        </form>
     82
     83        <form method="post">
     84                <?php
     85                $requests_table->display();
     86                $requests_table->embed_scripts();
     87                ?>
     88        </form>
     89</div>
     90
     91<?php
     92include( ABSPATH . 'wp-admin/admin-footer.php' );
  • new file src/wp-admin/export-personal-data.php

    diff --git a/src/wp-admin/export-personal-data.php b/src/wp-admin/export-personal-data.php
    new file mode 100644
    index 0000000000..c9fe075fb1
    - +  
     1<?php
     2
     3/**
     4 * Personal data export.
     5 *
     6 * @since 4.9.6
     7 * @access private
     8 */
     9
     10/** WordPress Administration Bootstrap */
     11require_once( dirname( __FILE__ ) . '/admin.php' );
     12
     13if ( ! current_user_can( 'export_others_personal_data' ) ) {
     14        wp_die( __( 'Sorry, you are not allowed to export personal data on this site.' ) );
     15}
     16
     17if ( ! class_exists( 'WP_Privacy_Data_Export_Requests_Table' ) ) {
     18        require_once( ABSPATH . 'wp-admin/includes/class-wp-privacy-data-export-requests-table.php' );
     19}
     20
     21// Handle list table actions.
     22_wp_personal_data_handle_actions();
     23
     24// Cleans up failed and expired requests before displaying the list table.
     25_wp_personal_data_cleanup_requests();
     26
     27// Enqueue Privacy scripts.
     28wp_enqueue_script( 'privacy' );
     29
     30$requests_table = new WP_Privacy_Data_Export_Requests_Table(
     31        array(
     32                'plural'   => 'privacy_requests',
     33                'singular' => 'privacy_request',
     34                'screen'   => 'export_personal_data',
     35        )
     36);
     37
     38$requests_table->screen->set_screen_reader_content(
     39        array(
     40                'heading_views'      => __( 'Filter export personal data list' ),
     41                'heading_pagination' => __( 'Export personal data list navigation' ),
     42                'heading_list'       => __( 'Export personal data list' ),
     43        )
     44);
     45
     46$requests_table->process_bulk_action();
     47$requests_table->prepare_items();
     48
     49require_once( ABSPATH . 'wp-admin/admin-header.php' );
     50?>
     51
     52<div class="wrap nosubsub">
     53        <h1><?php esc_html_e( 'Export Personal Data' ); ?></h1>
     54        <hr class="wp-header-end" />
     55
     56        <?php settings_errors(); ?>
     57
     58        <form action="<?php echo esc_url( admin_url( 'export-personal-data.php' ) ); ?>" method="post" class="wp-privacy-request-form">
     59                <h2><?php esc_html_e( 'Add Data Export Request' ); ?></h2>
     60                <p><?php esc_html_e( 'An email will be sent to the user at this email address asking them to verify the request.' ); ?></p>
     61
     62                <div class="wp-privacy-request-form-field">
     63                        <label for="username_or_email_for_privacy_request"><?php esc_html_e( 'Username or email address' ); ?></label>
     64                        <input type="text" required class="regular-text" id="username_or_email_for_privacy_request" name="username_or_email_for_privacy_request" />
     65                        <?php submit_button( __( 'Send Request' ), 'secondary', 'submit', false ); ?>
     66                </div>
     67                <?php wp_nonce_field( 'personal-data-request' ); ?>
     68                <input type="hidden" name="action" value="add_export_personal_data_request" />
     69                <input type="hidden" name="type_of_action" value="export_personal_data" />
     70        </form>
     71        <hr />
     72
     73        <?php $requests_table->views(); ?>
     74
     75        <form class="search-form wp-clearfix">
     76                <?php $requests_table->search_box( __( 'Search Requests' ), 'requests' ); ?>
     77                <input type="hidden" name="page" value="export_personal_data" />
     78                <input type="hidden" name="filter-status" value="<?php echo isset( $_REQUEST['filter-status'] ) ? esc_attr( sanitize_text_field( $_REQUEST['filter-status'] ) ) : ''; ?>" />
     79                <input type="hidden" name="orderby" value="<?php echo isset( $_REQUEST['orderby'] ) ? esc_attr( sanitize_text_field( $_REQUEST['orderby'] ) ) : ''; ?>" />
     80                <input type="hidden" name="order" value="<?php echo isset( $_REQUEST['order'] ) ? esc_attr( sanitize_text_field( $_REQUEST['order'] ) ) : ''; ?>" />
     81        </form>
     82
     83        <form method="post">
     84                <?php
     85                $requests_table->display();
     86                $requests_table->embed_scripts();
     87                ?>
     88        </form>
     89</div>
     90
     91<?php
     92include( ABSPATH . 'wp-admin/admin-footer.php' );
  • src/wp-admin/includes/admin-filters.php

    diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php
    index 06bb612dd9..ec0465f1a3 100644
    a b add_action( 'admin_head', 'wp_site_icon' ); 
    4646add_action( 'admin_head', '_ipad_meta' );
    4747
    4848// Privacy tools
    49 add_action( 'admin_menu', '_wp_privacy_hook_requests_page' );
    5049add_action( 'load-tools_page_export_personal_data', '_wp_privacy_requests_screen_options' );
    5150add_action( 'load-tools_page_remove_personal_data', '_wp_privacy_requests_screen_options' );
    5251
  • src/wp-admin/includes/admin.php

    diff --git a/src/wp-admin/includes/admin.php b/src/wp-admin/includes/admin.php
    index af4f7cc04c..4bc3f2f07f 100644
    a b require_once( ABSPATH . 'wp-admin/includes/update.php' ); 
    7979/** WordPress Deprecated Administration API */
    8080require_once( ABSPATH . 'wp-admin/includes/deprecated.php' );
    8181
     82/** WordPress Privacy Functions */
     83require_once( ABSPATH . 'wp-admin/includes/privacy.php' );
     84
    8285/** WordPress Multisite support API */
    8386if ( is_multisite() ) {
    8487        require_once( ABSPATH . 'wp-admin/includes/ms-admin-filters.php' );
  • new file src/wp-admin/includes/class-wp-privacy-data-export-requests-table.php

    diff --git a/src/wp-admin/includes/class-wp-privacy-data-export-requests-table.php b/src/wp-admin/includes/class-wp-privacy-data-export-requests-table.php
    new file mode 100644
    index 0000000000..30e43e3757
    - +  
     1<?php
     2
     3if ( ! class_exists( 'WP_Privacy_Requests_Table' ) ) {
     4        require_once( ABSPATH . 'wp-admin/includes/class-wp-privacy-requests-table.php' );
     5}
     6
     7/**
     8 * WP_Privacy_Data_Export_Requests_Table class.
     9 *
     10 * @since 4.9.6
     11 */
     12class WP_Privacy_Data_Export_Requests_Table extends WP_Privacy_Requests_Table {
     13        /**
     14         * Action name for the requests this table will work with.
     15         *
     16         * @since 4.9.6
     17         *
     18         * @var string $request_type Name of action.
     19         */
     20        protected $request_type = 'export_personal_data';
     21
     22        /**
     23         * Post type for the requests.
     24         *
     25         * @since 4.9.6
     26         *
     27         * @var string $post_type The post type.
     28         */
     29        protected $post_type = 'user_request';
     30
     31        /**
     32         * Actions column.
     33         *
     34         * @since 4.9.6
     35         *
     36         * @param WP_User_Request $item Item being shown.
     37         * @return string Email column markup.
     38         */
     39        public function column_email( $item ) {
     40                /** This filter is documented in wp-admin/includes/ajax-actions.php */
     41                $exporters       = apply_filters( 'wp_privacy_personal_data_exporters', array() );
     42                $exporters_count = count( $exporters );
     43                $request_id      = $item->ID;
     44                $nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
     45
     46                $download_data_markup = '<div class="export-personal-data" ' .
     47                        'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
     48                        'data-request-id="' . esc_attr( $request_id ) . '" ' .
     49                        'data-nonce="' . esc_attr( $nonce ) .
     50                        '">';
     51
     52                $download_data_markup .= '<span class="export-personal-data-idle"><button type="button" class="button-link export-personal-data-handle">' . __( 'Download Personal Data' ) . '</button></span>' .
     53                        '<span style="display:none" class="export-personal-data-processing" >' . __( 'Downloading Data...' ) . '</span>' .
     54                        '<span style="display:none" class="export-personal-data-success"><button type="button" class="button-link export-personal-data-handle">' . __( 'Download Personal Data Again' ) . '</button></span>' .
     55                        '<span style="display:none" class="export-personal-data-failed">' . __( 'Download failed.' ) . ' <button type="button" class="button-link">' . __( 'Retry' ) . '</button></span>';
     56
     57                $download_data_markup .= '</div>';
     58
     59                $row_actions = array(
     60                        'download-data' => $download_data_markup,
     61                );
     62
     63                return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), $item->email, $this->row_actions( $row_actions ) );
     64        }
     65
     66        /**
     67         * Displays the next steps column.
     68         *
     69         * @since 4.9.6
     70         *
     71         * @param WP_User_Request $item Item being shown.
     72         */
     73        public function column_next_steps( $item ) {
     74                $status = $item->status;
     75
     76                switch ( $status ) {
     77                        case 'request-pending':
     78                                esc_html_e( 'Waiting for confirmation' );
     79                                break;
     80                        case 'request-confirmed':
     81                                /** This filter is documented in wp-admin/includes/ajax-actions.php */
     82                                $exporters       = apply_filters( 'wp_privacy_personal_data_exporters', array() );
     83                                $exporters_count = count( $exporters );
     84                                $request_id      = $item->ID;
     85                                $nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
     86
     87                                echo '<div class="export-personal-data" ' .
     88                                        'data-send-as-email="1" ' .
     89                                        'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
     90                                        'data-request-id="' . esc_attr( $request_id ) . '" ' .
     91                                        'data-nonce="' . esc_attr( $nonce ) .
     92                                        '">';
     93
     94                                ?>
     95                                <span class="export-personal-data-idle"><button type="button" class="button export-personal-data-handle"><?php _e( 'Send Export Link' ); ?></button></span>
     96                                <span style="display:none" class="export-personal-data-processing button updating-message" ><?php _e( 'Sending Email...' ); ?></span>
     97                                <span style="display:none" class="export-personal-data-success success-message" ><?php _e( 'Email sent.' ); ?></span>
     98                                <span style="display:none" class="export-personal-data-failed"><?php _e( 'Email could not be sent.' ); ?> <button type="button" class="button export-personal-data-handle"><?php _e( 'Retry' ); ?></button></span>
     99                                <?php
     100
     101                                echo '</div>';
     102                                break;
     103                        case 'request-failed':
     104                                submit_button( __( 'Retry' ), 'secondary', 'privacy_action_email_retry[' . $item->ID . ']', false );
     105                                break;
     106                        case 'request-completed':
     107                                echo '<a href="' . esc_url(
     108                                        wp_nonce_url(
     109                                                add_query_arg(
     110                                                        array(
     111                                                                'action'     => 'delete',
     112                                                                'request_id' => array( $item->ID ),
     113                                                        ),
     114                                                        admin_url( 'export-personal-data.php' )
     115                                                ),
     116                                                'bulk-privacy_requests'
     117                                        )
     118                                ) . '" class="button">' . esc_html__( 'Remove request' ) . '</a>';
     119                                break;
     120                }
     121        }
     122}
  • new file src/wp-admin/includes/class-wp-privacy-data-removal-requests-table.php

    diff --git a/src/wp-admin/includes/class-wp-privacy-data-removal-requests-table.php b/src/wp-admin/includes/class-wp-privacy-data-removal-requests-table.php
    new file mode 100644
    index 0000000000..b7f15a13a2
    - +  
     1<?php
     2
     3if ( ! class_exists( 'WP_Privacy_Requests_Table' ) ) {
     4        require_once( ABSPATH . 'wp-admin/includes/class-wp-privacy-requests-table.php' );
     5}
     6
     7/**
     8 * WP_Privacy_Data_Removal_Requests_Table class.
     9 *
     10 * @since 4.9.6
     11 */
     12class WP_Privacy_Data_Removal_Requests_Table extends WP_Privacy_Requests_Table {
     13        /**
     14         * Action name for the requests this table will work with.
     15         *
     16         * @since 4.9.6
     17         *
     18         * @var string $request_type Name of action.
     19         */
     20        protected $request_type = 'remove_personal_data';
     21
     22        /**
     23         * Post type for the requests.
     24         *
     25         * @since 4.9.6
     26         *
     27         * @var string $post_type The post type.
     28         */
     29        protected $post_type = 'user_request';
     30
     31        /**
     32         * Actions column.
     33         *
     34         * @since 4.9.6
     35         *
     36         * @param WP_User_Request $item Item being shown.
     37         * @return string Email column markup.
     38         */
     39        public function column_email( $item ) {
     40                $row_actions = array();
     41
     42                // Allow the administrator to "force remove" the personal data even if confirmation has not yet been received.
     43                $status = $item->status;
     44                if ( 'request-confirmed' !== $status ) {
     45                        /** This filter is documented in wp-admin/includes/ajax-actions.php */
     46                        $erasers       = apply_filters( 'wp_privacy_personal_data_erasers', array() );
     47                        $erasers_count = count( $erasers );
     48                        $request_id    = $item->ID;
     49                        $nonce         = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id );
     50
     51                        $remove_data_markup = '<div class="remove-personal-data force-remove-personal-data" ' .
     52                                'data-erasers-count="' . esc_attr( $erasers_count ) . '" ' .
     53                                'data-request-id="' . esc_attr( $request_id ) . '" ' .
     54                                'data-nonce="' . esc_attr( $nonce ) .
     55                                '">';
     56
     57                        $remove_data_markup .= '<span class="remove-personal-data-idle"><button type="button" class="button-link remove-personal-data-handle">' . __( 'Force Erase Personal Data' ) . '</button></span>' .
     58                                '<span style="display:none" class="remove-personal-data-processing" >' . __( 'Erasing Data...' ) . '</span>' .
     59                                '<span style="display:none" class="remove-personal-data-failed">' . __( 'Force Erase has failed.' ) . ' <button type="button" class="button-link remove-personal-data-handle">' . __( 'Retry' ) . '</button></span>';
     60
     61                        $remove_data_markup .= '</div>';
     62
     63                        $row_actions = array(
     64                                'remove-data' => $remove_data_markup,
     65                        );
     66                }
     67
     68                return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), $item->email, $this->row_actions( $row_actions ) );
     69        }
     70
     71        /**
     72         * Next steps column.
     73         *
     74         * @since 4.9.6
     75         *
     76         * @param WP_User_Request $item Item being shown.
     77         */
     78        public function column_next_steps( $item ) {
     79                $status = $item->status;
     80
     81                switch ( $status ) {
     82                        case 'request-pending':
     83                                esc_html_e( 'Waiting for confirmation' );
     84                                break;
     85                        case 'request-confirmed':
     86                                /** This filter is documented in wp-admin/includes/ajax-actions.php */
     87                                $erasers       = apply_filters( 'wp_privacy_personal_data_erasers', array() );
     88                                $erasers_count = count( $erasers );
     89                                $request_id    = $item->ID;
     90                                $nonce         = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id );
     91
     92                                echo '<div class="remove-personal-data" ' .
     93                                        'data-force-erase="1" ' .
     94                                        'data-erasers-count="' . esc_attr( $erasers_count ) . '" ' .
     95                                        'data-request-id="' . esc_attr( $request_id ) . '" ' .
     96                                        'data-nonce="' . esc_attr( $nonce ) .
     97                                        '">';
     98
     99                                ?>
     100                                <span class="remove-personal-data-idle"><button type="button" class="button remove-personal-data-handle"><?php _e( 'Erase Personal Data' ); ?></button></span>
     101                                <span style="display:none" class="remove-personal-data-processing button updating-message" ><?php _e( 'Erasing Data...' ); ?></span>
     102                                <span style="display:none" class="remove-personal-data-failed"><?php _e( 'Erasing Data has failed.' ); ?> <button type="button" class="button remove-personal-data-handle"><?php _e( 'Retry' ); ?></button></span>
     103                                <?php
     104
     105                                echo '</div>';
     106
     107                                break;
     108                        case 'request-failed':
     109                                submit_button( __( 'Retry' ), 'secondary', 'privacy_action_email_retry[' . $item->ID . ']', false );
     110                                break;
     111                        case 'request-completed':
     112                                echo '<a href="' . esc_url(
     113                                        wp_nonce_url(
     114                                                add_query_arg(
     115                                                        array(
     116                                                                'action'     => 'delete',
     117                                                                'request_id' => array( $item->ID ),
     118                                                        ),
     119                                                        admin_url( 'erase-personal-data.php' )
     120                                                ),
     121                                                'bulk-privacy_requests'
     122                                        )
     123                                ) . '" class="button">' . esc_html__( 'Remove request' ) . '</a>';
     124                                break;
     125                }
     126        }
     127
     128}
  • new file src/wp-admin/includes/class-wp-privacy-requests-table.php

    diff --git a/src/wp-admin/includes/class-wp-privacy-requests-table.php b/src/wp-admin/includes/class-wp-privacy-requests-table.php
    new file mode 100644
    index 0000000000..ea60b59db6
    - +  
     1<?php
     2
     3/**
     4 * WP_Privacy_Requests_Table class.
     5 *
     6 * @since 4.9.6
     7 */
     8abstract class WP_Privacy_Requests_Table extends WP_List_Table {
     9
     10        /**
     11         * Action name for the requests this table will work with. Classes
     12         * which inherit from WP_Privacy_Requests_Table should define this.
     13         *
     14         * Example: 'export_personal_data'.
     15         *
     16         * @since 4.9.6
     17         *
     18         * @var string $request_type Name of action.
     19         */
     20        protected $request_type = 'INVALID';
     21
     22        /**
     23         * Post type to be used.
     24         *
     25         * @since 4.9.6
     26         *
     27         * @var string $post_type The post type.
     28         */
     29        protected $post_type = 'INVALID';
     30
     31        /**
     32         * Get columns to show in the list table.
     33         *
     34         * @since 4.9.6
     35         *
     36         * @return array Array of columns.
     37         */
     38        public function get_columns() {
     39                $columns = array(
     40                        'cb'                => '<input type="checkbox" />',
     41                        'email'             => __( 'Requester' ),
     42                        'status'            => __( 'Status' ),
     43                        'created_timestamp' => __( 'Requested' ),
     44                        'next_steps'        => __( 'Next Steps' ),
     45                );
     46                return $columns;
     47        }
     48
     49        /**
     50         * Get a list of sortable columns.
     51         *
     52         * @since 4.9.6
     53         *
     54         * @return array Default sortable columns.
     55         */
     56        protected function get_sortable_columns() {
     57                // The initial sorting is by 'Requested' (post_date) and descending.
     58                // With initial sorting, the first click on 'Requested' should be ascending.
     59                // With 'Requester' sorting active, the next click on 'Requested' should be descending.
     60                $desc_first = isset( $_GET['orderby'] );
     61
     62                return array(
     63                        'email'             => 'requester',
     64                        'created_timestamp' => array( 'requested', $desc_first ),
     65                );
     66        }
     67
     68        /**
     69         * Default primary column.
     70         *
     71         * @since 4.9.6
     72         *
     73         * @return string Default primary column name.
     74         */
     75        protected function get_default_primary_column_name() {
     76                return 'email';
     77        }
     78
     79        /**
     80         * Count number of requests for each status.
     81         *
     82         * @since 4.9.6
     83         *
     84         * @return object Number of posts for each status.
     85         */
     86        protected function get_request_counts() {
     87                global $wpdb;
     88
     89                $cache_key = $this->post_type . '-' . $this->request_type;
     90                $counts    = wp_cache_get( $cache_key, 'counts' );
     91
     92                if ( false !== $counts ) {
     93                        return $counts;
     94                }
     95
     96                $query = "
     97                        SELECT post_status, COUNT( * ) AS num_posts
     98                        FROM {$wpdb->posts}
     99                        WHERE post_type = %s
     100                        AND post_name = %s
     101                        GROUP BY post_status";
     102
     103                $results = (array) $wpdb->get_results( $wpdb->prepare( $query, $this->post_type, $this->request_type ), ARRAY_A );
     104                $counts  = array_fill_keys( get_post_stati(), 0 );
     105
     106                foreach ( $results as $row ) {
     107                        $counts[ $row['post_status'] ] = $row['num_posts'];
     108                }
     109
     110                $counts = (object) $counts;
     111                wp_cache_set( $cache_key, $counts, 'counts' );
     112
     113                return $counts;
     114        }
     115
     116        /**
     117         * Get an associative array ( id => link ) with the list of views available on this table.
     118         *
     119         * @since 4.9.6
     120         *
     121         * @return array Associative array of views in the format of $view_name => $view_markup.
     122         */
     123        protected function get_views() {
     124                $current_status = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : '';
     125                $statuses       = _wp_privacy_statuses();
     126                $views          = array();
     127                $admin_url      = admin_url( 'tools.php?page=' . $this->request_type );
     128                $counts         = $this->get_request_counts();
     129                $total_requests = absint( array_sum( (array) $counts ) );
     130
     131                $current_link_attributes = empty( $current_status ) ? ' class="current" aria-current="page"' : '';
     132                $status_label            = sprintf(
     133                        /* translators: %s: all requests count */
     134                        _nx(
     135                                'All <span class="count">(%s)</span>',
     136                                'All <span class="count">(%s)</span>',
     137                                $total_requests,
     138                                'requests'
     139                        ),
     140                        number_format_i18n( $total_requests )
     141                );
     142
     143                $views['all'] = sprintf(
     144                        '<a href="%s"%s>%s</a>',
     145                        esc_url( $admin_url ),
     146                        $current_link_attributes,
     147                        $status_label
     148                );
     149
     150                foreach ( $statuses as $status => $label ) {
     151                        $post_status = get_post_status_object( $status );
     152                        if ( ! $post_status ) {
     153                                continue;
     154                        }
     155
     156                        $current_link_attributes = $status === $current_status ? ' class="current" aria-current="page"' : '';
     157                        $total_status_requests   = absint( $counts->{$status} );
     158                        $status_label            = sprintf(
     159                                translate_nooped_plural( $post_status->label_count, $total_status_requests ),
     160                                number_format_i18n( $total_status_requests )
     161                        );
     162                        $status_link             = add_query_arg( 'filter-status', $status, $admin_url );
     163
     164                        $views[ $status ] = sprintf(
     165                                '<a href="%s"%s>%s</a>',
     166                                esc_url( $status_link ),
     167                                $current_link_attributes,
     168                                $status_label
     169                        );
     170                }
     171
     172                return $views;
     173        }
     174
     175        /**
     176         * Get bulk actions.
     177         *
     178         * @since 4.9.6
     179         *
     180         * @return array List of bulk actions.
     181         */
     182        protected function get_bulk_actions() {
     183                return array(
     184                        'delete' => __( 'Remove' ),
     185                        'resend' => __( 'Resend email' ),
     186                );
     187        }
     188
     189        /**
     190         * Process bulk actions.
     191         *
     192         * @since 4.9.6
     193         */
     194        public function process_bulk_action() {
     195                $action      = $this->current_action();
     196                $request_ids = isset( $_REQUEST['request_id'] ) ? wp_parse_id_list( wp_unslash( $_REQUEST['request_id'] ) ) : array();
     197
     198                $count = 0;
     199
     200                if ( $request_ids ) {
     201                        check_admin_referer( 'bulk-privacy_requests' );
     202                }
     203
     204                switch ( $action ) {
     205                        case 'delete':
     206                                foreach ( $request_ids as $request_id ) {
     207                                        if ( wp_delete_post( $request_id, true ) ) {
     208                                                $count ++;
     209                                        }
     210                                }
     211
     212                                add_settings_error(
     213                                        'bulk_action',
     214                                        'bulk_action',
     215                                        /* translators: %d: number of requests */
     216                                        sprintf( _n( 'Deleted %d request', 'Deleted %d requests', $count ), $count ),
     217                                        'updated'
     218                                );
     219                                break;
     220                        case 'resend':
     221                                foreach ( $request_ids as $request_id ) {
     222                                        $resend = _wp_privacy_resend_request( $request_id );
     223
     224                                        if ( $resend && ! is_wp_error( $resend ) ) {
     225                                                $count++;
     226                                        }
     227                                }
     228
     229                                add_settings_error(
     230                                        'bulk_action',
     231                                        'bulk_action',
     232                                        /* translators: %d: number of requests */
     233                                        sprintf( _n( 'Re-sent %d request', 'Re-sent %d requests', $count ), $count ),
     234                                        'updated'
     235                                );
     236                                break;
     237                }
     238        }
     239
     240        /**
     241         * Prepare items to output.
     242         *
     243         * @since 4.9.6
     244         * @since 5.1.0 Added support for column sorting.
     245         */
     246        public function prepare_items() {
     247                global $wpdb;
     248
     249                $this->items    = array();
     250                $posts_per_page = $this->get_items_per_page( $this->request_type . '_requests_per_page' );
     251                $args           = array(
     252                        'post_type'      => $this->post_type,
     253                        'post_name__in'  => array( $this->request_type ),
     254                        'posts_per_page' => $posts_per_page,
     255                        'offset'         => isset( $_REQUEST['paged'] ) ? max( 0, absint( $_REQUEST['paged'] ) - 1 ) * $posts_per_page : 0,
     256                        'post_status'    => 'any',
     257                        's'              => isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '',
     258                );
     259
     260                $orderby_mapping = array(
     261                        'requester' => 'post_title',
     262                        'requested' => 'post_date',
     263                );
     264
     265                if ( isset( $_REQUEST['orderby'] ) && isset( $orderby_mapping[ $_REQUEST['orderby'] ] ) ) {
     266                        $args['orderby'] = $orderby_mapping[ $_REQUEST['orderby'] ];
     267                }
     268
     269                if ( isset( $_REQUEST['order'] ) && in_array( strtoupper( $_REQUEST['order'] ), array( 'ASC', 'DESC' ), true ) ) {
     270                        $args['order'] = strtoupper( $_REQUEST['order'] );
     271                }
     272
     273                if ( ! empty( $_REQUEST['filter-status'] ) ) {
     274                        $filter_status       = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : '';
     275                        $args['post_status'] = $filter_status;
     276                }
     277
     278                $requests_query = new WP_Query( $args );
     279                $requests       = $requests_query->posts;
     280
     281                foreach ( $requests as $request ) {
     282                        $this->items[] = wp_get_user_request_data( $request->ID );
     283                }
     284
     285                $this->items = array_filter( $this->items );
     286
     287                $this->set_pagination_args(
     288                        array(
     289                                'total_items' => $requests_query->found_posts,
     290                                'per_page'    => $posts_per_page,
     291                        )
     292                );
     293        }
     294
     295        /**
     296         * Checkbox column.
     297         *
     298         * @since 4.9.6
     299         *
     300         * @param WP_User_Request $item Item being shown.
     301         * @return string Checkbox column markup.
     302         */
     303        public function column_cb( $item ) {
     304                return sprintf( '<input type="checkbox" name="request_id[]" value="%1$s" /><span class="spinner"></span>', esc_attr( $item->ID ) );
     305        }
     306
     307        /**
     308         * Status column.
     309         *
     310         * @since 4.9.6
     311         *
     312         * @param WP_User_Request $item Item being shown.
     313         * @return string Status column markup.
     314         */
     315        public function column_status( $item ) {
     316                $status        = get_post_status( $item->ID );
     317                $status_object = get_post_status_object( $status );
     318
     319                if ( ! $status_object || empty( $status_object->label ) ) {
     320                        return '-';
     321                }
     322
     323                $timestamp = false;
     324
     325                switch ( $status ) {
     326                        case 'request-confirmed':
     327                                $timestamp = $item->confirmed_timestamp;
     328                                break;
     329                        case 'request-completed':
     330                                $timestamp = $item->completed_timestamp;
     331                                break;
     332                }
     333
     334                echo '<span class="status-label status-' . esc_attr( $status ) . '">';
     335                echo esc_html( $status_object->label );
     336
     337                if ( $timestamp ) {
     338                        echo ' (' . $this->get_timestamp_as_date( $timestamp ) . ')';
     339                }
     340
     341                echo '</span>';
     342        }
     343
     344        /**
     345         * Convert timestamp for display.
     346         *
     347         * @since 4.9.6
     348         *
     349         * @param int $timestamp Event timestamp.
     350         * @return string Human readable date.
     351         */
     352        protected function get_timestamp_as_date( $timestamp ) {
     353                if ( empty( $timestamp ) ) {
     354                        return '';
     355                }
     356
     357                $time_diff = time() - $timestamp;
     358
     359                if ( $time_diff >= 0 && $time_diff < DAY_IN_SECONDS ) {
     360                        /* translators: human readable timestamp */
     361                        return sprintf( __( '%s ago' ), human_time_diff( $timestamp ) );
     362                }
     363
     364                return date_i18n( get_option( 'date_format' ), $timestamp );
     365        }
     366
     367        /**
     368         * Default column handler.
     369         *
     370         * @since 4.9.6
     371         *
     372         * @param WP_User_Request $item        Item being shown.
     373         * @param string          $column_name Name of column being shown.
     374         * @return string Default column output.
     375         */
     376        public function column_default( $item, $column_name ) {
     377                $cell_value = $item->$column_name;
     378
     379                if ( in_array( $column_name, array( 'created_timestamp' ), true ) ) {
     380                        return $this->get_timestamp_as_date( $cell_value );
     381                }
     382
     383                return $cell_value;
     384        }
     385
     386        /**
     387         * Actions column. Overridden by children.
     388         *
     389         * @since 4.9.6
     390         *
     391         * @param WP_User_Request $item Item being shown.
     392         * @return string Email column markup.
     393         */
     394        public function column_email( $item ) {
     395                return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), $item->email, $this->row_actions( array() ) );
     396        }
     397
     398        /**
     399         * Next steps column. Overridden by children.
     400         *
     401         * @since 4.9.6
     402         *
     403         * @param WP_User_Request $item Item being shown.
     404         */
     405        public function column_next_steps( $item ) {}
     406
     407        /**
     408         * Generates content for a single row of the table,
     409         *
     410         * @since 4.9.6
     411         *
     412         * @param WP_User_Request $item The current item.
     413         */
     414        public function single_row( $item ) {
     415                $status = $item->status;
     416
     417                echo '<tr id="request-' . esc_attr( $item->ID ) . '" class="status-' . esc_attr( $status ) . '">';
     418                $this->single_row_columns( $item );
     419                echo '</tr>';
     420        }
     421
     422        /**
     423         * Embed scripts used to perform actions. Overridden by children.
     424         *
     425         * @since 4.9.6
     426         */
     427        public function embed_scripts() {}
     428}
  • new file src/wp-admin/includes/privacy.php

    diff --git a/src/wp-admin/includes/privacy.php b/src/wp-admin/includes/privacy.php
    new file mode 100644
    index 0000000000..f6e21d3366
    - +  
     1<?php
     2
     3/**
     4 * Resend an existing request and return the result.
     5 *
     6 * @since 4.9.6
     7 * @access private
     8 *
     9 * @param int $request_id Request ID.
     10 * @return bool|WP_Error Returns true/false based on the success of sending the email, or a WP_Error object.
     11 */
     12function _wp_privacy_resend_request( $request_id ) {
     13        $request_id = absint( $request_id );
     14        $request    = get_post( $request_id );
     15
     16        if ( ! $request || 'user_request' !== $request->post_type ) {
     17                return new WP_Error( 'privacy_request_error', __( 'Invalid request.' ) );
     18        }
     19
     20        $result = wp_send_user_request( $request_id );
     21
     22        if ( is_wp_error( $result ) ) {
     23                return $result;
     24        } elseif ( ! $result ) {
     25                return new WP_Error( 'privacy_request_error', __( 'Unable to initiate confirmation request.' ) );
     26        }
     27
     28        return true;
     29}
     30
     31/**
     32 * Marks a request as completed by the admin and logs the current timestamp.
     33 *
     34 * @since 4.9.6
     35 * @access private
     36 *
     37 * @param  int          $request_id Request ID.
     38 * @return int|WP_Error $result Request ID on success or WP_Error.
     39 */
     40function _wp_privacy_completed_request( $request_id ) {
     41        $request_id = absint( $request_id );
     42        $request    = wp_get_user_request_data( $request_id );
     43
     44        if ( ! $request ) {
     45                return new WP_Error( 'privacy_request_error', __( 'Invalid request.' ) );
     46        }
     47
     48        update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() );
     49
     50        $result = wp_update_post(
     51                array(
     52                        'ID'          => $request_id,
     53                        'post_status' => 'request-completed',
     54                )
     55        );
     56
     57        return $result;
     58}
     59
     60/**
     61 * Handle list table actions.
     62 *
     63 * @since 4.9.6
     64 * @access private
     65 */
     66function _wp_personal_data_handle_actions() {
     67        if ( isset( $_POST['privacy_action_email_retry'] ) ) {
     68                check_admin_referer( 'bulk-privacy_requests' );
     69
     70                $request_id = absint( current( array_keys( (array) wp_unslash( $_POST['privacy_action_email_retry'] ) ) ) );
     71                $result     = _wp_privacy_resend_request( $request_id );
     72
     73                if ( is_wp_error( $result ) ) {
     74                        add_settings_error(
     75                                'privacy_action_email_retry',
     76                                'privacy_action_email_retry',
     77                                $result->get_error_message(),
     78                                'error'
     79                        );
     80                } else {
     81                        add_settings_error(
     82                                'privacy_action_email_retry',
     83                                'privacy_action_email_retry',
     84                                __( 'Confirmation request sent again successfully.' ),
     85                                'updated'
     86                        );
     87                }
     88        } elseif ( isset( $_POST['action'] ) ) {
     89                $action = isset( $_POST['action'] ) ? sanitize_key( wp_unslash( $_POST['action'] ) ) : '';
     90
     91                switch ( $action ) {
     92                        case 'add_export_personal_data_request':
     93                        case 'add_remove_personal_data_request':
     94                                check_admin_referer( 'personal-data-request' );
     95
     96                                if ( ! isset( $_POST['type_of_action'], $_POST['username_or_email_for_privacy_request'] ) ) {
     97                                        add_settings_error(
     98                                                'action_type',
     99                                                'action_type',
     100                                                __( 'Invalid action.' ),
     101                                                'error'
     102                                        );
     103                                }
     104                                $action_type               = sanitize_text_field( wp_unslash( $_POST['type_of_action'] ) );
     105                                $username_or_email_address = sanitize_text_field( wp_unslash( $_POST['username_or_email_for_privacy_request'] ) );
     106                                $email_address             = '';
     107
     108                                if ( ! in_array( $action_type, _wp_privacy_action_request_types(), true ) ) {
     109                                        add_settings_error(
     110                                                'action_type',
     111                                                'action_type',
     112                                                __( 'Invalid action.' ),
     113                                                'error'
     114                                        );
     115                                }
     116
     117                                if ( ! is_email( $username_or_email_address ) ) {
     118                                        $user = get_user_by( 'login', $username_or_email_address );
     119                                        if ( ! $user instanceof WP_User ) {
     120                                                add_settings_error(
     121                                                        'username_or_email_for_privacy_request',
     122                                                        'username_or_email_for_privacy_request',
     123                                                        __( 'Unable to add this request. A valid email address or username must be supplied.' ),
     124                                                        'error'
     125                                                );
     126                                        } else {
     127                                                $email_address = $user->user_email;
     128                                        }
     129                                } else {
     130                                        $email_address = $username_or_email_address;
     131                                }
     132
     133                                if ( empty( $email_address ) ) {
     134                                        break;
     135                                }
     136
     137                                $request_id = wp_create_user_request( $email_address, $action_type );
     138
     139                                if ( is_wp_error( $request_id ) ) {
     140                                        add_settings_error(
     141                                                'username_or_email_for_privacy_request',
     142                                                'username_or_email_for_privacy_request',
     143                                                $request_id->get_error_message(),
     144                                                'error'
     145                                        );
     146                                        break;
     147                                } elseif ( ! $request_id ) {
     148                                        add_settings_error(
     149                                                'username_or_email_for_privacy_request',
     150                                                'username_or_email_for_privacy_request',
     151                                                __( 'Unable to initiate confirmation request.' ),
     152                                                'error'
     153                                        );
     154                                        break;
     155                                }
     156
     157                                wp_send_user_request( $request_id );
     158
     159                                add_settings_error(
     160                                        'username_or_email_for_privacy_request',
     161                                        'username_or_email_for_privacy_request',
     162                                        __( 'Confirmation request initiated successfully.' ),
     163                                        'updated'
     164                                );
     165                                break;
     166                }
     167        }
     168}
     169
     170/**
     171 * Cleans up failed and expired requests before displaying the list table.
     172 *
     173 * @since 4.9.6
     174 * @access private
     175 */
     176function _wp_personal_data_cleanup_requests() {
     177        /** This filter is documented in wp-includes/user.php */
     178        $expires = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS );
     179
     180        $requests_query = new WP_Query(
     181                array(
     182                        'post_type'      => 'user_request',
     183                        'posts_per_page' => -1,
     184                        'post_status'    => 'request-pending',
     185                        'fields'         => 'ids',
     186                        'date_query'     => array(
     187                                array(
     188                                        'column' => 'post_modified_gmt',
     189                                        'before' => $expires . ' seconds ago',
     190                                ),
     191                        ),
     192                )
     193        );
     194
     195        $request_ids = $requests_query->posts;
     196
     197        foreach ( $request_ids as $request_id ) {
     198                wp_update_post(
     199                        array(
     200                                'ID'            => $request_id,
     201                                'post_status'   => 'request-failed',
     202                                'post_password' => '',
     203                        )
     204                );
     205        }
     206}
     207
     208/**
     209 * Add options for the privacy requests screens.
     210 *
     211 * @since 4.9.8
     212 * @access private
     213 */
     214function _wp_privacy_requests_screen_options() {
     215        $args = array(
     216                'option' => str_replace( 'tools_page_', '', get_current_screen()->id ) . '_requests_per_page',
     217        );
     218        add_screen_option( 'per_page', $args );
     219}
     220
     221/**
     222 * Mark erasure requests as completed after processing is finished.
     223 *
     224 * This intercepts the Ajax responses to personal data eraser page requests, and
     225 * monitors the status of a request. Once all of the processing has finished, the
     226 * request is marked as completed.
     227 *
     228 * @since 4.9.6
     229 *
     230 * @see wp_privacy_personal_data_erasure_page
     231 *
     232 * @param array  $response      The response from the personal data eraser for
     233 *                              the given page.
     234 * @param int    $eraser_index  The index of the personal data eraser. Begins
     235 *                              at 1.
     236 * @param string $email_address The email address of the user whose personal
     237 *                              data this is.
     238 * @param int    $page          The page of personal data for this eraser.
     239 *                              Begins at 1.
     240 * @param int    $request_id    The request ID for this personal data erasure.
     241 * @return array The filtered response.
     242 */
     243function wp_privacy_process_personal_data_erasure_page( $response, $eraser_index, $email_address, $page, $request_id ) {
     244        /*
     245         * If the eraser response is malformed, don't attempt to consume it; let it
     246         * pass through, so that the default Ajax processing will generate a warning
     247         * to the user.
     248         */
     249        if ( ! is_array( $response ) ) {
     250                return $response;
     251        }
     252
     253        if ( ! array_key_exists( 'done', $response ) ) {
     254                return $response;
     255        }
     256
     257        if ( ! array_key_exists( 'items_removed', $response ) ) {
     258                return $response;
     259        }
     260
     261        if ( ! array_key_exists( 'items_retained', $response ) ) {
     262                return $response;
     263        }
     264
     265        if ( ! array_key_exists( 'messages', $response ) ) {
     266                return $response;
     267        }
     268
     269        $request = wp_get_user_request_data( $request_id );
     270
     271        if ( ! $request || 'remove_personal_data' !== $request->action_name ) {
     272                wp_send_json_error( __( 'Invalid request ID when processing eraser data.' ) );
     273        }
     274
     275        /** This filter is documented in wp-admin/includes/ajax-actions.php */
     276        $erasers        = apply_filters( 'wp_privacy_personal_data_erasers', array() );
     277        $is_last_eraser = count( $erasers ) === $eraser_index;
     278        $eraser_done    = $response['done'];
     279
     280        if ( ! $is_last_eraser || ! $eraser_done ) {
     281                return $response;
     282        }
     283
     284        _wp_privacy_completed_request( $request_id );
     285
     286        /**
     287         * Fires immediately after a personal data erasure request has been marked completed.
     288         *
     289         * @since 4.9.6
     290         *
     291         * @param int $request_id The privacy request post ID associated with this request.
     292         */
     293        do_action( 'wp_privacy_personal_data_erased', $request_id );
     294
     295        return $response;
     296}
  • src/wp-admin/includes/user.php

    diff --git a/src/wp-admin/includes/user.php b/src/wp-admin/includes/user.php
    index 558ff24970..e4b5534c3e 100644
    a b Please click the following link to activate your user account: 
    582582                wp_specialchars_decode( translate_user_role( $role['name'] ) )
    583583        );
    584584}
    585 
    586 /**
    587  * Resend an existing request and return the result.
    588  *
    589  * @since 4.9.6
    590  * @access private
    591  *
    592  * @param int $request_id Request ID.
    593  * @return bool|WP_Error Returns true/false based on the success of sending the email, or a WP_Error object.
    594  */
    595 function _wp_privacy_resend_request( $request_id ) {
    596         $request_id = absint( $request_id );
    597         $request    = get_post( $request_id );
    598 
    599         if ( ! $request || 'user_request' !== $request->post_type ) {
    600                 return new WP_Error( 'privacy_request_error', __( 'Invalid request.' ) );
    601         }
    602 
    603         $result = wp_send_user_request( $request_id );
    604 
    605         if ( is_wp_error( $result ) ) {
    606                 return $result;
    607         } elseif ( ! $result ) {
    608                 return new WP_Error( 'privacy_request_error', __( 'Unable to initiate confirmation request.' ) );
    609         }
    610 
    611         return true;
    612 }
    613 
    614 /**
    615  * Marks a request as completed by the admin and logs the current timestamp.
    616  *
    617  * @since 4.9.6
    618  * @access private
    619  *
    620  * @param  int          $request_id Request ID.
    621  * @return int|WP_Error $result Request ID on success or WP_Error.
    622  */
    623 function _wp_privacy_completed_request( $request_id ) {
    624         $request_id = absint( $request_id );
    625         $request    = wp_get_user_request_data( $request_id );
    626 
    627         if ( ! $request ) {
    628                 return new WP_Error( 'privacy_request_error', __( 'Invalid request.' ) );
    629         }
    630 
    631         update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() );
    632 
    633         $result = wp_update_post(
    634                 array(
    635                         'ID'          => $request_id,
    636                         'post_status' => 'request-completed',
    637                 )
    638         );
    639 
    640         return $result;
    641 }
    642 
    643 /**
    644  * Handle list table actions.
    645  *
    646  * @since 4.9.6
    647  * @access private
    648  */
    649 function _wp_personal_data_handle_actions() {
    650         if ( isset( $_POST['privacy_action_email_retry'] ) ) {
    651                 check_admin_referer( 'bulk-privacy_requests' );
    652 
    653                 $request_id = absint( current( array_keys( (array) wp_unslash( $_POST['privacy_action_email_retry'] ) ) ) );
    654                 $result     = _wp_privacy_resend_request( $request_id );
    655 
    656                 if ( is_wp_error( $result ) ) {
    657                         add_settings_error(
    658                                 'privacy_action_email_retry',
    659                                 'privacy_action_email_retry',
    660                                 $result->get_error_message(),
    661                                 'error'
    662                         );
    663                 } else {
    664                         add_settings_error(
    665                                 'privacy_action_email_retry',
    666                                 'privacy_action_email_retry',
    667                                 __( 'Confirmation request sent again successfully.' ),
    668                                 'updated'
    669                         );
    670                 }
    671         } elseif ( isset( $_POST['action'] ) ) {
    672                 $action = isset( $_POST['action'] ) ? sanitize_key( wp_unslash( $_POST['action'] ) ) : '';
    673 
    674                 switch ( $action ) {
    675                         case 'add_export_personal_data_request':
    676                         case 'add_remove_personal_data_request':
    677                                 check_admin_referer( 'personal-data-request' );
    678 
    679                                 if ( ! isset( $_POST['type_of_action'], $_POST['username_or_email_for_privacy_request'] ) ) {
    680                                         add_settings_error(
    681                                                 'action_type',
    682                                                 'action_type',
    683                                                 __( 'Invalid action.' ),
    684                                                 'error'
    685                                         );
    686                                 }
    687                                 $action_type               = sanitize_text_field( wp_unslash( $_POST['type_of_action'] ) );
    688                                 $username_or_email_address = sanitize_text_field( wp_unslash( $_POST['username_or_email_for_privacy_request'] ) );
    689                                 $email_address             = '';
    690 
    691                                 if ( ! in_array( $action_type, _wp_privacy_action_request_types(), true ) ) {
    692                                         add_settings_error(
    693                                                 'action_type',
    694                                                 'action_type',
    695                                                 __( 'Invalid action.' ),
    696                                                 'error'
    697                                         );
    698                                 }
    699 
    700                                 if ( ! is_email( $username_or_email_address ) ) {
    701                                         $user = get_user_by( 'login', $username_or_email_address );
    702                                         if ( ! $user instanceof WP_User ) {
    703                                                 add_settings_error(
    704                                                         'username_or_email_for_privacy_request',
    705                                                         'username_or_email_for_privacy_request',
    706                                                         __( 'Unable to add this request. A valid email address or username must be supplied.' ),
    707                                                         'error'
    708                                                 );
    709                                         } else {
    710                                                 $email_address = $user->user_email;
    711                                         }
    712                                 } else {
    713                                         $email_address = $username_or_email_address;
    714                                 }
    715 
    716                                 if ( empty( $email_address ) ) {
    717                                         break;
    718                                 }
    719 
    720                                 $request_id = wp_create_user_request( $email_address, $action_type );
    721 
    722                                 if ( is_wp_error( $request_id ) ) {
    723                                         add_settings_error(
    724                                                 'username_or_email_for_privacy_request',
    725                                                 'username_or_email_for_privacy_request',
    726                                                 $request_id->get_error_message(),
    727                                                 'error'
    728                                         );
    729                                         break;
    730                                 } elseif ( ! $request_id ) {
    731                                         add_settings_error(
    732                                                 'username_or_email_for_privacy_request',
    733                                                 'username_or_email_for_privacy_request',
    734                                                 __( 'Unable to initiate confirmation request.' ),
    735                                                 'error'
    736                                         );
    737                                         break;
    738                                 }
    739 
    740                                 wp_send_user_request( $request_id );
    741 
    742                                 add_settings_error(
    743                                         'username_or_email_for_privacy_request',
    744                                         'username_or_email_for_privacy_request',
    745                                         __( 'Confirmation request initiated successfully.' ),
    746                                         'updated'
    747                                 );
    748                                 break;
    749                 }
    750         }
    751 }
    752 
    753 /**
    754  * Cleans up failed and expired requests before displaying the list table.
    755  *
    756  * @since 4.9.6
    757  * @access private
    758  */
    759 function _wp_personal_data_cleanup_requests() {
    760         /** This filter is documented in wp-includes/user.php */
    761         $expires = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS );
    762 
    763         $requests_query = new WP_Query(
    764                 array(
    765                         'post_type'      => 'user_request',
    766                         'posts_per_page' => -1,
    767                         'post_status'    => 'request-pending',
    768                         'fields'         => 'ids',
    769                         'date_query'     => array(
    770                                 array(
    771                                         'column' => 'post_modified_gmt',
    772                                         'before' => $expires . ' seconds ago',
    773                                 ),
    774                         ),
    775                 )
    776         );
    777 
    778         $request_ids = $requests_query->posts;
    779 
    780         foreach ( $request_ids as $request_id ) {
    781                 wp_update_post(
    782                         array(
    783                                 'ID'            => $request_id,
    784                                 'post_status'   => 'request-failed',
    785                                 'post_password' => '',
    786                         )
    787                 );
    788         }
    789 }
    790 
    791 /**
    792  * Personal data export.
    793  *
    794  * @since 4.9.6
    795  * @access private
    796  */
    797 function _wp_personal_data_export_page() {
    798         if ( ! current_user_can( 'export_others_personal_data' ) ) {
    799                 wp_die( __( 'Sorry, you are not allowed to export personal data on this site.' ) );
    800         }
    801 
    802         _wp_personal_data_handle_actions();
    803         _wp_personal_data_cleanup_requests();
    804 
    805         // "Borrow" xfn.js for now so we don't have to create new files.
    806         wp_enqueue_script( 'xfn' );
    807 
    808         $requests_table = new WP_Privacy_Data_Export_Requests_Table(
    809                 array(
    810                         'plural'   => 'privacy_requests',
    811                         'singular' => 'privacy_request',
    812                         'screen'   => 'export_personal_data',
    813                 )
    814         );
    815 
    816         $requests_table->screen->set_screen_reader_content(
    817                 array(
    818                         'heading_views'      => __( 'Filter export personal data list' ),
    819                         'heading_pagination' => __( 'Export personal data list navigation' ),
    820                         'heading_list'       => __( 'Export personal data list' ),
    821                 )
    822         );
    823 
    824         $requests_table->process_bulk_action();
    825         $requests_table->prepare_items();
    826         ?>
    827         <div class="wrap nosubsub">
    828                 <h1><?php esc_html_e( 'Export Personal Data' ); ?></h1>
    829                 <hr class="wp-header-end" />
    830 
    831                 <?php settings_errors(); ?>
    832 
    833                 <form action="<?php echo esc_url( admin_url( 'tools.php?page=export_personal_data' ) ); ?>" method="post" class="wp-privacy-request-form">
    834                         <h2><?php esc_html_e( 'Add Data Export Request' ); ?></h2>
    835                         <p><?php esc_html_e( 'An email will be sent to the user at this email address asking them to verify the request.' ); ?></p>
    836 
    837                         <div class="wp-privacy-request-form-field">
    838                                 <label for="username_or_email_for_privacy_request"><?php esc_html_e( 'Username or email address' ); ?></label>
    839                                 <input type="text" required class="regular-text" id="username_or_email_for_privacy_request" name="username_or_email_for_privacy_request" />
    840                                 <?php submit_button( __( 'Send Request' ), 'secondary', 'submit', false ); ?>
    841                         </div>
    842                         <?php wp_nonce_field( 'personal-data-request' ); ?>
    843                         <input type="hidden" name="action" value="add_export_personal_data_request" />
    844                         <input type="hidden" name="type_of_action" value="export_personal_data" />
    845                 </form>
    846                 <hr />
    847 
    848                 <?php $requests_table->views(); ?>
    849 
    850                 <form class="search-form wp-clearfix">
    851                         <?php $requests_table->search_box( __( 'Search Requests' ), 'requests' ); ?>
    852                         <input type="hidden" name="page" value="export_personal_data" />
    853                         <input type="hidden" name="filter-status" value="<?php echo isset( $_REQUEST['filter-status'] ) ? esc_attr( sanitize_text_field( $_REQUEST['filter-status'] ) ) : ''; ?>" />
    854                         <input type="hidden" name="orderby" value="<?php echo isset( $_REQUEST['orderby'] ) ? esc_attr( sanitize_text_field( $_REQUEST['orderby'] ) ) : ''; ?>" />
    855                         <input type="hidden" name="order" value="<?php echo isset( $_REQUEST['order'] ) ? esc_attr( sanitize_text_field( $_REQUEST['order'] ) ) : ''; ?>" />
    856                 </form>
    857 
    858                 <form method="post">
    859                         <?php
    860                         $requests_table->display();
    861                         $requests_table->embed_scripts();
    862                         ?>
    863                 </form>
    864         </div>
    865         <?php
    866 }
    867 
    868 /**
    869  * Personal data anonymization.
    870  *
    871  * @since 4.9.6
    872  * @access private
    873  */
    874 function _wp_personal_data_removal_page() {
    875         /*
    876          * Require both caps in order to make it explicitly clear that delegating
    877          * erasure from network admins to single-site admins will give them the
    878          * ability to affect global users, rather than being limited to the site
    879          * that they administer.
    880          */
    881         if ( ! current_user_can( 'erase_others_personal_data' ) || ! current_user_can( 'delete_users' ) ) {
    882                 wp_die( __( 'Sorry, you are not allowed to erase data on this site.' ) );
    883         }
    884 
    885         _wp_personal_data_handle_actions();
    886         _wp_personal_data_cleanup_requests();
    887 
    888         // "Borrow" xfn.js for now so we don't have to create new files.
    889         wp_enqueue_script( 'xfn' );
    890 
    891         $requests_table = new WP_Privacy_Data_Removal_Requests_Table(
    892                 array(
    893                         'plural'   => 'privacy_requests',
    894                         'singular' => 'privacy_request',
    895                         'screen'   => 'remove_personal_data',
    896                 )
    897         );
    898 
    899         $requests_table->screen->set_screen_reader_content(
    900                 array(
    901                         'heading_views'      => __( 'Filter erase personal data list' ),
    902                         'heading_pagination' => __( 'Erase personal data list navigation' ),
    903                         'heading_list'       => __( 'Erase personal data list' ),
    904                 )
    905         );
    906 
    907         $requests_table->process_bulk_action();
    908         $requests_table->prepare_items();
    909 
    910         ?>
    911         <div class="wrap nosubsub">
    912                 <h1><?php esc_html_e( 'Erase Personal Data' ); ?></h1>
    913                 <hr class="wp-header-end" />
    914 
    915                 <?php settings_errors(); ?>
    916 
    917                 <form action="<?php echo esc_url( admin_url( 'tools.php?page=remove_personal_data' ) ); ?>" method="post" class="wp-privacy-request-form">
    918                         <h2><?php esc_html_e( 'Add Data Erasure Request' ); ?></h2>
    919                         <p><?php esc_html_e( 'An email will be sent to the user at this email address asking them to verify the request.' ); ?></p>
    920 
    921                         <div class="wp-privacy-request-form-field">
    922                                 <label for="username_or_email_for_privacy_request"><?php esc_html_e( 'Username or email address' ); ?></label>
    923                                 <input type="text" required class="regular-text" id="username_or_email_for_privacy_request" name="username_or_email_for_privacy_request" />
    924                                 <?php submit_button( __( 'Send Request' ), 'secondary', 'submit', false ); ?>
    925                         </div>
    926                         <?php wp_nonce_field( 'personal-data-request' ); ?>
    927                         <input type="hidden" name="action" value="add_remove_personal_data_request" />
    928                         <input type="hidden" name="type_of_action" value="remove_personal_data" />
    929                 </form>
    930                 <hr />
    931 
    932                 <?php $requests_table->views(); ?>
    933 
    934                 <form class="search-form wp-clearfix">
    935                         <?php $requests_table->search_box( __( 'Search Requests' ), 'requests' ); ?>
    936                         <input type="hidden" name="page" value="remove_personal_data" />
    937                         <input type="hidden" name="filter-status" value="<?php echo isset( $_REQUEST['filter-status'] ) ? esc_attr( sanitize_text_field( $_REQUEST['filter-status'] ) ) : ''; ?>" />
    938                         <input type="hidden" name="orderby" value="<?php echo isset( $_REQUEST['orderby'] ) ? esc_attr( sanitize_text_field( $_REQUEST['orderby'] ) ) : ''; ?>" />
    939                         <input type="hidden" name="order" value="<?php echo isset( $_REQUEST['order'] ) ? esc_attr( sanitize_text_field( $_REQUEST['order'] ) ) : ''; ?>" />
    940                 </form>
    941 
    942                 <form method="post">
    943                         <?php
    944                         $requests_table->display();
    945                         $requests_table->embed_scripts();
    946                         ?>
    947                 </form>
    948         </div>
    949         <?php
    950 }
    951 
    952 /**
    953  * Mark erasure requests as completed after processing is finished.
    954  *
    955  * This intercepts the Ajax responses to personal data eraser page requests, and
    956  * monitors the status of a request. Once all of the processing has finished, the
    957  * request is marked as completed.
    958  *
    959  * @since 4.9.6
    960  *
    961  * @see wp_privacy_personal_data_erasure_page
    962  *
    963  * @param array  $response      The response from the personal data eraser for
    964  *                              the given page.
    965  * @param int    $eraser_index  The index of the personal data eraser. Begins
    966  *                              at 1.
    967  * @param string $email_address The email address of the user whose personal
    968  *                              data this is.
    969  * @param int    $page          The page of personal data for this eraser.
    970  *                              Begins at 1.
    971  * @param int    $request_id    The request ID for this personal data erasure.
    972  * @return array The filtered response.
    973  */
    974 function wp_privacy_process_personal_data_erasure_page( $response, $eraser_index, $email_address, $page, $request_id ) {
    975         /*
    976          * If the eraser response is malformed, don't attempt to consume it; let it
    977          * pass through, so that the default Ajax processing will generate a warning
    978          * to the user.
    979          */
    980         if ( ! is_array( $response ) ) {
    981                 return $response;
    982         }
    983 
    984         if ( ! array_key_exists( 'done', $response ) ) {
    985                 return $response;
    986         }
    987 
    988         if ( ! array_key_exists( 'items_removed', $response ) ) {
    989                 return $response;
    990         }
    991 
    992         if ( ! array_key_exists( 'items_retained', $response ) ) {
    993                 return $response;
    994         }
    995 
    996         if ( ! array_key_exists( 'messages', $response ) ) {
    997                 return $response;
    998         }
    999 
    1000         $request = wp_get_user_request_data( $request_id );
    1001 
    1002         if ( ! $request || 'remove_personal_data' !== $request->action_name ) {
    1003                 wp_send_json_error( __( 'Invalid request ID when processing eraser data.' ) );
    1004         }
    1005 
    1006         /** This filter is documented in wp-admin/includes/ajax-actions.php */
    1007         $erasers        = apply_filters( 'wp_privacy_personal_data_erasers', array() );
    1008         $is_last_eraser = count( $erasers ) === $eraser_index;
    1009         $eraser_done    = $response['done'];
    1010 
    1011         if ( ! $is_last_eraser || ! $eraser_done ) {
    1012                 return $response;
    1013         }
    1014 
    1015         _wp_privacy_completed_request( $request_id );
    1016 
    1017         /**
    1018          * Fires immediately after a personal data erasure request has been marked completed.
    1019          *
    1020          * @since 4.9.6
    1021          *
    1022          * @param int $request_id The privacy request post ID associated with this request.
    1023          */
    1024         do_action( 'wp_privacy_personal_data_erased', $request_id );
    1025 
    1026         return $response;
    1027 }
    1028 
    1029 /**
    1030  * Add requests pages.
    1031  *
    1032  * @since 4.9.6
    1033  * @access private
    1034  */
    1035 function _wp_privacy_hook_requests_page() {
    1036         add_submenu_page( 'tools.php', __( 'Export Personal Data' ), __( 'Export Personal Data' ), 'export_others_personal_data', 'export_personal_data', '_wp_personal_data_export_page' );
    1037         add_submenu_page( 'tools.php', __( 'Erase Personal Data' ), __( 'Erase Personal Data' ), 'erase_others_personal_data', 'remove_personal_data', '_wp_personal_data_removal_page' );
    1038 }
    1039 
    1040 /**
    1041  * Add options for the privacy requests screens.
    1042  *
    1043  * @since 4.9.8
    1044  * @access private
    1045  */
    1046 function _wp_privacy_requests_screen_options() {
    1047         $args = array(
    1048                 'option' => str_replace( 'tools_page_', '', get_current_screen()->id ) . '_requests_per_page',
    1049         );
    1050         add_screen_option( 'per_page', $args );
    1051 }
    1052 
    1053 // TODO: move the following classes in new files.
    1054 if ( ! class_exists( 'WP_List_Table' ) ) {
    1055         require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
    1056 }
    1057 
    1058 /**
    1059  * WP_Privacy_Requests_Table class.
    1060  *
    1061  * @since 4.9.6
    1062  */
    1063 abstract class WP_Privacy_Requests_Table extends WP_List_Table {
    1064 
    1065         /**
    1066          * Action name for the requests this table will work with. Classes
    1067          * which inherit from WP_Privacy_Requests_Table should define this.
    1068          *
    1069          * Example: 'export_personal_data'.
    1070          *
    1071          * @since 4.9.6
    1072          *
    1073          * @var string $request_type Name of action.
    1074          */
    1075         protected $request_type = 'INVALID';
    1076 
    1077         /**
    1078          * Post type to be used.
    1079          *
    1080          * @since 4.9.6
    1081          *
    1082          * @var string $post_type The post type.
    1083          */
    1084         protected $post_type = 'INVALID';
    1085 
    1086         /**
    1087          * Get columns to show in the list table.
    1088          *
    1089          * @since 4.9.6
    1090          *
    1091          * @return array Array of columns.
    1092          */
    1093         public function get_columns() {
    1094                 $columns = array(
    1095                         'cb'                => '<input type="checkbox" />',
    1096                         'email'             => __( 'Requester' ),
    1097                         'status'            => __( 'Status' ),
    1098                         'created_timestamp' => __( 'Requested' ),
    1099                         'next_steps'        => __( 'Next Steps' ),
    1100                 );
    1101                 return $columns;
    1102         }
    1103 
    1104         /**
    1105          * Get a list of sortable columns.
    1106          *
    1107          * @since 4.9.6
    1108          *
    1109          * @return array Default sortable columns.
    1110          */
    1111         protected function get_sortable_columns() {
    1112                 // The initial sorting is by 'Requested' (post_date) and descending.
    1113                 // With initial sorting, the first click on 'Requested' should be ascending.
    1114                 // With 'Requester' sorting active, the next click on 'Requested' should be descending.
    1115                 $desc_first = isset( $_GET['orderby'] );
    1116 
    1117                 return array(
    1118                         'email'             => 'requester',
    1119                         'created_timestamp' => array( 'requested', $desc_first ),
    1120                 );
    1121         }
    1122 
    1123         /**
    1124          * Default primary column.
    1125          *
    1126          * @since 4.9.6
    1127          *
    1128          * @return string Default primary column name.
    1129          */
    1130         protected function get_default_primary_column_name() {
    1131                 return 'email';
    1132         }
    1133 
    1134         /**
    1135          * Count number of requests for each status.
    1136          *
    1137          * @since 4.9.6
    1138          *
    1139          * @return object Number of posts for each status.
    1140          */
    1141         protected function get_request_counts() {
    1142                 global $wpdb;
    1143 
    1144                 $cache_key = $this->post_type . '-' . $this->request_type;
    1145                 $counts    = wp_cache_get( $cache_key, 'counts' );
    1146 
    1147                 if ( false !== $counts ) {
    1148                         return $counts;
    1149                 }
    1150 
    1151                 $query = "
    1152                         SELECT post_status, COUNT( * ) AS num_posts
    1153                         FROM {$wpdb->posts}
    1154                         WHERE post_type = %s
    1155                         AND post_name = %s
    1156                         GROUP BY post_status";
    1157 
    1158                 $results = (array) $wpdb->get_results( $wpdb->prepare( $query, $this->post_type, $this->request_type ), ARRAY_A );
    1159                 $counts  = array_fill_keys( get_post_stati(), 0 );
    1160 
    1161                 foreach ( $results as $row ) {
    1162                         $counts[ $row['post_status'] ] = $row['num_posts'];
    1163                 }
    1164 
    1165                 $counts = (object) $counts;
    1166                 wp_cache_set( $cache_key, $counts, 'counts' );
    1167 
    1168                 return $counts;
    1169         }
    1170 
    1171         /**
    1172          * Get an associative array ( id => link ) with the list of views available on this table.
    1173          *
    1174          * @since 4.9.6
    1175          *
    1176          * @return array Associative array of views in the format of $view_name => $view_markup.
    1177          */
    1178         protected function get_views() {
    1179                 $current_status = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : '';
    1180                 $statuses       = _wp_privacy_statuses();
    1181                 $views          = array();
    1182                 $admin_url      = admin_url( 'tools.php?page=' . $this->request_type );
    1183                 $counts         = $this->get_request_counts();
    1184                 $total_requests = absint( array_sum( (array) $counts ) );
    1185 
    1186                 $current_link_attributes = empty( $current_status ) ? ' class="current" aria-current="page"' : '';
    1187                 $status_label            = sprintf(
    1188                         /* translators: %s: all requests count */
    1189                         _nx(
    1190                                 'All <span class="count">(%s)</span>',
    1191                                 'All <span class="count">(%s)</span>',
    1192                                 $total_requests,
    1193                                 'requests'
    1194                         ),
    1195                         number_format_i18n( $total_requests )
    1196                 );
    1197 
    1198                 $views['all'] = sprintf(
    1199                         '<a href="%s"%s>%s</a>',
    1200                         esc_url( $admin_url ),
    1201                         $current_link_attributes,
    1202                         $status_label
    1203                 );
    1204 
    1205                 foreach ( $statuses as $status => $label ) {
    1206                         $post_status = get_post_status_object( $status );
    1207                         if ( ! $post_status ) {
    1208                                 continue;
    1209                         }
    1210 
    1211                         $current_link_attributes = $status === $current_status ? ' class="current" aria-current="page"' : '';
    1212                         $total_status_requests   = absint( $counts->{$status} );
    1213                         $status_label            = sprintf(
    1214                                 translate_nooped_plural( $post_status->label_count, $total_status_requests ),
    1215                                 number_format_i18n( $total_status_requests )
    1216                         );
    1217                         $status_link             = add_query_arg( 'filter-status', $status, $admin_url );
    1218 
    1219                         $views[ $status ] = sprintf(
    1220                                 '<a href="%s"%s>%s</a>',
    1221                                 esc_url( $status_link ),
    1222                                 $current_link_attributes,
    1223                                 $status_label
    1224                         );
    1225                 }
    1226 
    1227                 return $views;
    1228         }
    1229 
    1230         /**
    1231          * Get bulk actions.
    1232          *
    1233          * @since 4.9.6
    1234          *
    1235          * @return array List of bulk actions.
    1236          */
    1237         protected function get_bulk_actions() {
    1238                 return array(
    1239                         'delete' => __( 'Remove' ),
    1240                         'resend' => __( 'Resend email' ),
    1241                 );
    1242         }
    1243 
    1244         /**
    1245          * Process bulk actions.
    1246          *
    1247          * @since 4.9.6
    1248          */
    1249         public function process_bulk_action() {
    1250                 $action      = $this->current_action();
    1251                 $request_ids = isset( $_REQUEST['request_id'] ) ? wp_parse_id_list( wp_unslash( $_REQUEST['request_id'] ) ) : array();
    1252 
    1253                 $count = 0;
    1254 
    1255                 if ( $request_ids ) {
    1256                         check_admin_referer( 'bulk-privacy_requests' );
    1257                 }
    1258 
    1259                 switch ( $action ) {
    1260                         case 'delete':
    1261                                 foreach ( $request_ids as $request_id ) {
    1262                                         if ( wp_delete_post( $request_id, true ) ) {
    1263                                                 $count ++;
    1264                                         }
    1265                                 }
    1266 
    1267                                 add_settings_error(
    1268                                         'bulk_action',
    1269                                         'bulk_action',
    1270                                         /* translators: %d: number of requests */
    1271                                         sprintf( _n( 'Deleted %d request', 'Deleted %d requests', $count ), $count ),
    1272                                         'updated'
    1273                                 );
    1274                                 break;
    1275                         case 'resend':
    1276                                 foreach ( $request_ids as $request_id ) {
    1277                                         $resend = _wp_privacy_resend_request( $request_id );
    1278 
    1279                                         if ( $resend && ! is_wp_error( $resend ) ) {
    1280                                                 $count++;
    1281                                         }
    1282                                 }
    1283 
    1284                                 add_settings_error(
    1285                                         'bulk_action',
    1286                                         'bulk_action',
    1287                                         /* translators: %d: number of requests */
    1288                                         sprintf( _n( 'Re-sent %d request', 'Re-sent %d requests', $count ), $count ),
    1289                                         'updated'
    1290                                 );
    1291                                 break;
    1292                 }
    1293         }
    1294 
    1295         /**
    1296          * Prepare items to output.
    1297          *
    1298          * @since 4.9.6
    1299          * @since 5.1.0 Added support for column sorting.
    1300          */
    1301         public function prepare_items() {
    1302                 global $wpdb;
    1303 
    1304                 $this->items    = array();
    1305                 $posts_per_page = $this->get_items_per_page( $this->request_type . '_requests_per_page' );
    1306                 $args           = array(
    1307                         'post_type'      => $this->post_type,
    1308                         'post_name__in'  => array( $this->request_type ),
    1309                         'posts_per_page' => $posts_per_page,
    1310                         'offset'         => isset( $_REQUEST['paged'] ) ? max( 0, absint( $_REQUEST['paged'] ) - 1 ) * $posts_per_page : 0,
    1311                         'post_status'    => 'any',
    1312                         's'              => isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '',
    1313                 );
    1314 
    1315                 $orderby_mapping = array(
    1316                         'requester' => 'post_title',
    1317                         'requested' => 'post_date',
    1318                 );
    1319 
    1320                 if ( isset( $_REQUEST['orderby'] ) && isset( $orderby_mapping[ $_REQUEST['orderby'] ] ) ) {
    1321                         $args['orderby'] = $orderby_mapping[ $_REQUEST['orderby'] ];
    1322                 }
    1323 
    1324                 if ( isset( $_REQUEST['order'] ) && in_array( strtoupper( $_REQUEST['order'] ), array( 'ASC', 'DESC' ), true ) ) {
    1325                         $args['order'] = strtoupper( $_REQUEST['order'] );
    1326                 }
    1327 
    1328                 if ( ! empty( $_REQUEST['filter-status'] ) ) {
    1329                         $filter_status       = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : '';
    1330                         $args['post_status'] = $filter_status;
    1331                 }
    1332 
    1333                 $requests_query = new WP_Query( $args );
    1334                 $requests       = $requests_query->posts;
    1335 
    1336                 foreach ( $requests as $request ) {
    1337                         $this->items[] = wp_get_user_request_data( $request->ID );
    1338                 }
    1339 
    1340                 $this->items = array_filter( $this->items );
    1341 
    1342                 $this->set_pagination_args(
    1343                         array(
    1344                                 'total_items' => $requests_query->found_posts,
    1345                                 'per_page'    => $posts_per_page,
    1346                         )
    1347                 );
    1348         }
    1349 
    1350         /**
    1351          * Checkbox column.
    1352          *
    1353          * @since 4.9.6
    1354          *
    1355          * @param WP_User_Request $item Item being shown.
    1356          * @return string Checkbox column markup.
    1357          */
    1358         public function column_cb( $item ) {
    1359                 return sprintf( '<input type="checkbox" name="request_id[]" value="%1$s" /><span class="spinner"></span>', esc_attr( $item->ID ) );
    1360         }
    1361 
    1362         /**
    1363          * Status column.
    1364          *
    1365          * @since 4.9.6
    1366          *
    1367          * @param WP_User_Request $item Item being shown.
    1368          * @return string Status column markup.
    1369          */
    1370         public function column_status( $item ) {
    1371                 $status        = get_post_status( $item->ID );
    1372                 $status_object = get_post_status_object( $status );
    1373 
    1374                 if ( ! $status_object || empty( $status_object->label ) ) {
    1375                         return '-';
    1376                 }
    1377 
    1378                 $timestamp = false;
    1379 
    1380                 switch ( $status ) {
    1381                         case 'request-confirmed':
    1382                                 $timestamp = $item->confirmed_timestamp;
    1383                                 break;
    1384                         case 'request-completed':
    1385                                 $timestamp = $item->completed_timestamp;
    1386                                 break;
    1387                 }
    1388 
    1389                 echo '<span class="status-label status-' . esc_attr( $status ) . '">';
    1390                 echo esc_html( $status_object->label );
    1391 
    1392                 if ( $timestamp ) {
    1393                         echo ' (' . $this->get_timestamp_as_date( $timestamp ) . ')';
    1394                 }
    1395 
    1396                 echo '</span>';
    1397         }
    1398 
    1399         /**
    1400          * Convert timestamp for display.
    1401          *
    1402          * @since 4.9.6
    1403          *
    1404          * @param int $timestamp Event timestamp.
    1405          * @return string Human readable date.
    1406          */
    1407         protected function get_timestamp_as_date( $timestamp ) {
    1408                 if ( empty( $timestamp ) ) {
    1409                         return '';
    1410                 }
    1411 
    1412                 $time_diff = time() - $timestamp;
    1413 
    1414                 if ( $time_diff >= 0 && $time_diff < DAY_IN_SECONDS ) {
    1415                         /* translators: human readable timestamp */
    1416                         return sprintf( __( '%s ago' ), human_time_diff( $timestamp ) );
    1417                 }
    1418 
    1419                 return date_i18n( get_option( 'date_format' ), $timestamp );
    1420         }
    1421 
    1422         /**
    1423          * Default column handler.
    1424          *
    1425          * @since 4.9.6
    1426          *
    1427          * @param WP_User_Request $item        Item being shown.
    1428          * @param string          $column_name Name of column being shown.
    1429          * @return string Default column output.
    1430          */
    1431         public function column_default( $item, $column_name ) {
    1432                 $cell_value = $item->$column_name;
    1433 
    1434                 if ( in_array( $column_name, array( 'created_timestamp' ), true ) ) {
    1435                         return $this->get_timestamp_as_date( $cell_value );
    1436                 }
    1437 
    1438                 return $cell_value;
    1439         }
    1440 
    1441         /**
    1442          * Actions column. Overridden by children.
    1443          *
    1444          * @since 4.9.6
    1445          *
    1446          * @param WP_User_Request $item Item being shown.
    1447          * @return string Email column markup.
    1448          */
    1449         public function column_email( $item ) {
    1450                 return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), $item->email, $this->row_actions( array() ) );
    1451         }
    1452 
    1453         /**
    1454          * Next steps column. Overridden by children.
    1455          *
    1456          * @since 4.9.6
    1457          *
    1458          * @param WP_User_Request $item Item being shown.
    1459          */
    1460         public function column_next_steps( $item ) {}
    1461 
    1462         /**
    1463          * Generates content for a single row of the table,
    1464          *
    1465          * @since 4.9.6
    1466          *
    1467          * @param WP_User_Request $item The current item.
    1468          */
    1469         public function single_row( $item ) {
    1470                 $status = $item->status;
    1471 
    1472                 echo '<tr id="request-' . esc_attr( $item->ID ) . '" class="status-' . esc_attr( $status ) . '">';
    1473                 $this->single_row_columns( $item );
    1474                 echo '</tr>';
    1475         }
    1476 
    1477         /**
    1478          * Embed scripts used to perform actions. Overridden by children.
    1479          *
    1480          * @since 4.9.6
    1481          */
    1482         public function embed_scripts() {}
    1483 }
    1484 
    1485 /**
    1486  * WP_Privacy_Data_Export_Requests_Table class.
    1487  *
    1488  * @since 4.9.6
    1489  */
    1490 class WP_Privacy_Data_Export_Requests_Table extends WP_Privacy_Requests_Table {
    1491         /**
    1492          * Action name for the requests this table will work with.
    1493          *
    1494          * @since 4.9.6
    1495          *
    1496          * @var string $request_type Name of action.
    1497          */
    1498         protected $request_type = 'export_personal_data';
    1499 
    1500         /**
    1501          * Post type for the requests.
    1502          *
    1503          * @since 4.9.6
    1504          *
    1505          * @var string $post_type The post type.
    1506          */
    1507         protected $post_type = 'user_request';
    1508 
    1509         /**
    1510          * Actions column.
    1511          *
    1512          * @since 4.9.6
    1513          *
    1514          * @param WP_User_Request $item Item being shown.
    1515          * @return string Email column markup.
    1516          */
    1517         public function column_email( $item ) {
    1518                 /** This filter is documented in wp-admin/includes/ajax-actions.php */
    1519                 $exporters       = apply_filters( 'wp_privacy_personal_data_exporters', array() );
    1520                 $exporters_count = count( $exporters );
    1521                 $request_id      = $item->ID;
    1522                 $nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
    1523 
    1524                 $download_data_markup = '<div class="export-personal-data" ' .
    1525                         'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
    1526                         'data-request-id="' . esc_attr( $request_id ) . '" ' .
    1527                         'data-nonce="' . esc_attr( $nonce ) .
    1528                         '">';
    1529 
    1530                 $download_data_markup .= '<span class="export-personal-data-idle"><button type="button" class="button-link export-personal-data-handle">' . __( 'Download Personal Data' ) . '</button></span>' .
    1531                         '<span style="display:none" class="export-personal-data-processing" >' . __( 'Downloading Data...' ) . '</span>' .
    1532                         '<span style="display:none" class="export-personal-data-success"><button type="button" class="button-link export-personal-data-handle">' . __( 'Download Personal Data Again' ) . '</button></span>' .
    1533                         '<span style="display:none" class="export-personal-data-failed">' . __( 'Download failed.' ) . ' <button type="button" class="button-link">' . __( 'Retry' ) . '</button></span>';
    1534 
    1535                 $download_data_markup .= '</div>';
    1536 
    1537                 $row_actions = array(
    1538                         'download-data' => $download_data_markup,
    1539                 );
    1540 
    1541                 return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), $item->email, $this->row_actions( $row_actions ) );
    1542         }
    1543 
    1544         /**
    1545          * Displays the next steps column.
    1546          *
    1547          * @since 4.9.6
    1548          *
    1549          * @param WP_User_Request $item Item being shown.
    1550          */
    1551         public function column_next_steps( $item ) {
    1552                 $status = $item->status;
    1553 
    1554                 switch ( $status ) {
    1555                         case 'request-pending':
    1556                                 esc_html_e( 'Waiting for confirmation' );
    1557                                 break;
    1558                         case 'request-confirmed':
    1559                                 /** This filter is documented in wp-admin/includes/ajax-actions.php */
    1560                                 $exporters       = apply_filters( 'wp_privacy_personal_data_exporters', array() );
    1561                                 $exporters_count = count( $exporters );
    1562                                 $request_id      = $item->ID;
    1563                                 $nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
    1564 
    1565                                 echo '<div class="export-personal-data" ' .
    1566                                         'data-send-as-email="1" ' .
    1567                                         'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
    1568                                         'data-request-id="' . esc_attr( $request_id ) . '" ' .
    1569                                         'data-nonce="' . esc_attr( $nonce ) .
    1570                                         '">';
    1571 
    1572                                 ?>
    1573                                 <span class="export-personal-data-idle"><button type="button" class="button export-personal-data-handle"><?php _e( 'Send Export Link' ); ?></button></span>
    1574                                 <span style="display:none" class="export-personal-data-processing button updating-message" ><?php _e( 'Sending Email...' ); ?></span>
    1575                                 <span style="display:none" class="export-personal-data-success success-message" ><?php _e( 'Email sent.' ); ?></span>
    1576                                 <span style="display:none" class="export-personal-data-failed"><?php _e( 'Email could not be sent.' ); ?> <button type="button" class="button export-personal-data-handle"><?php _e( 'Retry' ); ?></button></span>
    1577                                 <?php
    1578 
    1579                                 echo '</div>';
    1580                                 break;
    1581                         case 'request-failed':
    1582                                 submit_button( __( 'Retry' ), 'secondary', 'privacy_action_email_retry[' . $item->ID . ']', false );
    1583                                 break;
    1584                         case 'request-completed':
    1585                                 echo '<a href="' . esc_url(
    1586                                         wp_nonce_url(
    1587                                                 add_query_arg(
    1588                                                         array(
    1589                                                                 'action'     => 'delete',
    1590                                                                 'request_id' => array( $item->ID ),
    1591                                                         ),
    1592                                                         admin_url( 'tools.php?page=export_personal_data' )
    1593                                                 ),
    1594                                                 'bulk-privacy_requests'
    1595                                         )
    1596                                 ) . '" class="button">' . esc_html__( 'Remove request' ) . '</a>';
    1597                                 break;
    1598                 }
    1599         }
    1600 }
    1601 
    1602 /**
    1603  * WP_Privacy_Data_Removal_Requests_Table class.
    1604  *
    1605  * @since 4.9.6
    1606  */
    1607 class WP_Privacy_Data_Removal_Requests_Table extends WP_Privacy_Requests_Table {
    1608         /**
    1609          * Action name for the requests this table will work with.
    1610          *
    1611          * @since 4.9.6
    1612          *
    1613          * @var string $request_type Name of action.
    1614          */
    1615         protected $request_type = 'remove_personal_data';
    1616 
    1617         /**
    1618          * Post type for the requests.
    1619          *
    1620          * @since 4.9.6
    1621          *
    1622          * @var string $post_type The post type.
    1623          */
    1624         protected $post_type = 'user_request';
    1625 
    1626         /**
    1627          * Actions column.
    1628          *
    1629          * @since 4.9.6
    1630          *
    1631          * @param WP_User_Request $item Item being shown.
    1632          * @return string Email column markup.
    1633          */
    1634         public function column_email( $item ) {
    1635                 $row_actions = array();
    1636 
    1637                 // Allow the administrator to "force remove" the personal data even if confirmation has not yet been received.
    1638                 $status = $item->status;
    1639                 if ( 'request-confirmed' !== $status ) {
    1640                         /** This filter is documented in wp-admin/includes/ajax-actions.php */
    1641                         $erasers       = apply_filters( 'wp_privacy_personal_data_erasers', array() );
    1642                         $erasers_count = count( $erasers );
    1643                         $request_id    = $item->ID;
    1644                         $nonce         = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id );
    1645 
    1646                         $remove_data_markup = '<div class="remove-personal-data force-remove-personal-data" ' .
    1647                                 'data-erasers-count="' . esc_attr( $erasers_count ) . '" ' .
    1648                                 'data-request-id="' . esc_attr( $request_id ) . '" ' .
    1649                                 'data-nonce="' . esc_attr( $nonce ) .
    1650                                 '">';
    1651 
    1652                         $remove_data_markup .= '<span class="remove-personal-data-idle"><button type="button" class="button-link remove-personal-data-handle">' . __( 'Force Erase Personal Data' ) . '</button></span>' .
    1653                                 '<span style="display:none" class="remove-personal-data-processing" >' . __( 'Erasing Data...' ) . '</span>' .
    1654                                 '<span style="display:none" class="remove-personal-data-failed">' . __( 'Force Erase has failed.' ) . ' <button type="button" class="button-link remove-personal-data-handle">' . __( 'Retry' ) . '</button></span>';
    1655 
    1656                         $remove_data_markup .= '</div>';
    1657 
    1658                         $row_actions = array(
    1659                                 'remove-data' => $remove_data_markup,
    1660                         );
    1661                 }
    1662 
    1663                 return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), $item->email, $this->row_actions( $row_actions ) );
    1664         }
    1665 
    1666         /**
    1667          * Next steps column.
    1668          *
    1669          * @since 4.9.6
    1670          *
    1671          * @param WP_User_Request $item Item being shown.
    1672          */
    1673         public function column_next_steps( $item ) {
    1674                 $status = $item->status;
    1675 
    1676                 switch ( $status ) {
    1677                         case 'request-pending':
    1678                                 esc_html_e( 'Waiting for confirmation' );
    1679                                 break;
    1680                         case 'request-confirmed':
    1681                                 /** This filter is documented in wp-admin/includes/ajax-actions.php */
    1682                                 $erasers       = apply_filters( 'wp_privacy_personal_data_erasers', array() );
    1683                                 $erasers_count = count( $erasers );
    1684                                 $request_id    = $item->ID;
    1685                                 $nonce         = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id );
    1686 
    1687                                 echo '<div class="remove-personal-data" ' .
    1688                                         'data-force-erase="1" ' .
    1689                                         'data-erasers-count="' . esc_attr( $erasers_count ) . '" ' .
    1690                                         'data-request-id="' . esc_attr( $request_id ) . '" ' .
    1691                                         'data-nonce="' . esc_attr( $nonce ) .
    1692                                         '">';
    1693 
    1694                                 ?>
    1695                                 <span class="remove-personal-data-idle"><button type="button" class="button remove-personal-data-handle"><?php _e( 'Erase Personal Data' ); ?></button></span>
    1696                                 <span style="display:none" class="remove-personal-data-processing button updating-message" ><?php _e( 'Erasing Data...' ); ?></span>
    1697                                 <span style="display:none" class="remove-personal-data-failed"><?php _e( 'Erasing Data has failed.' ); ?> <button type="button" class="button remove-personal-data-handle"><?php _e( 'Retry' ); ?></button></span>
    1698                                 <?php
    1699 
    1700                                 echo '</div>';
    1701 
    1702                                 break;
    1703                         case 'request-failed':
    1704                                 submit_button( __( 'Retry' ), 'secondary', 'privacy_action_email_retry[' . $item->ID . ']', false );
    1705                                 break;
    1706                         case 'request-completed':
    1707                                 echo '<a href="' . esc_url(
    1708                                         wp_nonce_url(
    1709                                                 add_query_arg(
    1710                                                         array(
    1711                                                                 'action'     => 'delete',
    1712                                                                 'request_id' => array( $item->ID ),
    1713                                                         ),
    1714                                                         admin_url( 'tools.php?page=remove_personal_data' )
    1715                                                 ),
    1716                                                 'bulk-privacy_requests'
    1717                                         )
    1718                                 ) . '" class="button">' . esc_html__( 'Remove request' ) . '</a>';
    1719                                 break;
    1720                 }
    1721         }
    1722 
    1723 }
  • src/wp-admin/menu.php

    diff --git a/src/wp-admin/menu.php b/src/wp-admin/menu.php
    index d126339552..d5c74a273e 100644
    a b $menu[75] = array( __( 'Tools' ), 'edit_posts', 'tools.php', 
    264264        $submenu['tools.php'][10] = array( __( 'Import' ), 'import', 'import.php' );
    265265        $submenu['tools.php'][15] = array( __( 'Export' ), 'export', 'export.php' );
    266266        $submenu['tools.php'][20] = array( __( 'Site Health' ), 'install_plugins', 'site-health.php' );
     267        $submenu['tools.php'][25] = array( __( 'Export Personal Data' ), 'export_others_personal_data', 'export-personal-data.php' );
     268        $submenu['tools.php'][30] = array( __( 'Erase Personal Data' ), 'erase_others_personal_data', 'erase-personal-data.php' );
    267269if ( is_multisite() && ! is_main_site() ) {
    268         $submenu['tools.php'][25] = array( __( 'Delete Site' ), 'delete_site', 'ms-delete-site.php' );
     270        $submenu['tools.php'][35] = array( __( 'Delete Site' ), 'delete_site', 'ms-delete-site.php' );
    269271}
    270272if ( ! is_multisite() && defined( 'WP_ALLOW_MULTISITE' ) && WP_ALLOW_MULTISITE ) {
    271273        $submenu['tools.php'][50] = array( __( 'Network Setup' ), 'setup_network', 'network.php' );
  • src/wp-includes/script-loader.php

    diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php
    index eb0f37098a..f3824181fd 100644
    a b function wp_default_scripts( &$scripts ) { 
    15421542                );
    15431543
    15441544                $scripts->add( 'xfn', "/wp-admin/js/xfn$suffix.js", array( 'jquery' ), false, 1 );
    1545                 did_action( 'init' ) && $scripts->localize(
    1546                         'xfn',
    1547                         'privacyToolsL10n',
    1548                         array(
    1549                                 'noDataFound'     => __( 'No personal data was found for this user.' ),
    1550                                 'foundAndRemoved' => __( 'All of the personal data found for this user was erased.' ),
    1551                                 'noneRemoved'     => __( 'Personal data was found for this user but was not erased.' ),
    1552                                 'someNotRemoved'  => __( 'Personal data was found for this user but some of the personal data found was not erased.' ),
    1553                                 'removalError'    => __( 'An error occurred while attempting to find and erase personal data.' ),
    1554                                 'noExportFile'    => __( 'No personal data export file was generated.' ),
    1555                                 'exportError'     => __( 'An error occurred while attempting to export personal data.' ),
    1556                         )
    1557                 );
    15581545
    15591546                $scripts->add( 'postbox', "/wp-admin/js/postbox$suffix.js", array( 'jquery-ui-sortable' ), false, 1 );
    15601547                did_action( 'init' ) && $scripts->localize(
    function wp_default_scripts( &$scripts ) { 
    16931680                $scripts->add( 'site-health', "/wp-admin/js/site-health$suffix.js", array( 'clipboard', 'jquery', 'wp-util', 'wp-a11y', 'wp-i18n' ), false, 1 );
    16941681                $scripts->set_translations( 'site-health' );
    16951682
     1683                $scripts->add( 'privacy', "/wp-admin/js/privacy$suffix.js", array( 'jquery' ), false, 1 );
     1684                did_action( 'init' ) && $scripts->localize(
     1685                        'privacy',
     1686                        'privacyToolsL10n',
     1687                        array(
     1688                                'noDataFound'     => __( 'No personal data was found for this user.' ),
     1689                                'foundAndRemoved' => __( 'All of the personal data found for this user was erased.' ),
     1690                                'noneRemoved'     => __( 'Personal data was found for this user but was not erased.' ),
     1691                                'someNotRemoved'  => __( 'Personal data was found for this user but some of the personal data found was not erased.' ),
     1692                                'removalError'    => __( 'An error occurred while attempting to find and erase personal data.' ),
     1693                                'noExportFile'    => __( 'No personal data export file was generated.' ),
     1694                                'exportError'     => __( 'An error occurred while attempting to export personal data.' ),
     1695                        )
     1696                );
     1697
    16961698                $scripts->add( 'updates', "/wp-admin/js/updates$suffix.js", array( 'jquery', 'wp-util', 'wp-a11y' ), false, 1 );
    16971699                did_action( 'init' ) && $scripts->localize(
    16981700                        'updates',