Ticket #43895: 43895.diff
File 43895.diff, 92.5 KB (added by , 5 years ago) |
---|
-
Gruntfile.js
diff --git a/Gruntfile.js b/Gruntfile.js index 333b5252f6..7fc89313ca 100644
a b module.exports = function(grunt) { 264 264 [ WORKING_DIR + 'wp-admin/js/tags-suggest.js' ]: [ './src/js/_enqueues/admin/tags-suggest.js' ], 265 265 [ WORKING_DIR + 'wp-admin/js/tags.js' ]: [ './src/js/_enqueues/admin/tags.js' ], 266 266 [ 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' ], 267 268 [ WORKING_DIR + 'wp-admin/js/theme-plugin-editor.js' ]: [ './src/js/_enqueues/wp/theme-plugin-editor.js' ], 268 269 [ WORKING_DIR + 'wp-admin/js/theme.js' ]: [ './src/js/_enqueues/wp/theme.js' ], 269 270 [ 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( $ ) { 21 21 $( '#link_rel' ).val( ( isMe ) ? 'me' : inputs.substr( 0,inputs.length - 1 ) ); 22 22 }); 23 23 }); 24 25 // Privacy request action handling26 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: sendAsEmail119 },120 method: 'post'121 }122 ).done( function( response ) {123 var responseData = response.data;124 125 if ( ! response.success ) {126 127 // e.g. invalid request ID128 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 failure144 onExportFailure( error );145 });146 }147 148 // And now, let's begin149 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: nonce208 },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 begin241 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 */ 11 require_once( dirname( __FILE__ ) . '/admin.php' ); 12 13 if ( ! 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 17 if ( ! 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. 28 wp_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 49 require_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 92 include( 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 */ 11 require_once( dirname( __FILE__ ) . '/admin.php' ); 12 13 if ( ! current_user_can( 'export_others_personal_data' ) ) { 14 wp_die( __( 'Sorry, you are not allowed to export personal data on this site.' ) ); 15 } 16 17 if ( ! 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. 28 wp_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 49 require_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 92 include( 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' ); 46 46 add_action( 'admin_head', '_ipad_meta' ); 47 47 48 48 // Privacy tools 49 add_action( 'admin_menu', '_wp_privacy_hook_requests_page' );50 49 add_action( 'load-tools_page_export_personal_data', '_wp_privacy_requests_screen_options' ); 51 50 add_action( 'load-tools_page_remove_personal_data', '_wp_privacy_requests_screen_options' ); 52 51 -
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' ); 79 79 /** WordPress Deprecated Administration API */ 80 80 require_once( ABSPATH . 'wp-admin/includes/deprecated.php' ); 81 81 82 /** WordPress Privacy Functions */ 83 require_once( ABSPATH . 'wp-admin/includes/privacy.php' ); 84 82 85 /** WordPress Multisite support API */ 83 86 if ( is_multisite() ) { 84 87 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 3 if ( ! 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 */ 12 class 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 3 if ( ! 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 */ 12 class 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 */ 8 abstract 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 */ 12 function _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 */ 40 function _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 */ 66 function _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 */ 176 function _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 */ 214 function _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 */ 243 function 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: 582 582 wp_specialchars_decode( translate_user_role( $role['name'] ) ) 583 583 ); 584 584 } 585 586 /**587 * Resend an existing request and return the result.588 *589 * @since 4.9.6590 * @access private591 *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.6618 * @access private619 *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.6647 * @access private648 */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.6757 * @access private758 */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.6795 * @access private796 */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 <?php860 $requests_table->display();861 $requests_table->embed_scripts();862 ?>863 </form>864 </div>865 <?php866 }867 868 /**869 * Personal data anonymization.870 *871 * @since 4.9.6872 * @access private873 */874 function _wp_personal_data_removal_page() {875 /*876 * Require both caps in order to make it explicitly clear that delegating877 * erasure from network admins to single-site admins will give them the878 * ability to affect global users, rather than being limited to the site879 * 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 <?php944 $requests_table->display();945 $requests_table->embed_scripts();946 ?>947 </form>948 </div>949 <?php950 }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, and956 * monitors the status of a request. Once all of the processing has finished, the957 * request is marked as completed.958 *959 * @since 4.9.6960 *961 * @see wp_privacy_personal_data_erasure_page962 *963 * @param array $response The response from the personal data eraser for964 * the given page.965 * @param int $eraser_index The index of the personal data eraser. Begins966 * at 1.967 * @param string $email_address The email address of the user whose personal968 * 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 it977 * pass through, so that the default Ajax processing will generate a warning978 * 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.61021 *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.61033 * @access private1034 */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.81044 * @access private1045 */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.61062 */1063 abstract class WP_Privacy_Requests_Table extends WP_List_Table {1064 1065 /**1066 * Action name for the requests this table will work with. Classes1067 * which inherit from WP_Privacy_Requests_Table should define this.1068 *1069 * Example: 'export_personal_data'.1070 *1071 * @since 4.9.61072 *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.61081 *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.61090 *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.61108 *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.61127 *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.61138 *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_posts1153 FROM {$wpdb->posts}1154 WHERE post_type = %s1155 AND post_name = %s1156 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.61175 *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_label1203 );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_label1224 );1225 }1226 1227 return $views;1228 }1229 1230 /**1231 * Get bulk actions.1232 *1233 * @since 4.9.61234 *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.61248 */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.61299 * @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.61354 *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.61366 *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.61403 *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.61426 *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.61445 *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.61457 *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.61466 *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.61481 */1482 public function embed_scripts() {}1483 }1484 1485 /**1486 * WP_Privacy_Data_Export_Requests_Table class.1487 *1488 * @since 4.9.61489 */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.61495 *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.61504 *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.61513 *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.61548 *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 <?php1578 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.61606 */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.61612 *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.61621 *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.61630 *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.61670 *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 <?php1699 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', 264 264 $submenu['tools.php'][10] = array( __( 'Import' ), 'import', 'import.php' ); 265 265 $submenu['tools.php'][15] = array( __( 'Export' ), 'export', 'export.php' ); 266 266 $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' ); 267 269 if ( 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' ); 269 271 } 270 272 if ( ! is_multisite() && defined( 'WP_ALLOW_MULTISITE' ) && WP_ALLOW_MULTISITE ) { 271 273 $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 ) { 1542 1542 ); 1543 1543 1544 1544 $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 );1558 1545 1559 1546 $scripts->add( 'postbox', "/wp-admin/js/postbox$suffix.js", array( 'jquery-ui-sortable' ), false, 1 ); 1560 1547 did_action( 'init' ) && $scripts->localize( … … function wp_default_scripts( &$scripts ) { 1693 1680 $scripts->add( 'site-health', "/wp-admin/js/site-health$suffix.js", array( 'clipboard', 'jquery', 'wp-util', 'wp-a11y', 'wp-i18n' ), false, 1 ); 1694 1681 $scripts->set_translations( 'site-health' ); 1695 1682 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 1696 1698 $scripts->add( 'updates', "/wp-admin/js/updates$suffix.js", array( 'jquery', 'wp-util', 'wp-a11y' ), false, 1 ); 1697 1699 did_action( 'init' ) && $scripts->localize( 1698 1700 'updates',