class-wp-privacy-requests-table.php 11 KB

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