class-wp-privacy-requests-table.php (13736B)
1 <?php 2 /** 3 * List Table API: WP_Privacy_Requests_Table class 4 * 5 * @package WordPress 6 * @subpackage Administration 7 * @since 4.9.6 8 */ 9 10 abstract class WP_Privacy_Requests_Table extends WP_List_Table { 11 12 /** 13 * Action name for the requests this table will work with. Classes 14 * which inherit from WP_Privacy_Requests_Table should define this. 15 * 16 * Example: 'export_personal_data'. 17 * 18 * @since 4.9.6 19 * 20 * @var string $request_type Name of action. 21 */ 22 protected $request_type = 'INVALID'; 23 24 /** 25 * Post type to be used. 26 * 27 * @since 4.9.6 28 * 29 * @var string $post_type The post type. 30 */ 31 protected $post_type = 'INVALID'; 32 33 /** 34 * Get columns to show in the list table. 35 * 36 * @since 4.9.6 37 * 38 * @return string[] Array of column titles keyed by their column name. 39 */ 40 public function get_columns() { 41 $columns = array( 42 'cb' => '<input type="checkbox" />', 43 'email' => __( 'Requester' ), 44 'status' => __( 'Status' ), 45 'created_timestamp' => __( 'Requested' ), 46 'next_steps' => __( 'Next steps' ), 47 ); 48 return $columns; 49 } 50 51 /** 52 * Normalize the admin URL to the current page (by request_type). 53 * 54 * @since 5.3.0 55 * 56 * @return string URL to the current admin page. 57 */ 58 protected function get_admin_url() { 59 $pagenow = str_replace( '_', '-', $this->request_type ); 60 61 if ( 'remove-personal-data' === $pagenow ) { 62 $pagenow = 'erase-personal-data'; 63 } 64 65 return admin_url( $pagenow . '.php' ); 66 } 67 68 /** 69 * Get a list of sortable columns. 70 * 71 * @since 4.9.6 72 * 73 * @return array Default sortable columns. 74 */ 75 protected function get_sortable_columns() { 76 /* 77 * The initial sorting is by 'Requested' (post_date) and descending. 78 * With initial sorting, the first click on 'Requested' should be ascending. 79 * With 'Requester' sorting active, the next click on 'Requested' should be descending. 80 */ 81 $desc_first = isset( $_GET['orderby'] ); 82 83 return array( 84 'email' => 'requester', 85 'created_timestamp' => array( 'requested', $desc_first ), 86 ); 87 } 88 89 /** 90 * Default primary column. 91 * 92 * @since 4.9.6 93 * 94 * @return string Default primary column name. 95 */ 96 protected function get_default_primary_column_name() { 97 return 'email'; 98 } 99 100 /** 101 * Count number of requests for each status. 102 * 103 * @since 4.9.6 104 * 105 * @return object Number of posts for each status. 106 */ 107 protected function get_request_counts() { 108 global $wpdb; 109 110 $cache_key = $this->post_type . '-' . $this->request_type; 111 $counts = wp_cache_get( $cache_key, 'counts' ); 112 113 if ( false !== $counts ) { 114 return $counts; 115 } 116 117 $query = " 118 SELECT post_status, COUNT( * ) AS num_posts 119 FROM {$wpdb->posts} 120 WHERE post_type = %s 121 AND post_name = %s 122 GROUP BY post_status"; 123 124 $results = (array) $wpdb->get_results( $wpdb->prepare( $query, $this->post_type, $this->request_type ), ARRAY_A ); 125 $counts = array_fill_keys( get_post_stati(), 0 ); 126 127 foreach ( $results as $row ) { 128 $counts[ $row['post_status'] ] = $row['num_posts']; 129 } 130 131 $counts = (object) $counts; 132 wp_cache_set( $cache_key, $counts, 'counts' ); 133 134 return $counts; 135 } 136 137 /** 138 * Get an associative array ( id => link ) with the list of views available on this table. 139 * 140 * @since 4.9.6 141 * 142 * @return string[] An array of HTML links keyed by their view. 143 */ 144 protected function get_views() { 145 $current_status = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : ''; 146 $statuses = _wp_privacy_statuses(); 147 $views = array(); 148 $counts = $this->get_request_counts(); 149 $total_requests = absint( array_sum( (array) $counts ) ); 150 151 // Normalized admin URL. 152 $admin_url = $this->get_admin_url(); 153 154 $current_link_attributes = empty( $current_status ) ? ' class="current" aria-current="page"' : ''; 155 $status_label = sprintf( 156 /* translators: %s: Number of requests. */ 157 _nx( 158 'All <span class="count">(%s)</span>', 159 'All <span class="count">(%s)</span>', 160 $total_requests, 161 'requests' 162 ), 163 number_format_i18n( $total_requests ) 164 ); 165 166 $views['all'] = sprintf( 167 '<a href="%s"%s>%s</a>', 168 esc_url( $admin_url ), 169 $current_link_attributes, 170 $status_label 171 ); 172 173 foreach ( $statuses as $status => $label ) { 174 $post_status = get_post_status_object( $status ); 175 if ( ! $post_status ) { 176 continue; 177 } 178 179 $current_link_attributes = $status === $current_status ? ' class="current" aria-current="page"' : ''; 180 $total_status_requests = absint( $counts->{$status} ); 181 182 if ( ! $total_status_requests ) { 183 continue; 184 } 185 186 $status_label = sprintf( 187 translate_nooped_plural( $post_status->label_count, $total_status_requests ), 188 number_format_i18n( $total_status_requests ) 189 ); 190 191 $status_link = add_query_arg( 'filter-status', $status, $admin_url ); 192 193 $views[ $status ] = sprintf( 194 '<a href="%s"%s>%s</a>', 195 esc_url( $status_link ), 196 $current_link_attributes, 197 $status_label 198 ); 199 } 200 201 return $views; 202 } 203 204 /** 205 * Get bulk actions. 206 * 207 * @since 4.9.6 208 * 209 * @return array Array of bulk action labels keyed by their action. 210 */ 211 protected function get_bulk_actions() { 212 return array( 213 'resend' => __( 'Resend confirmation requests' ), 214 'complete' => __( 'Mark requests as completed' ), 215 'delete' => __( 'Delete requests' ), 216 ); 217 } 218 219 /** 220 * Process bulk actions. 221 * 222 * @since 4.9.6 223 * @since 5.6.0 Added support for the `complete` action. 224 */ 225 public function process_bulk_action() { 226 $action = $this->current_action(); 227 $request_ids = isset( $_REQUEST['request_id'] ) ? wp_parse_id_list( wp_unslash( $_REQUEST['request_id'] ) ) : array(); 228 229 if ( empty( $request_ids ) ) { 230 return; 231 } 232 233 $count = 0; 234 $failures = 0; 235 236 check_admin_referer( 'bulk-privacy_requests' ); 237 238 switch ( $action ) { 239 case 'resend': 240 foreach ( $request_ids as $request_id ) { 241 $resend = _wp_privacy_resend_request( $request_id ); 242 243 if ( $resend && ! is_wp_error( $resend ) ) { 244 $count++; 245 } else { 246 $failures++; 247 } 248 } 249 250 if ( $failures ) { 251 add_settings_error( 252 'bulk_action', 253 'bulk_action', 254 sprintf( 255 /* translators: %d: Number of requests. */ 256 _n( 257 '%d confirmation request failed to resend.', 258 '%d confirmation requests failed to resend.', 259 $failures 260 ), 261 $failures 262 ), 263 'error' 264 ); 265 } 266 267 if ( $count ) { 268 add_settings_error( 269 'bulk_action', 270 'bulk_action', 271 sprintf( 272 /* translators: %d: Number of requests. */ 273 _n( 274 '%d confirmation request re-sent successfully.', 275 '%d confirmation requests re-sent successfully.', 276 $count 277 ), 278 $count 279 ), 280 'success' 281 ); 282 } 283 284 break; 285 286 case 'complete': 287 foreach ( $request_ids as $request_id ) { 288 $result = _wp_privacy_completed_request( $request_id ); 289 290 if ( $result && ! is_wp_error( $result ) ) { 291 $count++; 292 } 293 } 294 295 add_settings_error( 296 'bulk_action', 297 'bulk_action', 298 sprintf( 299 /* translators: %d: Number of requests. */ 300 _n( 301 '%d request marked as complete.', 302 '%d requests marked as complete.', 303 $count 304 ), 305 $count 306 ), 307 'success' 308 ); 309 break; 310 311 case 'delete': 312 foreach ( $request_ids as $request_id ) { 313 if ( wp_delete_post( $request_id, true ) ) { 314 $count++; 315 } else { 316 $failures++; 317 } 318 } 319 320 if ( $failures ) { 321 add_settings_error( 322 'bulk_action', 323 'bulk_action', 324 sprintf( 325 /* translators: %d: Number of requests. */ 326 _n( 327 '%d request failed to delete.', 328 '%d requests failed to delete.', 329 $failures 330 ), 331 $failures 332 ), 333 'error' 334 ); 335 } 336 337 if ( $count ) { 338 add_settings_error( 339 'bulk_action', 340 'bulk_action', 341 sprintf( 342 /* translators: %d: Number of requests. */ 343 _n( 344 '%d request deleted successfully.', 345 '%d requests deleted successfully.', 346 $count 347 ), 348 $count 349 ), 350 'success' 351 ); 352 } 353 354 break; 355 } 356 } 357 358 /** 359 * Prepare items to output. 360 * 361 * @since 4.9.6 362 * @since 5.1.0 Added support for column sorting. 363 */ 364 public function prepare_items() { 365 $this->items = array(); 366 $posts_per_page = $this->get_items_per_page( $this->request_type . '_requests_per_page' ); 367 $args = array( 368 'post_type' => $this->post_type, 369 'post_name__in' => array( $this->request_type ), 370 'posts_per_page' => $posts_per_page, 371 'offset' => isset( $_REQUEST['paged'] ) ? max( 0, absint( $_REQUEST['paged'] ) - 1 ) * $posts_per_page : 0, 372 'post_status' => 'any', 373 's' => isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '', 374 ); 375 376 $orderby_mapping = array( 377 'requester' => 'post_title', 378 'requested' => 'post_date', 379 ); 380 381 if ( isset( $_REQUEST['orderby'] ) && isset( $orderby_mapping[ $_REQUEST['orderby'] ] ) ) { 382 $args['orderby'] = $orderby_mapping[ $_REQUEST['orderby'] ]; 383 } 384 385 if ( isset( $_REQUEST['order'] ) && in_array( strtoupper( $_REQUEST['order'] ), array( 'ASC', 'DESC' ), true ) ) { 386 $args['order'] = strtoupper( $_REQUEST['order'] ); 387 } 388 389 if ( ! empty( $_REQUEST['filter-status'] ) ) { 390 $filter_status = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : ''; 391 $args['post_status'] = $filter_status; 392 } 393 394 $requests_query = new WP_Query( $args ); 395 $requests = $requests_query->posts; 396 397 foreach ( $requests as $request ) { 398 $this->items[] = wp_get_user_request( $request->ID ); 399 } 400 401 $this->items = array_filter( $this->items ); 402 403 $this->set_pagination_args( 404 array( 405 'total_items' => $requests_query->found_posts, 406 'per_page' => $posts_per_page, 407 ) 408 ); 409 } 410 411 /** 412 * Checkbox column. 413 * 414 * @since 4.9.6 415 * 416 * @param WP_User_Request $item Item being shown. 417 * @return string Checkbox column markup. 418 */ 419 public function column_cb( $item ) { 420 return sprintf( '<input type="checkbox" name="request_id[]" value="%1$s" /><span class="spinner"></span>', esc_attr( $item->ID ) ); 421 } 422 423 /** 424 * Status column. 425 * 426 * @since 4.9.6 427 * 428 * @param WP_User_Request $item Item being shown. 429 * @return string Status column markup. 430 */ 431 public function column_status( $item ) { 432 $status = get_post_status( $item->ID ); 433 $status_object = get_post_status_object( $status ); 434 435 if ( ! $status_object || empty( $status_object->label ) ) { 436 return '-'; 437 } 438 439 $timestamp = false; 440 441 switch ( $status ) { 442 case 'request-confirmed': 443 $timestamp = $item->confirmed_timestamp; 444 break; 445 case 'request-completed': 446 $timestamp = $item->completed_timestamp; 447 break; 448 } 449 450 echo '<span class="status-label status-' . esc_attr( $status ) . '">'; 451 echo esc_html( $status_object->label ); 452 453 if ( $timestamp ) { 454 echo ' (' . $this->get_timestamp_as_date( $timestamp ) . ')'; 455 } 456 457 echo '</span>'; 458 } 459 460 /** 461 * Convert timestamp for display. 462 * 463 * @since 4.9.6 464 * 465 * @param int $timestamp Event timestamp. 466 * @return string Human readable date. 467 */ 468 protected function get_timestamp_as_date( $timestamp ) { 469 if ( empty( $timestamp ) ) { 470 return ''; 471 } 472 473 $time_diff = time() - $timestamp; 474 475 if ( $time_diff >= 0 && $time_diff < DAY_IN_SECONDS ) { 476 /* translators: %s: Human-readable time difference. */ 477 return sprintf( __( '%s ago' ), human_time_diff( $timestamp ) ); 478 } 479 480 return date_i18n( get_option( 'date_format' ), $timestamp ); 481 } 482 483 /** 484 * Default column handler. 485 * 486 * @since 4.9.6 487 * @since 5.7.0 Added `manage_{$this->screen->id}_custom_column` action. 488 * 489 * @param WP_User_Request $item Item being shown. 490 * @param string $column_name Name of column being shown. 491 */ 492 public function column_default( $item, $column_name ) { 493 /** 494 * Fires for each custom column of a specific request type in the Requests list table. 495 * 496 * Custom columns are registered using the {@see 'manage_export-personal-data_columns'} 497 * and the {@see 'manage_erase-personal-data_columns'} filters. 498 * 499 * @since 5.7.0 500 * 501 * @param string $column_name The name of the column to display. 502 * @param WP_User_Request $item The item being shown. 503 */ 504 do_action( "manage_{$this->screen->id}_custom_column", $column_name, $item ); 505 } 506 507 /** 508 * Created timestamp column. Overridden by children. 509 * 510 * @since 5.7.0 511 * 512 * @param WP_User_Request $item Item being shown. 513 * @return string Human readable date. 514 */ 515 public function column_created_timestamp( $item ) { 516 return $this->get_timestamp_as_date( $item->created_timestamp ); 517 } 518 519 /** 520 * Actions column. Overridden by children. 521 * 522 * @since 4.9.6 523 * 524 * @param WP_User_Request $item Item being shown. 525 * @return string Email column markup. 526 */ 527 public function column_email( $item ) { 528 return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), $item->email, $this->row_actions( array() ) ); 529 } 530 531 /** 532 * Next steps column. Overridden by children. 533 * 534 * @since 4.9.6 535 * 536 * @param WP_User_Request $item Item being shown. 537 */ 538 public function column_next_steps( $item ) {} 539 540 /** 541 * Generates content for a single row of the table, 542 * 543 * @since 4.9.6 544 * 545 * @param WP_User_Request $item The current item. 546 */ 547 public function single_row( $item ) { 548 $status = $item->status; 549 550 echo '<tr id="request-' . esc_attr( $item->ID ) . '" class="status-' . esc_attr( $status ) . '">'; 551 $this->single_row_columns( $item ); 552 echo '</tr>'; 553 } 554 555 /** 556 * Embed scripts used to perform actions. Overridden by children. 557 * 558 * @since 4.9.6 559 */ 560 public function embed_scripts() {} 561 }