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
10abstract 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 * Gets 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 * Normalizes 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 * Gets 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 * Returns the 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 * Counts the number of requests for each status.
102 *
103 * @since 4.9.6
104 *
105 * @global wpdb $wpdb WordPress database abstraction object.
106 *
107 * @return object Number of posts for each status.
108 */
109 protected function get_request_counts() {
110 global $wpdb;
111
112 $cache_key = $this->post_type . '-' . $this->request_type;
113 $counts = wp_cache_get( $cache_key, 'counts' );
114
115 if ( false !== $counts ) {
116 return $counts;
117 }
118
119 $results = (array) $wpdb->get_results(
120 $wpdb->prepare(
121 "SELECT post_status, COUNT( * ) AS num_posts
122 FROM {$wpdb->posts}
123 WHERE post_type = %s
124 AND post_name = %s
125 GROUP BY post_status",
126 $this->post_type,
127 $this->request_type
128 ),
129 ARRAY_A
130 );
131
132 $counts = array_fill_keys( get_post_stati(), 0 );
133
134 foreach ( $results as $row ) {
135 $counts[ $row['post_status'] ] = $row['num_posts'];
136 }
137
138 $counts = (object) $counts;
139 wp_cache_set( $cache_key, $counts, 'counts' );
140
141 return $counts;
142 }
143
144 /**
145 * Gets an associative array ( id => link ) with the list of views available on this table.
146 *
147 * @since 4.9.6
148 *
149 * @return string[] An array of HTML links keyed by their view.
150 */
151 protected function get_views() {
152 $current_status = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : '';
153 $statuses = _wp_privacy_statuses();
154 $views = array();
155 $counts = $this->get_request_counts();
156 $total_requests = absint( array_sum( (array) $counts ) );
157
158 // Normalized admin URL.
159 $admin_url = $this->get_admin_url();
160
161 $status_label = sprintf(
162 /* translators: %s: Number of requests. */
163 _nx(
164 'All <span class="count">(%s)</span>',
165 'All <span class="count">(%s)</span>',
166 $total_requests,
167 'requests'
168 ),
169 number_format_i18n( $total_requests )
170 );
171
172 $views['all'] = array(
173 'url' => esc_url( $admin_url ),
174 'label' => $status_label,
175 'current' => empty( $current_status ),
176 );
177
178 foreach ( $statuses as $status => $label ) {
179 $post_status = get_post_status_object( $status );
180 if ( ! $post_status ) {
181 continue;
182 }
183
184 $total_status_requests = absint( $counts->{$status} );
185
186 if ( ! $total_status_requests ) {
187 continue;
188 }
189
190 $status_label = sprintf(
191 translate_nooped_plural( $post_status->label_count, $total_status_requests ),
192 number_format_i18n( $total_status_requests )
193 );
194
195 $status_link = add_query_arg( 'filter-status', $status, $admin_url );
196
197 $views[ $status ] = array(
198 'url' => esc_url( $status_link ),
199 'label' => $status_label,
200 'current' => $status === $current_status,
201 );
202 }
203
204 return $this->get_views_links( $views );
205 }
206
207 /**
208 * Gets bulk actions.
209 *
210 * @since 4.9.6
211 *
212 * @return array Array of bulk action labels keyed by their action.
213 */
214 protected function get_bulk_actions() {
215 return array(
216 'resend' => __( 'Resend confirmation requests' ),
217 'complete' => __( 'Mark requests as completed' ),
218 'delete' => __( 'Delete requests' ),
219 );
220 }
221
222 /**
223 * Process bulk actions.
224 *
225 * @since 4.9.6
226 * @since 5.6.0 Added support for the `complete` action.
227 */
228 public function process_bulk_action() {
229 $action = $this->current_action();
230 $request_ids = isset( $_REQUEST['request_id'] ) ? wp_parse_id_list( wp_unslash( $_REQUEST['request_id'] ) ) : array();
231
232 if ( empty( $request_ids ) ) {
233 return;
234 }
235
236 $count = 0;
237 $failures = 0;
238
239 check_admin_referer( 'bulk-privacy_requests' );
240
241 switch ( $action ) {
242 case 'resend':
243 foreach ( $request_ids as $request_id ) {
244 $resend = _wp_privacy_resend_request( $request_id );
245
246 if ( $resend && ! is_wp_error( $resend ) ) {
247 ++$count;
248 } else {
249 ++$failures;
250 }
251 }
252
253 if ( $failures ) {
254 add_settings_error(
255 'bulk_action',
256 'bulk_action',
257 sprintf(
258 /* translators: %d: Number of requests. */
259 _n(
260 '%d confirmation request failed to resend.',
261 '%d confirmation requests failed to resend.',
262 $failures
263 ),
264 $failures
265 ),
266 'error'
267 );
268 }
269
270 if ( $count ) {
271 add_settings_error(
272 'bulk_action',
273 'bulk_action',
274 sprintf(
275 /* translators: %d: Number of requests. */
276 _n(
277 '%d confirmation request re-sent successfully.',
278 '%d confirmation requests re-sent successfully.',
279 $count
280 ),
281 $count
282 ),
283 'success'
284 );
285 }
286
287 break;
288
289 case 'complete':
290 foreach ( $request_ids as $request_id ) {
291 $result = _wp_privacy_completed_request( $request_id );
292
293 if ( $result && ! is_wp_error( $result ) ) {
294 ++$count;
295 }
296 }
297
298 add_settings_error(
299 'bulk_action',
300 'bulk_action',
301 sprintf(
302 /* translators: %d: Number of requests. */
303 _n(
304 '%d request marked as complete.',
305 '%d requests marked as complete.',
306 $count
307 ),
308 $count
309 ),
310 'success'
311 );
312 break;
313
314 case 'delete':
315 foreach ( $request_ids as $request_id ) {
316 if ( wp_delete_post( $request_id, true ) ) {
317 ++$count;
318 } else {
319 ++$failures;
320 }
321 }
322
323 if ( $failures ) {
324 add_settings_error(
325 'bulk_action',
326 'bulk_action',
327 sprintf(
328 /* translators: %d: Number of requests. */
329 _n(
330 '%d request failed to delete.',
331 '%d requests failed to delete.',
332 $failures
333 ),
334 $failures
335 ),
336 'error'
337 );
338 }
339
340 if ( $count ) {
341 add_settings_error(
342 'bulk_action',
343 'bulk_action',
344 sprintf(
345 /* translators: %d: Number of requests. */
346 _n(
347 '%d request deleted successfully.',
348 '%d requests deleted successfully.',
349 $count
350 ),
351 $count
352 ),
353 'success'
354 );
355 }
356
357 break;
358 }
359 }
360
361 /**
362 * Prepares items to output.
363 *
364 * @since 4.9.6
365 * @since 5.1.0 Added support for column sorting.
366 */
367 public function prepare_items() {
368 $this->items = array();
369 $posts_per_page = $this->get_items_per_page( $this->request_type . '_requests_per_page' );
370 $args = array(
371 'post_type' => $this->post_type,
372 'post_name__in' => array( $this->request_type ),
373 'posts_per_page' => $posts_per_page,
374 'offset' => isset( $_REQUEST['paged'] ) ? max( 0, absint( $_REQUEST['paged'] ) - 1 ) * $posts_per_page : 0,
375 'post_status' => 'any',
376 's' => isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '',
377 );
378
379 $orderby_mapping = array(
380 'requester' => 'post_title',
381 'requested' => 'post_date',
382 );
383
384 if ( isset( $_REQUEST['orderby'] ) && isset( $orderby_mapping[ $_REQUEST['orderby'] ] ) ) {
385 $args['orderby'] = $orderby_mapping[ $_REQUEST['orderby'] ];
386 }
387
388 if ( isset( $_REQUEST['order'] ) && in_array( strtoupper( $_REQUEST['order'] ), array( 'ASC', 'DESC' ), true ) ) {
389 $args['order'] = strtoupper( $_REQUEST['order'] );
390 }
391
392 if ( ! empty( $_REQUEST['filter-status'] ) ) {
393 $filter_status = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : '';
394 $args['post_status'] = $filter_status;
395 }
396
397 $requests_query = new WP_Query( $args );
398 $requests = $requests_query->posts;
399
400 foreach ( $requests as $request ) {
401 $this->items[] = wp_get_user_request( $request->ID );
402 }
403
404 $this->items = array_filter( $this->items );
405
406 $this->set_pagination_args(
407 array(
408 'total_items' => $requests_query->found_posts,
409 'per_page' => $posts_per_page,
410 )
411 );
412 }
413
414 /**
415 * Returns the markup for the Checkbox column.
416 *
417 * @since 4.9.6
418 *
419 * @param WP_User_Request $item Item being shown.
420 * @return string Checkbox column markup.
421 */
422 public function column_cb( $item ) {
423 return sprintf(
424 '<input type="checkbox" name="request_id[]" id="requester_%1$s" value="%1$s" />' .
425 '<label for="requester_%1$s"><span class="screen-reader-text">%2$s</span></label><span class="spinner"></span>',
426 esc_attr( $item->ID ),
427 /* translators: Hidden accessibility text. %s: Email address. */
428 sprintf( __( 'Select %s' ), $item->email )
429 );
430 }
431
432 /**
433 * Status column.
434 *
435 * @since 4.9.6
436 *
437 * @param WP_User_Request $item Item being shown.
438 * @return string|void Status column markup. Returns a string if no status is found,
439 * otherwise it displays the markup.
440 */
441 public function column_status( $item ) {
442 $status = get_post_status( $item->ID );
443 $status_object = get_post_status_object( $status );
444
445 if ( ! $status_object || empty( $status_object->label ) ) {
446 return '-';
447 }
448
449 $timestamp = false;
450
451 switch ( $status ) {
452 case 'request-confirmed':
453 $timestamp = $item->confirmed_timestamp;
454 break;
455 case 'request-completed':
456 $timestamp = $item->completed_timestamp;
457 break;
458 }
459
460 echo '<span class="status-label status-' . esc_attr( $status ) . '">';
461 echo esc_html( $status_object->label );
462
463 if ( $timestamp ) {
464 echo '<span class="status-date">' . $this->get_timestamp_as_date( $timestamp ) . '</span>';
465 }
466
467 echo '</span>';
468 }
469
470 /**
471 * Converts a timestamp for display.
472 *
473 * @since 4.9.6
474 *
475 * @param int $timestamp Event timestamp.
476 * @return string Human readable date.
477 */
478 protected function get_timestamp_as_date( $timestamp ) {
479 if ( empty( $timestamp ) ) {
480 return '';
481 }
482
483 $time_diff = time() - $timestamp;
484
485 if ( $time_diff >= 0 && $time_diff < DAY_IN_SECONDS ) {
486 /* translators: %s: Human-readable time difference. */
487 return sprintf( __( '%s ago' ), human_time_diff( $timestamp ) );
488 }
489
490 return sprintf(
491 /* translators: 1: privacy request date format, 2: privacy request time format. */
492 __( '%1$s at %2$s' ),
493 /* translators: privacy request date format. See https://www.php.net/manual/en/datetime.format.php */
494 date_i18n( __( 'Y/m/d' ), $timestamp ),
495 /* translators: privacy request time format. See https://www.php.net/manual/en/datetime.format.php */
496 date_i18n( __( 'g:i a' ), $timestamp )
497 );
498 }
499
500 /**
501 * Handles the default column.
502 *
503 * @since 4.9.6
504 * @since 5.7.0 Added `manage_{$this->screen->id}_custom_column` action.
505 *
506 * @param WP_User_Request $item Item being shown.
507 * @param string $column_name Name of column being shown.
508 */
509 public function column_default( $item, $column_name ) {
510 /**
511 * Fires for each custom column of a specific request type in the Privacy Requests list table.
512 *
513 * Custom columns are registered using the {@see 'manage_export-personal-data_columns'}
514 * and the {@see 'manage_erase-personal-data_columns'} filters.
515 *
516 * The dynamic portion of the hook name, `$this->screen->id`, refers to the ID given to the list table
517 * according to which screen it's displayed on.
518 *
519 * Possible hook names include:
520 *
521 * - `manage_export-personal-data_custom_column`
522 * - `manage_erase-personal-data_custom_column`
523 *
524 * @since 5.7.0
525 *
526 * @param string $column_name The name of the column to display.
527 * @param WP_User_Request $item The item being shown.
528 */
529 do_action( "manage_{$this->screen->id}_custom_column", $column_name, $item );
530 }
531
532 /**
533 * Returns the markup for the Created timestamp column. Overridden by children.
534 *
535 * @since 5.7.0
536 *
537 * @param WP_User_Request $item Item being shown.
538 * @return string Human readable date.
539 */
540 public function column_created_timestamp( $item ) {
541 return $this->get_timestamp_as_date( $item->created_timestamp );
542 }
543
544 /**
545 * Actions column. Overridden by children.
546 *
547 * @since 4.9.6
548 *
549 * @param WP_User_Request $item Item being shown.
550 * @return string Email column markup.
551 */
552 public function column_email( $item ) {
553 return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), $item->email, $this->row_actions( array() ) );
554 }
555
556 /**
557 * Returns the markup for the next steps column. Overridden by children.
558 *
559 * @since 4.9.6
560 *
561 * @param WP_User_Request $item Item being shown.
562 */
563 public function column_next_steps( $item ) {}
564
565 /**
566 * Generates content for a single row of the table,
567 *
568 * @since 4.9.6
569 *
570 * @param WP_User_Request $item The current item.
571 */
572 public function single_row( $item ) {
573 $status = $item->status;
574
575 echo '<tr id="request-' . esc_attr( $item->ID ) . '" class="status-' . esc_attr( $status ) . '">';
576 $this->single_row_columns( $item );
577 echo '</tr>';
578 }
579
580 /**
581 * Embeds scripts used to perform actions. Overridden by children.
582 *
583 * @since 4.9.6
584 */
585 public function embed_scripts() {}
586}
587